数据结构与算法——LeetCode刷题记录

文章目录

  • 一. 数据结构
    • 1. 栈
      • 1.1 栈
      • 1.2 单调栈
    • 2. 链表
    • 3. 二叉树
    • 4. 队列
      • 4.1 优先队列/堆
      • 4.2 双端队列/单调队列
    • 5. HashSet/HashMap
    • 6. 并查集
  • 二. 算法
    • 1. 双指针
      • 1.1 双指针
      • 1.2 滑动窗口
      • 1.3 快慢指针
    • 2. 二分查找
    • 3. BFS
    • 4. DFS + 回溯
      • 4.1 洪水问题
      • 4.2 排列、组合、子集相关问题
      • 4.3 数字问题
      • 4.4 游戏问题
      • 4.5 一般类型问题
    • 5. DP
      • 5.1 背包问题
      • 5.2 网格二维DP
      • 5.3 子序列问题
      • 5.4 一般类型问题
    • 6. 分治
    • 7. 排序
      • 7.1 排序
      • 7.2 归并排序
      • 7.3 快速排序
      • 7.4 拓扑排序
      • 7.5 桶排序/计数排序
    • 8. 贪心
    • 9. 前缀和
    • 10. 状态压缩
    • 11. 摩尔投票法
  • 三. 常见类型题
    • 1. 区间问题
    • 2. 最大子序和问题
    • 3. 岛屿问题(网格DFS+BFS)
    • 4. 位运算
    • 5. 设计类问题
    • 6. 回文数/串
    • 7. 数字操作
    • 8. 股票问题(增加dp维数消除后效性)
    • 9. 打家劫舍问题(增加dp维数消除后效性)
    • 10. 数学推理
    • 11. 操作矩阵
    • 12. n数之和
    • 13. 最大矩形
    • 14. TOPK问题

写在前面,许多问题并不只有一种解答思路

一. 数据结构

1. 栈

1.1 栈

  • 简单

    • 20. 有效的括号
    • 1021. 删除最外层的括号
  • 中等

    • 394. 字符串解码
  • 困难

    • 32. 最长有效括号

1.2 单调栈

单调栈问题有2大特点:(1)涉及到元素的比较(从小到大?字典序小?)(2)保持原有的相对顺序

保持栈中元素从大到小排列:

for () {
	int cur = //当前元素
    while(!stack.isEmpty() && cur > stack.peek()) { // 当前元素大于栈顶元素
        stack.pop(); // 出栈        
    }
    stack.push(i); //入栈
}
  • 简单
    • 496. 下一个更大元素 I
  • 中等
    • 402. 移掉K位数字
    • 503. 下一个更大元素 II
    • 581. 最短无序连续子数组
    • 739. 每日温度
    • 901. 股票价格跨度
    • 1081. 不同字符的最小子序列
  • 困难
    • 42. 接雨水
    • 84. 柱状图中最大的矩形
    • 85. 最大矩形
    • 316. 去除重复字母
    • 321. 拼接最大数

2. 链表

基本结构:

public class ListNode {
	public int val;
    public ListNode next;
    public ListNode() {
	}
    public ListNode(int x) { 
    	val = x; 
    }
}

基本操作:

  • 插入
temp = 待插入位置的前驱节点.next
待插入位置的前驱节点.next = 待插入指针
待插入指针.next = temp
  • 删除
待删除位置的前驱节点.next = 待删除位置的前驱节点.next.next

基本技巧:

  • 增加虚拟头节点
ListNode dummy = new ListNode(-1);
dummy.next = head;
//...
return dummy.next;

例题:

  • 简单
    • 21. 合并两个有序链表
    • 160. 相交链表
    • 206. 反转链表
    • 234. 回文链表
  • 中等
    • 2. 两数相加
    • 19. 删除链表的倒数第N个节点
    • 24. 两两交换链表中的节点
  • 困难

3. 二叉树

