前言
算法题目一般分为以下几类,常见的用红色背景标出:
算法思想:贪心、动态规划
、分治、递归
。 排序和搜索:排序
、二分查找
、DFS
、BFS
、回溯
数据结构:字符串
、数组/矩阵
、栈
、队列、哈希表
、链表
、二叉树
、图、并查集 数学:位运算
、概率计算、数字处理
、经典算法 特殊解法:滑动窗口
、双指针
设计:LRU、LFU、满足O(1)的数据结构组合
参考
https://github.com/labuladong/fucking-algorithm 这个项目详细介绍了LeetCode题型和解题模板https://github.com/liguigui/CyC2018-CS-Notes 这个项目的算法部分总结了题型和代表性题目《剑指Offer:名企面试官精讲典型编程题(第2版)》 这本书不仅介绍了各种题型、数据结构,而且从题目理解、沟通交流、思路产生、代码编写、算法优化
等方面全方位地告诉读者如何参与一场算法面试。
更多参考文献、书籍、网址、博客写在文中对应部分
一. 排序
1. 快速排序
private void quickSort ( int [ ] nums, int left, int right) {
swap ( nums, left, left+ rand. nextInt ( right- left+ 1 ) ) ;
if ( left>= right) return ;
int p = left;
for ( int i = left+ 1 ; i <= right ; i++ ) {
if ( nums[ i] <= nums[ left] ) {
swap ( nums, ++ p, i) ;
}
}
swap ( nums, left, p) ;
quickSort ( nums, left, p- 1 ) ;
quickSort ( nums, p+ 1 , right) ;
}
2 归并排序
private void mergeSort ( int [ ] nums, int left, int right, int [ ] temp) {
if ( left>= right) return ;
int mid = left + ( right- left) / 2 ;
mergeSort ( nums, left, mid, temp) ;
mergeSort ( nums, mid+ 1 , right, temp) ;
if ( nums[ mid] <= nums[ mid+ 1 ] ) return ;
int i = left, j = mid+ 1 , k = 0 ;
while ( i<= mid && j<= right) temp[ k++ ] = nums[ i] < nums[ j] ? nums[ i++ ] : nums[ j++ ] ;
while ( i<= mid) temp[ k++ ] = nums[ i++ ] ;
while ( j<= right) temp[ k++ ] = nums[ j++ ] ;
System . arraycopy ( temp, 0 , nums, left, right- left+ 1 ) ;
}
3 堆排序
private void heapSort ( int [ ] nums) {
for ( int i = nums. length/ 2 - 1 ; i>= 0 ; i-- ) {
adjustHeap ( nums, i, nums. length- 1 ) ;
}
int end = nums. length- 1 ;
while ( end> 0 ) {
swap ( nums, 0 , end) ;
end-- ;
adjustHeap ( nums, 0 , end) ;
}
}
private void adjustHeap ( int [ ] nums, int i, int end) {
int k = i;
while ( k* 2 + 1 <= end) {
int j = 2 * k + 1 ;
if ( j+ 1 <= end && nums[ j+ 1 ] > nums[ j] ) {
j++ ;
}
if ( nums[ k] < nums[ j] ) {
swap ( nums, k, j) ;
k = j;
} else {
break ;
}
}
}
4 其他排序
名称 时间复杂度 解释 冒泡排序 O(n^2) 遍历n-1次数组,每次遍历就把最大的放在末尾,末尾元素不会进入下一次遍历。可以添加isSorted变量,如果在一次遍历中没有交换元素,就代表有序了,不用继续向下遍历。 选择排序 O(n^2) 每次选择最大的元素,然后跟末尾元素交换即可,末尾元素不会进入下一次遍历。 插入排序 O(n^2) 从i=1开始,依次将元素插入到前面的有序数组之中。 希尔排序 O(n^2) 对插入排序的优化,对数据按间隔分组,组内不断进行插入排序,然后逐渐缩小间隔到1 桶排序 O(n) 设置若干个桶,使用一个映射函数将所有元素放入固定大小的桶中,然后在桶内部可以使用任意排序方法对所有元素进行排序,最后依次从全部桶中拿出所有元素即可。两种主要的桶排序方法是计数排序和基数排序。 计数排序
O(n) 使用辅助数组记录每个元素出现的次数,然后复原即可,要求待排序数组的值在一个适当的范围 基数排序 O(n) 根据元素的数位来分配桶,例如0到9十个桶代表元素的十位数字,那么(0至9)分配到桶0,(10至19)分配到桶1,…
5. 第K大/小问题(快速选择算法)
求第K大
是快速排序算法的应用,称为快速选择算法
,不需要完全排序,只需要在某一次安置pivot时,pivot的索引刚好等于k即可,如果不等于k,只需要对其中一个区间继续搜索。
private int findKthLargest ( int [ ] nums , int left, int right, int k) {
swap ( nums, left, left+ rand. nextInt ( right- left+ 1 ) ) ;
if ( left>= right) return nums[ left] ;
int p = left;
for ( int i = left+ 1 ; i<= right; i++ ) {
if ( nums[ i] < nums[ left] ) {
swap ( nums, i, ++ p) ;
}
}
swap ( nums, p, left) ;
if ( p == nums. length- k) {
return nums[ p] ;
} else if ( p > nums. length- k) {
return findKthLargest ( nums, left, p- 1 , k) ;
} else {
return findKthLargest ( nums, p+ 1 , right, k) ;
}
}
6. 有序数据合并问题
二、二分查找
1. 基本二分查找
public int search ( int [ ] nums, int target) {
int left= 0 ;
int right = nums. length - 1 ;
while ( left <= right) {
int mid = left + ( right - left) / 2 ;
if ( nums[ mid] == target) {
return mid;
} else if ( nums[ mid] > target) {
right = mid - 1 ;
} else {
left = mid + 1 ;
}
}
return - 1 ;
}
2. 二分查找左边界
public intsearch ( int [ ] nums, int target) {
int left = 0 ;
int right = nums. length - 1 ;
while ( left <= right) {
int mid = left + ( right- left) / 2 ;
if ( nums[ mid] >= target) {
right = mid - 1 ;
} else {
left = mid + 1 ;
}
}
if ( left == nums. length || nums[ left] != target) {
return - 1 ;
}
return left;
}
3. 二分查找右边界
public int search ( int [ ] nums, int target) {
int left = 0 ;
int right = nums. length - 1 ;
while ( left <= right) {
int mid = left + ( right- left) / 2 ;
if ( nums[ mid] <= target) {
left = mid + 1 ;
} else {
right = mid - 1 ;
}
}
if ( right == - 1 || nums[ right] != target) {
return - 1 ;
}
return right;
}
4. 相关题目
三、搜索
搜索主要包括广度优先搜索
和深度优先搜索
,回溯属于深度优先搜索。搜索很多时候相当于是暴力的解法,遍历所有可能性,因此解题模式比较套路化。
1. DFS
可以定义全局变量如count, list, hashmap等,表示每一步做出的修改,比如添加一个结果,或者计数加1 等。。。
dfs ( . . . ) ;
返回值 dfs ( 参数) {
if ( 达到边界条件) return ;
if ( 搜索到一个符合条件的结果) {
添加结果到集合、更新共享变量、向上一步返回一些信息等。。。
return ;
}
没有搜索到,就继续向下搜索,先获取或计算可以做出的选择(能够传递给下一层的参数)
for ( 选择 in 选择列表) {
dfs ( 参数、选择. . . )
}
}
2. 回溯
和DFS基本类似,主要多了一个恢复状态的过程
result = [ ]
def backtrack ( 路径, 选择列表) :
if 满足结束条件:
result. add( 路径)
return
for 选择 in 选择列表:
做选择
backtrack( 路径,选择列表)
撤销选择
3. BFS
BFS需要结合队列使用
void bfs ( Node start, Node target) {
初始化队列q
队列中添加初始1 个或多个值 q. offer ( ) . . .
while ( 队列不为空) {
获取队列大小size
for ( int i = 0 ; i< size; i++ ) {
Node cur = q. poll ( ) ;
弹出每个节点,对每个节点进行处理
然后将每个节点相邻的节点继续加入队列
}
}
}
4. 排列组合问题
5. 矩阵搜索问题
6. 其他DFS/BFS问题
四、树与递归
把二叉树的遍历方式弄清楚,大部分二叉树问题就很好解决了
1. 二叉树的前中后序遍历(迭代法)
public List < Integer > preorderTraversal ( TreeNode root) {
Stack < TreeNode > stack = new Stack < > ( ) ;
List < Integer > result = new ArrayList ( ) ;
if ( root == null ) return result;
stack. push ( root) ;
while ( ! stack. isEmpty ( ) ) {
TreeNode node = stack. pop ( ) ;
result. add ( node. val) ;
if ( node. right!= null ) stack. push ( node. right) ;
if ( node. left!= null ) stack. push ( node. left) ;
}
return result;
}
private void iter ( TreeNode root) {
if ( root == null ) return ;
Stack < TreeNode > stack = new Stack < > ( ) ;
TreeNode current = root;
while ( current!= null || ! stack. isEmpty ( ) ) {
while ( current!= null ) {
stack. push ( current) ;
current = current. left;
}
TreeNode node = stack. pop ( ) ;
result. add ( node. val) ;
current = node. right;
}
}
public List < Integer > postorderTraversal ( TreeNode root) {
List < Integer > result = new ArrayList < > ( ) ;
Stack < TreeNode > stack = new Stack < > ( ) ;
if ( root == null ) return result;
stack. push ( root) ;
TreeNode pre = null ;
while ( ! stack. isEmpty ( ) ) {
TreeNode cur = stack. peek ( ) ;
if ( ( cur. left == null && cur. right == null ) || ( pre!= null && ( pre == cur. left || pre == cur. right) ) ) {
result. add ( cur. val) ;
pre = stack. pop ( ) ;
} else {
if ( cur. right!= null ) stack. push ( cur. right) ;
if ( cur. left!= null ) stack. push ( cur. left) ;
}
}
return result;
}
2. 二叉树的层序遍历
LeetCode 102. 二叉树的层序遍历
public List < List < Integer > > levelOrder ( TreeNode root) {
List < List < Integer > > result = new ArrayList < > ( ) ;
if ( root == null ) return result;
Queue < TreeNode > queue = new LinkedList < > ( ) ;
queue. offer ( root) ;
while ( ! queue. isEmpty ( ) ) {
int size = queue. size ( ) ;
List < Integer > list = new ArrayList < > ( ) ;
for ( int i = 0 ; i < size ; i++ ) {
TreeNode node = queue. poll ( ) ;
list. add ( node. val) ;
if ( node. left!= null ) queue. offer ( node. left) ;
if ( node. right!= null ) queue. offer ( node. right) ;
}
result. add ( list) ;
}
return result;
}
3. 树的递归问题
一般解决树的问题都需要使用递归思想 解决递归问题需要明确3点:递归函数的含义、参数的含义、返回值的含义
3. 二叉搜索树
二叉搜索树一般会想到其中序遍历是有序的
4. Trie
五、其他数据结构
1. 链表
链表问题一般画图、举例子解决,关键点是确定指针的指向、作用、状态
2. 哈希表
哈希表的查找只需要O(1),因此很多时候可以用来以空间换时间 有时候数组也可以当做hash表使用
3. 栈
4. 优先级队列
在上面提到的第K大小问题中也可以使用PriorityQueue解决
求第K大,就建立容量为k的最小堆保存最大的k个值,堆顶元素是第k大 求第k小,就建立容量为k的最大堆保存最小的k个值,堆顶元素是第k小 这两个问题可以互相转化,如第K大,相当于第n-k+1小。
public int findKthLargest ( int [ ] nums, int k) {
PriorityQueue < Integer > queue = new PriorityQueue < > ( ) ;
for ( int num : nums) {
if ( queue. size ( ) < k) {
queue. offer ( num) ;
} else if ( num > queue. peek ( ) ) {
queue. poll ( ) ;
queue. offer ( num) ;
}
}
return queue. peek ( ) ;
}
5. 并查集
六、贪心思想
贪心思想需要每一步取得最优的结果,那么最后的结果就是最优的
七、动态规划
八、数学
1. 位运算
2. 概率计算
3. 字符串运算
4. 排列组合数
题目 备注 LeetCode 96. 不同的二叉搜索树 组合数。设G(n)表示n个节点组成BST的数目,那么G(n)即为所求。令F(i)表示以第i个节点作为根节点的BST数目,那么G(n) = F(1)+F(2)+…+F(n)。在F(i)处分为两部分,F(i) = G(i-1)*G(n-i)。综合:G(n)=G(0)*G(n-1)+G(1)*G(n-2)+…+G(n-1)*G(0)。G(0) = 1,G(1)=1 LeetCode 62. 不同路径 从m+n-2步中选m-1步或n-1步
5. 进制问题
6. 最大公约数
7. 摩尔投票法
8. 质数问题
9. 快速幂算法
LeetCode 50. Pow(x, n) 看n/2是奇数还是偶数,递归求解可以每次扩大为原来的一倍。
public double myPow ( double x, int n) {
if ( n == 0 ) return 1 ;
if ( n < 0 ) return 1.0 / quickPow ( x, - n) ;
return quickPow ( x, n) ;
}
public double quickPow ( double x, int n) {
if ( n == 0 ) return 1 ;
double a = quickPow ( x, n/ 2 ) ;
if ( ( n& 1 ) == 1 ) {
return a* a* x;
} else {
return a* a;
}
}
九、滑动窗口
十、设计
1. LRU缓存
LeetCode 146. LRU缓存机制
由于需要O(1)的查找因此需要哈希表{Integer -> Node},由于需要维护最久未使用的数据因此需要双向链表(头结点存最近访问的节点)。 添加虚拟头尾节点避免null判断 get()方法:如果key存在,就把该Node放置到头部; put()方法:如果key存在,就更新值;否则,新建节点,插入头部;如果超过容量,删除尾结点。
class LRUCache {
static class DLNode {
DLNode ( ) { }
DLNode ( int key, int value) {
this . key = key;
this . value = value;
}
int key;
int value;
DLNode next;
DLNode pre;
}
private int capacity = 0 ;
private DLNode head = new DLNode ( ) ;
private DLNode tail = new DLNode ( ) ;
private Map < Integer , DLNode > map = new HashMap < > ( ) ;
public LRUCache ( int capacity) {
if ( capacity< 1 ) throw new IllegalArgumentException ( ) ;
this . capacity = capacity;
head. next = tail;
tail. pre = head;
}
public int get ( int key) {
DLNode node = map. get ( key) ;
if ( node == null ) return - 1 ;
removeNode ( node) ;
insertHead ( node) ;
return node. value;
}
public void put ( int key, int value) {
if ( map. containsKey ( key) ) {
DLNode node = map. get ( key) ;
node. value = value;
removeNode ( node) ;
insertHead ( node) ;
} else {
DLNode node = new DLNode ( key, value) ;
map. put ( key, node) ;
insertHead ( node) ;
}
if ( map. size ( ) > capacity) {
map. remove ( tail. pre. key) ;
removeNode ( tail. pre) ;
}
}
private void removeNode ( DLNode node) {
node. pre. next = node. next;
node. next. pre = node. pre;
}
private void insertHead ( DLNode node) {
node. next = head. next;
head. next. pre = node;
head. next = node;
node. pre = head;
}
}
2. LFU缓存
LeetCode 460. LFU缓存
由于需要O(1)查找,因此使用Hash表(key -> Node),由于需要维护频率(增加频率,最小频率),因此需要在O(1)的时间找到频率最小的Node,考虑使用另一个hash表(频率 -> 链表),Node中存key,value,频率。
class LFUCache {
static class DLNode {
int key = 0 ;
int value = 0 ;
int freq = 1 ;
DLNode ( int key, int value) {
this . key = key;
this . value = value;
}
DLNode ( ) { }
}
private int capacity = 0 ;
private Map < Integer , LinkedList < DLNode > > freqMap = new HashMap < > ( ) ;
private Map < Integer , DLNode > keyMap = new HashMap < > ( ) ;
private int minFreq = 0 ;
public LFUCache ( int capacity) {
this . capacity = capacity;
}
public int get ( int key) {
if ( capacity == 0 ) return - 1 ;
DLNode node = keyMap. get ( key) ;
if ( node == null ) return - 1 ;
incrFreq ( node) ;
return node. value;
}
public void put ( int key, int value) {
if ( capacity == 0 ) return ;
DLNode node = keyMap. get ( key) ;
if ( node != null ) {
node. value = value;
incrFreq ( node) ;
} else {
if ( keyMap. size ( ) == capacity) {
removeMinFreqNode ( ) ;
}
DLNode newNode = new DLNode ( key, value) ;
insert ( newNode) ;
keyMap. put ( key, newNode) ;
minFreq = 1 ;
}
}
private void removeMinFreqNode ( ) {
DLNode minFreqNode = freqMap. get ( minFreq) . getLast ( ) ;
remove ( minFreqNode) ;
keyMap. remove ( minFreqNode. key) ;
}
private void incrFreq ( DLNode node) {
remove ( node) ;
node. freq += 1 ;
insert ( node) ;
}
private void remove ( DLNode node) {
LinkedList < DLNode > list = freqMap. get ( node. freq) ;
list. remove ( node) ;
if ( list. isEmpty ( ) ) {
freqMap. remove ( node. freq) ;
if ( minFreq == node. freq) minFreq+= 1 ;
}
}
private void insert ( DLNode node) {
LinkedList < DLNode > list = freqMap. getOrDefault ( node. freq, new LinkedList < > ( ) ) ;
list. addFirst ( node) ;
if ( list. size ( ) == 1 ) {
freqMap. put ( node. freq, list) ;
}
}
}
3. 特殊数据结构