【算法+LeetCode题解+剑指Offer题解】算法思想、排序搜索、数据结构、数学、特殊解法、设计(全总结)

前言

算法题目一般分为以下几类,常见的用红色背景标出:

  1. 算法思想:贪心、动态规划、分治、递归
  2. 排序和搜索:排序二分查找DFSBFS回溯
  3. 数据结构:字符串数组/矩阵、队列、哈希表链表二叉树、图、并查集
  4. 数学:位运算、概率计算、数字处理、经典算法
  5. 特殊解法:滑动窗口双指针
  6. 设计:LRU、LFU、满足O(1)的数据结构组合

参考

  1. https://github.com/labuladong/fucking-algorithm
    这个项目详细介绍了LeetCode题型和解题模板
  2. https://github.com/liguigui/CyC2018-CS-Notes
    这个项目的算法部分总结了题型和代表性题目
  3. 《剑指Offer:名企面试官精讲典型编程题(第2版)》
    这本书不仅介绍了各种题型、数据结构,而且从题目理解、沟通交流、思路产生、代码编写、算法优化等方面全方位地告诉读者如何参与一场算法面试。

更多参考文献、书籍、网址、博客写在文中对应部分

一. 排序

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);
    }
}
题目方法
LeetCode 215. 数组中的第K个最大元素标准的求第K大
LeetCode 324. 摆动排序 II使用快速选择算法求出中位数,求中位数等价于求第k大,k为length/2,数组的后半部分大于前半部分,然后依次取一个,构造摆动排序
LeetCode 347. 前 K 个高频元素前K大问题,数组变成了矩阵或Entry数组
LeetCode 462. 最少移动次数使数组元素相等 II这道题目的核心就是求中位数,求中位数等价于求第k大,k为length/2,同样可以使用快速选择算法
LeetCode 973. 最接近原点的 K 个点同样是求前K小问题 ,只是数组变成了矩阵

6. 有序数据合并问题

题目方法
LeetCode 21. 合并两个有序链表按照归并排序的merge算法,如果有剩余数据,可以直接接在后面,不用再逐个复制
LeetCode 88. 合并两个有序数组从后向前合并,三指针
LeetCode 148. 排序链表归并排序链表
LeetCode 315. 计算右侧小于当前元素的个数在merge的时候计算逆序对
剑指 Offer 51. 数组中的逆序对跟上题一样

二、二分查找

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. 相关题目

题目方法
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置查找左右边界
LeetCode 35. 搜索插入位置直接二分查找
LeetCode 69. x 的平方根在1到x二分查找
LeetCode 74. 搜索二维矩阵有序矩阵的二分查找,注意索引转换
LeetCode 33. 搜索旋转排序数组数组部分有序也可以使用二分查找,只要能确定下个查找区间
LeetCode 153. 寻找旋转排序数组中的最小值和上题一样
LeetCode 162. 寻找峰值如果是峰值就返回,如果是上升序列,就在右边找,否则在左边找
LeetCode 278. 第一个错误的版本二分找左边界
LeetCode 367. 有效的完全平方数1到x二分查找
LeetCode 540. 有序数组中的单一元素把与之相邻的重复元素拿掉后,选择区间长度为奇数的继续查找
LeetCode 658. 找到 K 个最接近的元素二分查找定位最接近target的元素
LeetCode 744. 寻找比目标字母大的最小字母找右边界

三、搜索

搜索主要包括广度优先搜索深度优先搜索,回溯属于深度优先搜索。搜索很多时候相当于是暴力的解法,遍历所有可能性,因此解题模式比较套路化。

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. 排列组合问题

题目方法
LeetCode 46. 全排列(不含重复元素)回溯,做选择后把元素交换到前面,然后传递给下一步开始做选择的索引
LeetCode 47. 全排列 II(包含重复元素)回溯,每一步的选择需要去重,即提前剪枝
LeetCode 39. 组合总和(无重复的正整数集合,可重复选)下一步的候选集合从i开始
LeetCode 40. 组合总和 II(包含重复的正整数集合,不可重复选)先排序,在进行选择的时候去重
LeetCode 216. 组合总和 III注意结束条件判断
LeetCode 78. 子集(不含重复元素)标准回溯算法
LeetCode 90. 子集 II(包含重复元素)和LeetCode40题一样
LeetCode 17. 电话号码的字母组合标准回溯问题

5. 矩阵搜索问题

题目方法
LeetCode 37. 解数独在每一个空白格子尝试每一种可能
LeetCode 51. N 皇后每一行有n个列可供选择,尝试每一种可能
LeetCode 79. 单词搜索在每个位置进行DFS,需要用visit矩阵储存是否访问
LeetCode 130. 被围绕的区域搜索所有与边界O相连的O
LeetCode 200. 岛屿数量在值为1的位置开始dfs,把与之相邻的变为其他数字
LeetCode 329. 矩阵中的最长递增路径使用辅助数组储存从(i,j)出发的最长路径
LeetCode 542. 01 矩阵需要修改的是1,遍历一遍对1做标记,记录0的位置,然后从0开始BFS

