本文的很多内容参考leetcode题解。
1.要想做题做得快,需要:
- 快速明白题目考查点。因此需要熟悉常用的数据结构和算法的适用场景
- 背模板并套用模版
算法
二分查找
涉及题号:209. 长度最小的子数组
在顺序数组nums中找满足条件(如大于target)的第一个数,可以用二分查找。
left = 0
right = n-1 # n为顺序数组长度
res = n
while left<=right:
mid = (left+right)>>1 # 二分
if nums[mid]>target: # 1.筛选条件,这里是为了找大于target的数
res = mid
right = mid-1 # 2.在[left, 新right=mid-1]之间是否有大于target的数
else:
left = mid+1 # 3.在[新left=mid+1, right]之间是否有大于target的数
return res # 记录大于target的第一个数
1.筛选条件可以根据实际情况修改,根据筛选条件的不同,2和3也要相应进行修改。
2.如果不是专门的二分查找题目或者面试官要求必须实现二分查找函数,而是希望在顺序数组中找到大于等于某个数的第一个数,则可以用现成的库函数实现,如c++的lower_bound, Java的Arrays.binarySearch, C#的Arrary.BinarySearch, Python的bisect.bisect_left。
滑动窗口
涉及题号:209. 长度最小的子数组
模板(以求和为例)
n = len(nums)
left, right = 0, 0
while(right < n): # 也可以改成for循环
total += nums[right]
while(total > target): # 移动左端点,每次移动,total都对应[left, right]区间内的和。
total -= nums[left]
left += 1
res = max(res, right-left+1) # res的赋值语句位置不固定,可能再while里面,也可能在while外面,根据具体题意而定。
right += 1
return res
感悟
1.滑动窗口一定是枚举右端点,移动左端点。因为移动左端点,先减去去左端点的数,左端点再自增1,对应的和就是[left, right]区间内的和。
BFS(广度优先搜索)
BFS模版1-不需要确定遍历层数
queue = collections.deque(最先入队节点)
while queue 不空:
cur = queue.popleft()
visit.add(cur) # 当前节点已访问
for 节点 in cur的所有相邻节点:
if 该节点有效且未访问过
queue.append(该节点)
BFS模版2-需要确定遍历层数
level = 0
queue = collections.deque(最先入队节点)
while queue 不空:
size = queue.size() # 记录当前层的元素数
while (size --):
cur = queue.pop()
visit.add(cur) # 当前节点已访问
for 节点 in cur的所有相邻节点:
if 该节点有效且未被访问过:
queue.append(该节点)
level ++;
感悟
1.有时候在遍历相邻节点的时候,未防止重复添加节点,在遍历阶段就将节点记录为已访问状态。
2.上诉模板是利用广度优先搜索遍历一个连通分支的节点。如果要对整个数据集利用广度优先搜索进行遍历,则需要在外面再套一层for循环遍历整个数据集。
DFS(深度优先搜索)
DFS模板
def dfs(cur):
visit.add(cur) # 当前节点已访问
for 节点 in cur的所有相邻节点:
if 该节点有效且未被访问过:
dfs(该节点)
dfs(初始节点)
感悟
1.DFS是在尝试一种情况到底后才尝试其他情况,而BFS是同时尝试多种情况。如果只是要找一个结果存在,那么DFS更优。如果要找所有可能中的最优结果,那么BFS更优。参考题目:1091.二进制矩阵中的最短路径
回溯算法
模板
def backtracking(path, choice):
if 终止条件:
res.append(path)# 存放结果
return
for i in choice:
处理节点 # 如 path.append(i)
backtracking(path,choice) # 递归
回溯,撤销处理结果 # 如temp.pop()
动态规划
涉及题号:leetcode70.爬楼梯
动态规划五部曲:
- 确定dp数组及其下标含义
- 确定状态转移方程
- 初始化状态
- 遍历顺序
- 返回值
模板
初始化状态
for 遍历顺序:
状态转移方程
return 返回值
感悟
如果一个动态规划问题,第n位的状态只与n-1位和n-2位有关,则不需要构建整个dp数组,而可用[滚动数组的思想]将空间复杂度优化为O(1)。
矩阵快速幂算法
涉及题号:leetcode70.爬楼梯
什么时候使用矩阵快速幂
- 如果一个问题可与转化为求解一个矩阵的 n 次方的形式,那么可以用快速幂来加速计算。
- 如果一个递归式为形如 f ( n ) = ∑ i = 1 m a i f ( n − i ) f(n) = \sum_{i = 1}^{m} a_i f(n - i) f(n)=∑i=1maif(n−i)的齐次线性递推式,我们就可以把数列的递推关系转化为矩阵的递推关系,然后转化为求解一个矩阵的 n 次方的形式。
- 对一个非齐次线性递推式,如果可以把非齐次线性递推转化为齐次线性递推,则最终可以转化为求解一个矩阵的 n 次方的形式。
模板
def pow(a, n):
mat # 单位矩阵
while n:
if n&1==1: # 当前位为1
mat = multiply(mat, a) # 矩阵乘法
n = n>>1
a = multiply(a, a)
return mat
时间复杂度: O ( l o g n ) O(logn) O(logn)
并查集
涉及题号:200.岛屿数量
1.并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
2.它通常包含两种操作:
- 查找(Find):查询两个元素是否在同一个集合中
- 合并(Union):把两个不相交的集合合并为一个集合。
模板
# 构造并查集类
class UnionFind(object):
def __init__(self, grid):
m, n = len(grid), len(grid[0])
self.count = 0 # 记录连通分支数
self.parent = [0]*(m*n) # 记录并查集的父节点
self.rank = [0]*(m*n) # 记录并查集的秩
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
self.parent[n*i+j] = n*i+j
self.count += 1
def find(self, x): # 找节点x的根节点
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路经压缩,将父节点设置为根节点,减少后续查找次数
return self.parent[x]
def union(self, x, y): # 合并节点x和节点y
rootx = self.find(x)
rooty = self.find(y)
if rootx != rooty: # x和y的根结点不同,将y的根结点合并入x的根结点
if self.rank[rootx] < self.rank[rooty]: # 保证x的根结点的rank值更大
rootx, rooty = rooty, rootx
self.parent[rooty] = rootx
self.count -= 1
if self.rank[rootx] == self.rank[rooty]:
self.rank[rootx] += 1
def getCount(self): # 获得连通分支数
return self.count
KMP算法
涉及题号:572.另一棵树的子树
KMP算法是一种改进的字符串匹配算法。其核心是利用匹配失败后的信息,尽量减少模式串和主串的匹配次数以达到快速匹配的 目的。具体实现是通过一个next数组实现,数组包含了模式串的局部匹配信息。KMP算法的时间复杂度为O(m+n)。
def buildNext(patt): # 构建记录最长公共前后缀长度的数组next
next = [0] # 初始化next数组,第一个字符没有公共前缀,所以最长公共前后缀长度为0
prefixLen = 0 # 记录当前的到上一个字符结尾的最长公共前后缀长度
i = 1
while i<len(patt):
if patt[prefixLen] == patt[i]: # 上一个最长公共前缀的后一个字符与上一个最长公共后缀的后一个字符相同
prefixLen += 1
next.append(prefixLen)
i += 1
elif prefixLen == 0: # 不相同,且prefixLen=0,数组值直接置为0且且i自增
next.append(0)
i += 1
else: # 不相同,但prefixLen不为0,此时从最长公共前缀中找到更小的最长公共前后缀长度
prefixLen = next[prefixLen-1]
return next
def kmpSearch(string, patt): # kmp算法匹配主串和模式串
next = buildNext(patt)
i, j = 0, 0 # 主串和模式串的指针,指向待匹配的字符
while i<len(string):
if string[i] == patt[j]: # 待匹配字符相同,指针均后移
i += 1
j += 1
elif j == 0: # 待匹配字符不相同,且模式串已经指向第一个字符,主串后移
i += 1
else: # 待匹配字符不相同,但模式串并非指向第一个字符,模式串指针回退到上一个字符的最大公共前缀的后一个位置
j = next[j-1]
if j == len(patt): # 每进行一次匹配就对j做一次判断,如果j等于模式串长度,则说明匹配成功
return True # 主串中匹配的模式串的起始下标为i-j
return False
欧拉筛法(求解小于等于n的所有素数)
涉及题号:0572. 另一棵树的子树
参考资料:欧拉筛法详解
def get_prime(): # 获得小于等于n的所有素数
n = 1005 # 筛选出<=n的素数
cnt = 0 # 已经筛选出的素数个数
vis = [0]*n # vis[i]表示i是否是素数,0表示是素数,1表示不是素数,初始化全为0
prime = [0] # 已经筛选出的素数表
vis[0], vis[1] = 1, 1 # 0和1都不是素数
for i in range(2, n):
if vis[i]==0: # 如果i没有被前面的素数筛掉,则i是素数
cnt += 1
prime.append(i)
for j in range(1, cnt+1): # 遍历prime表中的已有素数
if i*prime[j]<n: # 因为2一定是最小素因数,所以先筛2的倍数
vis[i*prime[j]] = 1 # 筛掉i**prime[j]为合数
if i % prime[j] == 0: # 保证一个合数一定是因为他的最小素因数筛掉,避免重复
break
return prime
A*寻路算法
涉及题目:A*寻路算法视频介绍
参考资料:Introduction to the A* Algorithm
def a_star_search(graph, start, goal):
frontier = PriorityQueue() # 优先队列
frontier.put(0, start) # 优先队列根据第一个值排序
came_from = {} # 用于最后查看路径
cost_so_far = {} # 记录目前已经探索的情况下,与起点之间的路径长
cost_so_far[start] = 0 # 初始化
while frontier: # 仍有边界未访问
current = frontier.get()[1] # 优先队列的最优点出列
if current == goal: # 找到目标点
break
for next in graph.neighbors(current): # 当前节点的相邻节点
new_cost = cost_so_far[current] + graph.cost(current, next) # 计算当前节点到相邻节点的距离
if next not in cost_so_far or new_cost < cost_so_far[next]: # 如果相邻节点没有被访问过或者存在当前节点到next的距离比next离起点的历史距离更近
cost_so_far[next] = new_cost # 更新历史距离
priority = new_cost + heuristic(goal, next) # 根据评估函数F=G+H, 计算优先度
frontier.put((priority, next)) # next入队
came_from[next] = current # 记录next节点的父节点,方便回溯
return came_from, cost_so_far
感悟
1.虽然A算法是启发式算法,如果起点到目标点之间不存在路径,则使用A寻路算法会比深度优先搜索的时间复杂度更高。
数据结构
链表
- 要舍得用变量,节省变量的后果就是容易被指针的指向绕晕。
- 头节点可能会改动时,先增加一个哨兵节点或者哑节点dummy,返回时直接返回dummy.next即可。
二进制位
常用位运算
- x & 1 = = 1 x\&1==1 x&1==1 等价于 x % 2 = = 1 x\%2==1 x%2==1,即 x x x为奇数
- x & 1 = = 0 x\&1==0 x&1==0 等价于 x % 2 = = 0 x\%2==0 x%2==0,即 x x x为偶数
- x > > 1 = = 1 x>>1==1 x>>1==1 等价于 x / 2 x/2 x/2,即 x x x除2的商
- x & ( x − 1 ) x\&(x-1) x&(x−1),把最低位的1去掉
- x & ( − x ) x\&(-x) x&(−x),得到最低位的1
- x & ∼ x x\&\sim x x&∼x,得到0
指定位置的位运算
- 将 x x x的最右边的 n n n位清零: x & ( ∼ 0 < < n ) x\&(\sim0<<n) x&(∼0<<n)
- 获得 x x x的第 n n n位值: ( x > > n ) & 1 (x>>n)\&1 (x>>n)&1
- 获得 x x x的第 n n n位的幂值: x & ( 1 < < n ) x\&(1<<n) x&(1<<n)
- 仅将第 n n n位置为1: x ∣ ( 1 < < n ) x|(1<<n) x∣(1<<n)
- 仅将第 n n n位置为0: x & ( ∼ ( 1 < < n ) ) x\&(\sim(1<<n)) x&(∼(1<<n))
- 将 x x x最高位至第 n n n位(含)清零: x & ( ( 1 < < n ) − 1 ) x\&((1<<n)-1) x&((1<<n)−1)
- 将第 n n n位至第 0 0 0位(含)清零: x & ∼ ( ( 1 < < ( n + 1 ) ) − 1 ) x\&\sim((1<<(n+1))-1) x&∼((1<<(n+1))−1)
大小字母位运算技巧
- 大写变小写、小写变大写:字符 ^= 32 (大写 ^= 32 相当于 +32,小写 ^= 32 相当于 -32)
- 大写变小写、小写变小写:字符 |= 32 (大写 |= 32 就相当于+32,小写 |= 32 不变)
- 大写变大写、小写变大写:字符 &= -33 (大写 ^= -33 不变,小写 ^= -33 相当于 -32)
异或运算
1.0和a做异或运算得到a;a和a做异或运算得到0;异或运算满足交换律和结合律。
Python感悟
1.python中的str类型是不可变对象。想要改变python字符串中的某个字符,只能对字符串重新赋值。