文章目录
- 前言
- 参考
- 一. 排序
- 1. 快速排序
- 2 归并排序
- 3 堆排序
- 4 其他排序
- 5. 第K大/小问题(快速选择算法)
- 6. 有序数据合并问题
- 二、二分查找
- 1. 基本二分查找
- 2. 二分查找左边界
- 3. 二分查找右边界
- 4. 相关题目
- 三、搜索
- 1. DFS
- 2. 回溯
- 3. BFS
- 4. 排列组合问题
- 5. 矩阵搜索问题
- 6. 其他DFS/BFS问题
- 四、树与递归
- 1. 二叉树的前中后序遍历(迭代法)
- 2. 二叉树的层序遍历
- 3. 树的递归问题
- 3. 二叉搜索树
- 4. Trie
- 五、其他数据结构
- 1. 链表
- 2. 哈希表
- 3. 栈
- 4. 优先级队列
- 5. 并查集
- 六、贪心思想
- 七、动态规划
- 八、数学
- 1. 位运算
- 2. 概率计算
- 3. 字符串运算
- 4. 排列组合数
- 5. 进制问题
- 6. 最大公约数
- 7. 摩尔投票法
- 8. 质数问题
- 9. 快速幂算法
- 九、滑动窗口
- 十、设计
- 1. LRU缓存
- 2. LFU缓存
- 3. 特殊数据结构
前言
算法题目一般分为以下几类,常见的用红色背景标出:
- 算法思想:贪心、
动态规划
、分治、递归
。 - 排序和搜索:
排序
、二分查找
、DFS
、BFS
、回溯
- 数据结构:
字符串
、数组/矩阵
、栈
、队列、哈希表
、链表
、二叉树
、图、并查集 - 数学:
位运算
、概率计算、数字处理
、经典算法 - 特殊解法:
滑动窗口
、双指针
- 设计:LRU、LFU、满足O(1)的数据结构组合
参考
- https://github.com/labuladong/fucking-algorithm 这个项目详细介绍了LeetCode题型和解题模板
- https://github.com/liguigui/CyC2018-CS-Notes 这个项目的算法部分总结了题型和代表性题目
- 《剑指Offer:名企面试官精讲典型编程题(第2版)》
这本书不仅介绍了各种题型、数据结构,而且从题目理解、沟通交流、思路产生、代码编写、算法优化
等方面全方位地告诉读者如何参与一场算法面试。
更多参考文献、书籍、网址、博客写在文中对应部分
一. 排序
- 常见的8种排序方法可以参考这道题目,里面有各种排序方法的标准实现
LeetCode 912. 排序数组 - 参考题解 复习基础排序算法(Java),十二种排序算法包你满意(带GIF图解)
- 常考的排序有
快速排序,归并排序,堆排序
三种,是必须要掌握的,包括算法的各种细节部分。
1. 快速排序
// 快速排序,随机选择1个数作为pivot,放在最左边,安置到数组合适的位置,使其大于左边的全部元素,小于右边的全部元素
// 时间O(nlogn),空间O(logn)
private void quickSort(int[] nums, int left, int right){
swap(nums,left,left+rand.nextInt(right-left+1));//随机挑选一个数作为pivot
if(left>=right) return;
int p = left;//p指针指向小于等于pivot的第一个元素
for(int i = left+1 ; i <= right ; i++){
//如果有小于等于pivot的元素,与p+1交换(因为p+1指向的是大于pivot的第一个元素)
if(nums[i] <= nums[left]){
swap(nums,++p,i);
}
}
//最后将left与p交换,p就是pivot的索引
swap(nums,left,p);
//继续排序左边和右边的区间
quickSort(nums,left,p-1);
quickSort(nums,p+1,right);
}
2 归并排序
// 归并排序,temp为全局临时数组,这样可以避免反复开辟空间
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);//继续调整堆
}
}
//调整节点i及其后续节点使其满足大顶堆定义,调整范围为[0,end]
private void adjustHeap(int[] nums, int i, int end){
int k = i;
//如果k存在子节点
while(k*2+1 <= end){
int j = 2*k + 1;//j指向左子节点
//如果右子节点存在,并且右子节点大于左子节点,那么把j指向右子节点(即j指向子节点中的较大值)
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);
}
}
题目 | 方法 |
标准的求第K大 | |
使用快速选择算法求出 | |
求 | |
这道题目的核心就是 | |
同样是求 |
6. 有序数据合并问题
题目 | 方法 |
按照归并排序的merge算法,如果有剩余数据,可以直接接在后面,不用再逐个复制 | |
从后向前合并,三指针 | |
归并排序链表 | |
在merge的时候计算逆序对 | |
跟上题一样 |
二、二分查找
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到x二分查找 | |
有序矩阵的二分查找,注意索引转换 | |
数组部分有序也可以使用二分查找,只要能确定下个查找区间 | |
和上题一样 | |
如果是峰值就返回,如果是上升序列,就在右边找,否则在左边找 | |
二分找左边界 | |
1到x二分查找 | |
把与之相邻的重复元素拿掉后,选择区间长度为奇数的继续查找 | |
二分查找定位最接近target的元素 | |
找右边界 |
三、搜索
搜索主要包括广度优先搜索
和深度优先搜索
,回溯属于深度优先搜索。搜索很多时候相当于是暴力的解法,遍历所有可能性,因此解题模式比较套路化。
1. DFS
可以定义全局变量如count,list,hashmap等,表示每一步做出的修改,比如添加一个结果,或者计数加1等。。。
//可以从一个或多个起点调用dfs
dfs(...);
//定义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 (队列不为空){
//每次while循环都是新的一层
获取队列大小size
for(int i = 0 ;i<size;i++){
Node cur = q.poll();
弹出每个节点,对每个节点进行处理
然后将每个节点相邻的节点继续加入队列
}
}
}
4. 排列组合问题
题目 | 方法 |
回溯,做选择后把元素交换到前面,然后传递给下一步开始做选择的索引 | |
回溯,每一步的选择需要去重,即提前剪枝 | |
下一步的候选集合从i开始 | |
先排序,在进行选择的时候去重 | |
注意结束条件判断 | |
标准回溯算法 | |
和LeetCode40题一样 | |
标准回溯问题 |
5. 矩阵搜索问题
题目 | 方法 |
在每一个空白格子尝试每一种可能 | |
每一行有n个列可供选择,尝试每一种可能 | |
在每个位置进行DFS,需要用visit矩阵储存是否访问 | |
搜索所有与边界O相连的O | |
在值为1的位置开始dfs,把与之相邻的变为其他数字 | |
使用辅助数组储存从(i,j)出发的最长路径 | |
需要修改的是1,遍历一遍对1做标记,记录0的位置,然后从0开始BFS |
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;//current指向目前正在遍历的树的根节点
//当前元素不为空或者栈不为空,这两种情况下,都还有元素需要遍历
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()){
//栈顶元素出栈的两种情况:
//1.是叶子节点
//2.上一个访问的节点是其左右子节点
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. 二叉树的层序遍历
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点:递归函数的含义、参数的含义、返回值的含义
题目 | 备注 |
根节点相同并且左右子树也相同 | |
当前节点相同并且p的左子节点与q的右子节点镜像对称,p的右子节点与q的左子节点镜像对称 | |
max(左子树的最大深度,右子树的最大深度)+1 | |
balance(root) = depth(l)-depth( r) <= 1,depth(root) = max(depth(l)+depth( r))+1,求depth的同时记录是否平衡 | |
左右子节点如果为空则不计入 | |
定义f(root)为以root为起点的最大路径和(左右子树只能选1个)定义g(root)为以root为起点的最大路径和(左右子树都选)) | |
交互左右子节点,然后继续翻转左右子树 | |
定义递归函数:p或者q是否存在于root为根节点的树中,两种LCA的情况:pq分别位于左右子树,pq一个是根节点一个在左右子树 | |
两个函数diam(root) = depth(l)+depth( r),depth(root) = max(depth(l),depth( r))+1,因此在求depth的时候同时记录直径 | |
与上题思想一样 | |
两个递归函数,isSubTree和isSameTree | |
合并根节点,然后递归合并左右子树 | |
前序遍历的第一个数字是根节点,从中序遍历中找到这个数字,然后得到左子树的数量,递归生成左右子树,注意索引 | |
与上题思路一样 |
3. 二叉搜索树
二叉搜索树一般会想到其中序遍历是有序的
题目 | 备注 |
构建递归函数List build(int start,int end)表示从start到end每个节点构成的所有的BST,可以从start到end依次构建,对于节点i,递归获取所有左BST,所有右BST,然后两两结合。 | |
定义递归函数isValidNode(TreeNode root,long lower,long upper),每个节点的值需要在一个区间内 | |
中序遍历,遍历到第k个位置结束 | |
只要qp在root两边,root就是最近 | |
首先找到这个节点,然后分3种情况处理删除逻辑 | |
记录前一个访问的节点、当前重复次数、最大重复次数 | |
中序遍历,记录前一个访问节点 | |
反向中序遍历 | |
中序遍历后双指针或哈希表 | |
中序遍历+合并有序数组 |
4. Trie
题目 | 备注 |
五、其他数据结构
1. 链表
链表问题一般画图、举例子解决,关键点是确定指针的指向、作用、状态
题目 | 备注 |
添加header节点,双指针一次遍历 | |
递归+双指针 | |
跟上题一样 | |
画图确定指针指向 | |
使用2个header节点,向将元素接在后面,同时断开原链表的指针连接 | |
双指针操作 | |
快慢指针,有环会相遇 | |
Floyd判圈算法,相遇后,fast从头开始走,和slow一起每次走一步,再次相遇时就是环的入口节点 | |
快慢指针找中点,然后前后依次取1个 |
2. 哈希表
哈希表的查找只需要O(1),因此很多时候可以用来以空间换时间
有时候数组也可以当做hash表使用
题目 | 备注 |
遍历过的数字放入hash表中 | |
原地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();
}
- 还有一些题目也用到优先级队列保存最大的K个值问题
题目 | 备注 |
容量为k的最小堆,每次弹出最小值,然后将下个节点加入堆 | |
使用1个最大堆B保存较小的数字,最小堆A保存较大的数字,并且保证queueA的size大于等于queueB的size,queueA的所有值大于等于queueB的所有值(即queueA的堆顶>=queueB的堆顶) | |
参考合并k个升序链表算法,想象成几个链表,使用最小堆弹出k次 |
5. 并查集
六、贪心思想
贪心思想需要每一步取得最优的结果,那么最后的结果就是最优的
题目 | 备注 |
每一步尽量选大的映射 | |
每一步找最远可达的位置 | |
首先找规律如何移除1位数字使最小,那么移除K位也是最小的 |
七、动态规划
八、数学
1. 位运算
题目 | 备注 |
n&(n-1)可以消去最后一个1,循环直到n=0 | |
异或后找二进制1的个数 | |
和全1异或 | |
转换为求二进制的公共前缀问题 | |
&运算获取需要进位的位,^运算获取无进位相加的结果 | |
相同数字/字符异或为0 | |
和上题一样 | |
用数组记录,每个元素的二进制和(不进位),这样如果有3个重复的数字,那么他们的二进制不进位和肯定是3,也就是说最后的结果,数组每一位对3取余数,就得到出现1次的数字。该方法对于数组中除1个元素外,其他元素都重复m次通用。 | |
先全部异或得到的是3和5的异或结果,然后按照异或结果的二进制中为1的那一位将原数组分组,这样3和5被分在不同的组中,相等的元素分在同一组中,然后每组异或即可。 | |
按位求,每一位的1的数目*0的数目,累加到结果 |
2. 概率计算
- 拒绝采样算法
题目 | 备注 |
大范围的随机数可以使用拒绝采样实现小范围的随机数,因此需要使用rand7构造更大范围的随机数,构造方法是(randX-1)*Y+randY = randXY | |
在正方形内生成,不在圆内的舍弃 |
- 蓄水池算法
题目 | 备注 |
第 | |
第 |
3. 字符串运算
题目 | 备注 |
注意进位问题 | |
竖式乘法,先相乘,然后使用上面的字符串相加,注意补0 |
4. 排列组合数
题目 | 备注 |
组合数。设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 | |
从m+n-2步中选m-1步或n-1步 |
5. 进制问题
题目 | 备注 |
短除法做进制转换,负数转成正数处理 | |
10进制转26进制,注意10进制对应关系是从1开始的, 需要先减1 |
6. 最大公约数
题目 | 备注 |
求数字个数的最大公约数是否大于2 | |
截取字符串长度的最大公约数 |
7. 摩尔投票法
题目 | 备注 |
假定1个元素是对数,然后每遇到1次,投票+1,遇到其他数,投票减1,当投票为0时,替换为其他数字 | |
因为找的是超过1/3的元素,因次最多有2个元素,因次找到第一多和第二多的元素即可,然后判断两个的元素个数是否超过1/3就可以 |
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缓存
- 由于需要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;
//添加虚拟头尾节点,避免null判断
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;
}
//如果key存在,就把node提升到头部,
public int get(int key) {
DLNode node = map.get(key);
if(node == null) return -1;
removeNode(node);
insertHead(node);
return node.value;
}
//如果key存在,就更新值;否则,新建节点,插入头部;如果超过容量,删除尾结点
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缓存
- 由于需要O(1)查找,因此使用Hash表(key -> Node),由于需要维护频率(增加频率,最小频率),因此需要在O(1)的时间找到频率最小的Node,考虑使用另一个hash表(频率 -> 链表),Node中存key,value,频率。
//使用两个HashMap和链表解决
class LFUCache {
static class DLNode {
int key = 0;
int value = 0;
int freq = 1;//默认频率为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<>();//key -> DLNode
private int minFreq = 0;//记录最小频率,方便超过容量时删除
public LFUCache(int capacity) {
this.capacity = capacity;
}
//从keyMap中查找,不存在返回-1;存在,增加节点频率
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;
}
//从keyMap中查找,存在,更新值,增加频率;
//不存在,如果size等于容量,删除频率最小节点;然后新建节点,插入频率链表中,更新最小频率为1。
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;
}
}
//删除频率最小节点 : 在freqMap和keyMap中同时删除
private void removeMinFreqNode(){
DLNode minFreqNode = freqMap.get(minFreq).getLast();
remove(minFreqNode);
keyMap.remove(minFreqNode.key);
}
//增加节点频率:从原频率链表中删除;频率加1;插入新的频率链表
private void incrFreq(DLNode node) {
remove(node);
node.freq += 1;
insert(node);
}
//从频率链表中删除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. 特殊数据结构