二叉树路径 / 深度:

  • 简单
    • 104. 二叉树的最大深度
    • 111. 二叉树的最小深度
    • 112. 路径总和
    • 543. 二叉树的直径
  • 中等
    • 687. 最长同值路径
    • 437. 路径总和 III
  • 困难
    • 124. 二叉树中的最大路径和

遍历二叉树:

  • 中等
    • 94. 二叉树的中序遍历
    • 102. 二叉树的层序遍历
    • 103. 二叉树的锯齿形层次遍历
    • 114. 二叉树展开为链表
    • 144. 二叉树的前序遍历
    • 145. 二叉树的后序遍历
    • 199. 二叉树的右视图
    • 236. 二叉树的最近公共祖先
    • 1110. 删点成林
  • 困难
    • 297. 二叉树的序列化与反序列化

构造二叉树:

  • 简单
    • 226. 翻转二叉树
    • 617. 合并二叉树
  • 中等
    • 105. 从前序与中序遍历序列构造二叉树
    • 538. 把二叉搜索树转换为累加树

利用二叉树的性质:

  • 简单
    • 100. 相同的树
    • 101. 对称二叉树
  • 中等
    • 98. 验证二叉搜索树

4. 队列

4.1 优先队列/堆

  • 简单
    • 1046. 最后一块石头的重量
  • 中等
    • 215. 数组中的第K个最大元素
    • 347. 前 K 个高频元素
    • 767. 重构字符串
  • 困难
    • 23. 合并K个升序链表
    • 295. 数据流的中位数

4.2 双端队列/单调队列

两个特征:单调性(从队列头部到队列尾部保持递减的顺序)、双端操作

思路:

  1. 给定初始【队列】
  2. 从【队列】尾部插入元素时,提前取出【单调队列】中所有比该元素小的元素,等价于维护单调队列的单调性
  3. 从【队列】头部删除元素时,需要判断待删除元素和【单调队列】头部元素是否相同,如果相同,则一同删除;如果不同,则表明待删除元素不是当前队列最大值

例题:

  • 中等
    • 剑指 Offer 59 - II. 队列的最大值——已做
  • 困难
    • 239. 滑动窗口最大值——已做

5. HashSet/HashMap

  • 简单
    • 1. 两数之和
    • 136. 只出现一次的数字
    • 387. 字符串中的第一个唯一字符
    • 448. 找到所有数组中消失的数字
  • 中等
    • 49. 字母异位词分组
    • 454. 四数相加 II
  • 困难
    • 41. 缺失的第一个正数 —— 原地哈希
    • 128. 最长连续序列

6. 并查集

  • 中等
    • 399. 除法求值
  • 困难
    • 128. 最长连续序列

二. 算法

1. 双指针

1.1 双指针

  • 简单
    • 283. 移动零
  • 中等
    • 11. 盛最多水的容器
    • 15. 三数之和
    • 31. 下一个排列

1.2 滑动窗口

大字符串包含小字符串

  • 模板思路
// 1. 构造数组、set、map存放滑动窗口中出现的元素
int[] letter = new int[26];
Set<Character> set = new HashSet<>();
Map<Character, Integer> map = new HashMap<>();

// 2. 定义窗口左右边界:[left, right)
int left = 0;
int right = 0;
int need_len = 0; // 统计小串中的元素在大串中出现的个数
// 比如小串为"aabc" ,窗口中出现"abc",need_len = 3,窗口中出现"aabc",need_len = 4

// 3. 右移窗口
while (right < len) {
	char r = right处的字符
	if (r在小串出现了) {
		窗口中r的个数 + 1
		if (窗口中r的个数 <= 小串中r的个数) need_len ++;
	}
	right ++; // 右移窗口
	
	// 出错debug位置
	System.out.println("left = " + left +", right = " + right);

	// 判断窗口是否需要收缩
    while (窗口满足要求,即need_len = 小串的长度) {
    	//可能在这里输出结果
    	//...
    	char l = left处的字符
    	if (l在小串出现了) {
    		if (窗口中l的个数 <= 小串中l的个数) need_len --;
			窗口中l的个数 - 1
		}       
        left ++;
    }   
}
  • 简单
  • 中等
    • 3. 无重复字符的最长子串
    • 438. 找到字符串中所有字母异位词
    • 567. 字符串的排列
    • 713. 乘积小于K的子数组
    • 1004. 最大连续1的个数 III
  • 困难
    • 76. 最小覆盖子串
    • 239. 滑动窗口最大值

