diff --git a/README.md b/README.md index 6e9ab30..0d2a994 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,12 @@ Python 抽象程度比较高, 我们能用更少的代码来实现功能,同 ## 算法可视化 学习算法的过程中有时候会比较抽象,这里给大家推荐一些可视化的网站,方便更直观地理解各种算法和数据结构的执行步骤: +遇到一个算法或数据结构,你可以 google 搜索 "名称+ visualization" 找到一些可视化网站方便理解,比如学习跳跃表的时候笔者就 +可以通过 goole "skip list visualization" 搜到一些可视化网站帮助你理解它的工作原理。 - https://github.com/algorithm-visualizer/algorithm-visualizer - https://www.cs.usfca.edu/~galles/visualization/Algorithms.html +- https://cmps-people.ok.ubc.ca/ylucet/DS/Algorithms.html - https://runestone.academy/runestone/books/published/pythonds/index.html# ## 讲课形式 diff --git "a/docs/03_\351\223\276\350\241\250/lru_cache.py" "b/docs/03_\351\223\276\350\241\250/lru_cache.py" index 426a309..e1683bb 100644 --- "a/docs/03_\351\223\276\350\241\250/lru_cache.py" +++ "b/docs/03_\351\223\276\350\241\250/lru_cache.py" @@ -132,3 +132,83 @@ def test(): if __name__ == '__main__': test() + + +######################################### 使用双链表实现 LRUcache #################################################### +""" +一般面试中不会让我们直接用内置结构,所以这里提供一个自己实现的双链表+map lru 缓存。这也是力扣上的一道真题: +[146] LRU 缓存 https://leetcode-cn.com/problems/lru-cache/description/ +""" + +class ListNode: + def __init__(self, key=None, value=None): + self.key = key + self.value = value + self.prev = self.next = None + + +class List: + def __init__(self): + """循环双链表。注意增加了虚拟头尾结点 head,tail 方便处理""" + self.head = ListNode() + self.tail = ListNode() + self.head.prev = self.head.next = self.tail + self.tail.next = self.tail.prev = self.head + + def delete_node(self, node): # 删除指定节点 + node.prev.next = node.next + node.next.prev = node.prev + + def add_to_head(self, node): # 指定节点添加到 self.head 后 + nextnode = self.head.next + node.next = nextnode + node.prev = self.head + self.head.next = node + nextnode.prev = node + + +class LRUCache(object): + + def __init__(self, capacity): + """ + 思路:循环双链表 + 字典 + :type capacity: int + """ + self.map = dict() + self.ll = List() + self.capacity = capacity + + def get(self, key): + """ + :type key: int + :rtype: int + """ + if key not in self.map: + return -1 + + node = self.map[key] + self.ll.delete_node(node) + self.ll.add_to_head(node) + return node.value + + def put(self, key, value): + """ + :type key: int + :type value: int + :rtype: None + """ + if key in self.map: # 更新不会改变元素个数,这里不用判断是否需要剔除 + node = self.map[key] + node.value = value # 修改结构体会也会修改 map 对应 value 的引用 + self.ll.delete_node(node) + self.ll.add_to_head(node) + else: + if len(self.map) >= self.capacity: # 直接用 len(self.map) ,不需要self.size 字段了 + tailnode = self.ll.tail.prev + self.ll.delete_node(tailnode) + del self.map[tailnode.key] + + node = ListNode(key, value) + self.map[key] = node + self.ll.add_to_head(node) + diff --git "a/docs/15_\345\240\206\344\270\216\345\240\206\346\216\222\345\272\217/min_heap.py" "b/docs/15_\345\240\206\344\270\216\345\240\206\346\216\222\345\272\217/min_heap.py" new file mode 100644 index 0000000..b424331 --- /dev/null +++ "b/docs/15_\345\240\206\344\270\216\345\240\206\346\216\222\345\272\217/min_heap.py" @@ -0,0 +1,66 @@ +# python3 +class MinHeap: + def __init__(self): + """ + 这里提供一个最小堆实现。如果面试不让用内置的堆非让你自己实现的话,考虑用这个简版的最小堆实现。 + 一般只需要实现 heqppop,heappush 两个操作就可以应付面试题了 + parent: (i-1)//2。注意这么写 int((n-1)/2), python3 (n-1)//2当n=0结果是-1而不是0 + left: 2*i+1 + right: 2*i+2 + 参考: + https://favtutor.com/blogs/heap-in-python + https://runestone.academy/ns/books/published/pythonds/Trees/BinaryHeapImplementation.html + https://www.askpython.com/python/examples/min-heap + """ + self.pq = [] + + def min_heapify(self, nums, k): + """递归调用,维持最小堆特性""" + l = 2*k+1 # 左节点位置 + r = 2*k+2 # 右节点 + if l < len(nums) and nums[l] < nums[k]: + smallest = l + else: + smallest = k + if r < len(nums) and nums[r] < nums[smallest]: + smallest = r + if smallest != k: + nums[k], nums[smallest] = nums[smallest], nums[k] + self.min_heapify(nums, smallest) + + def heappush(self, num): + """列表最后就加入一个元素,之后不断循环调用维持堆特性""" + self.pq.append(num) + n = len(self.pq) - 1 + # 注意必须加上n>0。因为 python3 (n-1)//2 当n==0 的时候结果是-1而不是0! + while n > 0 and self.pq[n] < self.pq[(n-1)//2]: # parent 交换 + self.pq[n], self.pq[(n-1)//2] = self.pq[(n-1)//2], self.pq[n] # swap + n = (n-1)//2 + + def heqppop(self): # 取 pq[0],之后和pq最后一个元素pq[-1]交换之后调用 min_heapify(0) + minval = self.pq[0] + last = self.pq[-1] + self.pq[0] = last + self.min_heapify(self.pq, 0) + self.pq.pop() + return minval + + def heapify(self, nums): + n = int((len(nums)//2)-1) + for k in range(n, -1, -1): + self.min_heapify(nums, k) + + +def test_MinHeqp(): + import random + l = list(range(1, 9)) + random.shuffle(l) + pq = MinHeap() + for num in l: + pq.heappush(num) + res = [] + for _ in range(len(l)): + res.append(pq.heqppop()) # 利用 heqppop,heqppush 实现堆排序 + + def issorted(l): return all(l[i] <= l[i+1] for i in range(len(l) - 1)) + assert issorted(res) diff --git "a/docs/19_python\345\206\205\347\275\256\345\270\270\347\224\250\347\256\227\346\263\225\345\222\214\346\225\260\346\215\256\347\273\223\346\236\204/builtins.md" "b/docs/19_python\345\206\205\347\275\256\345\270\270\347\224\250\347\256\227\346\263\225\345\222\214\346\225\260\346\215\256\347\273\223\346\236\204/builtins.md" index 1665c9e..a38b5ef 100644 --- "a/docs/19_python\345\206\205\347\275\256\345\270\270\347\224\250\347\256\227\346\263\225\345\222\214\346\225\260\346\215\256\347\273\223\346\236\204/builtins.md" +++ "b/docs/19_python\345\206\205\347\275\256\345\270\270\347\224\250\347\256\227\346\263\225\345\222\214\346\225\260\346\215\256\347\273\223\346\236\204/builtins.md" @@ -28,21 +28,43 @@ 如果你使用 python2 or python3 刷题(比如力扣leetcode),有一些坑或者技巧需要注意: -- python3 和 python2 的 dict 有所用不同,python3.7 之后的 dict 会保持插入顺序, python2 不要依赖 dict 迭代顺序,请使用 OrderedDict -- 正确初始化一个二维数组:`dp = [[0 for _ in range(col)] for _ in range(row)]`,不要用 `dp = [[0] * n] * m`, 否则里边都 -引用的同一个 list,修改一个都会变 -- python在数值范围建议用:`MAXINT = 2**63-1; MININT = -2**63` 。因为 python2 sys.maxint 和 python3 sys.maxsize 不统一 +- 字典顺序。python3 和 python2 的 dict 有所用不同,python3.7 之后的 dict 会保持插入顺序(不是字典序), python2 不要依赖 dict 迭代顺序,请使用 OrderedDict +- 矩阵。正确初始化一个不可变对象的二维数组:`dp = [ [0]*col for _ in range(row) ]`,不要用 `dp = [[0] * n] * m`, 否则里边都 +引用的同一个 list,修改一个都会变。`[[0 for _ in range(col)] for _ in range(row)]` 也可以(稍慢),因为数字 0 是不可变对象 +- 深浅拷贝。经常在回溯题中需要`res,path=[],[]`,path 是用来回溯的路径。找到一个结果的时候需要用 `res.append(path[:])` 而 +不是`res.append(path)#错!` ,因为这里append的path的引用,之后修改了 path 结果就是错的!(或者用copy模块,不过不如[:]语法简洁) +- int范围。python在数值范围建议用:`MAXINT = 2**63-1; MININT = -2**63` 。因为 python2 sys.maxint 和 python3 sys.maxsize 不统一 - 优先级队列:使用内置queue.PriorityQueue or heapq ,定义一个 Item 类实现"小于" 魔术方法就可以实现,下边有代码演示 -- python3 的 functools 模块自带了 cache(等价于lru_cache(maxsize=None)) 和 lru_cache 装饰器,在一些需要递归记忆化搜索的时候会很方便 -- 除法变更:python2和 python3 除法做了变更要注意。还有负数除法。 python2 `int(6/-123)==-1`,但是 python3 `int(6/-123)==0`。 -整数除法统一用"//"。比如二分求中间值 `mid=(l+r)//2` 或者 `mid=l+(r-l)//2`,因为python天生支持大数不会溢出两种写法都行 +- 缓存。python3 的 functools 模块自带了 cache(等价于lru_cache(maxsize=None)) 和 lru_cache 装饰器,在一些需要递归记忆化搜索的时候会很方便 +- 除法变更:python2和 python3 除法做了变更要注意。还有负数除法。 python2 `int(6/-123)==-1, int(-3/2)==-2`,但是 python3 `int(6/-123)==0, int(-3/2)==-1`。 +正数的整数除法统一用"//"。比如二分求中间值 `mid=(l+r)//2` 或者 `mid=l+(r-l)//2`,因为python天生支持大数不会溢出两种写法都行。负数整数除法统一写 int(a/b)。 +凡是遇到除法运算的题目建议统一使用 python3 提交。 - 自定义排序函数。python2 可以用 `nums.sort(cmp=lambda a, b: a - b)`,但是python3移除了cmp参数。 python3如果想要用自定义排序函数可以使用 functools.cmp_to_key 函数改成 `nums.sort(key=cmp_to_key(lambda a, b: a - b))` +# python 递归暴栈(栈溢出) + +python 递归函数默认递归深度比较小,你可以通过 `sys.getrecursionlimit()` 函数打印出来。 +我在 mac 机器上测试的时候,以下结果 python2 输出 1000。这就导致一些递归函数测试用例稍微多一些就会报错。 +(一个用例超过上千个数据就会报错了) + +```py +import sys +print(sys.getrecursionlimit()) # 我的 mac 机器上输出 1000 +``` + +可以把以下代码设置最大栈深度,放到文件开头,在牛客上提交代码的时候可以避免一些递归代码报错。 +(leetcode 似乎给设置了,类似的题目发现力扣上提交不会栈溢出但是在牛客就会) + +```py +import sys +sys.setrecursionlimit(100000) # 设置函数栈深度足够大,避免栈溢出错误 +``` + # python int 值范围 ``` -# 乘方 (比较推荐,py2/3 都兼容不容易出错) +# 乘方 (比较推荐⭐️,py2/3 都兼容不容易出错) MAXINT = 2**63-1 MININT = -2**63 @@ -59,18 +81,81 @@ MAXINT = (1<<63) - 1 MININT = ~MAXINT ``` -# python dict 排序 +# python 负数位运算的坑 +1. Python3 中的整型是补码形式存储的 +2. Python3 中 bin 一个负数(十进制表示),输出的是它的原码的二进制表示加上个负号 +3. 为了获得负数(十进制表示)的补码,需要手动将其和十六进制数 0xffffffff 进行按位与操作,得到结果是个十六进制数,再交给 bin() 进行输出, +得到的才是你想要的补码表示。 + +```py +# 整数转换 https://leetcode-cn.com/problems/convert-integer-lcci/ +class Solution: + def convertInteger(self, A: int, B: int) -> int: + return bin((A & 0xffffffff) ^ (B & 0xffffffff)).count('1') +``` + +参考: +- https://www.runoob.com/w3cnote/python-negative-storage.html +- https://leetcode-cn.com/problems/convert-integer-lcci/solution/python3-zhu-yi-qi-dui-yu-fu-shu-de-cun-chu-fang-sh/ + +# python list 技巧 + +```py +# 排序嵌套 list,比如元素值是一个 tuple 或者 list +l = [('a', 1), ('c', 2), ('b',3)] +sorted(l, key=lambda p:p[0]) # 根据第1个值排序,[('a', 1), ('b', 3), ('c', 2)] +sorted(l, key=lambda p:p[1]) # 根据第2个值排序,[('a', 1), ('c', 2), ('b', 3)] +sorted(l, key=lambda p:(-p[0], p[1])) # 先根据第一个倒排,如果相等再根据第二个正排序 + +# 同时获取最大值的下标和值 +l = [1,2,5,4,3] +maxi, maxval = max(enumerate(l), key=lambda iv: iv[1]) # 2, 5 + +# python3 排序list自定义函数(python2 直接用 cmp 参数, python3 需要用 cmp_to_key 转成 key 参数) +from functools import cmp_to_key +nums = [3,2,1,4,5] +sorted(nums, key=cmp_to_key(lambda a,b: a-b) ) # [1 ,2 ,3, 4, 5] +sorted(nums, key=cmp_to_key(lambda a,b: b-a) ) # [5, 4, 3, 2, 1] + +# 一行代码判断列表是否有序 +issorted = all(l[i] <= l[i+1] for i in range(len(l) - 1)) + +# python3 一行代码求前缀和 +from itertools import accumulate +presums = list(accumulate([1,2,3])) # [1, 3, 6] + +# 一行代码求矩阵元素总和 https://stackoverflow.com/questions/10713150/how-to-sum-a-2d-array-in-python +allsum = sum(map(sum, matrix)) # 或者 allsum = sum((sum(row) for row in matrix)) +# 一行代码判断一个元素是否在矩阵中,比如判断 1 是否在矩阵matrix中 +any(1 in row for row in matrix) +# 一行代码获取矩阵最大、最小值 +maxval = max(map(max, matrix)) +``` + +# python dict 技巧 ```py -# 补充 python 根据 key,value 排序字典的 +# python 根据 key,value 排序字典 d = {'d': 4, 'a': 1, 'b': 2, 'c':3} -# sort by key and reverse +# dict sort by **key** and reverse dict(sorted(d.items())) # {'a': 1, 'b': 2, 'c': 3, 'd': 4} dict(sorted(d.items(), reverse=True)) # {'d': 4, 'c': 3, 'b': 2, 'a': 1} -# sort by value and reverse +# dict sort by **value** and reverse dict(sorted(d.items(), key = lambda kv:kv[1])) # {'a': 1, 'b': 2, 'c': 3, 'd': 4} dict(sorted(d.items(), key = lambda kv:kv[1], reverse=True)) # {'d': 4, 'c': 3, 'b': 2, 'a': 1} + +# 获取字典对应的最大值对应的 key,value +mydict = {'A':4,'B':10,'C':0,'D':87} +maximum = max(mydict, key=mydict.get) # Just use 'min' instead of 'max' for minimum. +maxk, maxv = maximum, mydict[maximum] +# 或者 +maxk, maxv = max(mydict.items(), key=lambda k: k[1]) + +# 支持默认值的有序字典 (OrderedDict and defaultdict) (注意是 key 插入顺序不是字典序) +# https://stackoverflow.com/questions/6190331/how-to-implement-an-ordered-default-dict +od = OrderedDict() # collections.OrderedDict() +od[i] = od.get(i, 0) + 1 # 间接实现了 defaultdict(int) ,同时保持了插入字典的 key 顺序 ``` # 链表题目调试函数 @@ -173,11 +258,11 @@ class Item: def __lt__(self, other): # heapq 源码实现只用了 小于 比较,这里定义了就可以 push 一个 item 类 return self.weight < other.weight - def __eq__(self, other): # 这个可以省略,只要定义了 __lt__ 魔法函数就可以了 - return self.weight == other.weight - - def __str__(self): - return '{}:{}'.format(self.key,self.weight) +# def __eq__(self, other): # 这个可以省略,只要定义了 __lt__ 魔法函数就可以了 +# return self.weight == other.weight +# +# def __str__(self): +# return '{}:{}'.format(self.key,self.weight) # Item.__lt__ = lambda self, other: self.weight < other.weight # 对于已有的类,直接加一句就可以实现作为 heap item 了 @@ -238,7 +323,9 @@ def maxheapval(h): # lru_cache/cache 优化记忆化搜索 python3 functools 模块的 cache 功能和 lru_cache(maxsize=None) 一样,不过更加轻量更快。在记忆化递归搜索的时候很方便。 +注意这里的参数 `maxsize=None` 一定要设置为 None,否则默认的 maxsize=128。 举一个力扣上的例子,如果不加 cache 递归函数因为会大量重复计算直接超时,但是加一个装饰器就可以通过。 +当然了如果你用 python2 没有这个装饰器,你可以直接用 python 的 dict 来实现。(存在就返回,否则计算结果保存到 dict 里) ```py """ @@ -318,7 +405,7 @@ def gen_tree_from_lc_input(vals_str): # [1,2,3] -> root TreeNode return None nodemap = {} for i in range(len(vals)): - if vals[i] is not None: # 一开始写的 if vals[i],但是 0 节点就错了!!! + if vals[i] is not None: # 一开始写的 if vals[i],但是 0 节点就错了! 应该显示判断是否为 None(空节点) nodemap[i] = TreeNode(vals[i]) else: nodemap[i] = None @@ -396,7 +483,7 @@ def gen_tree(vals): return root ``` -# python 交换列表元素的坑 +# python 交换列表元素的坑(交换副作用) ``` # 41. 缺失的第一个正数 https://leetcode-cn.com/problems/first-missing-positive/ @@ -418,7 +505,7 @@ class Solution(object): while 1 <= nums[i] <= n and nums[nums[i]-1] != nums[i]: # NOTE: 注意这一句交换右边有副作用的,不能颠倒!!! # nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i] # 这么写死循环! - nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1] + nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1] # 有副作用的放前边 for i in range(n): if nums[i] != i+1: return i+1 @@ -426,7 +513,7 @@ class Solution(object): return n+1 ``` -# 兼容代码提交格式 +# 兼容代码ACM/核心提交格式 注意牛客网有两种模式,一种是和 leetcode 一样的提交(无需处理输入),只需要提交核心代码。 一种是 ACM 模式,还需要自己处理输入和输出。 diff --git "a/docs/20_\351\235\242\350\257\225\346\214\207\345\215\227/interview.md" "b/docs/20_\351\235\242\350\257\225\346\214\207\345\215\227/interview.md" index 5958458..57d2492 100644 --- "a/docs/20_\351\235\242\350\257\225\346\214\207\345\215\227/interview.md" +++ "b/docs/20_\351\235\242\350\257\225\346\214\207\345\215\227/interview.md" @@ -41,3 +41,9 @@ leetcode 上的基础题目练练手感。 - [《程序员面试金典(第5版)》](https://book.douban.com/subject/25753386/) - [《剑指Offer》](https://book.douban.com/subject/25910559/) - [python leetCode](https://github.com/HuberTRoy/leetCodek) + +# 刷题网站 +leetcode 和牛客网是国内常用的两个刷题网站,笔者建议刷一下高频的 200 道题左右,基本可以应付大部分公司的算法面试了。 + +- https://leetcode-cn.com/ +- https://www.nowcoder.com/exam/oj diff --git "a/\345\211\221\346\214\207offer/40_NumbersAppearOnce(\346\225\260\347\273\204\344\270\255\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227).py" "b/\345\211\221\346\214\207offer/40_NumbersAppearOnce(\346\225\260\347\273\204\344\270\255\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227).py" index 30ada80..fa6f8fc 100644 --- "a/\345\211\221\346\214\207offer/40_NumbersAppearOnce(\346\225\260\347\273\204\344\270\255\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227).py" +++ "b/\345\211\221\346\214\207offer/40_NumbersAppearOnce(\346\225\260\347\273\204\344\270\255\345\217\252\345\207\272\347\216\260\344\270\200\346\254\241\347\232\204\346\225\260\345\255\227).py" @@ -31,6 +31,7 @@ class Solution1: def singleNumber(self, nums): """ + 类似题:求只出现一次的数字,其他都出现两次。 :type nums: List[int] :rtype: int """ @@ -58,12 +59,45 @@ def get_single_num(nums): while single & mask == 0: mask = mask << 1 - print(mask,'||||||||||||||||||') + print(mask, '||||||||||||||||||') left = [i for i in nums if i & mask] right = [i for i in nums if not(i & mask)] return [get_single_num(left), get_single_num(right)] +class Solution(object): + def singleNumber(self, nums): + """ + 思路:异或。 + + 136 题做过只出现一次的一个数字,本题有两个只出现一次的数字。 + 核心在于把数组分成两个。怎么分组呢? + + 假设只出现一次的数字式 x1,x2,所有元素异或结果是 x。一定有 x=x1^x2。 + x不能是0,否则x1==x2,就不是只出现一次的数字了。 + *可以用位运算 x&-x 取出x 二进制位最低位的 1,设其实是第 L 位置。* + 可以据此分类数组,一组是二进制第 L位置为 0 的数字,一组L 位置为 1的数字。 + x1,x2会分别出现在两个组中。这样第一组全部异或得到x1, 第二组全部异或得到 x2 + + :type nums: List[int] + :rtype: List[int] + """ + x = 0 + for num in nums: + x ^= num + + low1pos = x & -x # 这里也可以用移位运算,x&-x 不太好想,类似上边那个解法 + x1, x2 = 0, 0 + + for num in nums: + if num & low1pos: + x1 ^= num + else: + x2 ^= num + + return x1, x2 + + def test(): s = Solution() assert s.singleNumber([1, 2, 1, 3, 2, 5]) == [3, 5]