Recursion
- Recursion or Stack: stack的方法是自己手动维护一个stack。因为每次在调用递归函数的时候,其实就是相当于让系统帮你创建一个stack,把要调用的函数和它对应的参数依次押入stack中。
- 模版:
def recursion(level, param1, param2, …):
#recursion terminator
if level > MAX_LEVEL:
process_result
return
#process logic in current level
process(level, data, …)
#drill down
self.recursion(level+1, p1, …)
#reverse the current level status if needed
Divide & Conquer
def divide_conquer(problem, param1, param2, …):
## 1. recursion terminator
if problem is None:
print_result
return
## 2. process current logic
# prepare data
data = prepare_data(problem)
subproblems = split_problem(problem, data)
## 3. drill down
# conquer subproblems
sub_result1 = self.divide_conquer(subproblems[0], p1, …)
sub_result2 = self.divide_conquer(subproblems[1], p1, …)
sub_result3 = self.divide_conquer(subproblems[2], p1, …)
# process and generate the final result
result = process_result(sub_result1, sub_result2, sub_result3, …)
## 4. revert the current level states
Depth First Search
# binary search tree
def dfs(node):
if node in visited:
# already visited
return
visited.add(node)
# process current node
dfs(node.left)
dfs(node.right)
# general
visited = set()
def dfs(node, visited):
if node in visited:
return
visited.add(node)
# process current logic
for next_node in node.children():
if not next_node in visited:
dfs(next_node, visited)
# stack
def dfs(tree):
if tree.root is None:
return []
visited, stack = [], [tree.root] # literally use stack (FILO)
while stack:
node = stack.pop()
visited.add(node)
process(node)
nodes = generate_related_nodes(node)
stack.push(nodes)
Breath First Search
def bfs(graph, start, end):
queue = [] # literally use queue (FIFO)
queue.append([start])
visited.add(start)
while queue:
node = queue.pop()
visited.add(node)
process(node)
nodes = generate_related_nodes(node)
queue.push(nodes)
## 双向BFS
front = set()
end = set()
while front:
next_front = set()
for element in front:
new_element = generate_new_nodes(element)
if new_element in end: # 前后相遇
return
next_front.add(new_element)
front = next_front
if len(end) < len(front):
front, end = end, front
Binary Search
- 前提:
- 目标函数单调性(有序)
- 存在上下界
- 能够通过索引访问
- 模版:
left, right = 0, len(array) - 1
while left <= right:
mid = (left + right) / 2
if array[mid] == target:
# find the target
break or return result
elif array[mid] < target:
left = mid + 1
else:
right = mid - 1
Dynamic Programming
基础
-
分治+最优子结构(在中间的步骤 只存最优的值,把次优的状态去掉)
-
动态规划和递归或分治没有根本上的区别(关键看有无最优的字结构)
- 共性:找到重复子问题
- 差异性:最优子结构、中途可以淘汰次优解
自顶向下:递归+memoization,符合思维习惯,记忆化搜索(记录已经算过的值)
自底向上(bottom up):循环
- 关键点:
-
最优子结构 opt[n] = best_of(opt[n-1], opt[n-2], …)
-
储存中间状态 opt[i]
-
递推公式(状态转移方程或者DP方程)
Fib: opt[i] = opt[i-1] + opt[i-2]
二维路径:opt[i,j] = opt[i+1, j] + opt[i, j+1] (且判断a[i,j]是否空地)
高级动态规划常见题型
爬楼梯
斐波那契数列、找零钱
f(n) = f(n-1) + f(n-2), f(1) = 1, f(0) = 0
时间复杂度O(n)
,内存可以优化为O(1)
不同路径
二维,从左上角走到右上角,可能有障碍物
f(i, j) = f(i - 1, j) + f(i, j - 1)
时间复杂度O(mn)
,空间复杂度O(mn)
打家劫舍
每隔一户才能偷
dp[i]状态的定义:max $ of robbing A[0 -> i]
dp[i] = max(dp[i - 2] + nums[i], dp[i-1])
二维解法:
dp[i][0]状态定义:max $ of robbing A[0 -> i] 且没偷nums[i]
dp[i][1]状态定义:max $ of robbing A[0 -> i] 且偷了nums[i]
dp[i][0] = max(dp[i-1][0], dp[i-1][1])
dp[i][1] = dp[i-1][0] + nums[i]
最小路径和
路径上有赋值
dp[i][j]状态的定义:minPath(A[1 -> i][1 -> j])
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + A[i][j]
股票买卖
根据题目分解成多个维度,持股可以sell/rest,未持股可以buy/rest
i:第几天,n为最大天数
k:第几次交易,K为最多大交易次数
[0,1]:有没有股票
总状态数:nK2
for 0 <= i < n:
for 1 <= k <= K:
for s in [0, 1]:
dp[i][k][s] = max(buy, sell, rest)
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
= max( 选择 rest, 选择sell )
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
= max( 选择 rest, 选择buy )
初始状态:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
String
- python和java的字符串是不可变的。增加或删减其实是新创建了string。
- java:
x == y
是比较他们的指针,不是字符串的内容。即使字符串的内容一样,指向的地址却是不同的。比较字符串要用x.equals(y)
结合动态规划的常见题型
- Longest common sequence(最长子序列)
if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- Longest common substring(最长子串)
if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
else: dp[i][j] = 0
- Edit distance(编辑距离)
if s1[i] == s2[j]: dp[i][j] = dp[i-1][j-1]
else: dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
- 最长回文串:
dp[i][j] = true/false (s[i:j] 是否为回文串)
dp[i][j] = dp[i+1][j-1] && (s[i] == s[j] || j - i <2)
字符串匹配算法
- 暴力法(brute force)O(mn)
- Rabin-Karp算法(用子串的hash value先比较一下来加速)
在朴素算法中,我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么,能否有别的方法可以用来判断目标字符串是否包含子串呢?为了避免挨个字符对目标字符串和子串进行比较,我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的hash function。通过它我们可以算出子串的hash value,然后将它和目标字符串中的子串的hash value进行比较。
算法思想:- 假设子串的长度为M (pat),目标字符串的长度为N (txt)
- 计算子串的hash value:hash_pat
- 计算目标字符串txt中每个长度为M的子串的hash value(共需要计算N-M+1次)
- 比较hash value:如果不同,必然不匹配;如果相同,还需要用朴素算法再比较
*要设计合适的O(1) hash function,如果调用系统的string hash 还是O(n)
- KMP算法(最大的前缀/后缀)
思想:当子串与目标字符串不匹配时,其实你已经知道了前面已经匹配成功那一部分的字符(包括子串和目标字符串),设法利用这个已知信息,不要把“搜索未知”移回已经比较过的位置,继续把它向后移,这样就提高了效率。
Trie
-
数据结构:又称单词查找树或键树,是一种树形结构。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。优点是:最大限度的减少无谓的字符串比较,查询效率比哈希表高。
-
核心思想:空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
-
基本性质:
-
结点本身不存完整单词
-
从根结点到某一结点,路径上经过的字符连接起来,为该节点对应的字符串
-
每个节点的所有子节点路径代表的字符都不相同
- 模版
class Trie(object):
def __init__(self):
self.root = {}
self.end_of_word = "#"
def insert(self, word):
node = self.root
for char in word:
node = node.setdefault(char, {})
node[self.end_of_word] = self.end_of_word
def search(self, word):
node = self.root
for char in word:
if char not in node:
return False
node = node[char]
return self.end_of_word in node
def startsWith(self, prefix):
node = self.root
for char in prefix:
if char not in node:
return False
node = node[char]
return True
# 简化版
trie = {} # 构造字典树
for word in words:
node = trie
for char in word:
node = node.setdefault(char, {})
node['#'] = True
Disjoint Set
-
适用场景:组团、配对问题
-
基本操作:
- makeSet(s):建立一个新的并查集,其中包含s个单元素集合。
- unionSet(x, y):把元素x和元素y所在的集合合并,要求x和y所在的集合不相交,如果相交则不合并。
- find(x):找到元素x所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。
-
模版:
def init(p):
# for i = 0 .. n: p[i] = i;
p = [i for i in range(n)]
def union(self, p, i, j):
p1 = self.parent(p, i)
p2 = self.parent(p, j)
p[p1] = p2
def parent(self, p, i):
root = i
while p[root] != root:
root = p[root]
while p[i] != i: # 路径压缩 ?
x = i; i = p[i]; p[x] = root
return root
## root的条件:p[i] == i
Bitwise Operation
位运算符
左移: <<
0011 -> 0110
右移: >>
0110 -> 0011
按位或: |
0011 | 1011 -> 1011
按位与:&
0011 & 1011 -> 0011
按位取返:~
0011 ->1100
按位异或(相同为零 不同为一):^
0011 ^ 1011 -> 1000
- XOR 异或:相同为0,不同为1。也可用“不进位加法”来理解。
异或操作的一些特点:
x ^ 0 = x
x ^ 1s = ~x // 注意1s = ~0,1s是指全部都是1
x ^ (~x) = 1s
x ^ x = 0
c = a ^ b -> a ^ c = b, b ^ c = a // 交换两个数
a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c // associative
- 指定位置的位运算 :
将x最右边的n位清零:x & (~0 << n)
获取x的第n位值(0或者1):(x >> n) & 1
获取x的第n位的幂值:x & (1 << (n-1))
仅将第n位置为1:x | (1 << n)
仅将第n位置为0:x & (~(1 << n))
将x最高位至第n位(含)清零:x & ((1 << n) - 1)
将第n位至第0位(含)清零:x & (~((1 << (n + 1)) - 1))
实战位运算要点
判断奇偶:
x % 2 == 1 -> (x & 1) == 1
x % 2 == 0 -> (x & 1) == 0
x >> 1 -> x/2
即: x = x / 2 -> x = x >> 1
mid = (left + right) / 2 -> mid = (left + right) >> 1
x = x & (x - 1) 清零最低位的1
x & -x 得到最低位的1 (保留了最右边的 1,并将其他位设置为 0)
x & ~x -> 0
位运算的应用:步隆过滤器 bloom filter
一个很长的二进制向量和一系列随机映射函数。步隆过滤器可以用于检索一个元素是否在一个集合中。优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
- 案例:
- 比特币网络
- 分布式系统(Map-Reduce)Hadoop、search engine
- Redis缓存
- 垃圾邮件、评论等的过滤
- 代码:
from bitarray import bitarray
import mmh3
class BloomFilter:
def __init__(self, size, hash_num):
self.size = size
self.hash_num = hash_num
self.bit_array = bitarray(size)
self.bit_array.setall(0)
def add(self, s):
for seed in range(self.hash_num):
result = mmh3.hash(s, seed) % self.size
self.bit_array[result] = 1
def lookup(self, s):
for seed in range(self.hash_num):
result = mmh3.hash(s, seed) % self.size
if self.bit_array[result] == 0:
return "Nope"
return "Probably"
bf = BloomFilter(500000, 7)
bf.add("dantezhao")
print (bf.lookup("dantezhao"))
print (bf.lookup("yyj"))
LRU Cache
- 两个要素:大小、替换策略
Hash Table + Double LinkedList
O(1)查询,O(1)修改、更新 - least recently used
- 代码:
class LRUCache(object):
def __init__(self, capacity):
self.dic = collections.OrderedDict()
self.remain = capacity
def get(self, key):
if key not in self.dic:
return -1
v = self.dic.pop(key)
self.dic[key] = v # key as the newest one
return v
def put(self, key, value):
if key in self.dic:
self.dic.pop(key)
else:
if self.remain > 0:
self.remain -= 1
else: # self.dic is full
self.dic.popitem(last=False)
self.dic[key] = value
Sort
初级排序 O(n^2)
- 选择排序(Selection Sort)
每次找最小值,然后放到待排序数组的起始位置。 - 插入排序(Insertion Sort)
从前到后逐步构建有序序列;对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 - 冒泡排序(Bubble Sort)
嵌套循环,每次查看相邻的元素,如果逆序则交换。
高级排序 O(n*logn)
快速排序(Quick Sort)
数组取标杆pivot,将小元素放pivot左边,大元素放右侧,然后依次对右边和右边的子数组继续快拍 以达到整个序列有序。
public static void quickSort(int[] array, int begin, int end) {
if (end <= begin) return;
int pivot = partition(array, begin, end);
quickSort(array, begin, pivot - 1);
quickSort(array, pivot + 1, end);
}
static int partition(int[] a, int begin, int end) {
// pivot: 标杆位置,counter: 小于pivot的元素的个数
int pivot = end, counter = begin;
for (int i = begin; i < end; i++) {
if (a[i] < a[pivot]) {
int temp = a[counter]; a[counter] = a[i]; a[i] = temp;
counter++;
}
}
int temp = a[pivot]; a[pivot] = a[counter]; a[counter] = temp;
return counter;
}
归并排序(Merge Sort)
1.把长度为n的输入序列分成两个长度为n/2的子序列;2.对这两个子序列分别采用归并排序;3.将两个排序好的子序列合并成一个最终的排序序列。
归并和快排具有相似性,但步骤顺序相反。归并:先排序左右子数组,然后合并两个有序子数组。快排:先调配出左右子数组,然后对于左右子数组进行排序
public static void mergeSort(int[] array, int left, int right) {
if (right <= left) return;
int mid = (left + right) >> 1; // (left + right) / 2
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);
merge(array, left, mid, right);
}
public static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1]; // 中间数组
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
for (int p = 0; p < temp.length; p++) {
arr[left + p] = temp[p];
}
// 也可以用 System.arraycopy(a, start1, b, start2, length)
}
堆排序(heap sort)
堆插入/删除 O(logN),取最大/小值 O(1)
1.数组元素依次建立小顶堆。2.依次取堆顶元素并删除
特殊排序 O(n)
- 计数排序(Counting Sort)
计数排序要求输入的数据必须是有确定范围的整数。将输入的数据值转化成键存储在额外开辟的数组空间中;然后依次把计数大于1的填充回原数组 - 桶排序(Bucket Sort)
桶排序的工作原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。 - 基数排序(Redix Sort)
基数排序是按照低位先排序,然后收集;再按高位排序,然后再收集;以此类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。