1.3 快慢指针

  • 简单
    • 141. 环形链表
    • 202. 快乐数
  • 中等
    • 142. 环形链表 II
    • 287. 寻找重复数

2. 二分查找

技巧:

  • 划分区间:左闭右闭区间
  • int mid = left + (right - left) / 2; 防止越界

模板1:在循环体内部【查找】元素

  • while (left <= right )
  • 退出循环的时候 leftright 不重合
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:在循环体内部【排除】元素,可以解决绝大部分问题,重点掌握!

  • while (left < right )
  • 退出循环的时候 leftright 重合
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;
        }
        else {
            left= mid + 1;
        }	
    }
    return nums[left] == target ? left : -1;
}

题型1:在数组中查找符合条件的元素的下标

  • 704. 二分查找 —— 基本模板
  • 852. 山脉数组的峰顶索引 —— 山脉数组
  • 1095. 山脉数组中查找目标值 —— 山脉数组
  • 33. 搜索旋转排序数组 —— 旋转数组
  • 81. 搜索旋转排序数组 II —— 旋转数组
  • 153. 寻找旋转排序数组中的最小值 —— 旋转数组
  • 154. 寻找旋转排序数组中的最小值 II —— 旋转数组
  • 34. 在排序数组中查找元素的第一个和最后一个位置 —— 区间搜索
  • 300. 最长递增子序列 —— 涨见识
  • 658. 找到 K 个最接近的元素
  • 4. 寻找两个正序数组的中位数

题型2:在一个有范围的区间里搜索一个整数

题型3:复杂的二分查找问题(判别条件需要遍历数组)

  • 中等
    • 240. 搜索二维矩阵 II
    • 287. 寻找重复数 —— 非常规二分
    • 540. 有序数组中的单一元素
  • 困难
    • 4. 寻找两个正序数组的中位数

参考链接:写对二分查找不能靠模板,需要理解加练习 (附练习题,持续更新)

3. BFS

  • 中等
    • 102. 二叉树的层序遍历
    • 994. 腐烂的橘子

4. DFS + 回溯

数据结构与算法——深度优先搜索(DFS)

DFS + 回溯的核心思路是画图!依据回溯图写代码!

liweiwei1419回溯算法入门级详解 + 练习:46. 全排列

数据结构与算法——LeetCode刷题记录_第1张图片

4.1 洪水问题

洪水问题的标准解法是设置 visited 数组,设置方向数组,抽取私有方法

private static final int[][] Dir = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
private boolean[][] visited;
  • 洪水问题(从一点出发,将所有与该点相连的点改变状态):
    • 733. 图像渲染 —— 洪水问题
    • 79. 单词搜索 —— 洪水问题
    • 130. 被围绕的区域 —— 洪水问题
    • 200. 岛屿数量 —— 洪水问题

排列、组合、子集相关问题解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 begin 变量,理解状态变量设计的想法

4.2 排列、组合、子集相关问题

  • 排列、组合、子集相关问题:
    • 77. 组合 —— 组合问题
    • 39. 组合总和 —— 组合问题
    • 40. 组合总和 II —— 组合问题
    • 46. 全排列 —— 排列问题
    • 47. 全排列 II —— 排列问题
    • 60. 排列序列 —— 排列问题
    • 784. 字母大小写全排列 —— 排列问题
    • 78. 子集 —— 子集问题
    • 90. 子集 II —— 子集问题

4.3 数字问题

  • 数字问题(需要注意数字过大的问题,另外一种形式的回溯问题)
    • 306. 累加数 —— 数字问题
    • 842. 将数组拆分成斐波那契序列 —— 数字问题

