Python 算法
Python 算法
Bit Manipulation
a&b, a|b, a^b(Exclusive OR), ~a(NOT), 1<<bit(left shift)
Switch
map:
list = [1,2,3]
incremented_list = map(lambda x: x+1, list) # [2,3,4]
filter:
a = [0,1,2]
filtered_a = list(filter(lambda x: x>0, a)) # [1,2]
dedup:
l = [1,1,2]
deduped = list(set(l)) # [1,2]
Optional number: Optional[int] means that the number can be an integer or None(python’s null)
l2 = ListNode()
print(l2.children) # 包括1!
queue = deque([1,2,3])
queue.append(4)
queue.extend([4,5,6])
queue.popleft() # pop first element
len(queue)
queue[0] # peek first element
queue.pop() # pop last element
Heap/Priority Queue # Only support min heap. For max heap, add negative signal
import heapq
data = [2,3,5,1]
heapq.heapify(data)
Dict/Set Dict
dict = {}
dict['key'] = 1
del dict['key']
for key in dict:
for value in dict.values()
for key, value in dict.items()
Defaultdict
from collections import defaultdict
d = defaultdict(int)
print (d['invalidKey']) # print 0 as int is set as default
Set:
set = set([0,1]) # note that you CANNOT use {} to initialize a set!
set.add(1)
set.discard(1)
set1 | set2 # union
set1 & set2 # intersection
Multiple return values in function Def func(.....) -> Tuple[bool, bool, bool]:
Split:
s.split() # split by space. consecutive tab etc
s.split(" ") # strictly split by single space, e.g. 'a b' -> 'a', ' ', 'b'
Strip
s.strip('abc') # remove all a,b,c
Pass by Reference Value Python 是 pass by assignment. 如果re-assign value, 那么并不是改原指针指向的值 而是给一个新的指针; 如
果在原指针直接改 那么会改变值
val = [1,2,3]
def reassign(val):
val = [1,2,3,4]
reassign(val)
print (val) # doesn't change
def append(val):
val.append(4)
append(val)
print (val) # change
Algorithms
Common Sorting Algorithms
归并排序
● 先对前半和后半部分递归,最后归并两个有序部分
● 空间
○ 数组:是O(n)因为归并部分需要复制值。不是O(nlogn)因为最后归并的时候递归部分的空间已经释放了
○ 链表:是O(logn)因为归并部分不需要新的空间。
● 稳定
快速排序
● 任意选一个pivot然后根据pivot排序(一般是第一个元素)。注意pivot应该单独取出而不是放在 递归里,否则如分裂出一
个空的子集和一个非空子集,那么非空子集没有变小,会陷入死循环。
○ 数组:维护前后两个指针指向最小区末尾和最大区前面,每个循环不停自增/自减两个指针,直到找到第一个大
数和最后一个小数,然后交换
○ 链表:直接开大/小两个表头,然后加节点,合并两个表
● 空间就是递归层数,最好情况O(logn),最坏O(n)
● 同理,时间最好O(nlogn),最坏O(n^2) 但是总体是最快的
● 不稳定,因为交换的时候越靠前的元素会交换到越靠后
堆排序
● 不稳定,因为Pop的过程会把后面的元素放到前面
● 只用于数组
● 过程
○ Push: 先加到数组尾,然后sift up和父亲交换,直到小于父亲。父亲是(i-1)//2
○ Pop:把数组尾放到根,然后sift down和最大大的孩子交换,直到小于孩子或者。孩子是(2*i+1, 2*i+2)
○ 建堆: 从空开始,逐个push新的元素。如果已经非空,则逆序把非叶节点sift down,因为轮到某节点的时候,其
两个不稳定子树都已经建堆完毕。
非比较排序:都是稳定,都不是原地算法
基数排序
● 先按照个位排入桶,再按照十位入桶,此时因为已经按个位排好序,所以若十位相同则自动按个位排序,依次往复
● 额外空间为O(n+桶数),需要额外空间;时间是O(n+位数)
桶排序
● 设置M个桶(区间),把元素放到桶内。每个桶内部用其他算法排序
● 额外空间是O(n+桶数),一般桶数要比较大一点;时间理论上是常数,如果桶够多。
● 需要数据分布非常均匀,不然所有数都进同一个桶等于没排序。用得不多
计数排序
● 桶排序的进阶版。需要数值是有限集并且范围很小(如1-10的整数)
● 先走一遍计算每个值出现次数,再从1-10逐个打印,出现几次打印几次。
● 额外空间是O(桶数)
综合比较
● 对于数组用快排,因为空间优势,不用调用新的空间,并且因为不调用新空间快排更快。
● 对于链表用归并,因为链表的归并调用空间跟快排一样,并且归并保证每次均匀分配所以会更快。
● 堆排序一般不用于排序,而是Top K问题等
● 非比较排序要么应用场合有限(需要特殊条件如分布均匀或者有基数)要么需要额外空间,用得不多
Union Find ● 用处
○ 判断两个元素是否属于同一集合(如两点互相联通)
○ 数一个图里的集合个数
● 基本结构
○ 一般用数组表示。A[i]表示第i个元素的父亲。初始化是自己
○ union合并两个任意元素所在集合
○ find找到元素的父亲
有两种实现:https://www.jianshu.com/p/8d3e9bbe135a
1. Quick Find. 线性结构。 每次union的时候 把被改节点所有孩子的父亲都更新 O(1) Find, O(n) union. 建树O(n^2)
2. Quick Union 树结构(更常用)。
● 通过平衡树的方法,Find/Union 都是 O(logn)
● 加上路径压缩(每次find的时候顺便把经过节点的父亲改成根节点),则Union/Find 接近O(1),但还不是线性
Tree Traversal
All use recursion or stack with the current node.
解法1
InOrder: left, mid, right. 每个节点进出栈一次 出栈时左孩子已经打印过了
PreOrder: mid,left,right; 同 inOrder
PostOrder: left, right, mid. 因为一个节点要push两次,第一次遍历左边 第二次遍历右边 需要记录前一个打印的节点 如果是当
前的右孩子那么打印 如果不是那么继续push
解法2
把所有孩子反序入栈,前序和后序都可以用,N-ary也可以用 不需要当前节点指针 注意这样的话栈并不等 于路径因为兄弟节
点也在
(De)Serialize a Tree
● Serialize: DFS with parenthesis, e.g. [1[2][3]]表示1有两个孩子2和3
● DeSerialize: 同样DFS,维护一个全局索引指向当前字符,DFS里每次都从开括号开始,先建立节点,然后把索引指向
后面,不停递归孩子直到索引指向闭括号。最后返回当前节点
Trie
假设有N个字符串,每个长均为L,则
● 实现插入,查整字符串,查前缀操作,时间都是O(L)
● 使用场景
○ 从第一个字符开始,逐个字符进行遍历查找(如"a","ab","abc"),则查找时间缩短L倍
○ 查找前缀
● 比HashMap的优势
○ 可以查询某个前缀是否存在
○ 省空间。空间最差是O(NL),但最好是O(N),当所有字符串都有同样前缀
实现注意的点
● 直接用一个dict{}代表Trie Node,不用另外定义
● 用'$'的key代表isWord
● 参考
Graph DFS-Backtracking
算法
维持一个visited set/matrix, 每到一个节点
1. 先检查节点是否出边界(Null/index out of bound)
2. 节点是否visited(树可省略)
3. 节点是否找到答案(terminal), 若是则直接返回找到
4. 节点设置为visited(树可省略)
5. 对每个邻居 判断是否递归遍历
6. 节点设置为unvisited,返回未找到(树可省略)
7. 注意只有后序遍历能打印path!
Memorization
对每个节点保存状态 避免反复DFS。
时间复杂度
假设DFS/BFS的depth是L, 每个节点有N个邻居 时间复杂度是L^N
BFS
找最小路径:
● 先判定初始节点是否是终点/符合入队条件,若是则入队和加入访问集,步数为0
● 每一步的一开始步数自增
● 记录队列当前长度,出队所有元素
● 对每个元素找他的邻居
○ 如果邻居是终点,直接返回步数
○ 如果邻居已经访问,跳过
○ 把邻居加入访问集,入队。注意不能先入队再看是否加入访问集,会造成多次入队
DFS/BFS 比较
● BFS: 求最短路径
● DFS: 求所有路径
● 空间复杂度也重要。
○ 完全二叉树的问题,DFS (logn),BFS (n),用DFS优先
○ 矩阵问题,DFS(n*n), BFS一般(n)因为只包括边缘,用BFS优先
Dijkstra
对一个有权图,求所有节点到一个起点的最短距离(Google Map)
步骤
● 对每个顶点维护到起点已知最短距离,初始化无穷大
● 维护一个已访问集合装所有已经求得最小距离的顶点,初始化只包含起点
● 每一步:
○ 取出不在已访问集合里已知最短距离最小的顶点,将它加入已访问集合,其已知最短距离就是实际最短距离
○ 对该顶点所有邻居更新他们的已知最短距离
○ 反复一直到所有顶点入队
证明
● 假设对前N个加入的顶点成立
● 对第N+1个顶点A,其已知最短距离在未入队顶点最小
● 假设有一条路径是最短路径
1. 如果该最短路径路上所有顶点都在队内,则路径距离肯定已经计算到A已知最短距离
2. 如果该路径有顶点不在队内,那对于路径上第一个不在队内的顶点B来说,
■ 该路径B到起点的距离已经计算到他当前最短距离
■ 则B当前最短距离<路径上B到起点距离<路径上A到起点距离
■ 如果路径上A到起点距离小于当前最短距离,得到B当前最短距离<A当前最短距离,矛盾。
■ 所以只可能是情况1,A当前最短距离就是实际最短距离
复杂度
● 如果用数组维护未入队顶点的当前最短距离,则时间复杂度O(V*V),因为在每两个节点联通时每一步都是O(V),空间复
杂度除去图本身是O(V)
● 用堆维护的话,则时间复杂度O(E*logV),是每次加新节点时把其邻居的最新的最短距离入堆的时间(最后堆里可能有E
个元素),空间复杂度是O(E+V)
● 空间复杂度除去图本身都是O(V)
Bi-Directional BFS
● 维持两个队列/访问集,一个起点一个终点
● 每次选小的队列,把所有元素出队。这样保证队列存的是一整层,也保证路径最短
● 出队的元素,检查是否在对面的访问集,是的话结束遍历
无向联通图找到所有环里的边
算法
从任何节点开始DFS前序遍历。对每个节点维护
1. 他的遍历顺序
2. 他访问邻居的最小节点顺序(默认值是自己的顺序)
每个DFS中,对于一个现节点和他的每个邻居
1. 如果是父亲,略过(这个所有无向图都要注意)
2. 如果遍历顺序为空,说明没遍历过,递归遍历(现节点顺序+1)
3. 如果邻居的访问最小节点顺序大于现节点顺序,说明现节点和邻居的边不在环里。否则在环里
4. 最后更新现节点的邻居最小节点顺序
算法注释
1. 现节点顺序肯定大于邻居,否则邻居比现节点先遍历,在之前遍历的时候肯定会接触现节点,矛盾
不同动态规划类型,定义状态
● 坐标型。给一个矩阵/数组,起点终点,行走规则
○ 状态:F[i]表示起点到i状态的目标值。依赖于F[i-1], F[i-2]...F[i]里的一到多个
○ 状态可以是物理坐标,也可以是步数,高度等限制,依赖方程里不能有环
● 单序列型。给一个字符串/数组。
○ 状态:F[i]表示前i个元素(如prefix)的目标值。
○ 长度为N,则设置N+1个状态,F[0]单独做初始化
● 区间型。给了一个字符串/数组。
○ 状态:F[i][j]表示在[i,j]区间的目标值。依赖于区间长度(求最长/最短)或区间起点/终点
● 双序列型。给两个字符串/数组。
○ 状态:F[i][j]表示第一个序列前i个和第二个序列前j个子序列的目标值
● 策略型。两个人玩游戏(如取硬币),轮流做操作(如取一个或取两个),谁 问先手能否
先到一个目标(如100元)就赢。
赢。
○ 状态:跟目标相关(如钱数)。如果任何一个选项(选任何一个硬币)使得对手输,则我赢。反之我输。
常见操作
● 删除现节点后面的Node: cur.next = cur.next.next
● 现节点后面插入一个Node:
○ new.next = cur.next
○ cur.next = new
● 把一个Node从原来位置插入新的位置:先删除,再插入
● 找表的中间:注意要找中间前一个节点, 方便把中间节点从原链表分离(如Merge Sort)
○ midPrev, fast = head, head
○ while fast and fast.next and fast.next.next:
■ fast = fast.next.next
■ midPrev = midPrev.next
○ 表长为2,则midPrev指向第一个元素,表长为3则也指向第一个。表长为1应该直接返回
● 合并两个链表
○ while ptr1 and ptr2: (add ptr1 or/and ptr2)
○ append ptr1 or ptr2 to current ptr if not null, since at most one of them is non null
双指针
● 快慢指针用来链表找环:快指针比满指针快一倍。一直到快慢指针相遇。 LC141
● 如果要返回环开始的地方:则一个指针放到链表头,另一个放到相遇点,匀速一起前进,直到相遇。原理是链表头到环起
点的距离与相遇点到环起点的距离相差环的长度的倍数。LC 142
● 去除离终点距离为N的节: 双指针设置距离为N,一起前进直到前面的指针到列尾 LC 19
● Partition/Sorting题目:不用交换,可以开新链表然后插入。 LC 86
○ 数组交换是为了in-place,但是链表的Node本来就可以移动,不会用到额外空间。
○ 数组交换也是为了保存数组在内存里的连续性,但链表的Node在内存中本来就是离散的。
● 倒转链表问题
○ 判定链表非空
○ 初始化为prev = None, cur = head
○ 每次 next = cur.next, cur.next=prev, cur,prev = next,cur
○ 最后的prev就是新的表头,因为cur已经空了
双链表
● 每个节点有prev, next, val
● 用于LRU, LFU
● 双链表本身维护Dummy Head, Dummy Tail. 支持以下功能
○ 从头删除(删除最老的值)
○ 从尾加入(加入最新的值)
○ 删除任意指定的节点(保证在链表里)。用于更新。
Arrays 连续子数组和的计算
● 存所有前缀数组和sum(0,i). 任意一个连续子数组的和sum(i,j)=sum(0,j)-sum(0,i)。把前缀数组和存到set里方
便查找(查的时候减一下),如是否是 另一个数K的倍数。
○ 注意初始化存一个0代表空集的和,便于查找sum(0,i)本身
Two Pointer
相向型
● 模板如下
left, right = 0,n-1
while (left < right):
if (num[left], num[right 满足某条件):
right--, 因为[left+1,right-1]和right的组合也满足该条件,可略过
else:
left++, 如上同理
● NSum两种解法
● 排序+相向指针。适用于N>3,除去排序是O(N)时间,O(1)空间
● HashSet。O(N)时间和空间
● 2N-Sum:可以分解成两个NSum以HashSet的形式解决
Partition型
● 不稳定算法,无法保证值的相对顺序
● 如果分成两个区,则两个指针。low指向低值区结尾(小于low全是低值,low自己未知),high指向高值区结尾。low若碰
到高值则swap(low,high),high--,反之low++
● 如果分成三个区,则在二区基础增加一个指针cur指向中值区结尾,移动cur并类似和low/high交换。
扫描线
● 用于解决多个区间重叠,求重叠状态/最大重叠个数等的情况
● 排序区间的起点/终点。有一条垂直于坐标轴的线从左到右扫描每个起点/终点。起点则重叠区间数自增,终点自减。
OOD
class InvalidImplementation:
@abstractedmethod
def method1(self):
print('method 1)
● Polymorphism:
○ For Class: multiple classes have different implementation with same method name(doesn't have to be
subclass)
○ For Function: same function can take multiple types
Class/Static variables ● Any variable defined under class is class variable; any variable defined within method is instance variable
● If you change class variable for an instance, it won't affect other instances; but if you change it for the class, it will
affect everything
class Student:
department = 'EE'
def __init__(self):
return
s1 = Student()
s2 = Student()
s1.department = 'CS' # won't affect s2
Student.department = 'CS' # will affect s2