6. 其他DFS/BFS问题

题目备注
LeetCode 22. 括号生成记录左右括号的数量
LeetCode 93. 复原IP地址注意筛选每一步的选择,还有提前剪枝的判断
LeetCode 282. 给表达式添加运算符
LeetCode 306. 累加数
LeetCode 401. 二进制手表构造选择集,注意去重
LeetCode 752. 打开转盘锁具体问题抽象成节点和图处理,问题转化为求起始节点到目标节点的最短距离

四、树与递归

把二叉树的遍历方式弄清楚,大部分二叉树问题就很好解决了

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. 二叉树的层序遍历

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点:递归函数的含义、参数的含义、返回值的含义

题目备注
LeetCode 100. 相同的树根节点相同并且左右子树也相同
LeetCode 101. 对称二叉树当前节点相同并且p的左子节点与q的右子节点镜像对称,p的右子节点与q的左子节点镜像对称
LeetCode 104. 二叉树的最大深度max(左子树的最大深度,右子树的最大深度)+1
LeetCode 110. 平衡二叉树balance(root) = depth(l)-depth( r) <= 1,depth(root) = max(depth(l)+depth( r))+1,求depth的同时记录是否平衡
LeetCode 111. 二叉树的最小深度左右子节点如果为空则不计入
LeetCode 124. 二叉树中的最大路径和定义f(root)为以root为起点的最大路径和(左右子树只能选1个)定义g(root)为以root为起点的最大路径和(左右子树都选))
LeetCode 226. 翻转二叉树交互左右子节点,然后继续翻转左右子树
LeetCode 236. 二叉树的最近公共祖先定义递归函数:p或者q是否存在于root为根节点的树中,两种LCA的情况:pq分别位于左右子树,pq一个是根节点一个在左右子树
LeetCode 543. 二叉树的直径两个函数diam(root) = depth(l)+depth( r),depth(root) = max(depth(l),depth( r))+1,因此在求depth的时候同时记录直径
LeetCode 563. 二叉树的坡度与上题思想一样
LeetCode 572. 另一个树的子树两个递归函数,isSubTree和isSameTree
LeetCode 617. 合并二叉树合并根节点,然后递归合并左右子树
LeetCode 105. 从前序与中序遍历序列构造二叉树前序遍历的第一个数字是根节点,从中序遍历中找到这个数字,然后得到左子树的数量,递归生成左右子树,注意索引
LeetCode 106. 从中序与后序遍历序列构造二叉树与上题思路一样

3. 二叉搜索树

二叉搜索树一般会想到其中序遍历是有序的

题目备注
LeetCode 95. 不同的二叉搜索树 II构建递归函数List build(int start,int end)表示从start到end每个节点构成的所有的BST,可以从start到end依次构建,对于节点i,递归获取所有左BST,所有右BST,然后两两结合。
LeetCode 98. 验证二叉搜索树定义递归函数isValidNode(TreeNode root,long lower,long upper),每个节点的值需要在一个区间内
LeetCode 230. 二叉搜索树中第K小的元素中序遍历,遍历到第k个位置结束
LeetCode 235. 二叉搜索树的最近公共祖先只要qp在root两边,root就是最近
LeetCode 450. 删除二叉搜索树中的节点首先找到这个节点,然后分3种情况处理删除逻辑
LeetCode 501. 二叉搜索树中的众数记录前一个访问的节点、当前重复次数、最大重复次数
LeetCode 530. 二叉搜索树的最小绝对差中序遍历,记录前一个访问节点
LeetCode 538. 把二叉搜索树转换为累加树反向中序遍历
LeetCode 653. 两数之和 IV - 输入 BST中序遍历后双指针或哈希表
LeetCode 1305. 两棵二叉搜索树中的所有元素中序遍历+合并有序数组

4. Trie

题目备注
LeetCode 208. 实现 Trie (前缀树)

五、其他数据结构

1. 链表

链表问题一般画图、举例子解决,关键点是确定指针的指向、作用、状态

题目备注
LeetCode 19. 删除链表的倒数第N个节点添加header节点,双指针一次遍历
LeetCode 24. 两两交换链表中的节点递归+双指针
LeetCode 25. K 个一组翻转链表跟上题一样
LeetCode 61. 旋转链表画图确定指针指向
LeetCode 86. 分隔链表使用2个header节点,向将元素接在后面,同时断开原链表的指针连接
LeetCode 92. 反转链表 II双指针操作
LeetCode 141. 环形链表快慢指针,有环会相遇
LeetCode 142. 环形链表 IIFloyd判圈算法,相遇后,fast从头开始走,和slow一起每次走一步,再次相遇时就是环的入口节点
LeetCode 143. 重排链表快慢指针找中点,然后前后依次取1个

2. 哈希表

哈希表的查找只需要O(1),因此很多时候可以用来以空间换时间
有时候数组也可以当做hash表使用