4.4 游戏问题

  • 游戏问题
    • 37. 解数独
    • 51. N 皇后
    • 52. N皇后 II
    • 488. 祖玛游戏
    • 529. 扫雷游戏

4.5 一般类型问题

  • 简单

  • 中等

    • 17. 电话号码的字母组合

    • 22. 括号生成

    • 79. 单词搜索

    • 93. 复原IP地址

    • 784. 字母大小写全排列

  • 困难

    • 301. 删除无效的括号

5. DP

数据结构与算法——动态规划(DP)

5.1 背包问题

  • 中等
    • 322. 零钱兑换 —— 完全背包
    • 416. 分割等和子集 —— 01背包
    • 474. 一和零 —— 01背包
    • 494. 目标和 —— 01背包
    • 1049. 最后一块石头的重量 II —— 01背包
  • 困难
    • 691. 贴纸拼词 —— 完全背包
    • 879. 盈利计划 —— 01背包
    • 1125. 最小的必要团队 —— 01背包

5.2 网格二维DP

  • 中等

    • 62. 不同路径:从网格的左上角到右下角有多少条不同的路径
    • 63. 不同路径 II:网格中有障碍物,从网格的左上角到右下角有多少条不同的路径
    • 64. 最小路径和:从网格的左上角到右下角的路径的和最小
    • 120. 三角形最小路径和:从三角形顶端到三角形底端的路径的和最小
    • 1594. 矩阵的最大非负积:从网格的左上角到右下角的路径的积最大
  • 困难

    • LCP 13. 寻宝:从网格的起点到终点拿起宝藏的路径
    • 980. 不同路径 III:从网格的起点到终点不能重复通过网格

5.3 子序列问题

  • 简单
    • 53. 最大子序和 —— 子序列问题
  • 中等
    • 152. 乘积最大子数组 —— 子序列问题
    • 300. 最长上升子序列 —— LIS
    • 376. 摆动序列 —— 子序列问题
  • 困难
    • 面试题 17.08. 马戏团人塔 —— 二维LIS
    • 面试题 08.13. 堆箱子 —— 三维LIS
    • 354. 俄罗斯套娃信封问题 —— 二维LIS
    • 960. 删列造序 III —— 子序列问题
    • 5644. 得到子序列的最少操作次数 —— 转化为最长上升子序列问题

5.4 一般类型问题

  • 简单

    • 70. 爬楼梯
  • 中等

    • 62. 不同路径
    • 64. 最小路径和
    • 96. 不同的二叉搜索树
    • 139. 单词拆分
    • 221. 最大正方形
    • 279. 完全平方数 —— 数字DP
    • 337. 打家劫舍 III —— 树形DP
    • 343. 整数拆分 —— 数字DP
  • 困难

    • 10. 正则表达式匹配
    • 32. 最长有效括号
    • 72. 编辑距离
    • 85. 最大矩形
    • 312. 戳气球 —— 区间DP

6. 分治

数据结构与算法——分治算法

  • 简单
    • 53. 最大子序和

7. 排序

7.1 排序

  • 中等
    • 406. 根据身高重建队列
    • 581. 最短无序连续子数组
    • 621. 任务调度器

7.2 归并排序

典型的分治思想

数据结构与算法——LeetCode刷题记录_第2张图片

数据结构与算法——LeetCode刷题记录_第3张图片

数据结构与算法——LeetCode刷题记录_第4张图片
代码:

