Overflow in Python

犹记得在 Java 中,int 占用 4 bytes 所以大小限制在 -2^32 和 2^32 -1 之间,如果考虑到溢出的情况,就要用 long 类型来转换。 但是在 Python 中似乎从来没有考虑过类似的问题,也不记得 int 是占几个字节,那么是说 Python 的数字永远不会溢出吗?不可能吧。 答案是对于 int 类型来说,可以认为不会;而 float 类型则需要注意溢出的情况。 首先说 int 类型,打印 sys.maxsize 可以看到 Python 会根据系统是 32 位还是 64 位来确定一个上限值,这点与 Java 等语言一致。不同的是我们仍然可以使用比这个上限大(得多)的整数,因为 Python 支持的是 Arbitrary-precision arithmetic。也就是说为了突破 CPU 字长带来的运算限制,通过在软件层面模拟计算过程,是可以完成更高位数和精度的运算的。比如在公钥加密的场景下,经常需要对上百位的整数进行运算,这时候就需要在软件层面支持。 这确实说明我们可以使用任意长度的 int 数字,只要不超出内存限制的话。因为如果给 Python 解释器分配了 2GB 内存,但是 2 * 1024 * 1024 * 1024 * 8 这么多位也不够表达的话,还是会出错的,只是并非 Overflow,而是 MemoryError. 再说 float. 打印 sys.float_info.max 可以看到 float 的上限值,如果超出之后是会报错的,即 OverflowError....

11-12 · 1 min

Understand Recursion Better

在 Simple Recursion 之后,我一度把递归当作一种算法。但通过比较 Divide and Conquer 和 Dynamic Programming,我才发现之前的理解有点问题。 一切还是要从 Algorithmic Paradigm 说起: An algorithmic paradigm or algorithm design paradigm is a generic model or framework which underlies the design of a class of algorithms. An algorithmic paradigm is an abstraction higher than the notion of an algorithm, just as an algorithm is an abstraction higher than a computer program. 算法范式,是在算法的层面上抽象出来的一种更泛化的思想,常用的有: Brute-force search 暴力解法 Backtracking 回溯算法 Greedy algorithm 贪心算法 Divide and conquer 分治法 Dynamic programming 动态规划 Divide and Conquer 的基本思路是把复杂的问题分解成多个类似的简单问题,解决之后再组合起来得到最终结果。这种算法有很多应用,比如排序中的 Merge Sort,先将数列分解成单个元素,然后再归并,这时子数组都已经排好顺序了,所以过程很快。...

11-08 · 2 min

Record Terminal as GIF

这几天在做一个 CLI 项目,因为涉及到命令行操作,所以想录制一段 GIF 放在 README 中展示。 印象里这种工具都是 JS 写的,但搜了搜居然发现有个 Python 的实现:asciinema 用法很简单,就是安装好之后在命令行执行 asciinema rec 便会启动一个新的 Shell 并开始录制,录制的时候就像正常使用 Terminal 一样即可,完成之后按 Ctrl-D 或者 Exit 退出,asciinema 会把录制好的 cast 文件保存到本地,也可以选择上传到他们的网站:asciinema.org. 那么 cast 文件是什么,又怎样得到 GIF 呢?其实这是 asciinema 自己定义的一种文件格式: A CAST file is a record of a terminal session recorded using asciinema, an open source terminal recording program. It contains a JSON-formatted header and a timestamped record of the text typed and printed during a terminal session....

11-07 · 2 min

Look and Say

无意中看到一种叫 look-and-say 的数列,很有意思,有点儿 Fibonacci 的感觉。数列如下: 1, 11, 21, 1211, 111221, 312211, 13112221, 1113213211, … 从第二位开始,每个数字都是对前一个数的计数结果的描述。比如 11 表示前面的 1 有一个 1,而 21 表示前面的 11 有两个 1,1121 表示 21 有一个 2 和一个 1,依次类推,可以无穷循环出新的结果。(除了 22 这个数字,因为会一直重复 22 本身) 后来查了查才发现原来 Leetcode 也有这道题,不过名字叫做 count-and-say。基本就是给 n 然后求此数列的第 n 项结果。 想了想思路并不难,无非是对一个数字的所有位数循环再计数就好了,不过写的时候很犯难,竟然还写出了一个无比冗长的 for 循环加 if 面条代码。这让我意识到了自己对于循环的认识有多么不够深刻。 之所以犯难,其实是不知道怎样把几种逻辑合并在一起,如果粗暴地列举所有分支大概是这样: def find_next(num: str): result = '' cur, count = num[0], 1 size = len(num) for i in range(1, size): if num[i] == cur and i == size - 1: # count 加一 # 更新 result elif num[i] == cur: # count 加一 elif i == size - 1: # 更新 result # 更新 cur&count # 更新 result else: # 更新 result # 更新 cur&count # 还有一种情况就是并没有进入 for 循环 # 那么也需要更新 result 直接按照这些分支把代码填满肯定很难看,所以要挑选合并。看起来只有第二个分支不需要更新 result,于是我决定把它单拎出来:如果 num[i]==cur 那么 count 加一;否则就更新 result 以及 cur&count....

11-07 · 2 min

Find Median

祸从天降的一天。 早上起不来,于是刷手机清醒一下,突然看到一个 ACMer 楼主提到自己没有刷过 Leetcode,面试的时候差点儿被打脸了。 看了一下题目,要求是 O(logn) 的复杂度,默默想了想,没有特别清晰的思路。 结果翻了翻评论,很多人都在蜻蜓点水般地表示二分查找不断分割就可以了。 要那么简单还用你说吗?起床了起床了。 题目是这样的: 给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。 我本来的想法是归并之后计算中位数,但只能做到 O(m+n),再优化感觉只能二分了。 于是又开始想分别取两个数组的中位数,比较之后就可以各扔掉一半,然后对两个折半的数组继续取中位数比较。 比如一开始找到的中位数是这样:[…5…], […10…],那么 5 的左边和 10 的右边就可以丢掉了,因为最终的中位数肯定在 5 和 10 中间。 如果接下来是这样:[5..8…], […7..10],那么 8 的右边和 7 的左边也可以丢掉,因为比 8 大的元素数量达不到这两个小数组的一半,所以中位数不会在里面,比 7 小的同理。 一直重复这个流程,最后得到的肯定就是一个或两个数,平均下就好了,时间复杂度也是符合要求的 O(logn). 结果等到看完标答后我才反应过来自己错在哪里了,这是后话。(因为 m 和 n 不一定一样大,所以显然折半的逻辑不对) 最优的解决方案是可以做到 O(log min(m, n)) 的,核心也是二分法,不过思路要复杂一些: 首先明确中位数的定义。对于奇数个数字来说是最中间的元素,偶数则是中间两个元素的平均值。 要做的是将两个数组各分一刀,假设划分的下标分别是 i 和 j,那么 nums1 被分成 nums1[0, i], nums1[i:],而 nums2 分为 nums2[0, j], nums2[j:]. 因为数组中元素的数量是奇是偶不一定,所以规定好是奇数的话多的一个放到左边。如此 i 的位置就是右边部分的第一个,而左半边正好有 i 个元素。 下标 i 和 j 之间是存在一个等式关系的,因为是中位数,所以 i + j 等于元素总数的一半,或者一半多一个(因为说好了左边多一个嘛)。那么就有 i+j=(m+n)/2 | i+j=(m+n)/2+1,合并起来写成 (m+n+1)/2....

11-03 · 3 min