题目备注
LeetCode 1. 两数之和遍历过的数字放入hash表中
LeetCode 442. 数组中重复的数据原地Hash,用索引标记元素状态
LeetCode 532. 数组中的K-diff数对和两数之和一样,储存数对时构造字符串作为键

3. 栈

题目备注
LeetCode 20. 有效的括号和栈顶括号匹配就出栈,不匹配就入栈
LeetCode 71. 简化路径遇到文件夹就入栈,遇到…就出栈
LeetCode 150. 逆波兰表达式求值数字都入栈,符号出栈两个做运算

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个值问题
题目备注
LeetCode 23. 合并K个升序链表容量为k的最小堆,每次弹出最小值,然后将下个节点加入堆
LeetCode 295. 数据流的中位数使用1个最大堆B保存较小的数字,最小堆A保存较大的数字,并且保证queueA的size大于等于queueB的size,queueA的所有值大于等于queueB的所有值(即queueA的堆顶>=queueB的堆顶)
LeetCode 373. 查找和最小的K对数字参考合并k个升序链表算法,想象成几个链表,使用最小堆弹出k次

5. 并查集

六、贪心思想

贪心思想需要每一步取得最优的结果,那么最后的结果就是最优的

题目备注
LeetCode 12. 整数转罗马数字每一步尽量选大的映射
LeetCode 45. 跳跃游戏 II每一步找最远可达的位置
LeetCode 402. 移掉K位数字首先找规律如何移除1位数字使最小,那么移除K位也是最小的

七、动态规划

八、数学

1. 位运算

题目备注
LeetCode 191. 位1的个数n&(n-1)可以消去最后一个1,循环直到n=0
LeetCode 461. 汉明距离异或后找二进制1的个数
LeetCode 476. 数字的补数和全1异或
LeetCode 201. 数字范围按位与转换为求二进制的公共前缀问题
LeetCode 371. 两整数之和&运算获取需要进位的位,^运算获取无进位相加的结果
LeetCode 389. 找不同相同数字/字符异或为0
LeetCode 136. 只出现一次的数字和上题一样
LeetCode 137. 只出现一次的数字 II用数组记录,每个元素的二进制和(不进位),这样如果有3个重复的数字,那么他们的二进制不进位和肯定是3,也就是说最后的结果,数组每一位对3取余数,就得到出现1次的数字。该方法对于数组中除1个元素外,其他元素都重复m次通用。
LeetCode 260. 只出现一次的数字 III先全部异或得到的是3和5的异或结果,然后按照异或结果的二进制中为1的那一位将原数组分组,这样3和5被分在不同的组中,相等的元素分在同一组中,然后每组异或即可。
LeetCode 477. 汉明距离总和按位求,每一位的1的数目*0的数目,累加到结果

2. 概率计算

  • 拒绝采样算法
题目备注
LeetCode 470. 用 Rand7() 实现 Rand10()大范围的随机数可以使用拒绝采样实现小范围的随机数,因此需要使用rand7构造更大范围的随机数,构造方法是(randX-1)*Y+randY = randXY
LeetCode 478. 在圆内随机生成点在正方形内生成,不在圆内的舍弃
  • 蓄水池算法
题目备注
LeetCode 398. 随机数索引I个数字有1.0/i的几率被保留
LeetCode 382. 链表随机节点i个节点有1.0/i的几率被保留(即1.0/i > rand.nextDouble()时替换节点)

3. 字符串运算

题目备注
LeetCode 415. 字符串相加注意进位问题
LeetCode 43. 字符串相乘竖式乘法,先相乘,然后使用上面的字符串相加,注意补0

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. 进制问题

题目备注
LeetCode 504. 七进制数短除法做进制转换,负数转成正数处理
LeetCode 168. Excel表列名称10进制转26进制,注意10进制对应关系是从1开始的, 需要先减1

6. 最大公约数

题目备注
LeetCode 914. 卡牌分组求数字个数的最大公约数是否大于2
LeetCode 1071. 字符串的最大公因子截取字符串长度的最大公约数

7. 摩尔投票法

题目备注
LeetCode 169. 多数元素假定1个元素是对数,然后每遇到1次,投票+1,遇到其他数,投票减1,当投票为0时,替换为其他数字
LeetCode 229. 求众数 II因为找的是超过1/3的元素,因次最多有2个元素,因次找到第一多和第二多的元素即可,然后判断两个的元素个数是否超过1/3就可以

8. 质数问题

题目备注
LeetCode 204. 计数质数一个数是质数,那么它的倍数不是质数

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;
    }
}

九、滑动窗口

题目备注
LeetCode 3. 无重复字符的最长子串
LeetCode 76. 最小覆盖子串
LeetCode 209. 长度最小的子数组
LeetCode 239. 滑动窗口最大值
LeetCode 424. 替换后的最长重复字符
LeetCode 438. 找到字符串中所有字母异位词
LeetCode 567. 字符串的排列

十、设计

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;
    //添加虚拟头尾节点,避免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缓存

LeetCode 460. 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. 特殊数据结构

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值