public static void main(String[] args) {
	int nums[] = { 8, 4, 55, 7, 1, 3, 6, 2 }; 
	int[] temp = new int[nums.length];//提前定义临时数组,避免递归中反复开辟内存空间
	
	mergeSort(nums, 0, nums.length - 1, temp);
}	
//分+合方法
public static void mergeSort(int[] nums, int left, int right,int[] temp) {
	if(left < right) {
		int mid = (left + right) / 2; //中间索引
		//向左递归进行分解
		mergeSort(nums, left, mid, temp);
		//向右递归进行分解
		mergeSort(nums, mid + 1, right, temp);
		//合并
		merge(nums, left, mid, right, temp);
	}
}
//合并两个有序数组:nums[left, mid] nums[mid + 1, right]
public static void merge(int[] nums, int left, int mid, int right, int[] temp) {	
	int l = left; // 初始化i, 左边有序序列的初始索引
	int r = mid + 1; //初始化j, 右边有序序列的初始索引
	
	//左边序列:[left-mid],右边序列:[mid+1,right]
	for(int k = left; k <= right; k ++) {
    	//左边数添加完
        if(l > mid) {
            temp[k] = nums[r ++];
        }
        //右边数添加完
        else if(r > right) {
            temp[k] = nums[l ++];
        }
        //左边数小,先添加左边数
        else if(nums[l] <= nums[r]) {
            temp[k] = nums[l ++];
        }
        //右边数小,先添加右边数
        else {
            temp[k] = nums[r ++];
            /
			//这里是计算逆序对的关键
			//应该先添加左边小的数,再添加右边大的数。
			//但右边数比左边数小,即右边数比左边[l,mid]范围内的数都小
			/
        }
    }
	
	//将temp[]拷贝到arr[],合并的数从left--right,right右边的位置不考虑
	for(int tempi = left; tempi <= right; tempi ++) {
		nums[tempi] = temp[tempi];
	}
}
  • 中等
    • 148. 排序链表
  • 困难
    • 315. 计算右侧小于当前元素的个数
    • 327. 区间和的个数
    • 493. 翻转对
    • 剑指 Offer 51. 数组中的逆序对

这类问题还有其他思路:从后往前插入排序、构造二叉排序树、树状数组、线段树等。
详细题解思路参考:5种思路总结

7.3 快速排序

  • 简单
    • 169. 多数元素
  • 中等
    • 75. 颜色分类
    • 215. 数组中的第K个最大元素

7.4 拓扑排序

  • 中等
    • 207. 课程表

7.5 桶排序/计数排序

  • 困难
    • 164. 最大间距

8. 贪心

在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

  • 简单
    • 944. 删列造序
  • 中等
    • 55. 跳跃游戏:尽可能跳远
    • 134. 加油站
    • 475. 供暖器
    • 649. Dota2 参议院:让最先出现的对面阵营的人禁止投票
    • 659. 分割数组为连续子序列
    • 738. 单调递增的数字:出现尽可能多的9
    • 955. 删列造序 II
    • 1247. 交换字符使得字符串相同
    • 1353. 最多可以参加的会议数目

9. 前缀和

前缀和算法是一种重要的预处理算法,能大大降低查询的时间复杂度。最简单的题目就是:给定n个数和m次询问,每次询问一段区间的和。

比如求和为k的连续子数组的个数:

  • 使用前缀和快速计算区间和(需要O(n)计算前缀和,再O(n*n)遍历区间和)
public int subarraySum(int[] nums, int k) {
    int len = nums.length;
    // 计算前缀和数组
    int[] preSum = new int[len + 1];
    preSum[0] = 0;
    for (int i = 0; i < len; i++) {
        preSum[i + 1] = preSum[i] + nums[i];
    }
    int count = 0;
    for (int left = 0; left < len; left++) {
        for (int right = left; right < len; right++) {
            // 区间和 [left..right],注意下标偏移
            if (preSum[right + 1] - preSum[left] == k) {
                count++;
            }
        }
    }
    return count;
}
  • 前缀和优化:哈希表(边遍历,边存储前缀和,O(n))
  • 前缀和优化:数组(明确哈希表的键有哪些时,可以使用数组代替哈希表)
public int subarraySum(int[] nums, int k) {
   // key:前缀和,value:key 对应的前缀和的个数
    Map<Integer, Integer> preSumFreq = new HashMap<>();
    // 对于下标为 0 的元素,前缀和为 0,个数为 1
    preSumFreq.put(0, 1);

    int preSum = 0;
    int count = 0;
    for (int num : nums) {
        preSum += num;

        // 先获得前缀和为 preSum - k 的个数,加到计数变量里
        if (preSumFreq.containsKey(preSum - k)) {
            count += preSumFreq.get(preSum - k);
        }

        // 然后维护 preSumFreq 的定义
        preSumFreq.put(preSum, preSumFreq.getOrDefault(preSum, 0) + 1);
    }
    return count;
}

