算法与数据结构&LeetCode
概述
主要结合 LeetCode ,对算法与数据结构进行总结和分析。
核心:
- 重在理解,避免死磕
- 重在练习,五遍刷题
- 机器思维,避免人肉
复杂度分析
时间复杂度
- 如何理解算法时间复杂度表示法
- 主定理
- O(1) 常数复杂度:Hash表,缓存
- O(log n) 对数复杂度:二分查找,二叉搜索树
- O(n) 线性复杂度:大多数遍历
- O(n^2): 双重for循环
- O(2^n):递归
空间复杂度
- 空间换时间,升维:在大多数情况下,会选择牺牲空间复杂度,来提高时间复杂度
- 一般在算法中,更多的是考虑是否为“原地算法”,即是否需要申请额外的空间。
数组与链表
非受限线性表:指的就是不受规则限制,区别于栈和队列。
- 数组和链表是所有数据结构的基础,基本上所有数据结构都可以基于数组和链表实现。
- 数组和链表之间重点掌握区别,数组的查找时间复杂度是O(1),而链表的增加和删除时间复杂度为O(1)。
- 数组:注意防止越界
- 链表:单链表、双向链表、循环链表、静态链表(借助数组,伴随向后继节点的指针)
LeetCode
Array
- 盛最多水的容器 – 左右双指针夹逼
- 移动零 – 快慢指针
- 两数之和 – 哈希表记录
- 三数之和 – 排序,左右双指针夹逼/哈希表记录
- 删除排序数组中的重复项 – 快慢指针
- 合并两个有序数组 – 额外空间+双指针,同于归并排序中的merge
- 加一
Linked
- 反转链表
- 两两交换链表中的节点
- 环形链表 – 缓存
- 合并两个有序链表
栈和队列
受限线性表:需要满足一定的规则,即先进后出或者先进先出等。
- 栈和队列的重点是掌握其在特殊场合下的应用。
- 栈:先进后出,FILO。应用:浏览器前进后退、括号匹配、表达式计算。
- 队列:先进先出,FIFO。
- 队列的拓展:双端队列(两边均可入队和出队),优先队列(根据优先级来出队)。
- Java PriorityQueue 源码
LeetCode
Stack
Queue
哈希表、映射、集合
- 哈希表:又称散列表。重点需要注意哈希冲突,设计好的哈希函数可以减少重复性,但不能完全避免哈希冲突。常用拉链表(散列表+链表)解决哈希冲突,如HashMap。
- 映射:Map。K/V键值对,key不重复
- 集合:Set。key不重复
LeetCode
- 有效的字母异位词
- 字母异位词分组
- 两数之和
二叉树、二叉搜索树
- 顺序和链式结构都可以实现。
- 遍历方式:广度优先遍历(BFS)、深度优先遍历(DFS);DFS分为前序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根)。重点掌握所有的模板。
- 分类:完全二叉树(除了叶子节点外,其余节点的度均为2的二叉树),满二叉树(最后一层全是叶子节点的完全二叉树),二叉搜索树等。
- 二叉搜索树的中序遍历即为有序遍历。
- 自平衡二叉搜索树(AVL)的动图演示
LeetCode
- 二叉树的前序遍历
- 二叉树的中序遍历
- N叉树的前序遍历
- N叉树的后序遍历
- N叉树的层序遍历 – 广度优先搜索
递归
- 递归模板
- 核心:递归四部曲,终止条件、执行当前层逻辑、下坠、清理当前层的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void recur(int level, int param) {
// terminator
if (level > MAX_LEVEL) {
// process result
return;
}
// process current logic
process(level, param);
// drill down
recur( level: level + 1, newParam);
// restore current status
}
LeetCode
- 爬楼梯问题 – 记忆化递归
- 括号生成
- 翻转二叉树
- 验证二叉搜索树
- 二叉树的最大深度、二叉树的最小深度
- 二叉树的序列化和反序列化
- 二叉树的最近公共祖先
- 从前序和中序遍历序列构造二叉树
- 组合
- 全排列、全排列 II
分治、回溯
1 | def divide_conquer(problem, param1, param2, ...): |
LeetCode
BFS、DFS
核心:维护一个queue队列,建立一个访问记录visited集合,有时候还需要在节点处记录当前的层数level。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def BFS(graph, start, end):
visited = set()
queue = []
queue.append([start])
while queue:
node = queue.pop()
visited.add(node)
process(node)
nodes = generate_related_nodes(node)
queue.push(nodes)
# other processing work
...核心:DFS在非递归写法时,是维护一个stack栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34//递归写法
visited = set()
def dfs(node, visited):
if node in visited: # terminator
# already visited
return
visited.add(node)
# process current node here.
...
for next_node in node.children():
if next_node not in visited:
dfs(next_node, visited)
//非递归写法
def DFS(self, tree):
if tree.root is None:
return []
visited, stack = [], [tree.root]
while stack:
node = stack.pop()
visited.add(node)
process (node)
nodes = generate_related_nodes(node)
stack.push(nodes)
# other processing work
...
LeetCode
贪心算法
- 贪心算法是弱化版的动态规划。很多时候都得先判断是否能用贪心算法解决,再使用贪心算法。如最经典的
找零问题,在保证大的币种肯定是小的币种的倍数的情况下,可以利用贪心算法解决问题。LeetCode
柠檬水找零
二分查找
- 特点:有序、有界、能够通过索引随机访问。
- 二分查找模板
1
2
3
4
5
6
7
8
9
10left, 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
注意,一般防止越界,第三行代码会改写为
mid = right + (left - right) >> 1
LeetCode
动态规划
动态规划在大多数情况下和记忆化递归的时间复杂度相近,如经典的爬楼梯问题,既可以使用斐波那契数列,
f(n) = f(n-1) + f(n-2),加上缓存cache的记忆化递归的方式求解,也可以使用动态规划,利用dp方程,dp[i] = dp[i-1] + dp[i-2]进行求解。
- 自底向上递推
- 核心:状态转移方程、最近重复子问题、最优子结构
LeetCode
- 不同路径
- 不同路径 II
- 最长公共子序列
- 三角形最短路径和
- 最大子序和
- 乘积最大子序列
- 硬币兑换
- 打家抢劫、打家抢劫 II
- 买买股票的最佳时机、股票 II、股票 III、股票(含冷冻期)、股票 IV、股票(含手续费) – 团灭股票问题方法
字典树
- 字典树,又称前缀树、Trie树。主要应用于搜索时前缀单词提示。
- 核心:理解和记忆Trie树的代码模板
- Trie树模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class 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
LeetCode
并查集
- 并查集,主要应用于解决站队问题,即朋友圈问题。
- 核心:熟悉并查集的基本操作,即初始化、查询、合并。
- 并查集模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class UnionFind {
private int count = 0;
private int[] parent;
public UnionFind(int n) {
count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int p) {
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
parent[rootP] = rootQ;
count--;
}
}
LeetCode
位运算
- 核心:记忆常见的位运算公式
- N皇后问题的位运算代码解法
常见的位运算公式
- 判断奇偶
| 含义 | 常规 | 位运算 |
|---|---|---|
| 判断偶数 | x % 2 == 0 | (x & 1) == 0 |
| 判断基数 | x % 2 == 1 | (x & 1) == 1 |
- 除以2
| 常规 | 位运算 |
|---|---|
| x / 2 | x >> 1 |
| int mid = (left + right)/2 | int mid = (left + right) >> 1 |
清零最低位的1(即:从第0位开始往前,第1个1置为0)
1
x = x & (x - 1)
获取最低位的1(即:从第0位开始往前,获取第1个1)
1
x & (-x)
取0
1
x & (~x)
LeetCode
布隆过滤器
- 核心:判断不存在百分之百正确,判断存在不一定正确
- 应用:布隆过滤器一般用于做最外层的过滤缓存
- 实现:利用Hash函数将待判断Key对应到多个位上(由于可能存在Key值对应的位重叠,所以判断存在不一定正确)
- 布隆过滤器的原理和实现
- 使用布隆过滤器解决缓存击穿、垃圾邮件识别、集合判重
- 代码实现:
LRU缓存
- 核心:散列表+双向链表、get和set都是O(1)的时间复杂度
- 关键词:最近最少使用
- 用简单的话理解开发容灾
- 替换算法总揽
- LRU Cache python 代码示例
- 基于JAVA的LinkedHashMap(本质即散列表+链表)实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
LeetCode
排序
- 核心:熟悉含义,了解代码
- (平均)时间复杂度:
- O(n^2):冒泡排序、插入排序、选择排序
- O(nlogn):快速排序、归并排序
- O(n):桶排序、基数排序、计数排序