比如求包含的元音均为偶数个的连续子数组的最大长度:连续子数组的含有元音的个数只能是偶数或者奇数,可以使用二进制表示,0 代表出现了偶数次,1 代表出现了奇数次

  • 前缀和进一步优化2:哈希表 + 状态压缩
  • 前缀和进一步优化2:数组 + 状态压缩
public int findTheLongestSubstring(String s) {
    int n = s.length();
    int[] pos = new int[1 << 5];
    Arrays.fill(pos, -1);
    int ans = 0, status = 0;
    pos[0] = 0;
    for (int i = 0; i < n; i++) {
        char ch = s.charAt(i);
        if (ch == 'a') {
            status ^= (1 << 0);
        } else if (ch == 'e') {
            status ^= (1 << 1);
        } else if (ch == 'i') {
            status ^= (1 << 2);
        } else if (ch == 'o') {
            status ^= (1 << 3);
        } else if (ch == 'u') {
            status ^= (1 << 4);
        }
        if (pos[status] >= 0) {
            ans = Math.max(ans, i + 1 - pos[status]);
        } else {
            pos[status] = i + 1;
        }
    }
    return ans;
}
  • 简单
  • 中等
    • 560. 和为K的子数组
    • 1248. 统计「优美子数组」
    • 1371. 每个元音包含偶数次的最长子字符串
  • 困难

10. 状态压缩

利用二进制表示状态

  • 简单
  • 中等
    • 464. 我能赢吗 DP + 状压
    • 1371. 每个元音包含偶数次的最长子字符串 —— 前缀和 + 状压
  • 困难
    • 51. N 皇后 —— 回溯 + 状压
    • 1349. 参加考试的最大学生数 —— DP + 状压
    • 1542. 找出最长的超赞子字符串 —— 前缀和 + 状压

11. 摩尔投票法

解决的问题是如何在任意多的候选人中,选出票数超过一半的那个人。

  1. 候选人(cand_num)初始化为nums[0],票数count初始化为1。
  2. 当遇到与cand_num相同的数,则票数count = count + 1,否则票数count = count - 1。
  3. 当票数count为0时,更换候选人,并将票数count重置为1。
  4. 遍历完数组后,cand_num即为最终答案。
  • 简单
    • 169. 多数元素
  • 中等
    • 229. 求众数 II

三. 常见类型题

1. 区间问题

  • 简单
    • 228. 汇总区间
  • 中等
    • 56. 合并区间
    • 452. 用最少数量的箭引爆气球
    • 1288. 删除被覆盖区间
  • 困难
    • 57. 插入区间
    • 354. 俄罗斯套娃信封问题

2. 最大子序和问题

  • 简单
    • 53. 最大子序和
  • 困难
    • 面试题 17.24. 最大子矩阵

3. 岛屿问题(网格DFS+BFS)

  • 简单
    • 463. 岛屿的周长
  • 中等
    • 200. 岛屿数量
    • 695. 岛屿的最大面积
  • 困难
    • 827. 最大人工岛

4. 位运算

求只出现一次的元素:使用异或 ^。规律:A ^ A = 0,A ^ 0 = A。

  1. 出现偶数次,异或结果为0。
  2. 两个数异或,二进制位相同的位置置0,不同的置1。
  • 简单
    • 136. 只出现一次的数字
    • 268. 丢失的数字
    • 389. 找不同
    • 461. 汉明距离
  • 中等
    • 137. 只出现一次的数字 II
    • 260. 只出现一次的数字 III

每个元素只出现一次,用二进制表示:0未出现、1出现

  • 中等
    • 78. 子集

统计二进制中1的个数:判断最后一位是否为1(n & 1)、右移(n >>> 1)

  • 中等
    • 338. 比特位计数

5. 设计类问题

  • 简单
    • 剑指 Offer 09. 用两个栈实现队列
    • 剑指 Offer 59 - II. 队列的最大值
    • 155. 最小栈
    • 225. 用队列实现栈
  • 中等
    • 146. LRU 缓存机制
    • 208. 实现 Trie (前缀树)
  • 困难

6. 回文数/串

  • 简单
    • 5. 最长回文子串
    • 9. 回文数
  • 中等
    • 131. 分割回文串
    • 647. 回文子串
  • 困难
    • 132. 分割回文串 II
    • 1278. 分割回文串 III

7. 数字操作

  • 简单
    • 7. 整数反转
  • 中等
    • 8. 字符串转换整数 (atoi)
    • 43. 字符串相乘
  • 困难

8. 股票问题(增加dp维数消除后效性)

状态1:第i天不持股
状态转移:第i - 1天不持股 or 第i - 1天持股 + 第i天卖出股票
状态2:第i天持股
状态转移:第i - 1天持股 or 第i - 1天持股 + 第i天买入股票

=====

所以第i天的状态与第i - 1天状态有关,违反了后效性,所以需要增加dp维数
dp[i][0] 不持股
dp[i][1] 持股

详细题解:股票问题系列通解(转载翻译)

  • 简单
    • 121. 买卖股票的最佳时机 / 剑指OFFER 63
    • 122. 买卖股票的最佳时机 II
  • 中等
    • 714. 买卖股票的最佳时机含手续费
  • 困难
    • 123. 买卖股票的最佳时机 III
    • 188. 买卖股票的最佳时机 IV
    • 309. 最佳买卖股票时机含冷冻期

9. 打家劫舍问题(增加dp维数消除后效性)

  • 简单
    • 198. 打家劫舍
  • 中等
    • 213. 打家劫舍 II
    • 337. 打家劫舍 III —— 树形DP

10. 数学推理

  • 中等
    • 238. 除自身以外数组的乘积
    • 621. 任务调度器

11. 操作矩阵

  • 中等
    • 48. 旋转图像
    • 54. 螺旋矩阵
    • 59. 螺旋矩阵 II
    • 861. 翻转矩阵后的得分
    • 面试题 01.08. 零矩阵

12. n数之和

  • 简单
    • 1. 两数之和
    • 167. 两数之和 II - 输入有序数组
  • 中等
    • 15. 三数之和
    • 16. 最接近的三数之和
    • 18. 四数之和
    • 454. 四数相加 II
    • 923. 三数之和的多种可能
    • 5642. 大餐计数

13. 最大矩形

  • 困难
    • 84. 柱状图中最大的矩形
    • 85. 最大矩形

14. TOPK问题

快速排序:直接通过快排切分,找到第 K 小的数(即下标为 k - 1) 【O(N)】
基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // 最后一个参数表示我们要找的是下标为k-1的数
        return quickSearch(arr, 0, arr.length - 1, k - 1);
    }

    private int[] quickSearch(int[] nums, int left, int right, int k) {
        // 每快排切分1次,找到排序后下标为index的元素,如果index恰好等于k就返回index以及index左边所有的数;
        int index = partition(nums, left, right);
        if (index  == k) {
            return Arrays.copyOf(nums, index + 1);
        }
        // 否则根据下标j与k的大小关系来决定继续切分左段还是右段。
        return index > k? quickSearch(nums, left, index - 1, k): quickSearch(nums, index + 1, right, k);
    }

    // 快排切分,返回下标index,使得比nums[index]小的数都在index的左边,比nums[index]大的数都在index的右边。
    private int partition(int[] nums, int left, int right) {
    	int temp = 0;
    	//基准数据
        int pivot = nums[left];
        //划分区间:基准左面的数小,基准右面的数大
		while (left < right) {
			//如果队尾的元素大于基准,左移
			while (nums[right] >= pivot && left < right) {
				right--;
			}
			//如果队尾元素小于pivot了,交换
			if (nums[right] < pivot) {
				temp = nums[left];
				nums[left] = nums[right];
				nums[right] = temp;
			}
				
			//如果队首的元素小于基准,右移
			while (nums[left] <= pivot && left < right) {
				left++;
			}
			//如果队首元素大于pivot时,交换
			if(nums[left] > pivot) {
				temp = nums[left];
				nums[left] = nums[right];
				nums[right] = temp;			
			}
		}
	
		//跳出循环时left和right相等,此时的left或right就是pivot的正确索引位置
		return left; 
    }
}

大根堆:自带PriorityQueue 【O(NlogK)】

// 保持堆的大小为K,然后遍历数组中的数字,遍历的时候做如下判断:
// 1. 若目前堆的大小小于K,将当前数字放入堆中。
// 2. 否则判断当前数字与大根堆堆顶元素的大小关系,如果当前数字比大根堆堆顶还大,这个数就直接跳过;
//    反之如果当前数字比大根堆堆顶小,先poll掉堆顶,再将该数字放入堆中。
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // 默认是小根堆,实现大根堆需要重写一下比较器。
        Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1);
        for (int num: arr) {
            if (pq.size() < k) {
                pq.offer(num);
            } else if (num < pq.peek()) {
                pq.poll();
                pq.offer(num);
            }
        }
        
        // 返回堆中的元素
        int[] res = new int[pq.size()];
        int idx = 0;
        for(int num: pq) {
            res[idx++] = num;
        }
        return res;
    }
}

二叉搜索树TreeMap,求得的前K大的数字是有序的 【O(NlogK)】

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // TreeMap的key是数字, value是该数字的个数。
        // cnt表示当前map总共存了多少个数字。
        TreeMap<Integer, Integer> map = new TreeMap<>();
        int cnt = 0;
        for (int num: arr) {
            // 1. 遍历数组,若当前map中的数字个数小于k,则map中当前数字对应个数+1
            if (cnt < k) {
                map.put(num, map.getOrDefault(num, 0) + 1);
                cnt++;
                continue;
            } 
            // 2. 否则,取出map中最大的Key(即最大的数字), 判断当前数字与map中最大数字的大小关系:
            //    若当前数字比map中最大的数字还大,就直接忽略;
            //    若当前数字比map中最大的数字小,则将当前数字加入map中,并将map中的最大数字的个数-1。
            Map.Entry<Integer, Integer> entry = map.lastEntry();
            if (entry.getKey() > num) {
                map.put(num, map.getOrDefault(num, 0) + 1);
                if (entry.getValue() == 1) {
                    map.pollLastEntry();
                } else {
                    map.put(entry.getKey(), entry.getValue() - 1);
                }
            }
            
        }

        // 最后返回map中的元素
        int[] res = new int[k];
        int idx = 0;
        for (Map.Entry<Integer, Integer> entry: map.entrySet()) {
            int freq = entry.getValue();
            while (freq-- > 0) {
                res[idx++] = entry.getKey();
            }
        }
        return res;
    }
}

数据范围有限,直接计数排序 【O(N)】
基本思想:开辟一个额外空间(数组),统计元素个数,然后遍历这个数组,依此添加个数大于0的元素
详情参考:4种解法秒杀TopK(快排/堆/二叉搜索树/计数排序)❤️

数据结构与算法——LeetCode刷题记录_第5张图片

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // 统计每个数字出现的次数
        int[] counter = new int[10001];
        for (int num: arr) {
            counter[num]++;
        }
        // 根据counter数组从头找出k个数作为返回结果
        int[] res = new int[k];
        int idx = 0;
        for (int num = 0; num < counter.length; num++) {
            while (counter[num]-- > 0 && idx < k) {
                res[idx++] = num;
            }
            if (idx == k) {
                break;
            }
        }
        return res;
    }
}
  • 简单
    • 剑指 Offer 40. 最小的k个数

你可能感兴趣的:(数据结构与算法)