- 数组
- 1、二分
- 35. 搜索插入位置">9.1 35. 搜索插入位置
- 704. 二分查找">9.1 704. 二分查找
- 367. 有效的完全平方数">9.2 367. 有效的完全平方数
- 69. x 的平方根">9.2 69. x 的平方根
- 2、移除元素
- 27. 移除元素 双指针—快慢指针">9.2 27. 移除元素 双指针—快慢指针
- 283. 移动零">9.3 283. 移动零
- 844. 比较含退格的字符串">9.3 844. 比较含退格的字符串
- 3、有序数组的平方
- 977. 有序数组的平方 双指针 头尾">9.5 977. 有序数组的平方 双指针 头尾
- 4、滑动窗口
- 209. 长度最小的子数组">M9.5-209. 长度最小的子数组
- 904. 水果成篮">M9.5-904. 水果成篮
- 76. 最小覆盖子串">H9.6-76. 最小覆盖子串
- 螺旋矩阵
59. 螺旋矩阵 II">//M9.7-59. 螺旋矩阵 II- 54. 螺旋矩阵">M9.7-54. 螺旋矩阵
剑指 Offer 29. 顺时针打印矩阵">//S9.8-剑指 Offer 29. 顺时针打印矩阵
- 1、二分
- 链表
203. 移除链表元素">//S9.8-203. 移除链表元素- 707. 设计链表">M9.8-707. 设计链表
206. 反转链表">//S9.8-206. 反转链表- 24. 两两交换链表中的节点">M9.9-24. 两两交换链表中的节点
- 19. 删除链表的倒数第 N 个结点">//M9.10-19. 删除链表的倒数第 N 个结点
- 面试题 02.07. 链表相交">S9.10-面试题 02.07. 链表相交
- 142. 环形链表 II — 找环的入口">M9.10-142. 环形链表 II — 找环的入口
- 哈希表
- 有效的字母异位词
- 242. 有效的字母异位词">//S9.11-242. 有效的字母异位词
- 383. 赎金信">S9.12-383. 赎金信
- 49. 字母异位词分组">M9.12-49. 字母异位词分组
- 438. 找到字符串中所有字母异位词">M9.13-438. 找到字符串中所有字母异位词
- 两个数组的交集
- 349. 两个数组的交集">S9.14-349. 两个数组的交集
- 350. 两个数组的交集 II">S9.14-350. 两个数组的交集 II
- 其他
- 202. 快乐数">S9.14-202. 快乐数
- 1. 两数之和">S9.14-1. 两数之和
- 454. 四数相加 II">M9.15-454. 四数相加 II
- 15. 三数之和">M9.16-15. 三数之和
- 18. 四数之和">M9.17-18. 四数之和
- 有效的字母异位词
- 字符串
- 541. 反转字符串 II">S9.18-541. 反转字符串 II
- 剑指 Offer 58 - II. 左旋转字符串 不使用额外空间,在原基础上操作">S9.18-剑指 Offer 58 - II. 左旋转字符串 不使用额外空间,在原基础上操作
- 28. 实现 strStr()">**S9.19-28. 实现 strStr()
- 459. 重复的子字符串">**S9.20-459. 重复的子字符串
- 双指针
- 151. 翻转字符串里的单词">M9.21-151. 翻转字符串里的单词
- 面试题 02.07. 链表相交">S9.23-面试题 02.07. 链表相交
- 19. 删除链表的倒数第 N 个结点">M9.23-19. 删除链表的倒数第 N 个结点
- 142. 环形链表 II">M9.23-142. 环形链表 II
- 15. 三数之和">M9.24-15. 三数之和
- 18. 四数之和">M9.24-18. 四数之和
- 栈与队列
- 225. 用队列实现栈">S9.25-225. 用队列实现栈
- 150. 逆波兰表达式求值">M9.25-150. 逆波兰表达式求值
- 239. 滑动窗口最大值">*H9.26-239. 滑动窗口最大值
- 347. 前 K 个高频元素">M9.27-347. 前 K 个高频元素
- 二叉树
- 144. 二叉树的前序遍历">S9.28-144. 二叉树的前序遍历
- 145. 二叉树的后序遍历">S9.28-145. 二叉树的后序遍历
- 94. 二叉树的中序遍历">S9.28-94. 二叉树的中序遍历
- 102. 二叉树的层序遍历">M9.29-102. 二叉树的层序遍历
- 107. 二叉树的层序遍历 II">M9.29-107. 二叉树的层序遍历 II
- 199. 二叉树的右视图">M10.2-199. 二叉树的右视图
- 226. 翻转二叉树">S10.6-226. 翻转二叉树
- 101. 对称二叉树">S10.6-101. 对称二叉树
- 104. 二叉树的最大深度">S10.7-104. 二叉树的最大深度
- 559. N 叉树的最大深度">S10.9-559. N 叉树的最大深度
- 111. 二叉树的最小深度">S10.9-111. 二叉树的最小深度
- 222. 完全二叉树的节点个数">M10.10-222. 完全二叉树的节点个数
- 110. 平衡二叉树">S10.12-110. 平衡二叉树
- 257. 二叉树的所有路径">S10.13-257. 二叉树的所有路径
- 100. 相同的树">S10.17-100. 相同的树
- 404. 左叶子之和">S10.17-404. 左叶子之和
- 513. 找树左下角的值">M10.17-513. 找树左下角的值
- 112. 路径总和">S10.18-112. 路径总和
- 113. 路径总和 II">M10.18-113. 路径总和 II
- 106. 从中序与后序遍历序列构造二叉树">M10.26-106. 从中序与后序遍历序列构造二叉树
- 105. 从前序与中序遍历序列构造二叉树">M10.26-105. 从前序与中序遍历序列构造二叉树
- 654. 最大二叉树">M10.27-654. 最大二叉树
- 617. 合并二叉树">S10.27-617. 合并二叉树
- 700. 二叉搜索树中的搜索">S10.28-700. 二叉搜索树中的搜索
- 98. 验证二叉搜索树">M10.29-98. 验证二叉搜索树
- 530. 二叉搜索树的最小绝对差">S10.30-530. 二叉搜索树的最小绝对差
- 501. 二叉搜索树中的众数">S10.31-501. 二叉搜索树中的众数
- 236. 二叉树的最近公共祖先">M11.1-236. 二叉树的最近公共祖先
- 235. 二叉搜索树的最近公共祖先">S11.3-235. 二叉搜索树的最近公共祖先
- 701. 二叉搜索树中的插入操作">M11.3-701. 二叉搜索树中的插入操作
- 450. 删除二叉搜索树中的节点">M11.04-450. 删除二叉搜索树中的节点
- 669. 修剪二叉搜索树">M11.05-669. 修剪二叉搜索树
- 108. 将有序数组转换为二叉搜索树">S11.06-108. 将有序数组转换为二叉搜索树
- 538. 把二叉搜索树转换为累加树">M11.07-538. 把二叉搜索树转换为累加树
- 回溯
- 77. 组合">M11.07-77. 组合
- 216. 组合总和 III">M11.08-216. 组合总和 III
- 17. 电话号码的字母组合">M11.09-17. 电话号码的字母组合
- 39. 组合总和">M11.10-39. 组合总和
- 40. 组合总和 II">M11.11-40. 组合总和 II
- 131. 分割回文串">M11.12-131. 分割回文串
- 93. 复原 IP 地址">M11.13-93. 复原 IP 地址
- 78. 子集">M11.14-78. 子集
- 90. 子集 II">M11.14-90. 子集 II
- 491. 递增子序列">M11.14-491. 递增子序列
- 46. 全排列">M11.14-46. 全排列
- 47. 全排列 II">M11.14-47. 全排列 II
- 332. 重新安排行程">*H11.18-332. 重新安排行程
- 51. N 皇后">*H11.19-51. N 皇后
- 37. 解数独">*H11.20-37. 解数独
- 贪心
- 455. 分发饼干">//S11.21-455. 分发饼干
- 376. 摆动序列">M11.21-376. 摆动序列
- 53. 最大子序和">S11.22-53. 最大子序和
- 122. 买卖股票的最佳时机 II">M11.22-122. 买卖股票的最佳时机 II
- 55. 跳跃游戏">M11.23-55. 跳跃游戏
- 45. 跳跃游戏 II">*M11.24-45. 跳跃游戏 II
- 1005. K 次取反后最大化的数组和">S11.25-1005. K 次取反后最大化的数组和
- 134. 加油站">*M11.26-134. 加油站
- 135. 分发糖果">*H11.27-135. 分发糖果
- 860. 柠檬水找零">S11.29-860. 柠檬水找零
- 406. 根据身高重建队列">*M11.29-406. 根据身高重建队列
- 452. 用最少数量的箭引爆气球">*M11.29-452. 用最少数量的箭引爆气球
- 435. 无重叠区间">*M11.30-435. 无重叠区间
- 763. 划分字母区间">*M12.1-763. 划分字母区间
- 56. 合并区间">M12.02-56. 合并区间
- 738. 单调递增的数字">M12.02-738. 单调递增的数字
- 714. 买卖股票的最佳时机含手续费">*M12.03-714. 买卖股票的最佳时机含手续费
- 968. 监控二叉树">*H12.05-968. 监控二叉树
- 动态规划
- 746. 使用最小花费爬楼梯">S12.06-746. 使用最小花费爬楼梯
- 62. 不同路径">M12.7-62. 不同路径
- 63. 不同路径 II">M12.7-63. 不同路径 II
- 343. 整数拆分">M12.8-343. 整数拆分
- 416. 分割等和子集">*M12.12-416. 分割等和子集
- 1049. 最后一块石头的重量 II">*M12.13-1049. 最后一块石头的重量 II
- 494. 目标和">*M12.13-494. 目标和
- 474. 一和零">*M12.18-474. 一和零
- 518. 零钱兑换 II">M12.20-518. 零钱兑换 II
- 322. 零钱兑换">*M12.22-322. 零钱兑换
- 279. 完全平方数">M12.23-279. 完全平方数
- 139. 单词拆分">*M12.27-139. 单词拆分
- 198. 打家劫舍">M12.29-198. 打家劫舍
- 213. 打家劫舍 II">M12.30-213. 打家劫舍 II
- 337. 打家劫舍 III">*M1.1-337. 打家劫舍 III
- 121. 买卖股票的最佳时机">S1.2-121. 买卖股票的最佳时机
- 122. 买卖股票的最佳时机 II">M1.2-122. 买卖股票的最佳时机 II
- 123. 买卖股票的最佳时机 III">H1.04-123. 买卖股票的最佳时机 III
- 188. 买卖股票的最佳时机 IV">H1.04-188. 买卖股票的最佳时机 IV
- 309. 最佳买卖股票时机含冷冻期">*M1.05-309. 最佳买卖股票时机含冷冻期
- 714. 买卖股票的最佳时机含手续费">M1.06-714. 买卖股票的最佳时机含手续费
- 300. 最长递增子序列">M1.07-300. 最长递增子序列
- 674. 最长连续递增序列">//S1.08-674. 最长连续递增序列
- 718. 最长重复子数组">*M1.08-718. 最长重复子数组
- 1143. 最长公共子序列">*M1.09-1143. 最长公共子序列
- 1035. 不相交的线">M1.11-1035. 不相交的线
- 53. 最大子数组和">S1.12-53. 最大子数组和
- 392. 判断子序列">S1.12-392. 判断子序列
- 115. 不同的子序列">*H1.18-115. 不同的子序列
- 583. 两个字符串的删除操作">*M1.19-583. 两个字符串的删除操作
- 72. 编辑距离">*H1.21-72. 编辑距离
- 647. 回文子串">*M2.3-647. 回文子串
- 516. 最长回文子序列">*M2.4-516. 最长回文子序列
- 单调栈
- 739. 每日温度">*M2.24-739. 每日温度
- 496. 下一个更大元素 I">S2.25-496. 下一个更大元素 I
- 503. 下一个更大元素 II">M2.26-503. 下一个更大元素 II
- 42. 接雨水">*H2.26-42. 接雨水
- 84. 柱状图中最大的矩形">*H2.27-84. 柱状图中最大的矩形
数组
1、二分
9.1 35. 搜索插入位置
9.1 704. 二分查找
9.2 367. 有效的完全平方数
9.2 69. x 的平方根
2、移除元素
9.2 27. 移除元素 双指针—快慢指针
9.3 283. 移动零
9.3 844. 比较含退格的字符串
双指针
class Solution {public boolean backspaceCompare(String s, String t) {int i = s.length() - 1, j = t.length() - 1;int skipS = 0, skipT = 0;while (i >= 0 || j >= 0) {while (i >= 0) {if (s.charAt(i) == '#') {skipS++;i--;}else if (skipS > 0) {skipS--;i--;}else {break;}}while (j >= 0) {if (t.charAt(j) == '#') {skipT++;j--;}else if (skipT > 0) {skipT--;j--;}else {break;}}if (i >= 0 && j >= 0) {if (s.charAt(i) != t.charAt(j)) {return false;}} else if (i >= 0 || j >= 0) {return false;}i--;j--;}return true;}}
3、有序数组的平方
9.5 977. 有序数组的平方 双指针 头尾
4、滑动窗口
M9.5-209. 长度最小的子数组
class Solution {public int minSubArrayLen(int target, int[] nums) {int left = 0, sum = 0, res = Integer.MAX_VALUE;for (int j = 0; j < nums.length; j++) {sum += nums[j];while (sum >= target) {res = Math.min(res, j - left + 1);sum -= nums[left++];}}return res == Integer.MAX_VALUE ? 0 : res;}}
M9.5-904. 水果成篮
class Solution {public int totalFruit(int[] fruits) {if (fruits == null || fruits.length == 0) return 0;int l = 0, ans = 0;HashMap<Integer, Integer> map = new HashMap<>();for (int i = 0; i < fruits.length; i++) {map.put(fruits[i], map.getOrDefault(fruits[i], 0) + 1);while (map.size() > 2) {map.put(fruits[l], map.get(fruits[l]) - 1);if (map.get(fruits[l]) == 0) map.remove(fruits[l]);l++;}ans = Math.max(ans, i - l + 1);}return ans;}}
H9.6-76. 最小覆盖子串
class Solution {Map<Character, Integer> need = new HashMap<>();Map<Character, Integer> window = new HashMap<>();public String minWindow(String s, String t) {if (s == null || t == null || s.length() < t.length() ) return "";for (int i = 0; i < t.length(); i++) {char c = t.charAt(i);need.put(c, need.getOrDefault(c, 0) + 1);}int l = 0, r = 0;int len = Integer.MAX_VALUE, start = 0;//表示window窗口中匹配成功的字符种类数(非字符个数)//例如子串AABC字符种类是3,匹配了两个A,valid才会+1int valid = 0;//如果右指针超过了母串边界,则表示已经遍历完所有窗口组合while (r < s.length()) {char c = s.charAt(r);r++; //右指针向右滑动//如果右指针指向的字符是子串中的字符,那么更新窗口中的数据if (need.containsKey(c)) {window.put(c, window.getOrDefault(c, 0) + 1);if (window.get(c).equals(need.get(c))) {valid++;}}//窗口收缩while (valid == need.size()) {//len用来记录最小窗口长度//start用来记录最小窗口开始的索引if (r - l < len) {start = l;len = r - l;}char d = s.charAt(l);l++;if (need.containsKey(d)) {if (window.get(d).equals(need.get(d))) {valid--;}window.put(d, window.getOrDefault(d, 0) - 1);}}}return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);}}
螺旋矩阵
//M9.7-59. 螺旋矩阵 II
M9.7-54. 螺旋矩阵
错误版本
List<Integer> res = new ArrayList<>();if (matrix == null || matrix.length == 0) return res;int l = 0, r = matrix[0].length - 1, top = 0, low = matrix.length - 1;// int count = 0;while (l <= r && top <= low) {for (int i = l; i <= r; i++) {res.add(matrix[top][i]);// count++;}top++;for (int i = top; i <= low; i++) {res.add(matrix[i][r]);// count++;}r--;for (int i = r; i >= l; i--) {res.add(matrix[low][i]);// count++;}low--;for (int i = low; i >= top; i--) {res.add(matrix[i][l]);// count++;}l++;}return res;}
class Solution {public List<Integer> spiralOrder(int[][] matrix) {List<Integer> res = new ArrayList<>();if (matrix == null || matrix.length == 0) return res;int l = 0, r = matrix[0].length - 1, top = 0, low = matrix.length - 1;while (l <= r && top <= low) {for (int i = l; i <= r; i++) {res.add(matrix[top][i]);}top++;for (int i = top; i <= low; i++) {res.add(matrix[i][r]);}r--;if (l <= r && top <= low) {for (int i = r; i >= l; i--) {res.add(matrix[low][i]);}low--;for (int i = low; i >= top; i--) {res.add(matrix[i][l]);}l++;}}return res;}}
//S9.8-剑指 Offer 29. 顺时针打印矩阵
链表
//S9.8-203. 移除链表元素
M9.8-707. 设计链表
//S9.8-206. 反转链表
M9.9-24. 两两交换链表中的节点
class Solution {public ListNode swapPairs(ListNode head) {if (head == null || head.next == null) return head;ListNode pre = head, cur = head.next, res = head.next;ListNode indicator = new ListNode(0);while (cur != null) {pre.next = cur.next;cur.next = pre;indicator.next = cur;indicator = pre;pre = pre.next;if (pre != null)cur = pre.next;else break;}return res;}}class Solution {public ListNode swapPairs(ListNode head) {ListNode indi = new ListNode(0);indi.next = head;ListNode pre = indi;while (head != null && head.next != null) {ListNode tmp = head.next.next;pre.next = head.next;head.next.next = head;head.next = tmp;pre = head;head = tmp;}return indi.next;}}
//M9.10-19. 删除链表的倒数第 N 个结点
S9.10-面试题 02.07. 链表相交
M9.10-142. 环形链表 II — 找环的入口
快慢指针,第一次相遇后,快指针从head开始, 然后快慢指针每次都只移动一步
public class Solution {public ListNode detectCycle(ListNode head) {if (head == null || head.next == null) return null;ListNode fast = head, slow = head;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) {break;}}if (fast == null || fast.next == null) return null;fast = head;while (fast != slow) {fast = fast.next;slow = slow.next;}return slow;}}
哈希表
有效的字母异位词
//S9.11-242. 有效的字母异位词
S9.12-383. 赎金信
M9.12-49. 字母异位词分组
class Solution {public List<List<String>> groupAnagrams(String[] strs) {//创建一个mapMap<String, List<String>> map = new HashMap<>();for (String str : strs) {char[] chars = str.toCharArray();//将字符排序Arrays.sort(chars);//排序后的字符作为keyString key = String.valueOf(chars);if (!map.containsKey(key)) {//如果没有key那么新建一个分组map.put(key, new ArrayList<String>());}//根据key往分组中加入字符串map.get(key).add(str);}//返回分组后的结果return new ArrayList<List<String>>(map.values());}}class Solution {public List<List<String>> groupAnagrams(String[] strs) {Map<String, List<String>> map = new HashMap<>();for (String str : strs) {char[] chars = str.toCharArray();int[] count = new int[26];for (char c : chars) {count[c - 'a']++;}StringBuffer sb = new StringBuffer();for (int i = 0; i < 26; i++) {if (count[i] != 0) {sb.append((char)('a' + i));sb.append(count[i]);}}String key = sb.toString();List<String> list = map.getOrDefault(key, new ArrayList<String>());list.add(str);map.put(key, list);}return new ArrayList<List<String>>(map.values());}}
M9.13-438. 找到字符串中所有字母异位词
class Solution {public List<Integer> findAnagrams(String s, String p) {List<Integer> res = new ArrayList<>();if (s.length() < p.length()) return res;//用来记录目标串各字母出现的个数int[] need = new int[26];for (char c : p.toCharArray()) {need[c - 'a']++;}//滑动窗口的左右边界int l = 0, r = 0;//用来存储当前窗口串各字母出现的个数int[] window = new int[26];char[] chars = s.toCharArray();while (r < s.length()) {//窗口扩大window[chars[r] - 'a']++;//如果窗口下字符串长度跟p一样判断是否相等if ((r - l + 1) == p.length()) {//如果相等,将左边界添加值结果中if (Arrays.equals(need, window)) res.add(l);//窗口缩小,去除相应的字符window[chars[l] - 'a']--;l++;}//窗口扩大r++;}return res;}}
两个数组的交集
S9.14-349. 两个数组的交集
S9.14-350. 两个数组的交集 II
其他
S9.14-202. 快乐数
===================快慢指针=======================class Solution {public boolean isHappy(int n) {int slow = n;int fast = getNext(n);while (fast != 1 && slow != fast) {slow = getNext(slow);fast = getNext(getNext(fast));}return fast == 1;}public int getNext(int n) {int totalSum = 0;while (n > 0) {int d = n % 10;n = n / 10;totalSum += d * d;}return totalSum;}}
S9.14-1. 两数之和
M9.15-454. 四数相加 II
class Solution {public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {if (nums1 == null || nums2 == null || nums3 == null || nums4 == null) return 0;if (nums1.length == 0) return 0;Map<Integer, Integer> map = new HashMap<>();for (int i : nums1) {for (int j : nums2) {int temp = i + j;map.put(temp, map.getOrDefault(temp, 0) + 1);}}int res = 0;for (int i : nums3) {for (int j : nums4) {int sum = i + j;if (map.containsKey(0 - sum)) {res += map.get(0 - sum);}}}return res;}}
M9.16-15. 三数之和
class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> res = new ArrayList<>();if (nums.length < 3 || nums == null) return res;Arrays.sort(nums);for (int i = 0; i < nums.length - 2; i++) {//如果最小的数都大于0了,那么不可能存在三个数和为0if (nums[i] > 0) return res;//跳过重复项if (i > 0 && nums[i] == nums[i - 1]) continue;int left = i + 1, right = nums.length - 1;while (left < right) {int sum = nums[i] + nums[left] + nums[right];if (sum > 0) right--;else if (sum < 0) left++;else {//等于0res.add(Arrays.asList(nums[i], nums[left], nums[right]));//跳过右边重复项while (right > left && nums[right] == nums[right - 1]) right--;//跳过左边重复项while (right > left && nums[left] == nums[left + 1]) left++;//收紧,因为当前已经等于0了,而数组是排序的所以头尾指针需要同时向中间移动//left++相当于指向更大的数,相应的right--相当于指向更小的数,这样才可能出现和为0right--;left++;}}}return res;}}
M9.17-18. 四数之和
思路与三数之和类似,但是注意不要判断num[i] >target时直接return 例如存在负数的情况 -5 + -3 + -2 + - 1 = -11,-5 > -11,但是是存在解的
字符串
S9.18-541. 反转字符串 II
class Solution {public String reverseStr(String s, int k) {if (s == null || s.length() == 1) return s;char[] ch = s.toCharArray();for (int i = 0; i < ch.length; i += 2*k) {int l = i, r = Math.min(ch.length - 1, l + k - 1);while (l < r) {ch[l] ^= ch[r];ch[r] ^= ch[l];ch[l] ^= ch[r];l++;r--;}}return new String(ch);}}
S9.18-剑指 Offer 58 - II. 左旋转字符串 不使用额外空间,在原基础上操作
class Solution {public String reverseLeftWords(String s, int n) {int len=s.length();StringBuilder sb=new StringBuilder(s);reverseString(sb,0,n-1);reverseString(sb,n,len-1);return sb.reverse().toString();}public void reverseString(StringBuilder sb, int start, int end) {while (start < end) {char temp = sb.charAt(start);sb.setCharAt(start, sb.charAt(end));sb.setCharAt(end, temp);start++;end--;}}}
**S9.19-28. 实现 strStr()
KMP算法
class Solution {public int strStr(String haystack, String needle) {if (needle.isEmpty()) return 0;char[] ss = haystack.toCharArray();char[] p = needle.toCharArray();// 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相关的)int[] next = new int[p.length];//初始化next数组for (int i = 1, j = 0; i < p.length; i++) {//匹配不成功,j回退到next[j-1]的位置while (j > 0 && p[i] != p[j]) j = next[j - 1];//匹配成功j+1;if (p[i] == p[j]) j++;//更新next[i],进入下一次循环next[i] = j;}//匹配模式串for (int i = 0, j = 0; i < ss.length; i++) {//匹配不成功,j回退到next[j-1]的位置while (j > 0 && p[j] != ss[i]) j = next[j - 1];//匹配成功j+1;if (ss[i] == p[j]) j++;//如果全部匹配完成返回下标if (j == p.length) return i - p.length + 1;}return -1;}}
**S9.20-459. 重复的子字符串
解法一: 效率更高
KMP算法,判断next[len - 1]是否有相同前后缀子串,如果有 判断len % (len - next[len -1]) == 0,成立即表示存在重复子串构成母串,其他情况均为false
class Solution {public boolean repeatedSubstringPattern(String s) {if (s.length() < 2 || s == null) return false;int len = s.length();int[] next = new int[s.length()];for (int i = 1, j = 0; i < len; i++) {while (j > 0 && s.charAt(j) != s.charAt(i)) j = next[j - 1];if (s.charAt(j) == s.charAt(i)) j++;next[i] = j;}if (next[len - 1] == 0) return false;return len % (len - next[len - 1]) == 0 ? true : false;}}
解法二:
如果字符串可以通过子串重复组成,那么每次可以移动一位,比较移动后的字符串是否与原字符串匹配,如果匹配那么则存在重复子串可组成原字符串。移动的次数最多不超过length-1次,因为移动length次就变成了原字符串。这样的话对于很长的字符串效率会很低,为了避免这种无用的环绕,可以创建一个新的字符串 str,它等于原来的字符串 S 再加上 S 自身,这样其实就包含了所有移动的字符串。
反例:
比如字符串:S = acd,那么str = S + S = acdacdacd移动的可能:dac、cda。其实都包含在了 str 中了。就像一个滑动窗口
一开始 acd (acd) ,移动一次 ac(dac)d,移动两次a(cda)cd。循环结束
所以可以直接判断 str 中去除首尾元素之后,是否包含自身元素。如果包含。则表明存在重复子串。
正例:S=abab str=abababababab移动的可能:baba、abab,一开始abab(abab),移动一次aba(baba)b,移动两次ab(abab)ab。找到了原字符串。
class Solution {public boolean repeatedSubstringPattern(String s) {String str = s + s;return str.substring(1, str.length() - 1).contains(s);}}
双指针
M9.21-151. 翻转字符串里的单词
不使用split
class Solution {public String reverseWords(String s) {//1.去除首尾以及中间多余的空格StringBuilder sb = removeSpace(s);//2.翻转整个字符串reverseString(sb, 0, sb.length() - 1);//3.翻转单个单词reverseEachWord(sb);return sb.toString();}private StringBuilder removeSpace(String s) {int start = 0, end = s.length() - 1;//去除头尾空格while (s.charAt(start) == ' ') start++;while (s.charAt(end) == ' ') end--;StringBuilder sb = new StringBuilder();while (start <= end) {char c = s.charAt(start);//只有在sb的最后一个字符是' '同时c也是' '时,表示单词中间出现了多余的空格//此时不添加字符到sb中,指针移动// if (!(c == ' ' && sb.charAt(sb.length() - 1) == ' ')) sb.append(c);if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') sb.append(c);start++;}return sb;}private void reverseString(StringBuilder sb, int l, int r) {while (l < r) {char tmp = sb.charAt(l);sb.setCharAt(l, sb.charAt(r));sb.setCharAt(r, tmp);l++;r--;}}private void reverseEachWord(StringBuilder sb) {int l = 0, r = 1;int n = sb.length();while (l < n) {//找到单词中间的空格,r的位置即为空格的indexwhile (r < n && sb.charAt(r) != ' ') r++;//[l, r - 1]位置上的字符就是一个单词,翻转reverseString(sb, l, r - 1);// 跳过空格l = r + 1;r = l + 1;}}}
S9.23-面试题 02.07. 链表相交
public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {ListNode a = headA, b = headB;while (a != b) {a = a == null ? headB : a.next;b = b == null ? headA : b.next;}return a;}}
M9.23-19. 删除链表的倒数第 N 个结点
class Solution {public ListNode removeNthFromEnd(ListNode head, int n) {ListNode tmp = new ListNode(-1);tmp.next = head;ListNode cur = tmp, fast = tmp;while (n >= 0 && fast != null) {fast = fast.next;n--;}while (fast != null) {fast = fast.next;cur = cur.next;}cur.next = cur.next.next;return tmp.next;}}
M9.23-142. 环形链表 II
public class Solution {public ListNode detectCycle(ListNode head) {ListNode slow = head, fast = head;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) {fast = head;break;}}if (fast == null || fast.next == null) return null;while (fast != slow) {fast = fast.next;slow = slow.next;}return fast;}}
M9.24-15. 三数之和
class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> res = new ArrayList<>();if (nums.length < 3 || nums == null) return res;Arrays.sort(nums);for (int i = 0; i < nums.length - 2; i++) {//第一个数大于0就不用找了if (nums[i] > 0) return res;//第一个数去重if (i > 0 && nums[i] == nums[i - 1]) continue;int l = i + 1, r = nums.length - 1;if (nums[i] + nums[l] > 0) return res;while (l < r) {int sum = nums[l] + nums[r] + nums[i];if (sum > 0) r--;else if (sum < 0) l++;else {res.add(Arrays.asList(nums[l], nums[r], nums[i]));while (l < r && nums[l] == nums[l + 1]) l++;while (l < r && nums[r - 1] == nums[r]) r--;l++;r--;}}}return res;}}
M9.24-18. 四数之和
class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {List<List<Integer>> res = new ArrayList<>();if (nums.length < 4 || nums == null) return res;Arrays.sort(nums);for (int i = 0; i < nums.length - 3; i++) {if (i > 0 && nums[i] == nums[i - 1]) continue;for (int j = i + 1; j < nums.length - 2; j++) {if (j > i + 1 && nums[j] == nums[j - 1]) continue;int l = j + 1, r = nums.length - 1;while (l < r) {int sum = nums[i] + nums[j] + nums[l] + nums[r];if (sum > target) r--;else if (sum < target) l++;else {res.add(Arrays.asList(nums[i], nums[j], nums[l], nums[r]));while (l < r && nums[l] == nums[l + 1]) l++;while (l < r && nums[r] == nums[r - 1]) r--;l++;r--;}}}}return res;}}
栈与队列
S9.25-225. 用队列实现栈
解法一:用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
class MyStack {Queue<Integer> que1;Queue<Integer> que2;public MyStack() {que1 = new LinkedList<>();que2 = new LinkedList<>();}public void push(int x) {que1.offer(x);}public int pop() {int ans = Integer.MIN_VALUE;int len = que1.size();for (int i = 0; i < len; i++) {if (i == len - 1) ans = que1.poll();else {que2.offer(que1.poll());}}while (!que2.isEmpty()) {que1.offer(que2.poll());}return ans;}public int top() {int a = pop();que1.offer(a);return a;}public boolean empty() {return que1.isEmpty();}}
优化解法:其实这道题目就是用一个队里就够了。
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
class MyStack {Queue<Integer> que1;public MyStack() {que1 = new LinkedList<>();}public void push(int x) {que1.offer(x);}public int pop() {int ans = Integer.MIN_VALUE;int len = que1.size();for (int i = 0; i < len; i++) {if (i == len - 1) ans = que1.poll();else {que1.offer(que1.poll());}}return ans;}public int top() {int a = pop();que1.offer(a);return a;}public boolean empty() {return que1.isEmpty();}}
M9.25-150. 逆波兰表达式求值
class Solution {public int evalRPN(String[] tokens) {Stack<Integer> stack = new Stack<>();for (String s : tokens) {if (s.equals("+") || s.equals("-") || s.equals("/") || s.equals("*")) {int a = stack.pop();int b = stack.pop();switch(s) {case "+" :stack.push(a + b);break;case "-" :stack.push(b - a);break;case "*" :stack.push(b * a);break;default : stack.push(b / a);}}else stack.push(Integer.parseInt(s));}return stack.pop();}}
*H9.26-239. 滑动窗口最大值
class Solution {public int[] maxSlidingWindow(int[] nums, int k) {int[] res = new int[nums.length - k + 1];if (k > nums.length) return new int[0];int j = 0;int index = 0;Deque<Integer> deque = new LinkedList<>();for (int i = 0; i < nums.length; i++) {while (!deque.isEmpty() && deque.peekLast() < nums[i]) {deque.pollLast();}deque.offer(nums[i]);if (i - j + 1 == k) {res[index++] = deque.peek();if (nums[j] == deque.peek())deque.pollFirst();j++;}}return res;}}
M9.27-347. 前 K 个高频元素
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
优先队列
class Solution {public int[] topKFrequent(int[] nums, int k) {PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue());Map<Integer, Integer> map = new HashMap<>();for (int num : nums) {map.put(num, map.getOrDefault(num, 0) + 1);}for (Map.Entry<Integer, Integer> entry : map.entrySet()) {queue.offer(entry);if (queue.size() > k) queue.poll();}int[] res = new int[k];for (int i = 0; i < k; i++) {res[i] = queue.poll().getKey();}return res;}}
桶排序
//桶排序public int[] topKFrequent(int[] nums, int k) {//map用来存储每个数字出现的次数Map<Integer, Integer> map = new HashMap<>();for (int num : nums) {map.put(num, map.getOrDefault(num, 0) + 1);}//桶排序,数字出现的次数对应桶的索引List<Integer>[] list = new List[nums.length + 1];for (int key : map.keySet()) {//获取数字出现的次数,作为桶list的下标int i = map.get(key);if (list[i] == null) {list[i] = new ArrayList<>();}//存储出现i次的数字,可能有多个数字出现次数相同list[i].add(key);}//存储结果,k个出现频率最高的数int[] res = new int[k];int index = 0;for (int i = list.length - 1; i > 0 && index < k; i--) {if (list[i] == null) continue;for (Integer num : list[i]) {res[index++] = num;}}return res;}
二叉树
S9.28-144. 二叉树的前序遍历
递归:
class Solution {List<Integer> res = new ArrayList<>();public List<Integer> inorderTraversal(TreeNode root) {preOrder(root);return res;}public void preOrder(TreeNode root) {if(root == null) return;res.add(root.val);preOrder(root.left);preOrder(root.right);}}
非递归:
class Solution {public List<Integer> preorderTraversal(TreeNode root) {if (root == null) return new ArrayList();List<Integer> res = new ArrayList<>();Stack<TreeNode> stack = new Stack<TreeNode>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();res.add(node.val);if (node.right != null)stack.push(node.right);if (node.left != null)stack.push(node.left);}return res;}}
S9.28-145. 二叉树的后序遍历
递归:
class Solution {List<Integer> res = new ArrayList<>();public List<Integer> postorderTraversal(TreeNode root) {postOrder(root);return res;}public void postOrder(TreeNode root) {if(root == null) return;postOrder(root.left);postOrder(root.right);res.add(root.val);}}
非递归:
class Solution {public List<Integer> postorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<>();if (root == null) return res;Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();res.add(node.val);if (node.left != null)stack.push(node.left);if (node.right != null)stack.push(node.right);}Collections.reverse(res);return res;}}
非递归统一写法:
class Solution {public List<Integer> postorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<>();if (root == null) return res;Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();// 通过null标记下一个节点为需要处理的节点// 如果当前节点不为null,则表示该节点只是访问而不处理if (node != null) {stack.push(node); // 添加中间节点stack.push(null); // 中间节点已访问,但是没有处理通过null标记// 添加右节点,空节点不入栈if (node.right != null) stack.push(node.right);// 添加左节点,空节点不入栈if (node.left != null) stack.push(node.left);}else { // 如果当前节点为null,这表示下一个节点是需要处理的节点node = stack.pop();res.add(node.val);}}return res;}}
S9.28-94. 二叉树的中序遍历
递归:
class Solution {List<Integer> res = new ArrayList<>();public List<Integer> inorderTraversal(TreeNode root) {inOrder(root);return res;}public void inOrder(TreeNode root) {if(root == null) return;inOrder(root.left);res.add(root.val);inOrder(root.right);}}
非递归:
class Solution {List<Integer> res = new ArrayList<>();public List<Integer> inorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>();while(root != null || !stack.isEmpty()) {//将当前节点,以及其左孩子全部入栈while (root != null) { //访问结点,只到最底层stack.push(root); //将访问的节点入栈root = root.left; //左子树}//从栈里面弹出的节点就是要遍历的节点TreeNode node = stack.pop();res.add(node.val);// 指向右子树root = node.right;}return res;}}
M9.29-102. 二叉树的层序遍历
递归: DFS
class Solution {List<List<Integer>> res = new ArrayList<>();public List<List<Integer>> levelOrder(TreeNode root) {DFS(root, 0);return res;}public void DFS(TreeNode node, int deep) {if (node == null) return;deep++; //当前在第几层if (res.size() < deep) {//当层级增加时,res中的Item也增加,用Item的数量来表示层级,//也就是通过res的索引来表示二叉树的第几层res.add(new ArrayList<Integer>());}res.get(deep - 1).add(node.val);DFS(node.left, deep);DFS(node.right, deep);}}
非递归:利用队列 BFS
class Solution {List<List<Integer>> res = new ArrayList<>();public List<List<Integer>> levelOrder(TreeNode root) {BFS(root);return res;}public void BFS(TreeNode node) {if (node == null) return;Queue<TreeNode> que = new LinkedList<>();que.offer(node);while (!que.isEmpty()) {int len = que.size();List<Integer> arr = new ArrayList<>();for (int i = 0; i < len; i++) {TreeNode tmp = que.poll();arr.add(tmp.val);if (tmp.left != null) que.offer(tmp.left);if (tmp.right != null) que.offer(tmp.right);}res.add(arr);}}}
M9.29-107. 二叉树的层序遍历 II
递归:
class Solution {List<List<Integer>> res = new ArrayList<>();public List<List<Integer>> levelOrderBottom(TreeNode root) {if (root == null) return res;DFS(root, 0);Collections.reverse(res);return res;}public void DFS(TreeNode root, int deep) {if (root == null) return;if (res.size() < deep + 1) {List<Integer> tmp = new ArrayList<>();res.add(new ArrayList<Integer>());}res.get(deep).add(root.val);DFS(root.left, deep + 1);DFS(root.right, deep + 1);}}
非递归:
class Solution {public List<List<Integer>> levelOrderBottom(TreeNode root) {List<List<Integer>> res = new ArrayList<>();if (root == null) return res;Queue<TreeNode> que = new LinkedList<>();que.offer(root);while (!que.isEmpty()) {int len = que.size();List<Integer> tmp = new ArrayList<>();for (int i = 0; i < len; i++) {TreeNode node = que.poll();tmp.add(node.val);if (node.left != null) que.offer(node.left);if (node.right != null) que.offer(node.right);}res.add(tmp);}Collections.reverse(res);return res;}}
M10.2-199. 二叉树的右视图
class Solution {public List<Integer> rightSideView(TreeNode root) {List<Integer> res = new ArrayList<>();if (root == null) return res;Queue<TreeNode> que = new LinkedList<>();que.offer(root);while (!que.isEmpty()) {int len = que.size();for (int i = 0; i < len; i++) {TreeNode node = que.poll();if (i == len - 1) res.add(node.val);if (node.left != null) que.offer(node.left);if (node.right != null) que.offer(node.right);}}return res;}}
S10.6-226. 翻转二叉树
S10.6-101. 对称二叉树
S10.7-104. 二叉树的最大深度
层序遍历:最大层数就是二叉树的最大深度
递归法:DFS
//后序遍历class Solution {public int maxDepth(TreeNode root) {if (root == null) return 0;//详细写法,分别求左右子树的深度,找到最大的那个// int leftDeep = maxDepth(root.left);// int rightDeep = maxDepth(root.right);// return (leftDeep < rightDeep ? rightDeep : leftDeep) + 1;//简洁写法return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;}}//前序遍历class Solution {int res = 0;public int maxDepth(TreeNode root) {preOrder(root, 0);return res;}public void preOrder(TreeNode node, int deep) {if (node == null) return;deep++;res = res < deep ? deep : res;preOrder(node.left, deep);preOrder(node.right, deep);return;}}
S10.9-559. N 叉树的最大深度
DFS
class Solution {public int maxDepth(Node root) {if (root == null) return 0;int deep = 0;for (int i = 0; i < root.children.size(); i++) {deep = Math.max(deep, maxDepth(root.children.get(i)));}return deep + 1;}}
S10.9-111. 二叉树的最小深度
//BFSclass Solution {public int minDepth(TreeNode root) {if (root == null) return 0;Queue<TreeNode> que = new LinkedList<>();que.offer(root);int deep = 0;// int res = 0;while (!que.isEmpty()) {int len = que.size();for (int i = 0; i < len; i++) {TreeNode node = que.poll();if (node.left == null && node.right == null) return deep + 1;if (node.left != null) que.offer(node.left);if (node.right != null) que.offer(node.right);}deep++;}return deep;// return res < deep ? res : deep;}}//DFSclass Solution {public int minDepth(TreeNode root) {if (root == null) return 0;int lDeep = minDepth(root.left);int rDeep = minDepth(root.right);if (root.left == null) return 1 + rDeep;if (root.right == null) return 1 + lDeep;return Math.min(lDeep, rDeep) + 1;}}
M10.10-222. 完全二叉树的节点个数
//DFS postOrder 遍历节点class Solution {public int countNodes(TreeNode root) {if (root == null) return 0;// int lNum = countNodes(root.left); //左// int rNum = countNodes(root.right); //右// int treeNum = lNum + rNum + 1; //中// return treeNum;return countNodes(root.left) + countNodes(root.right) + 1;}}// 使用完全二叉树的性质class Solution {public int countNodes(TreeNode root) {if (root == null) return 0;TreeNode l = root.left;TreeNode r = root.right;int ld = 0, rd = 0; // 这里初始为0是有目的的,为了下面求指数方便while (l != null) { // 求左子树深度l = l.left;ld++;}while (r != null) { // 求右子树深度r = r.right;rd++;}//如果左右子树深度相等,说明是满二叉树if (rd == ld) return (2 << ld) - 1; // 注意(2<<1) 相当于2^2,所以ld初始为0return countNodes(root.left) + countNodes(root.right) + 1;}}
S10.12-110. 平衡二叉树
===================递归==========================class Solution {public boolean isBalanced(TreeNode root) {if (root == null) return true;int lDeep = deep(root.left);int rDeep = deep(root.right);if (lDeep == -1 || rDeep == -1) return false;return Math.abs(lDeep - rDeep) > 1 ? false : true;}// 返回以该节点为根节点的二叉树的高度,如果不是二叉平衡树则返回-1public int deep(TreeNode root) {if (root == null) return 0;int ld = deep(root.left); //左if (ld == -1) return -1;int rd = deep(root.right); //右if (rd == -1) return -1;//中int res = 0;if (Math.abs(ld - rd) > 1) res = -1;else res = 1 + Math.max(ld, rd); //以当前节点为根节点的二叉平衡树的高度return res;}}==========================迭代========================
S10.13-257. 二叉树的所有路径
================================递归+回溯===============================class Solution {List<String> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<String> binaryTreePaths(TreeNode root) {if (root == null) return res;getPath(root);return res;}public void getPath(TreeNode node) {path.add(node.val);//叶子结点,递归出口,输出当前路径if (node.left == null && node.right == null) {StringBuilder sb = new StringBuilder();for (int i = 0; i < path.size() - 1; i++) {sb.append(path.get(i)).append("->");}sb.append(path.get(path.size() - 1));res.add(sb.toString());return;}//左子树if (node.left != null) {getPath(node.left);path.remove(path.size() - 1); //回溯}//右子树if (node.right != null) {getPath(node.right);path.remove(path.size() - 1); //回溯}}}======================迭代,无回溯============================class Solution {public List<String> binaryTreePaths(TreeNode root) {List<String> res = new ArrayList<>();if (root == null) return res;Stack<TreeNode> stack = new Stack<>();Stack<String> path = new Stack<>();stack.push(root);path.push("" + root.val);while (!stack.isEmpty()) {TreeNode node = stack.pop();String s = path.pop();if (node.left == null && node.right == null) res.add(s);if (node.right != null) {stack.push(node.right);path.push(s + "->" + node.right.val);}if (node.left != null) {stack.push(node.left);path.push(s + "->" + node.left.val);}}return res;}}
S10.17-100. 相同的树
====================递归========================class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {if (p == null && q == null) return true;if (p == null || q == null) return false;if (p.val != q.val) return false;// boolean compareLeft = isSameTree(p.left, q.left);// boolean compareRight = isSameTree(p.right, q.right);// return compareLeft && compareRight;return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);}}====================迭代========================class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {Stack<TreeNode> s1 = new Stack<>();Stack<TreeNode> s2 = new Stack<>();s1.push(p);s2.push(q);while (!s1.isEmpty() && !s2.isEmpty()) {TreeNode node1 = s1.pop();TreeNode node2 = s2.pop();if (node1 == null && node2 == null) continue;if (node1 == null || node2 == null) return false;if (node2.val != node1.val) return false;s1.push(node1.right);s1.push(node1.left);s2.push(node2.right);s2.push(node2.left);}return s1.isEmpty() && s2.isEmpty();}}
class Solution { // 递归int res = 0;public int sumOfLeftLeaves(TreeNode root) {calculateLeftSum(root);return res;}public void calculateLeftSum(TreeNode root) {if (root == null) return;if (root.left != null) {if (root.left.left == null && root.left.right == null)res += root.left.val;calculateLeftSum(root.left);}if (root.right != null) {calculateLeftSum(root.right);}return;}}class Solution { //迭代public int sumOfLeftLeaves(TreeNode root) {int res = 0;if (root == null) return res;Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();if (node.right != null) {stack.push(node.right);}if (node.left != null) {stack.push(node.left);if (node.left.left == null && node.left.right == null) {res += node.left.val;}}}return res;}}
S10.17-404. 左叶子之和
class Solution { //递归int res = 0;public int sumOfLeftLeaves(TreeNode root) {calculateLeftSum(root);return res;}public void calculateLeftSum(TreeNode root) {if (root == null) return;if (root.left != null) {if (root.left.left == null && root.left.right == null)res += root.left.val;calculateLeftSum(root.left);}if (root.right != null) {calculateLeftSum(root.right);}return;}}class Solution { //迭代public int sumOfLeftLeaves(TreeNode root) {int res = 0;if (root == null) return res;Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();if (node.right != null) {stack.push(node.right);}if (node.left != null) {stack.push(node.left);if (node.left.left == null && node.left.right == null) {res += node.left.val;}}}return res;}}
M10.17-513. 找树左下角的值
class Solution { //层序遍历public int findBottomLeftValue(TreeNode root) {Queue<TreeNode> que = new LinkedList<>();que.offer(root);int res = root.val;while (!que.isEmpty()) {int len = que.size();for (int i = 0; i < len; i++) {TreeNode node = que.poll();if (i == 0) res = node.val;if (node.left != null) que.offer(node.left);if (node.right != null) que.offer(node.right);}}return res;}}class Solution { //递归+回溯int minDepth = -1;int res;public int findBottomLeftValue(TreeNode root) {// res = root.val;preOrder(root, 0);return res;}public void preOrder(TreeNode root, int leftDepth) {if (root == null) return;if (root.left == null && root.right == null) {if (leftDepth > minDepth) {minDepth = leftDepth;res = root.val;}}if (root.left != null) {leftDepth++;preOrder(root.left, leftDepth);leftDepth--; //回溯// preOrder(root.left, leftDepth + 1); //简洁写法}if (root.right != null) {leftDepth++;preOrder(root.right, leftDepth);leftDepth--;// preOrder(root.right, leftDepth + 1); //简洁写法}}}
S10.18-112. 路径总和
class Solution { //递归boolean res;public boolean hasPathSum(TreeNode root, int targetSum) {if (root == null) return false;findSum(root, targetSum);return res;}public void findSum(TreeNode root, int targetSum) {//找到叶子节点了if (root.left == null && root.right == null) {if (root.val == targetSum) { //满足条件res = true;return;}}if (root.left != null) { //左孩子findSum(root.left, targetSum - root.val);}if (root.right != null) { //右孩子findSum(root.right, targetSum - root.val);}}}class Solution { //迭代public boolean hasPathSum(TreeNode root, int targetSum) {if (root == null) return false;Stack<TreeNode> stack = new Stack<>();Stack<Integer> sum = new Stack<>();stack.push(root);sum.push(root.val);while (!stack.isEmpty()) {TreeNode node = stack.pop();int total = sum.pop();// 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回trueif (node.left == null && node.right == null && total == targetSum) return true;// 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来if (node.right != null) {stack.push(node.right);sum.push(total + node.right.val);}// 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来if (node.left != null) {stack.push(node.left);sum.push(total + node.left.val);}}return false;}}
M10.18-113. 路径总和 II
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> pathSum(TreeNode root, int targetSum) {if (root == null) return res;findPath(root, targetSum);return res;}public void findPath(TreeNode root, int targetSum) {path.add(root.val);//叶子节点if (root.left == null && root.right == null) {//找到了和为targetSum的路径if (root.val == targetSum) {res.add(new ArrayList<>(path));}}if (root.left != null) {findPath(root.left, targetSum - root.val);}if (root.right != null) {findPath(root.right, targetSum - root.val);}path.remove(path.size() - 1); //回溯}}
M10.26-106. 从中序与后序遍历序列构造二叉树
class Solution {public TreeNode buildTree(int[] inorder, int[] postorder) {if (inorder == null || postorder == null) return null;if (inorder.length == 0 || postorder.length == 0) return null;return build(inorder, 0, inorder.length, postorder, 0, postorder.length);}public TreeNode build(int[] inorder, int inLeft, int inRight, int[] postorder, int posLeft, int posRight) {//没有元素了if (inRight <= inLeft) return null;// 根节点了,只有一个元素if (inRight - inLeft == 1) return new TreeNode(inorder[inLeft]);// 根节点的值,后序遍历最后一个节点为根节点int rootVal = postorder[posRight - 1];TreeNode root = new TreeNode(rootVal);int index = 0;// 找根节点在中序遍历中的位置,以划分左右子树for (int i = inLeft; i < inRight; i++) {if (inorder[i] == rootVal) index = i;}//根据root划分左子树root.left = build(inorder, inLeft, index, postorder, posLeft, posLeft + (index - inLeft));//根据root划分右子树root.right = build(inorder, index + 1, inRight, postorder, posLeft + (index - inLeft), posRight - 1);return root;}}======================使用map优化,但是不能存在重复元素==============================class Solution {//用map有个前提条件,就是不能有重复元素Map<Integer, Integer> map; //map的key存中序遍历的val,value存index,方便定位int[] inOrder; //中序遍历int[] postOrder; //后序遍历public TreeNode buildTree(int[] inorder, int[] postorder) {if (inorder == null || postorder == null) return null;if (inorder.length == 0 || postorder.length == 0) return null;map = new HashMap<>();inOrder = inorder;postOrder = postorder;for (int i = 0; i < inorder.length; i++) map.put(inorder[i], i);return build(0, inorder.length, 0, postorder.length);}public TreeNode build(int inLeft, int inRight, int posLeft, int posRight) {//没有元素了if (inRight <= inLeft) return null;// 根节点了,只有一个元素if (inRight - inLeft == 1) return new TreeNode(inOrder[inLeft]);// 根节点的值,后序遍历最后一个节点为根节点int rootVal = postOrder[posRight - 1];TreeNode root = new TreeNode(rootVal);// 找根节点在中序遍历中的位置,以划分左右子树int index = map.get(rootVal);//根据root划分左子树root.left = build(inLeft, index, posLeft, posLeft + (index - inLeft));//根据root划分右子树root.right = build(index + 1, inRight, posLeft + (index - inLeft), posRight - 1);return root;}}
M10.26-105. 从前序与中序遍历序列构造二叉树
class Solution {Map<Integer, Integer> map;int[] pre;int[] in;public TreeNode buildTree(int[] preorder, int[] inorder) {if (preorder == null || inorder == null || preorder.length == 0 || inorder.length == 0) return null;map = new HashMap<>();pre = preorder;in = inorder;for (int i = 0; i < in.length; i++) map.put(in[i], i);return build(0, pre.length, 0, in.length);}public TreeNode build (int preLeft, int preRight, int inLeft, int inRight) {//没有元素了if (preLeft >= preRight) return null;//只有一个元素if (preRight - preLeft == 1) return new TreeNode(pre[preLeft]);//根节点int rootVal = pre[preLeft];TreeNode root = new TreeNode(rootVal);//找根节点在中序遍历中的indexint index = map.get(rootVal);//划分左子树root.left = build(preLeft + 1, preLeft + 1 + index - inLeft, inLeft, index);//划分右子树root.right = build(preLeft + 1 + index - inLeft, preRight, index + 1, inRight);return root;}}
M10.27-654. 最大二叉树
class Solution {int[] nodes;public TreeNode constructMaximumBinaryTree(int[] nums) {if (nums == null || nums.length == 0) return null;nodes = nums;return buildTree(0, nums.length);}public TreeNode buildTree(int left, int right) {if (right <= left) return null;int index = left;for (int i = left; i < right; i++) {if (nodes[index] < nodes[i]) index = i;}int rootVal = nodes[index];TreeNode root = new TreeNode(rootVal);root.left = buildTree(left, index);root.right = buildTree(index + 1, right);return root;}}
S10.27-617. 合并二叉树
=====================详细写法,不改变原始树=========================class Solution {public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {if (root1 == null && root2 == null) return null;TreeNode root;if (root1 == null || root2 == null) {root = root1 == null ? new TreeNode(root2.val) : new TreeNode(root1.val);root.left = root1 == null ? mergeTrees(null, root2.left) : mergeTrees(root1.left, null);root.right = root1 == null ? mergeTrees(null, root2.right) : mergeTrees(root1.right, null);}else {root = new TreeNode(root1.val + root2.val);root.left = mergeTrees(root1.left, root2.left);root.right = mergeTrees(root1.right, root2.right);}return root;}}==============递归优化,改变原来的树===============class Solution {public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {if(root1 == null || root2 == null) return root1 == null ? root2 : root1;root1.val += root2.val;root1.left = mergeTrees(root1.left, root2.left);root1.right = mergeTrees(root1.right, root2.right);return root1;}}====================迭代法============================//层序遍历合并class Solution {public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {if(root1 == null || root2 == null) return root1 == null ? root2 : root1;Queue<TreeNode> que = new LinkedList<>();que.offer(root1);que.offer(root2);while (!que.isEmpty()) {TreeNode node1 = que.poll();TreeNode node2 = que.poll();// 此时两个节点一定不为空,val相加node1.val += node2.val;// 如果两棵树左节点都不为空,加入队列if (node1.left != null && node2.left != null) {que.offer(node1.left);que.offer(node2.left);}// 如果两棵树右节点都不为空,加入队列if (node1.right != null && node2.right != null) {que.offer(node1.right);que.offer(node2.right);}//当node1的左节点为空,node2的左节点不为空,就幅值过去if (node1.left == null && node2.left != null) {node1.left = node2.left;}//当node1的右节点为空,node2的右节点不为空,就幅值过去if (node1.right == null && node2.right != null) {node1.right = node2.right;}}return root1;}}
S10.28-700. 二叉搜索树中的搜索
================递归=================class Solution {public TreeNode searchBST(TreeNode root, int val) {if (root == null || root.val == val) return root;if (root.val < val) {return searchBST(root.right, val);}else {return searchBST(root.left, val);}}}============迭代====================class Solution {public TreeNode searchBST(TreeNode root, int val) {while (root != null) {if (root.val < val) root = root.right;else if (root.val > val) root = root.left;else return root;}return root;}}
M10.29-98. 验证二叉搜索树
思路:中序遍历,判断是否为递增序列
=======================递归========================class Solution {TreeNode pre = null; //用来记录前一个节点public boolean isValidBST(TreeNode root) {if (root == null) return true;boolean left = isValidBST(root.left);//如果前一个节点的值比当前大,表示不是二叉搜索树if (pre != null && pre.val >= root.val) return false;pre = root; // 记录前一个节点boolean right = isValidBST(root.right);return left && right;}}=====================迭代=====================class Solution {TreeNode pre = null; //用来记录前一个节点public boolean isValidBST(TreeNode root) {if (root == null) return true;Stack<TreeNode> stack = new Stack<>();// stack.push(root);while (root != null || !stack.isEmpty()) {while (root != null) {stack.push(root);root = root.left;}TreeNode node = stack.pop();if (pre != null && pre.val >= node.val) return false;pre = node;root = node.right;}return true;}}
S10.30-530. 二叉搜索树的最小绝对差
注意是二叉搜索树,二叉搜索树可是有序的。遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。那么二叉搜索树采用中序遍历,其实就是一个有序数组。
//递归class Solution {int min = Integer.MAX_VALUE;TreeNode pre = null;public int getMinimumDifference(TreeNode root) {getMin(root);return min;}public void getMin(TreeNode root) {if (root == null) return;getMin(root.left);if (pre != null) {int dif = Math.abs(root.val - pre.val);min = dif < min ? dif : min;}pre = root;getMin(root.right);}}//非递归class Solution {public int getMinimumDifference(TreeNode root) {Stack<TreeNode> st = new Stack<>();int min = Integer.MAX_VALUE;TreeNode pre = null;while (root != null || !st.isEmpty()) {while (root != null) {st.push(root);root = root.left;}TreeNode node = st.pop();if (pre != null) {int dif = Math.abs(node.val - pre.val);min = dif < min ? dif : min;}pre = node;if (node.right != null) root = node.right;}return min;}}
S10.31-501. 二叉搜索树中的众数
//递归class Solution {List<Integer> res;int count, maxCount;TreeNode pre;public int[] findMode(TreeNode root) {res = new ArrayList<>();count = 0;maxCount = 0;pre = null;find(root);int[] ans = new int[res.size()];for (int i = 0; i < res.size(); i++) {ans[i] = res.get(i);}return ans;}public void find(TreeNode root) {if (root == null) return;find(root.left);int rootVal = root.val;//初始化,或pre的值跟root值不同时,root从1开始重新计数if (pre == null || rootVal != pre.val) {count = 1;} else { //相同 count增加count++;}if (count > maxCount) { //count最大res.clear(); // 清空结果集res.add(rootVal); // 加入当前众数maxCount = count; // 更新众数出现的次数} else if (count == maxCount){ //出现了多个众数res.add(rootVal);}pre = root;find(root.right);}}//迭代class Solution {List<Integer> res;int count, maxCount;TreeNode pre;public int[] findMode(TreeNode root) {res = new ArrayList<>();count = 0;maxCount = 0;pre = null;find(root);int[] ans = new int[res.size()];for (int i = 0; i < res.size(); i++) {ans[i] = res.get(i);}return ans;}public void find(TreeNode root) {Stack<TreeNode> st = new Stack<>();while (root != null || !st.isEmpty()) {while (root != null) {st.push(root);root = root.left;}TreeNode node = st.pop();int curVal = node.val;if (pre == null || pre.val != curVal) {count = 1;} else {count++;}if (count > maxCount) {res.clear();res.add(curVal);maxCount = count;} else if (count == maxCount) {res.add(curVal);}pre = node;root = node.right;}}}
M11.1-236. 二叉树的最近公共祖先
后序遍历,回溯
class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if (root == null || root == p || root == q) return root;TreeNode left = lowestCommonAncestor(root.left, p, q);TreeNode right = lowestCommonAncestor(root.right, p, q);//左右子树分别找到了,说明当前节点就是最近公共祖先if (left != null && right != null) return root;// left为空,right不为空,说明目标节点通过right返回,反之亦然if (left == null && right != null) return right;// if (left != null && right == null) return left;// left 跟right 都没找到返回null// return null;// 上面两种情况可以整合return left;}}
S11.3-235. 二叉搜索树的最近公共祖先
利用二叉搜索树的性质,cur节点的值处于[p,q]区间内,cur就是最近公共祖先了。
class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if (root == null) return root;if (root.val >= p.val && root.val <= q.val) return root;if (root.val <= p.val && root.val >= q.val) return root;TreeNode left = lowestCommonAncestor(root.left, p, q);TreeNode right = lowestCommonAncestor(root.right, p, q);if (left != null) return left;return right;}}
M11.3-701. 二叉搜索树中的插入操作
使用前序遍历,利用BST的性质,如果当前节点的值小于目标值,那么直接遍历当前节点的右子树,反之遍历左子树;当遍历到的节点为空为时,那么就代表此节点就是插入是节点 点位置(递归出口)。
//有返回值的递归class Solution {public TreeNode insertIntoBST(TreeNode root, int val) {//递归出口,遍历到的节点为null的时候,就是要插入的位置if (root == null) {TreeNode node = new TreeNode(val);return node;}// 下面两步操作保证了插入的位置一定满足BST// 利用BST的性质,左子树的值一定比根节点小if (root.val > val) root.left = insertIntoBST(root.left, val);// 右子树的值一定比根节点大if (root.val < val) root.right = insertIntoBST(root.right, val);return root;}}//无返回值递归class Solution {TreeNode parent; //用来记录父节点public TreeNode insertIntoBST(TreeNode root, int val) {if (root == null) return new TreeNode(val);insert(root, val);return root;}public void insert(TreeNode root, int val) {if (root == null) {if (parent.val < val) parent.right = new TreeNode(val);else parent.left = new TreeNode(val);return;}parent = root; //必须在遍历左右子树前更新父节点if (root.val > val) insert(root.left, val);if (root.val < val) insert(root.right, val);}}//迭代class Solution {TreeNode parent;public TreeNode insertIntoBST(TreeNode root, int val) {if (root == null) return new TreeNode(val);Stack<TreeNode> st = new Stack<>();st.push(root);// TreeNode pre;while (!st.isEmpty()) {TreeNode node = st.pop();if (node.val < val) {if (node.right == null) {node.right = new TreeNode(val);break;}st.push(node.right);}if (node.val > val) {if (node.left == null) {node.left = new TreeNode(val);break;}st.push(node.left);}}return root;}}
M11.04-450. 删除二叉搜索树中的节点
class Solution {TreeNode pre;public TreeNode deleteNode(TreeNode root, int key) {// 情况1:没找到要删除的节点,遍历到空节点直接返回if (root == null) return root;//找到了删除节点if (root.val == key) {//情况2:左右孩子都为空,返回空节点//情况3:右孩子为空,返回左孩子if (root.right == null) return root.left;//情况4:左孩子为空,返回右孩子if (root.left == null) return root.right;//情况5:左右孩子均不为空,要把子树接到右子树最左边的节点else {TreeNode cur = root.right;while (cur.left != null) {cur = cur.left;}cur.left = root.left; //把删除节点(root)的左子树挂在右子树最左边的节点(cur)的左子树上root = root.right; //删除root;return root;}}if (root.val > key) root.left = deleteNode(root.left, key);if (root.val < key) root.right = deleteNode(root.right, key);return root;}}
M11.05-669. 修剪二叉搜索树
//递归class Solution {public TreeNode trimBST(TreeNode root, int low, int high) {// 空节点,说明满足区间if (root == null) return root;// 当前节点的值小于low,那么就要对右子树进行修剪if (root.val < low) {TreeNode right = trimBST(root.right, low, high); //修剪右子树return right; //返回修剪的结果给上一层//return trimBST(root.right, low, high);}// 当前节点的值大于high,那么就要对左子树进行修剪if (root.val > high) {TreeNode left = trimBST(root.left, low, high); //修剪左子树return left; //返回修剪的结果给上一层//return trimBST(root.left, low, high);}// 当前节点的值处于区间内,分别修剪左右子树root.left = trimBST(root.left, low, high);root.right = trimBST(root.right, low, high);return root;}}//迭代class Solution {public TreeNode trimBST(TreeNode root, int low, int high) {if (root == null) return null;// 处理头结点,让root移动到[low, high] 范围内,注意是左闭右闭while (root != null && (root.val < low || root.val > high)) {if (root.val < low) root = root.right;else root = root.left;}//此时root处于[low, high]区间内,处理左孩子小于low的情况TreeNode cur = root;while (cur != null) {while (cur.left != null && cur.left.val < low) {cur.left = cur.left.right;}cur = cur.left;}cur = root;while (cur != null) {while (cur.right != null && cur.right.val > high) {cur.right = cur.right.left;}cur = cur.right;}return root;}}
S11.06-108. 将有序数组转换为二叉搜索树
数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取,所以想构成不平衡的二叉树是自找麻烦。
//递归class Solution {int[] num;public TreeNode sortedArrayToBST(int[] nums) {if (nums == null) return null;num = nums;return build(0, nums.length);}public TreeNode build(int l, int r) {if (l >= r) return null;int index = (l + r) / 2;TreeNode node = new TreeNode(num[index]);node.left = build(l, index);node.right = build(index + 1, r);return node;}}//迭代class Solution {public TreeNode sortedArrayToBST(int[] nums) {if (nums == null || nums.length == 0) return null;TreeNode root = new TreeNode(-1); //初始化根节点Queue<TreeNode> nodeQue = new LinkedList<>(); //放遍历的节点Queue<Integer> leftQue = new LinkedList<>(); //保存区间左下标Queue<Integer> rightQue = new LinkedList<>(); //保存区间右下标nodeQue.offer(root); //根节点入队//存入初始区间下标,[0, nums.length)leftQue.offer(0); //0是区间左下标的初始位置rightQue.offer(nums.length); //nums.length是区间右下标的初始位置//开始构造树while (!nodeQue.isEmpty()) {TreeNode curNode = nodeQue.poll();//取当前区间的左右下标int left = leftQue.poll();int right = rightQue.poll();int mid = left + ((right - left) >> 1); //确定根节点的下标curNode.val = nums[mid]; //给根节点赋值//处理左区间 [left, mid)if (left < mid) {curNode.left = new TreeNode(-1);nodeQue.offer(curNode.left);leftQue.offer(left);rightQue.offer(mid);}//处理右区间 [mid + 1, right)if (mid + 1 < right) {curNode.right = new TreeNode(-1);nodeQue.offer(curNode.right);leftQue.offer(mid + 1);rightQue.offer(right);}}return root;}}
M11.07-538. 把二叉搜索树转换为累加树
BST的中序遍历有序,题目要求当前节点的值等与此节点到比他大的节点的累加,所以可以反向中序遍历的顺序就是由大到小,用一个变量记录之前的和即可:
//递归class Solution {int total = 0;public TreeNode convertBST(TreeNode root) {if (root == null) return null;convertBST(root.right);root.val += total;total = root.val;convertBST(root.left);return root;}}//迭代class Solution {int total = 0;public TreeNode convertBST(TreeNode root) {if (root == null) return null;Stack<TreeNode> st = new Stack<>();int total = 0;TreeNode node = root;while (node != null || !st.isEmpty()) {while (node != null) {st.push(node);node = node.right;}TreeNode cur = st.pop();cur.val += total;total = cur.val;node = cur.left;}return root;}}
回溯
回溯模板框架伪代码
void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯,撤销处理结果}}
M11.07-77. 组合
class Solution {List<List<Integer>> res = new ArrayList<>();LinkedList<Integer> path = new LinkedList<>();public List<List<Integer>> combine(int n, int k) {combine1(n, k, 1);return res;}/*** 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠cur* @param cur 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。*/public void combine1(int n, int k, int cur) {if (path.size() == k) {res.add(new ArrayList<>(path));return;}// n-(k-path.size())+1 剪枝操作//path.size() 已经选择的元素, k-path.size() 剩余需要选择的元素//那么i至多从 n-(k-path.size())+1开始才能保证有足够多的元素for (int i = cur; i <= n - (k - path.size()) + 1; i++) {path.add(i);combine1(n, k, i + 1);path.removeLast();}}}
M11.08-216. 组合总和 III
class Solution {List<List<Integer>> res = new ArrayList<>();LinkedList<Integer> path = new LinkedList<>();public List<List<Integer>> combinationSum3(int k, int n) {//k最多为9,判断k个数的和能否达到n,达不到直接返回if (k > 9 || ((19 - k) * k) >> 1 < n) return res;getC(k, n, 0, 1);return res;}//sum表示当前的和,cur表示当前到哪个数了public void getC(int k, int n, int sum, int cur) {if (path.size() == k && sum == n) {res.add(new ArrayList<>(path));return;}// 同时满足 i<=9 以及集合中的数没有达到k个才行// 过滤掉k个数小于n的情况。for (int i = cur; i <= 9 && path.size() < k; i++) {//剪枝,如果当前的和加上即将加入到集合的数的和大于n,那么后面的数都不用找了,因为是升序的。if (sum + i > n) return;sum += i;path.add(i);getC(k, n, sum, i + 1);sum -= i;path.removeLast();}}}
M11.09-17. 电话号码的字母组合
class Solution {List<String> res = new ArrayList<>();StringBuilder st = new StringBuilder();String[] map = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};public List<String> letterCombinations(String digits) {if (digits.length() == 0) return res;bt(0, digits);return res;}public void bt(int i, String digits) {if (i == digits.length()) {res.add(st.toString());return;}String s = map[digits.charAt(i) - '0']; //取第i个数字对应的字母组成的字符串char[] chars = s.toCharArray();for (char c : chars) {st.append(c);bt(i + 1, digits);st.deleteCharAt(st.length() - 1);}}}
M11.10-39. 组合总和
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {if (candidates == null || candidates.length == 0) return res;Arrays.sort(candidates);getSum(candidates, target, 0);return res;}public void getSum(int[] candidates, int target, int index) {// 找到了数字和为 target 的组合if (target == 0) {res.add(new ArrayList(path));return;}for (int i = index; i < candidates.length; i++) {//如果要加入的数,超过了目标值,直接跳出if (candidates[i] > target) break;path.add(candidates[i]);//选定一个元素后,下一个元素从当前位置开始选(只会出现223 不会出现232这样就避免了重复)getSum(candidates, target - candidates[i], i);path.remove(path.size() - 1); //回溯,移除最后一个元素}}}
M11.11-40. 组合总和 II
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {if (candidates == null || candidates.length == 0) return res;Arrays.sort(candidates);bk(candidates, target, 0);return res;}public void bk (int[] candidates, int target, int index) {if (target == 0) {res.add(new ArrayList(path));return;}for (int i = index; i < candidates.length; i++) {if (candidates[i] > target) break;//去重,如果之前已经求过该数字的解,跳过。//i > index 是保证搜索树在同一枝上,而不是同一层//如果不加i > index [1,1,2,3] 4 的结果 1 1 2会被剪枝掉if (i > 0 && candidates[i] == candidates[i - 1] && i > index) continue;path.add(candidates[i]);bk(candidates, target - candidates[i], i + 1);path.remove(path.size() - 1);}}}
M11.12-131. 分割回文串
class Solution {List<List<String>> res = new ArrayList<>();List<String> path = new ArrayList<>();public List<List<String>> partition(String s) {if (s == null || s.length() == 0) return res;bk(s, 0);return res;}public void bk(String s, int index) {if (index >= s.length()) {res.add(new ArrayList(path));return;}for (int i = index; i < s.length(); i++) {if (isPalindrome(s, index, i)) {String str = s.substring(index, i + 1);path.add(str);} else continue;bk(s, i + 1);path.remove(path.size() - 1);}}public boolean isPalindrome(String str, int l, int r) {while (l < r) {if (str.charAt(l) != str.charAt(r)) return false;l++;r--;}return true;}}
M11.13-93. 复原 IP 地址
class Solution {List<String> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<String> restoreIpAddresses(String s) {if (s == null || s.length() < 4) return res;bk(s, 0);return res;}public void bk (String s, int index) {//如果分割点位置超过字符串的长度,表示分割完毕,同时需要满足分割了4个数出来,IP的每位最多是个三位数。比如2551分割成了255.1这样就不符合要求if (index >= s.length() && path.size() == 4) {StringBuilder ip = new StringBuilder();for (int j = 0; j < 4; j++) {if (j == 3) ip.append(path.get(j));else ip.append(path.get(j)).append(".");}res.add(ip.toString());return;}//IP的某一位上最多是个三位数,所以下标最多往后挪三位,同时需要保证下标没有越界for (int i = index; i < index + 3 && i < s.length(); i++) {//剪枝操作,IP的一个位置至少需要1位数//判断当前子串的长度/剩余IP的位置是否大于1,大于一则表示IP剩余位置上至少能分到一位数,反之不能够构成IP直接结束循环。if (path.size() != 4 && ((s.length() - i) / (4 - path.size()) < 1)) break;//如果子串第一位是0,那么分割方式只有一种if (i == index && s.charAt(index) == '0') {path.add(0);bk(s, i + 1);path.remove(path.size() - 1);break;}String num = s.substring(index, i + 1);//确保分割出来的数小于等于255if (Integer.parseInt(num) > 255) break;path.add(Integer.parseInt(num));bk(s, i + 1);path.remove(path.size() - 1);}}}
M11.14-78. 子集
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> subsets(int[] nums) {if (nums == null || nums.length < 1) return res;bk(nums, 0);return res;}public void bk(int[] nums, int index) {res.add(new ArrayList(path));//if (index >= nums.length) return; //终止条件可以不加for (int i = index; i < nums.length; i++) {path.add(nums[i]);bk(nums, i + 1);path.remove(path.size() - 1);}}}
M11.14-90. 子集 II
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {if (nums == null || nums.length == 0) return res;Arrays.sort(nums);bk(nums, 0);return res;}public void bk(int[] nums, int index) {res.add(new ArrayList(path));for (int i = index; i < nums.length; i++) {if (i > index && nums[i] == nums[i - 1]) continue;path.add(nums[i]);bk(nums, i + 1);path.remove(path.size() - 1);}}}
M11.14-491. 递增子序列
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();// 需要一个used数组,来记录当前数字是否已选择boolean[] used;public List<List<Integer>> permute(int[] nums) {if (nums == null || nums.length == 0) return res;used = new boolean[nums.length];bk(nums);return res;}public void bk(int[] nums) {if (path.size() == nums.length) {res.add(new ArrayList(path));return;}//每次都从头开始for (int i = 0; i < nums.length; i++) {if (used[i]) continue; //如果已经选择过 跳过。used[i] = true;path.add(nums[i]);bk(nums);path.remove(path.size() - 1);used[i] = false;}}}
M11.14-46. 全排列
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();// 需要一个used数组,来记录当前数字是否已选择boolean[] used;public List<List<Integer>> permute(int[] nums) {if (nums == null || nums.length == 0) return res;used = new boolean[nums.length];bk(nums);return res;}public void bk(int[] nums) {if (path.size() == nums.length) {res.add(new ArrayList(path));return;}//每次都从头开始for (int i = 0; i < nums.length; i++) {if (used[i]) continue; //如果已经选择过 跳过。used[i] = true;path.add(nums[i]);bk(nums);path.remove(path.size() - 1);used[i] = false;}}}
M11.14-47. 全排列 II
class Solution {List<List<Integer>> res = new ArrayList<>();List<Integer> path = new ArrayList<>();// 需要一个used数组,来记录当前数字是否已选择boolean[] used;public List<List<Integer>> permuteUnique(int[] nums) {if (nums == null || nums.length == 0) return res;used = new boolean[nums.length];Arrays.sort(nums); // 因为要去重,所以需要数组有序bk(nums);return res;}public void bk(int[] nums) {//说明找到了一组解if (path.size() == nums.length) {res.add(new ArrayList(path));return;}//每次都从头开始for (int i = 0; i < nums.length; i++) {if (used[i]) continue; //如果已经选择过 跳过。// used[i - 1] == true,则表示已经使用,可以重复选取// used[i - 1] == false,则表示没有使用,说明已经回溯过了,需要去重,直接跳过if (i > 0 && nums[i] == nums[i -1] && !used[i - 1]) continue;used[i] = true;path.add(nums[i]);bk(nums);path.remove(path.size() - 1);used[i] = false;}}}
*H11.18-332. 重新安排行程
class Solution {List<String> res = new ArrayList<>();Map<String, Map<String, Integer>> map = new HashMap<>(); //用map存储出发到达机场,因为一个机场可以有多个到达机场,而且当前航线可能不仅仅飞一次,所以需要记录次数public List<String> findItinerary(List<List<String>> tickets) {if (tickets == null || tickets.size() < 1) return res;//初始化mapfor (List<String> list : tickets) {Map<String, Integer> temp;// 如果当前出发机场已经在map中了,我们只需要更新到达机场以及到达机场的次数if (map.containsKey(list.get(0))) {temp = map.get(list.get(0));//到达机场可能是第一次到达,所以需要判断,到达机场在不在到达map中temp.put(list.get(1), temp.getOrDefault(list.get(1), 0) + 1);} else { //当前出发机场不在map中,就需要初始化temp = new TreeMap<>(); //升序Map,这样到达机场在遍历时就是按题目要求顺序temp.put(list.get(1), 1);}map.put(list.get(0), temp); //更新当前出发、到达信息}res.add("JFK"); //从JFK出发bk(tickets.size());return res;}public boolean bk(int ticNum) {if (res.size() == ticNum + 1) return true;String last = res.get(res.size() - 1);if (map.containsKey(last)) { //防止出现null,即当前机场只有到达,没有出发,但是他又是最小的到达机场 比如[["JFK","KUL"],["JFK","NRT"],["NRT","JFK"]] 这里的["JFK","KUL"]for (Map.Entry<String, Integer> target : map.get(last).entrySet()) {//遍历起飞机场为last的到达机场int count = target.getValue(); //到达机场是否还有次数if (count > 0) { //有次数就从当前机场起飞,没有就继续遍历res.add(target.getKey()); //将当前机场加入路径target.setValue(count - 1); //较少当前机场到达次数if (bk(ticNum)) return true; //如果已经找到解了,一定是最优解,因为TreeMap是升序res.remove(res.size() - 1); //没找到解,回溯target.setValue(count); //到达次数也要一起回溯}}}return false;}}
*H11.19-51. N 皇后
class Solution {List<List<String>> res = new ArrayList<>();char[][] chessBoard;public List<List<String>> solveNQueens(int n) {chessBoard = new char[n][n];for (char[] c :chessBoard) {Arrays.fill(c, '.');}bk(n, 0);return res;}/*** 回溯的主体*/public void bk(int n, int row) {if (row == n) {res.add(array2List(chessBoard));return;}for (int col = 0; col < n; col++) {if (isValid(col, row, chessBoard, n)) {chessBoard[row][col] = 'Q'; //放置皇后bk(n, row + 1);chessBoard[row][col] = '.'; //回溯,撤销}}}/*** 用于判断当前位置放皇后是否合法*/public boolean isValid(int col, int row, char[][] chessBoard, int n) {// 检查当前列的前几行上是否存在皇后for (int i = 0; i < row; i++) { //这里其实是一个剪枝,检查当前列上有没有皇后,有的话直接退出if (chessBoard[i][col] == 'Q') return false;}//检查左上角斜线上是否有皇后for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {if (chessBoard[i][j] == 'Q') return false;}//检查右上角斜线上是否有皇后for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {if (chessBoard[i][j] == 'Q') return false;}return true;}/*** 将二维字符数组转换为 List*/public List array2List(char[][] chessBoard) {List<String> list = new ArrayList<>();for (char[] c : chessBoard) {list.add(String.copyValueOf(c));}return list;}}
*H11.20-37. 解数独
class Solution {public void solveSudoku(char[][] board) {bk(board);}public boolean bk(char[][] board) {for (int i = 0; i < board.length; i++) { //遍历行for (int j = 0; j < board[0].length; j++) { //遍历列if (board[i][j] != '.') continue;for (char k = '1'; k <= '9'; k++) {if (isValid(i, j, k, board)) { //[i, j]这个位置能否放置kboard[i][j] = k; //放置kif (bk(board)) return true; //如果找到合适的解,立即返回board[i][j] = '.'; //回溯,撤销k}}return false; //如果1-9这个位置都不能放,直接返回false// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」}}return true; //遍历完没有返回false,说明找到了解,返回true}/*** 判断棋盘是否合法有如下三个维度:* 同行是否重复* 同列是否重复* 9宫格里是否重复*/public boolean isValid(int row, int col, char k, char[][] board) {//检查行for (int i = 0; i < board.length; i++) {if (board[row][i] == k) return false;}//检查列for (int i = 0; i < board.length; i++) {if (board[i][col] == k) return false;}// 检查当前3*3内有没有重复int startRow = (row / 3) * 3;int startCol = (col / 3) * 3;for (int i = startRow; i < startRow + 3; i++) {for (int j = startCol; j < startCol + 3; j++) {if (board[i][j] == k) return false;}}return true;}}
贪心
//S11.21-455. 分发饼干
M11.21-376. 摆动序列
class Solution {public int wiggleMaxLength(int[] nums) {if (nums == null) return 0;if (nums.length <= 1) return 1;int curDif = 0;int preDif = 0;int count = 1;for (int i = 1; i < nums.length; i++) {curDif = nums[i] - nums[i - 1]; //计算当前差值if ((curDif > 0 && preDif <= 0) || (curDif < 0 && preDif >= 0)) {count++;preDif = curDif;}}return count;}}
S11.22-53. 最大子序和
class Solution {public int maxSubArray(int[] nums) {int res = nums[0];for(int i = 1; i < nums.length; i++){// 计算前面的子序列最大和跟当前值相加 有没有比 当前单独值大//如果没有,那么当前值组成的子序列就是最大nums[i] = Math.max(nums[i], nums[i] + nums[i - 1]);// 更新最大和res = nums[i] > res ? nums[i] : res;}return res;}}
M11.22-122. 买卖股票的最佳时机 II
// 贪心算法class Solution {public int maxProfit(int[] prices) {int profit = 0;for(int i = 0; i < prices.length - 1; i++){int temp = prices[i + 1] - prices[i];if(temp > 0) profit += temp;}return profit;}}// 动态规划class Solution {public int maxProfit(int[] prices) {if (prices.length <= 1) return 0;// [天数][是否持股]int[][] dp = new int[prices.length][2];dp[0][0] = 0;dp[0][1] = -prices[0];for (int i = 1; i < prices.length; i++) {//下面的dp更新公式是基于不能同时参与多笔交易,也就是说今天如果持股,就不能再买进股票//第i天不持股,要么今天没有交易,延续昨天不持股状态dp[i-1][0],要么今天将股票卖出dp[i-1][1] + prices[i]dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);//第i天持股,要么今天买入dp[i-1][0] - prices[i],要么今天没有交易dp[i-1][1],维持昨天的状态dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);}return dp[prices.length - 1][0];}}
M11.23-55. 跳跃游戏
class Solution {public boolean canJump(int[] nums) {int count = 0; // 用来记录最远可以跳到哪里// 这里判断条件是 i <= count,在当前覆盖范围下,不管哪里都可以跳到for (int i = 0; i <= count; i++) {//每跳到一个位置,更新最大访问范围count = Math.max(i + nums[i], count);//最大访问范围达到(大于等于)数组最大下标,就表示可以跳到,直接返回if (count >= nums.length - 1) return true;}// for循环执行完,则表示不能跳到最后一个位置return false;}}
*M11.24-45. 跳跃游戏 II
class Solution {public int jump(int[] nums) {if (nums.length == 1) return 0;int count = 0; //记录最小跳跃数int max = 0; //记录下一步覆盖的最远距离下标int end = 0; //记录当前覆盖范围内的最远位置// 这里i < nums.length - 1 很关键 到终点前一个位置,就不用遍历了// 在遍历数组时,我们不访问最后一个元素,这是因为在访问最后一个元素之前,我们的边界一定大于等于最后一个位置,否则就无法跳到最后一个位置了。for (int i = 0; i < nums.length - 1; i++) {max = Math.max(i + nums[i], max); //更新下一步覆盖的最远距离下标if (i == end) { // 已经跳到当前覆盖的最远位置了,相当于把当前能跳到的位置都遍历了一遍,要跳下一步了end = max; // 跳了一步后,最多可以到哪里,更新count ++; // 跳跃步数+1}}return count;}}
S11.25-1005. K 次取反后最大化的数组和
class Solution {public int largestSumAfterKNegations(int[] nums, int k) {int min = Integer.MAX_VALUE;int count = 0;Arrays.sort(nums); //排序for (int i = 0; i < nums.length; i++) {// 记录数组中绝对值最小的那个数min = Math.abs(nums[i]) < min ? Math.abs(nums[i]) : min;// 碰到负数了,同时可以取反,那么就取反// 因为数组排序了,所以一定是绝对值最大的负数先取反if ((k > 0) && (nums[i] < 0)) {count -= nums[i];k--;}else count += nums[i];}if ((k % 2) == 1 && min != 0) { //k如果没用完,剩余奇数次,那么就把绝对值最小的那个数减掉两次。因为加的时候是加的正数,而实际上应该加负数。count = count - 2 * min;}return count;}}
*M11.26-134. 加油站
//暴力解class Solution {public int canCompleteCircuit(int[] gas, int[] cost) {for (int i = 0; i < gas.length; i++) {int rest = gas[i] - cost[i];int index = (i + 1) % gas.length;while (rest > 0 && index != i) {rest = rest + gas[index] - cost[index];index = (index + 1) % gas.length;}if (rest >= 0 && index == i) return i;}return -1;}}// 解法1class Solution {public int canCompleteCircuit(int[] gas, int[] cost) {int curSum = 0;int min = Integer.MAX_VALUE; //计算从起点出发,油箱里的油量最小值for (int i = 0; i < gas.length; i++) {int rest = gas[i] - cost[i];curSum += rest;if (curSum < min) min = curSum;}if (curSum < 0) return -1; // 情况1,加油站的总油量比消耗的少,无法跑完if (min >= 0) return 0; //qk2,从第0天开始 每天的油都够,那么0就是起点//情况3,从0开始出现了油不够的情况//从后向遍历,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。for (int i = gas.length - 1; i >= 0; i--) {int rest = gas[i] - cost[i];min += rest;if (min >= 0) {return i;}}return -1;}}// 解法2class Solution {public int canCompleteCircuit(int[] gas, int[] cost) {int index = 0;int min = Integer.MAX_VALUE;int total = 0;for (int i = 0; i < gas.length; i++) {int cur = gas[i] - cost[i];total += cur;if (total < min) { // 找到遍历过程中最小的油耗差min = total;index = i;}}// 如果total < 0 那么怎么走都不行,反之从最小的油耗差后一个位置开始return total < 0 ? -1 : (index + 1) % gas.length;}}//解法3-贪心class Solution {public int canCompleteCircuit(int[] gas, int[] cost) {int index = 0;int curSum = 0; //用来记录从i开始,往后走的过程中的油耗差int total = 0; //用来记录整个过程的油耗差,判断能否走完for (int i = 0; i < gas.length; i++) {curSum += gas[i] - cost[i];total += gas[i] - cost[i];if (curSum < 0) { //当前路线油耗差一旦小于0,则表示当前路段上的任一节点出发都不行,只能从下一个节点出发index = (i + 1) % gas.length; //更新出发节点,i+1curSum = 0; //当前路线油耗差清零}}return total < 0 ? -1 : index;}}
*H11.27-135. 分发糖果
class Solution {public int candy(int[] ratings) {int res = 0;int[] candy = new int[ratings.length];candy[0] = 1;// 从左往右判断,只要当前的比左边的大,当前的就在左边的基础上+1,否则就是1for (int i = 1; i < ratings.length; i++) {if (ratings[i] > ratings[i - 1]) candy[i] = candy[i - 1] + 1;else candy[i] = 1;}// 从右往左判断for (int i = ratings.length - 2; i >= 0; i--) {// 当前的比右边的大if (ratings[i] > ratings[i + 1]) {// 为了保证比两边都大candy[i] = Math.max(candy[i], candy[i + 1] + 1);}}for (int num : candy) res += num;return res;}}
S11.29-860. 柠檬水找零
class Solution {public boolean lemonadeChange(int[] bills) {int cash_5 = 0; //记录5块的张数int cash_10 = 0; //记录10块的张数for (int i = 0; i < bills.length; i++) {if (bills[i] == 5) cash_5++;else if (bills[i] == 10) {cash_5--;cash_10++;}else if (bills[i] == 20) {if (cash_10 > 0) {cash_10--;cash_5--;}else cash_5 -= 3;}if (cash_5 < 0 || cash_10 < 0) return false;}return true;}}
*M11.29-406. 根据身高重建队列
class Solution {public int[][] reconstructQueue(int[][] people) {// 身高从大到小排(身高相同k小的站前面)Arrays.sort(people, (o1, o2) -> {//如果身高h相同,k小的放前面if (o1[0] == o2[0]) return o1[1] - o2[1];//身高从大到小排序return o2[0] - o1[0];});// Arrays.sort(people, new Comparator<int[]>() {// @Override// public int compare(int[] o1, int[] o2) {// if (o1[0] == o2[0]) return o1[1] - o2[1];// return o2[0] - o1[0];// }// });LinkedList<int[]> que = new LinkedList<>();for (int[] p : people) {que.add(p[1],p); // 直接根据k的位置插入}return que.toArray(new int[people.length][]);}}
*M11.29-452. 用最少数量的箭引爆气球
// 左边界排序class Solution {public int findMinArrowShots(int[][] points) {if (points.length == 0) return 0;Arrays.sort(points, (o1, o2) -> Integer.compare(o1[0], o2[0]));int count = 1;for (int i = 1; i < points.length; i++) {// 如果两个气球不重合,那么就需要多一根箭if (points[i][0] > points[i -1][1]) count++;// 如果两个气球重合,一根箭就可以引爆。// 同时需要更新重叠气球的最小右边界。else points[i][1] = Math.min(points[i - 1][1], points[i][1]);}return count;}}// 右边界排序class Solution {public int findMinArrowShots(int[][] points) {if (points.length == 0) return 0;// 这里是按右边界排序Arrays.sort(points, (o1, o2) -> o1[1] < o2[1] ? -1 : 1);int count = 1;int pre = points[0][1];for (int i = 1; i < points.length; i++) {// 如果两个气球不重合,那么就需要多一根箭if (points[i][0] > pre) {count++;//更新最左右边界pre = points[i][1];}}return count;}}
*M11.30-435. 无重叠区间
//按右边界排序,不用考虑左边界class Solution {public int eraseOverlapIntervals(int[][] intervals) {if (intervals == null || intervals.length == 0) return 0;int res = 0;//排序按照右边界排,小的放前面。如果右边界相同,长度短的放前面Arrays.sort(intervals, (o1, o2) -> {// if (o1[1] == o2[1]) {// return Integer.compare(o1[0],o2[0]);// }return Integer.compare(o1[1],o2[1]);});//记录最小右边界int flag = intervals[0][1];for (int i = 1; i < intervals.length; i++) {// 如果左边界小于了最小右边界,则表示重合if (intervals[i][0] < flag) res++;// 更新最小右边界else flag = intervals[i][1];}return res;}}
*M12.1-763. 划分字母区间

class Solution {List<Integer> res = new ArrayList<>();public List<Integer> partitionLabels(String s) {char[] str = s.toCharArray();int[] edge = new int[26]; //记录每个字符的最远边界for (int i = 0; i < str.length; i++) {edge[str[i] - 'a'] = i;}int idx = 0; //记录目前最远边界int last = -1; //用来记录上一个边界的位置for (int i = 0; i < str.length; i++) {//idx记录当前字符的最远边界,如果当前字符最远边界比idx大,则更新最远边界idx = Math.max(idx, edge[str[i] - 'a']);//下标跟之前出现过的字符最大下标相等,则表示需要分割if (i == idx) {res.add(i - last);last = i;}}return res;}}
M12.02-56. 合并区间
class Solution {public int[][] merge(int[][] intervals) {// 如果只有一个区间,直接返回if (intervals.length == 1) return intervals;// 按左边界排序,如果左边界相等,右边界小的放前面Arrays.sort(intervals, (o1, o2) -> {if (o1[0] == o2[0]) return Integer.compare(o1[1], o2[1]);return Integer.compare(o1[0], o2[0]);});List<int[]> res = new ArrayList<>();int head = intervals[0][0]; //记录当前区间的头int tail = intervals[0][1]; //记录当前区间的尾for (int i = 1; i < intervals.length; i++) {// 出现重合就合并if (intervals[i][0] <= tail) tail = Math.max(tail, intervals[i][1]);else { //否则将当前区间加入到结果中,更新当前区间头尾res.add(new int[]{head, tail});head = intervals[i][0];tail = intervals[i][1];}// 上面的逻辑最后一个区间无法加入结果,需要额外处理if (i == intervals.length - 1) res.add(new int[]{head, tail});}return res.toArray(new int[res.size()][]);}}
M12.02-738. 单调递增的数字
// 从前往后遍历class Solution {public int monotoneIncreasingDigits(int n) {if (n / 10 == 0) return n;char[] c = Integer.toString(n).toCharArray();int res = 0;if (c[0] > c[1]) { // 如果最高位大于第二位,最高位-1,后面全是9res += c[0] - '0' - 1;for (int i = 1; i < c.length; i++) {res = res * 10 + 9;}}else { //否则,找出不满足单调递增的位置int idx = 0; //记录最大单增长度的下标for (int i = 1; i < c.length; i++) {if (c[i - 1] <= c[i]) idx = i;else break;}if (idx != c.length - 1) {//单增序列最后一个数字开始,后面全是9,相应的该位上数要-1,不用担心是0c[idx] = (char) (c[idx] - 1);}for (int i = idx - 1; i >= 0; i--) { //遍历修改后的单增序列// 如果最后一位(最大位)-1了还满足单增,直接跳出if (c[i] <= c[i + 1]) break;c[i + 1] = '9'; // 否则该位变为9c[i] = (char) (c[i] - 1); //前一位-1}for (int i = 0; i < c.length; i++) { //计算最终结果if (i <= idx) res = res * 10 + c[i] - '0';else res = res * 10 + 9;}}return res;}}//优化版 从后往前遍历class Solution {public int monotoneIncreasingDigits(int n) {if (n / 10 == 0) return n;char[] c = Integer.toString(n).toCharArray();int idx = c.length;// 从后向前遍历,找不满足单增的最左断点for (int i = idx - 1; i > 0; i--) {// 如果当前位置比前一个小,那么前一个数-1if (c[i] < c[i - 1]) {c[i - 1] = (char) (c[i - 1] - 1);idx = i; //更新断点从idx到最后全是9}}int res = 0;for (int i = 0; i < c.length; i++) {if (i < idx) res = res * 10 + c[i] - '0';else res = res * 10 + 9;}return res;}}
*M12.03-714. 买卖股票的最佳时机含手续费
// 贪心class Solution {public int maxProfit(int[] prices, int fee) {int res = 0;int low = prices[0];for (int i = 1; i < prices.length; i++) {//if (prices[i] < low) low = prices[i];// 保持原有状态,不进行操作if (prices[i] > low && prices[i] <= low + fee) continue;if (prices[i] > low + fee) {res += prices[i] - low - fee;low = prices[i] - fee;}}return res;}}// DPclass Solution {public int maxProfit(int[] prices, int fee) {//dp[i][j] 表示第i+1天的持股、不持股分别有多少现金int[][] dp = new int[prices.length][2];dp[0][0] = 0;dp[0][1] = -prices[0];for (int i = 1; i < prices.length; i++) {dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);}return dp[prices.length - 1][0];}}
*H12.05-968. 监控二叉树
https://leetcode-cn.com/problems/binary-tree-cameras/
class Solution {int res = 0;public int minCameraCover(TreeNode root) {if (trval(root) == 0) res++;return res;}private int trval(TreeNode root) {// 空节点,该节点有覆盖if (root == null) return 2;int left = trval(root.left);int right = trval(root.right);// 情况1// 左右节点都有覆盖if (left == 2 && right == 2) return 0;// 情况2 只要左右有一个没有覆盖为0,那么这个节点必须得有摄像头// left == 0 && right == 0 左右节点无覆盖// left == 1 && right == 0 左节点有摄像头,右节点无覆盖// left == 0 && right == 1 左节点有无覆盖,右节点摄像头// left == 0 && right == 2 左节点无覆盖,右节点覆盖// left == 2 && right == 0 左节点覆盖,右节点无覆盖if (left == 0 || right == 0) {res++;return 1;}// 情况3 左右节点有一个有摄像头,那么该节点就被覆盖// left == 1 && right == 2 左节点有摄像头,右节点有覆盖// left == 2 && right == 1 左节点有覆盖,右节点有摄像头// left == 1 && right == 1 左右节点都有摄像头// 其他情况前段代码均已覆盖if (left == 1 || right == 1) return 2;return -1;}}
动态规划
动态规划五部曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
-
S12.06-746. 使用最小花费爬楼梯
class Solution {public int minCostClimbingStairs(int[] cost) {if (cost == null || cost.length == 0) return 0;//dp表示爬到第i阶楼梯需要的最小体力,dp长度比cost多1,因为要爬完cost所有的楼梯int[] dp = new int[cost.length + 1];//爬到第0和第1阶楼梯都不耗费体力dp[0] = 0;dp[1] = 0;for (int i = 2; i <= cost.length; i++) {// 爬到i有两条路,从前一阶爬或者从前两阶爬,所以最小消耗就是// 爬到前面位置耗费的体力+从前面位置爬过来要耗费的体力dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[cost.length];}}
M12.7-62. 不同路径
//计算组合数class Solution {public int uniquePaths(int m, int n) {int t = m + n - 2;int count = m - 1;int denominator = m - 1; //分子long res = 1L; //分母while (count-- > 0) {res *= (t--);while (denominator != 0 && res % denominator == 0) {res /= denominator;denominator--;}}return (int)res;}}//动态规划class Solution {public int uniquePaths(int m, int n) {int[][] dp = new int[m][n];for (int i = 0; i < n; i++) {dp[0][i] = 1;}for (int i = 0; i < m; i++) {dp[i][0] = 1;}for (int i = 1; i < n; i++) {for (int j = 1; j < m; j++) {dp[j][i] = dp[j][i - 1] + dp[j - 1][i];}}return dp[m - 1][n - 1];}}//图 深度优先,超时class Solution {public int uniquePaths(int m, int n) {return dfs(1, 1, m , n);}public int dfs (int i, int j, int m, int n) {if (i > m || j > n) return 0; //越界了if (i == m && j == n) return 1; //找到了一种方法return dfs(i + 1, j, m , n) + dfs(i, j + 1, m ,n);}}
M12.7-63. 不同路径 II
// 动态规划class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {if (obstacleGrid == null || obstacleGrid.length < 1) return 0;int m = obstacleGrid.length;int n = obstacleGrid[0].length;int[][] dp = new int[m][n];// 初始化第一列for (int i = 0; i < m; i++) {// 如果当前位置有障碍物,那么从这个位置到最下面的位置都到不了if (obstacleGrid[i][0] == 1) break;dp[i][0] = 1;}//初始化第一行for (int i = 0; i < n; i++) {// 如果当前位置有障碍物,那么从这个位置到最后面的位置都到不了if (obstacleGrid[0][i] == 1) break;dp[0][i] = 1;}for (int i = 1; i < m; i++) {for (int j = 1; j < n; j++) {// 如果当前位置没有障碍物,就更新路径,有障碍物直接跳过if (obstacleGrid[i][j] == 0)dp[i][j] = dp[i - 1][j] + dp[i][j - 1];}}return dp[m - 1][n - 1];}}
M12.8-343. 整数拆分
```java // 数学 class Solution { public int integerBreak(int n) {
if (n <= 2) return 1;if (n == 3) return 2;// 3越多越好int a = n / 3;int b = n % 3;if (b == 1) {a--;return (int)(Math.pow(3, a) * 4);}if (b == 2) return (int)(Math.pow(3, a) * 2);return (int) Math.pow(3, a);
} } // 数学优化版 class Solution { public int integerBreak(int n) {
if (n <= 2) return 1;if (n == 3) return 2;if (n == 4) return 4;// 3越多越好int res = 1;// 如果最后拆的只剩4了就不用拆了,因为2+2=2*2,这样就杜绝了出现1,然后用一个3和一个1拆成两个2的情况while (n > 4) {res *= 3;n -= 3;}return res *= n;
} }
//动态规划 class Solution { public int integerBreak(int n) { //dp[n]表示数字n拆分得到的最大乘积,n应该从2开始 int[] dp = new int[n + 1]; dp[2] = 1; //dp[2]就一种拆法 // i从3开始 for (int i = 3; i <= n; i++) { for (int j = 1; j < i - 1; j++) { //j最大为i - 2 //j (i - j) 是单纯的把整数拆分为两个数相乘,而j dp[i - j]是拆分成两个以及两个以上的个数相乘。 dp[i] = Math.max(dp[i], Math.max(j (i - j), j dp[i - j])); } } return dp[n]; } }
<a name="sQk1F"></a>#### M12.09-[96. 不同的二叉搜索树](https://leetcode-cn.com/problems/unique-binary-search-trees/)这题要这么理解,n个数(1-n)组成的二叉搜索树的种树,需要考虑从1到n分别作为根节点的情况。<br />当j作为根节点时,左子树的节点有j-1个,右子树节点有n-j个。(比如1、2、3,2作为时左子树有1个节点,右子树也只有一个节点)<br />那么dp数组的含义为:dp[i]表示i个节点构成的二叉搜索树的个数。那么dp[3] = dp[0]*dp[2] (根节点为1) + dp[1]*dp[1] (根节点为2) + dp[2]*dp[0] (节点为3)```javaclass Solution {public int numTrees(int n) {//初始化 dp 数组int[] dp = new int[n + 1];//初始化0个节点和1个节点的情况dp[0] = 1;dp[1] = 1;for (int i = 2; i <= n; i++) {for (int j = 1; j <= i; j++) {//对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加//一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-jdp[i] += dp[j - 1] * dp[i - j];}}return dp[n];}}
*M12.12-416. 分割等和子集
把题目转变为0-1背包问题,分成两个集合也就是说背包容量是所有数的和的一半,每个物品的重量和价值都是value[i]。那么题目就转换为,背包中能装的最大价值是否等于所有物品价值总和的一半
//优化背包class Solution {public boolean canPartition(int[] nums) {if (nums == null || nums.length < 2) return false;int n = nums.length;int sum = 0;for (int num : nums) sum += num;//如果集合总和为奇数,那么就分不了if (sum % 2 != 0) return false;sum /= 2;int[] dp = new int[sum + 1];for (int i = 0; i < n; i++) {for (int j = sum; j >= nums[i]; j--) {//物品 i 的重量是 nums[i],其价值也是 nums[i]dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);}}return sum == dp[sum];}}//未优化背包class Solution {public boolean canPartition(int[] nums) {if (nums == null || nums.length < 2) return false;int n = nums.length;int sum = 0;for (int num : nums) sum += num;//如果集合总和为奇数,那么就分不了if (sum % 2 != 0) return false;sum /= 2;int[][] dp = new int[n][sum + 1];for (int i = 0; i <= sum; i++) {if (i >= nums[0]) dp[0][i] = nums[0];}for (int i = 1; i < n; i++) {for (int j = 1; j <= sum; j++) {if (j < nums[i]) dp[i][j] = dp[i - 1][j];else {//物品 i 的重量是 nums[i],其价值也是 nums[i]dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);}}}return sum == dp[n - 1][sum];}}
*M12.13-1049. 最后一块石头的重量 II
该问题可以转换为0-1背包问题,把石头分成两份,尽量的使两份石头重量相同。这里每块石头的重量stones[i]等于其价值value[i]。
动态规划五部曲:
- dp[j]表示容量为j的背包,最多可以背dp[j]这么重的石头;
- 递推公式:dp[j]=max(dp[j], dp[j - stones[i]] + stones[i])
- 怎么初始化:dp[j]表示容量,那么最大容量是所有石头重量的一半向下取整;初始化为0
- 确定遍历顺序:物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒叙遍历!
- 举例推导dp
//压缩后的dpclass Solution {// 这题的关键在于,将问题转化为尽量把石头分成重量相同的两堆public int lastStoneWeightII(int[] stones) {if (stones.length == 1) return stones[0];int sum = 0;for (int s : stones) sum += s;int target = sum / 2; //背包的最大容量就是所有石头重量和的一半int[] dp = new int[target + 1];//遍历,尽可能的装最重的石头for (int i = 0; i < stones.length; i++) {for (int j = target; j >= stones[i]; j--) {dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);}}// 两堆的重量分别为sum - dp[target]和dp[target],他们的差就是最后剩下的石头的重量return sum - dp[target] - dp[target];}}
*M12.13-494. 目标和
sum表示所有数的和,left-(sum-left)=target -> left=(target+sum)/2,转为0-1背包问题
dp[j]表示装满j重量的背包有dp[j]种方法//0-1背包 压缩class Solution {public int findTargetSumWays(int[] nums, int target) {int sum = 0;for (int num : nums) sum += num;if (Math.abs(target) > sum) return 0;if ((target + sum) % 2 != 0) return 0;int size = (target + sum) / 2;//这里把二维dp数组压缩了int[] dp = new int[size + 1]; //dp[j]表示,容量为j时,使用[0,i]共有多少种方法dp[0] = 1; //初始化dp数组,dp[0] = 1 容量为0的时候只有一种方法,就是不放for (int i = 0; i < nums.length; i++) {for (int j = size; j >= nums[i]; j--) {// 这里理解成遍历到第i个数,那么第i个数使用,就有dp[j - nums[i]]种方法,// 如果第i个数不使用,则有上一轮dp[j]种方法,将这两种情况相加,那么就是// 当前dp[j]的值。dp[j] = dp[j] + dp[j - nums[i]];}}return dp[size];}}// 0-1背包未压缩class Solution {public int findTargetSumWays(int[] nums, int target) {int sum = 0;for (int num : nums) sum += num;if (Math.abs(target) > sum) return 0;if ((target + sum) % 2 != 0) return 0;int size = (target + sum) / 2;//dp[i][j]数组表示,放下标0-i这nums[i]的时候,和为j的放法int[][] dp = new int[nums.length][size + 1];dp[0][0] = 1; // 放入第0个数的时候,和为0的方法。为1,就是不放if (nums[0] == 0) dp[0][0] = 2; //如果第一个数是0,那么+-0都可以满足,所以是2for (int i = 1; i <= size; i++) { //初始化第一行if (i == nums[0]) dp[0][i] = 1; // 只有当i=nums[0]的时候,放入nums[0]重量才能为i,此时只有一种放法}for (int i = 1; i < nums.length; i++) dp[i][0] = 1;for (int i = 1; i < nums.length; i++) {for (int j = 0; j <= size; j++) {if (j < nums[i]) dp[i][j] = dp[i - 1][j];else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];}}return dp[nums.length - 1][size];}}
*M12.18-474. 一和零
class Solution {public int findMaxForm(String[] strs, int m, int n) {// dp[i][j]表示包含i个0,j个1的子集的个数为dp[i][j];int[][] dp = new int[m + 1][n + 1];for (String s : strs) {int one = 0;int zero = 0;// 这个for循环用来统计当前字符窜中0和1的个数for (char c : s.toCharArray()) {if (c == '1') one++;else zero++;}// 这里遍历要从后往前,自己推一下就知道不能从前往后遍历for (int i = m; i >= zero; i--) {for (int j = n; j >= one; j--) {dp[i][j] = Math.max(dp[i][j], dp[i - zero][j - one] + 1);}}}return dp[m][n];}}
M12.20-518. 零钱兑换 II
```java // 二维dp数组 class Solution { public int change(int amount, int[] coins) {
} }// dp[i][j] 表示用[0,i]内的coins凑成j共有dp[i][j]种凑法int[][] dp = new int[coins.length][amount + 1];dp[0][0] = 1;for (int i = 1; i <= amount; i++) {if (i % coins[0] == 0) dp[0][i] = 1;}for (int coin : coins) {for (int i = 1; i < coins.length; i++) {for (int j = 0; j <= amount; j++) {if (j < coins[i]) dp[i][j] = dp[i - 1][j];else {// 主要是理解这句话的含义,画画图推一下dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];}}}}return dp[coins.length - 1][amount];
// 滚动数组 class Solution { public int change(int amount, int[] coins) { int[] dp = new int[amount + 1]; dp[0] = 1; //凑成0只有一种方法,那就是都不放 for (int i = 0; i < coins.length; i++) { for (int j = coins[i]; j <= amount; j++) { //滚动数组 对应二维dp数组 // dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]; dp[j] += dp[j - coins[i]]; } } return dp[amount]; } }
<a name="fbkRO"></a>#### *M12.20-[377. 组合总和 Ⅳ](https://leetcode-cn.com/problems/combination-sum-iv/)```java//回溯,超时class Solution {int res = 0;public int combinationSum4(int[] nums, int target) {if (nums == null || nums.length == 0) return 0;bk(nums, 0, target);return res;}public void bk(int[] nums, int sum, int target) {if (sum == target) {res++;return;}for (int i = 0; i < nums.length; i++) {if (nums[i] > (target - sum)) continue;bk(nums, sum + nums[i], target);}}}// dp 滚动数组class Solution {public int combinationSum4(int[] nums, int target) {int[] dp = new int[target + 1];dp[0] = 1;for (int j = 1; j <= target; j++) { //先遍历容量for (int i = 0; i < nums.length; i++) { // 再遍历物品if (j >= nums[i]) dp[j] += dp[j - nums[i]];}}return dp[target];}}
*M12.22-322. 零钱兑换
//二维dp数组class Solution {public int coinChange(int[] coins, int amount) {//dp[i][j]表示使用0-i的conis[i]凑满j需要的最少硬币个数int[][] dp = new int[coins.length][amount + 1];for (int i = 0; i < coins.length; i++) {Arrays.fill(dp[i], Integer.MAX_VALUE);}//初始化dp数组第一行for (int i = 0; i <= amount; i++) {int n = i % coins[0];if (n == 0) dp[0][i] = i / coins[0];}//求的是钱币个数,钱币顺序并不影响个数,所以先遍历谁都行for (int i = 1; i < coins.length; i++) {for (int j = 0; j <= amount; j++) {//**这里要判断一下dp[i][j - coins[i]]防止数据溢出**//dp[i - 1][j]表示不使用coins[i]凑到j所需最少硬币//使用coins[i]凑足总额为j - coins[i]的最少个数为dp[i][j - coins[i]],那么只需要加上一个钱币coins[i]即dp[i][j - coins[i]] + 1if (j >= coins[i] && dp[i][j - coins[i]] < Integer.MAX_VALUE) dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i]] + 1);else dp[i][j] = dp[i - 1][j];}}return dp[coins.length - 1][amount] == Integer.MAX_VALUE ? -1 : dp[coins.length - 1][amount];}}//dp滚动数组class Solution {public int coinChange(int[] coins, int amount) {int[] dp = new int[amount + 1];Arrays.fill(dp, Integer.MAX_VALUE);dp[0] = 0; //金额为0的时候硬币数量为0for (int i = 0; i < coins.length; i++) {for (int j = coins[i]; j <= amount; j++) {//凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1if (dp[j - coins[i]] < Integer.MAX_VALUE) dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);}}return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];}}
M12.23-279. 完全平方数
//滚动数组,先遍历物品class Solution {public int numSquares(int n) {if (n % Math.sqrt(n) == 0) return 1;int[] dp = new int[n + 1];Arrays.fill(dp, Integer.MAX_VALUE);dp[0] = 0;for (int i = 1; i <= Math.sqrt(n); i++) {int num = i * i;for (int j = num; j <= n; j++) {if (dp[j - num] < Integer.MAX_VALUE) dp[j] = Math.min(dp[j], dp[j - num] + 1);}}return dp[n];}}//滚动数组,先遍历容量class Solution {public int numSquares(int n) {if (n % Math.sqrt(n) == 0) return 1;int[] dp = new int[n + 1];Arrays.fill(dp, Integer.MAX_VALUE);dp[0] = 0;for (int j = 1; j <= n; j++) {for (int i = 1; i * i <= j; i++) {dp[j] = Math.min(dp[j], dp[j - i * i] + 1);}}return dp[n];}}
*M12.27-139. 单词拆分
// dp完全背包问题class Solution {public boolean wordBreak(String s, List<String> wordDict) {// dp[i]数组表示[0, i)组成的字符串能否被字段中的单词表示boolean[] dp = new boolean[s.length() + 1];dp[0] = true;int start = 0;for (int i = 1; i <= s.length(); i++) { //遍历背包for (int j = 0; j < i; j++) { // 遍历物品(子串)// 如果[j,i)子串包含在字典中,且dp[j]为true,那么就表示当前[0,i)的//子串能够被拆分成字典中的字符串表示if (wordDict.contains(s.substring(j, i)) && dp[j]) {dp[i] = true;}}}return dp[s.length()];}}//回溯,超时class Solution {public boolean wordBreak(String s, List<String> wordDict) {if (s.length() == 0) return false;return bk(s, wordDict, 0);}public boolean bk(String s, List<String> wordDict, int index) {if (index >= s.length()) return true;for (int i = index; i < s.length(); i++) {String sub = s.substring(index, i + 1);if (wordDict.contains(sub) && bk(s, wordDict, i + 1)) {return true;}}return false;}}
M12.29-198. 打家劫舍
// 我自己的思路class Solution {public int rob(int[] nums) {if (nums.length == 1) return nums[0];if (nums.length == 2) return nums[0] > nums[1] ? nums[0] : nums[1];//dp[i]表示偷第i家能够获取的最大金额int[] dp = new int[nums.length];dp[0] = nums[0]; //第一家最大金额就是其本身dp[1] = nums[1]; //第二家最大金额就是其本身dp[2] = nums[0] + nums[2]; //第三家最大金额为第一家跟第三家的和for (int i = 3; i < nums.length; i++) {// 因为不能偷相邻的,所以可以偷前一个或者前两个dp[i] = Math.max(dp[i - 2], dp[i - 3]) + nums[i];}int l = dp.length;// 最后需要判断一下最后两家哪个大,有可能偷倒数第二家更大// 比如[1,100,1]return dp[l - 1] > dp[l - 2] ? dp[l - 1] : dp[l - 2];}}// 代码随想录思路,这个写法更简洁class Solution {public int rob(int[] nums) {if (nums == null || nums.length == 0) return 0;if (nums.length == 1) return nums[0];//dp[i]考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。int[] dp = new int[nums.length];dp[0] = nums[0]; //第一家最大金额就是其本身dp[1] = Math.max(nums[0], nums[1]);for (int i = 2; i < dp.length; i++) {// 如果偷第i间房,那么dp[i] = dp[i - 2] + nums[i]// 即:第i-1房一定是不考虑的,只需要找到i-2以内的房屋获得的最大金额+第i间房屋的金额// 如果不偷第i间房,那么dp[i] = dp[i - 1],即考虑i-1间房的最大金额// 比较上述两种情况的最大金额,即可获得全局最大金额dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);}return dp[dp.length - 1];}}
M12.30-213. 打家劫舍 II
这题跟19题差不多,唯一的区别是成环了,成环的话主要有三种情况:
- 情况一:不考虑首尾元素
- 情况二:不考虑首元素
- 情况三:不考虑尾元素
注意,这里是考虑,但不一定使用,所以情况2跟3包含了1,只需要考虑情况2跟3得到的即可。
class Solution {public int rob(int[] nums) {if (nums == null || nums.length == 0) return 0;if (nums.length == 1) return nums[0];//int result1 = robRange(nums, 0, nums.length - 2);int result2 = robRange(nums, 1, nums.length - 1);return result1 > result2 ? result1 : result2;}public int robRange(int[] nums, int start, int end) {if (end == start) return nums[start];int[] dp = new int[nums.length];dp[start] = nums[start];dp[start + 1] = Math.max(nums[start + 1], nums[start]);for (int i = start + 2; i <= end; i++) {dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);}return dp[end];}}
*M1.1-337. 打家劫舍 III
class Solution {public int rob(TreeNode root) {if (root == null) return 0;int[] res = robAction(root);return Math.max(res[0], res[1]);}public int[] robAction(TreeNode root) {// dp数组,dp[0]表示不偷当前节点能获得的最大值//dp[1] 表示偷当前节点能获得的最大值int[] dp = new int[2];if (root == null) return dp;// 后续遍历int[] left = robAction(root.left);int[] right = robAction(root.right);// 如果不偷当前节点,那么最大值为左右子树(不用考虑左右子树偷还是不偷)的最大值总和。dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);// 如果偷当前节点,那么最大值为不偷左右子树+当前节点值的和。dp[1] = root.val + left[0] + right[0];return dp;}}
S1.2-121. 买卖股票的最佳时机
// 贪心算法class Solution {public int maxProfit(int[] prices) {int low = Integer.MAX_VALUE;int res = 0;for (int i = 0; i < prices.length; i++) {low = Math.min(prices[i], low); // 取最左最小价格res = Math.max(prices[i] - low, res); // 直接取最大区间利润}return res;}}// 动态规划class Solution {public int maxProfit(int[] prices) {// dp[i][0]表示第i天持股最大利润,dp[i][1]表示第i天不持股最大利润int[][] dp = new int[prices.length][2];dp[0][0] = -prices[0];for (int i = 1; i < prices.length; i++) {// 第i天持股,分两种情况:延续前一天的状态或者今天买入// 因为只能买一次,所以如果是第i天买入,那么dp[i][0] = -prices[i]dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);// 第i天不持股,分两种情况:延续前一天的状态或者今天卖出dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);}return dp[prices.length - 1][1];}}
M1.2-122. 买卖股票的最佳时机 II
// 贪心 只要利润是正的就买class Solution {public int maxProfit(int[] prices) {int res = 0;for (int i = 1; i < prices.length; i++) {int profit = prices[i] - prices[i - 1];if (profit > 0) res += profit;}return res;}}// 动态规划class Solution {public int maxProfit(int[] prices) {int len = prices.length;//dp[i][0] 表示第i天持股所获最大利润//dp[i][1] 表示第i天不持股所获最大利润int[][] dp = new int[len][2];dp[0][0] = -prices[0];for (int i = 1; i < len; i++) {// 第i天持股,有两种情况:// 1. 延续前一天的状态dp[i - 1][0]// 2. 今天买入 dp[i - 1][1] - prices[i]// 这里要与只能买一次区分开,如果只能买一次那么dp[i][1] = -prices[i]dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);// 第i天不持股,有两种情况:// 1. 延续前一天的状态// 2. 今天卖出:dp[i - 1][0] + prices[i]dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);}return dp[len - 1][1];}}
H1.04-123. 买卖股票的最佳时机 III
// 动态规划1class Solution {public int maxProfit(int[] prices) {int len = prices.length;if (len <= 1) return 0;// dp[i][j]表示第i天持股状态j所获最大利润// j有5个取值:0表示不做任何操作,1表示第一次买入,2表示第一次卖出// 3表示第二次买入,4表示第二次卖出int[][] dp = new int[len][5];dp[0][1] = -prices[0];dp[0][3] = -prices[0];for (int i = 1; i < len; i++) {dp[i][0] = dp[i - 1][0]; // 不做任何操作直接延续前一天的状态// 第i天持股状态是第一次买入状态有两种情况:// 1. 直接延续前一天的状态 dp[i - 1][1]// 2. 第i天买入 dp[i - 1][0] - prices[i]dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);// 第i天持股状态是第一次卖出状态有两种情况:// 1. 直接延续前一天的状态 dp[i - 1][2]// 2. 第i天卖出 dp[i - 1][1] + prices[i]dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2]);// 第i天持股状态是第二次买入状态有两种情况:// 1. 直接延续前一天的状态 dp[i - 1][3]// 2. 第i天买入 dp[i - 1][2] - prices[i]dp[i][3] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][3]);// 第i天持股状态是第二次卖出状态有两种情况:// 1. 直接延续前一天的状态 dp[i - 1][4]// 2. 第i天买入 dp[i - 1][3] + prices[i]dp[i][4] = Math.max(dp[i - 1][3] + prices[i], dp[i - 1][4]);}return dp[len - 1][4];}}
H1.04-188. 买卖股票的最佳时机 IV
这一题跟上一题其实差不多,只不过交易次数是不固定的。这里我没有设置无操作的状态,因为无操作的状态都是0,可以节省部分内存。
class Solution {public int maxProfit(int k, int[] prices) {int len = prices.length;if (k == 0 || len <= 1) return 0;// dp[i][j] 表示第i天持股状态j下,获取的最大收益为dp[i][j]int[][] dp = new int[len][k * 2];for (int i = 0; i < 2 * k; i += 2) {dp[0][i] = -prices[0];}for (int i = 1; i < len; i++) {// 第j + 1次买入for (int j = 0; j < 2 * k; j += 2) {if (j == 0) {// 第i天处于第一次买入状态,有两种情况// 1. 维持前一天的第一次买入状态dp[i - 1][0]// 2. 第i天买入,-prices[i]dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);}else {// 第i天处于非第一次买入状态,有两种情况// 1. 维持前一天的第一次买入状态dp[i - 1][j]// 2. 第i天买入,前一天处于第一次卖出状态// dp[i - 1][j - 1] - prices[i]dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);}}// 第j次卖出for (int j = 1; j < 2 * k; j += 2) {// 卖出状态不需要分第一次还是非第一次// 第i天处于第j次卖出状态有两种情况:// 1. 维持前一天第j次卖出状态 dp[i - 1][j]// 2. 第i天卖出股票,前一天第j次买入收益+当前股价// dp[i - 1][j - 1] + prices[i]dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);}}return dp[len - 1][2 * k - 1];}}
*M1.05-309. 最佳买卖股票时机含冷冻期
这一题一定要弄清楚股票的四个状态:
状态0:今天买入 dp[i][0]
状态1:前两天卖出,度过冻结期的卖出状态 dp[i][1]
状态2:今天刚好卖出 dp[i][2]
状态3:今天是冻结期 dp[i][3]
然后一定要弄清楚每个状态的递推关系,dp数组怎么初始化!
class Solution {public int maxProfit(int[] prices) {if (prices == null || prices.length <= 1) return 0;int len = prices.length;// dp[i][j]表示在第i天持股状态为j下获得的最大利润// 持股状态有四种:// 0. 表示买入状态// 卖出状态: 1. 前两天卖出了股票,度过了冷冻期 2. 今天卖出了股票// 3. 冷冻期int[][] dp = new int[len][4];dp[0][0] = -prices[0];for (int i = 1; i < len; i++) {// 第i天为买入状态的,有三种情况:// 1. 维持前一天的买入状态 dp[i - 1][0]// 2. 前一天为非冻结期的卖出状态: dp[i - 1][1] - prices[i]// 3. 前一天为冻结期: dp[i - 1][3] - proces[i]dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][1], dp[i - 1][3]) - prices[i]);// 第i天为卖出状态// I、第i天为前两天卖出股票,度过冷冻期状态,有两种情况:// 1. 维持前一天的度过冷冻期卖出状态 dp[i - 1][1]// 2. 前一天为冷冻期 dp[i - 1][3]dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]);// II、第i天为今天卖出状态,只有一种情况// 前一天为买入,今天卖出: dp[i - 1][0] + prices[i]dp[i][2] = dp[i - 1][0] + prices[i];// 第i天为冻结期,只有一种情况,前一天卖股票dp[i][3] = dp[i - 1][2];}// 最后最大利润的状态可能分别是状态1、状态2和状态3// 状态1:前两天卖出,度过冻结期卖出状态// 状态2:今天卖出股票// 状态3:今天是冻结期return Math.max(dp[len - 1][3], Math.max(dp[len - 1][1], dp[len - 1][2]));}}
M1.06-714. 买卖股票的最佳时机含手续费
class Solution {public int maxProfit(int[] prices, int fee) {//dp[i][j] 表示第i+1天的持股、不持股分别有多少现金int[][] dp = new int[prices.length][2];dp[0][0] = - prices[0];for (int i = 1; i < prices.length; i++) {// 第i+1天持股状态分两种情况:// 1. 维持前一天的持股状态 dp[i - 1][0]// 2. 今天买入 dp[i - 1][0] - prices[i]dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);// 第i+1天不持股状态 分两种情况:// 1. 维持前一天的不持股状态 dp[i - 1][1]// 2. 今天卖出股票 dp[i - 1][0] + prices[i] - feedp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);}return dp[prices.length - 1][1];}}
M1.07-300. 最长递增子序列
这题看看力扣的题解,有加入二分查找时间复杂度更低的方法
// 动态规划class Solution {public int lengthOfLIS(int[] nums) {if (nums.length == 1) return 1;// dp[i] 表示0-i构成的数组的最长递增子序列为dp[i]int[] dp = new int[nums.length];Arrays.fill(dp, 1); // 不管数组元素是什么顺序,最长子序列最少是1int result = 1;for (int i = 1; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[j] + 1, dp[i]);}// 更新最长子序列result = result > dp[i] ? result : dp[i];}return result;}}
//S1.08-674. 最长连续递增序列
*M1.08-718. 最长重复子数组
解题思路:主要是dp数组的定义以及递推公式,dp[i][j]的状态只能通过dp[i-1][j-1]来推导,这一点的理解极为重要
class Solution {public int findLength(int[] nums1, int[] nums2) {// dp[i][j] 表示nums1中0到i-1元素组成的子数组// 和nums2中0到j-1元素组成的子数组的最长公共子数组为dp[i][j]// dp[0][j]和dp[i][0]都是没有意义的,均为0int[][] dp = new int[nums1.length + 1][nums2.length + 1];int result = 0;// 遍历顺序对最终结果不产生影响for (int i = 1; i <= nums1.length; i++) {for (int j = 1; j <= nums2.length; j++) {// 如果nums1[i - 1]和nums2[j - 1]相等那么两个数组的当前元素// 可以组成一个长度为1的子序列// 根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。// 即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;if (nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;if (dp[i][j] > result) result = dp[i][j];}}return result;}}
*M1.09-1143. 最长公共子序列
class Solution {public int longestCommonSubsequence(String text1, String text2) {// dp[i][j]表示长度为[0,i-1]的字符串text1和长度为[0,j-1]的字符串text2的最长公共子序列长度为dp[i][j]int[][] dp = new int[text1.length() + 1][text2.length() + 1];for (int i = 1; i < dp.length; i++) {for (int j = 1; j < dp[0].length; j++) {if (text1.charAt(i - 1) == text2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;else {dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}return dp[text1.length()][text2.length()];}}
M1.11-1035. 不相交的线
这题跟上一题1143. 最长公共子序列一样的思路
class Solution {public int maxUncrossedLines(int[] nums1, int[] nums2) {// dp[i][j] 表示nums1 [0,i-1]和nums2 [0,j-1]能构成的最多直线为dp[i][j]int[][] dp = new int[nums1.length + 1][nums2.length + 1];for (int i = 1; i <= nums1.length; i++) {for (int j = 1; j <= nums2.length; j++) {// 当nums1[i]与nums2[j]相等时,可以直接连一条线// 不会与前面的线交叉if (nums1[i - 1] == nums2[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;}else {// 不相等时,选择dp[i-1][j]和dp[i][j-1]中大的一个dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}return dp[nums1.length][nums2.length];}}
S1.12-53. 最大子数组和
// 动态规划class Solution {public int maxSubArray(int[] nums) {//dp[i]表示nums中[0,i]中构成的连续子数组最大和为dp[i]int[] dp = new int[nums.length];dp[0] = nums[0];int res = dp[0]; // 用于保存dp[i]的最大值for (int i = 1; i < nums.length; i++) {//dp[i]只有两个方向可以推出来://1. dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和//2. nums[i],即:从头开始计算当前连续子序列和dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);res = res > dp[i] ? res : dp[i];}return res;}}// 贪心算法class Solution {public int maxSubArray(int[] nums) {int res = nums[0];for (int i = 1; i < nums.length; i++) {// 计算前面的子序列最大和跟当前值相加 有没有比 当前单独值大// 如果没有,那么当前值组成的子序列就是最大nums[i] = Math.max(nums[i], nums[i] + nums[i - 1]);// 更新最大和res = nums[i] > res ? nums[i] : res;}return res;}}
S1.12-392. 判断子序列
这题用双指针更好,dp题解
// 动态规划class Solution {public boolean isSubsequence(String s, String t) {if (s.length() == 0) return true;if (t.length() == 0) return false;// dp[i][j]表示表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]int[][] dp = new int[s.length() + 1][t.length() + 1];int res = 0;for (int i = 1; i <= s.length(); i++) {for (int j = 1; j <= t.length(); j++) {// 当前两字符相等,相同子序列长度在dp[i - 1][j - 1]上增加1if (s.charAt(i - 1) == t.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1] + 1;}// 如果不相等,需要跳过t的第j-1个字符,因此dp[i][j]=dp[i][j-1]else {dp[i][j] = dp[i][j - 1];}res = res > dp[i][j] ? res : dp[i][j];if (res == s.length()) return true;}}return false;}}// 双指针class Solution {public boolean isSubsequence(String s, String t) {if (s.length() == 0) return true;if (t.length() == 0) return false;int index = 0; // 指向s中某个字符的指针// i表示指向t中某个字符的指针for (int i = 0; i < t.length(); i++) {// 如果i指向的字符与index指向的字符相等// 那么index向后挪一位if (t.charAt(i) == s.charAt(index)) {index++;}// 一旦index与s的长度相等,则表示匹配完毕if (index == s.length()) return true;}// 否则表示不能匹配return false;}}
*H1.18-115. 不同的子序列
class Solution {public int numDistinct(String s, String t) {// dp数组表示以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。int[][] dp = new int[s.length() + 1][t.length() + 1];// t为空串的时候跟任意s都可以匹配for (int i = 0; i <= s.length(); i++) dp[i][0] = 1;for (int i = 1; i <= s.length(); i++) {for (int j = 1; j <= t.length(); j++) {// 当指向字符相等,dp[i][j]可以有两部分组成。// 1. 用s中下标为i-1的字符来匹配,即dp[i - 1][j - 1]// 2. 不用s中下标为i-1的字符来匹配,即dp[i - 1][j];if (s.charAt(i - 1) == t.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];}else { // 指向字符不相等// 不相等时,只存在一种情况,即不用s中下标为i-1的字符匹配dp[i][j] = dp[i - 1][j];}}}return dp[s.length()][t.length()];}}
*M1.19-583. 两个字符串的删除操作
class Solution {public int minDistance(String word1, String word2) {// dp[i][j]表示word1以索引i-1结尾的字符串和word2以j-1结尾的字符串达到相等// 需要删除的元素最少次数为dp[i][j]int[][] dp = new int[word1.length() + 1][word2.length() + 1];// 初始化dp,当其中一个字符串为空时,剩下一个需要删除所有字符for (int i = 0; i <= word1.length(); i++) dp[i][0] = i;for (int j = 0; j <= word2.length(); j++) dp[0][j] = j;for (int i = 1; i <= word1.length(); i++) {for (int j = 1; j <= word2.length(); j++) {// 当word1[i-1]与word2[j-1]相等时,不需要进行删除操作// 直接延续word1[i-2]与word2[j-2]所需要删除的次数if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];}else {// 当word1[i-1]与word2[j-1]不相等时,存在三种情况// 1. 删除word1[i - 1]: dp[i - 1][j] + 1// 2. 删除word2[j - 1]: dp[i][j - 1] + 1// 3. 同时删除word1[i - 1]和word2[j - 1]: dp[i - 1][j - 1] + 2// 选其中最小的一个dp[i][j] = Math.min(dp[i - 1][j - 1] + 2, Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));}}}return dp[word1.length()][word2.length()];}}
*H1.21-72. 编辑距离
class Solution {public int minDistance(String word1, String word2) {// dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]int[][] dp = new int[word1.length() + 1][word2.length() + 1];// 当word2为空串时,word1[i-1]需要进行i次删除操作for (int i = 1; i <= word1.length(); i++) dp[i][0] = i;// 当word1为空串时,word1[i-1]需要进行i次插入操作for (int j = 1; j <= word2.length(); j++) dp[0][j] = j;for (int i = 1; i <= word1.length(); i++) {for (int j = 1; j <=word2.length(); j++) {// 如果word1[i-1]跟word2[j-1]相等,不进行操作if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];}else {// 如果word1[i-1]跟word2[j-1]不相等,需要进行增、删或者换// 增:word1以下标i-1结尾的字符串可以跟word2以下标j-2结尾的字符串匹配;可以理解为wrod2删除一个元素(因为word2删除一个元素相当于word1增加一个元素) dp[i][j - 1] + 1// 删:word1以下标i-2结尾的字符串已经可以跟word2以下标j-1的匹配 dp[i - 1][j] + 1// 换:dp[i - 1][j - 1] + 1dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;}}}return dp[word1.length()][word2.length()];}}
*M2.3-647. 回文子串
题解(动态规划和双指针)
动态规划一定要注意遍历的顺序;
双指针每次都以一个点和两点个为中心,像两边拓展遍历。
// 动态规划class Solution {public int countSubstrings(String s) {int res = 0;int len = s.length();if (s == null || len < 1) return res;//dp[i][j]:s字符串下标i到下标j的字串是否是一个回文串,即s[i, j]boolean dp[][] = new boolean[len][len];// 注意遍历顺序,因为在计算dp[i][j]的时候需要用到dp[i + 1][j - 1]// 所以需要保证i+1跟j-1在之前已经遍历过了for (int j = 0; j < len; j++) {for (int i = 0; i <= j; i++) {// 如果s[i]与s[j]相等,分三种情况// 1. i=j,即一个字符,那么就直接是true// 2. i和j之间相差1,那么也是true// 3. i和j之间相差大于1的时候,那么dp[i][j] = dp[i+1][j-1]if (s.charAt(i) == s.charAt(j)) {if (j - i <= 1) { // 情况1和情况2可以合并dp[i][j] = true;res++;}else if (dp[i + 1][j - 1]) { // 情况3dp[i][j] = true;res++;}}}}return res;}}// 双指针class Solution {public int countSubstrings(String s) {int len = s.length();int res = 0;if (s == null || len < 0) return 0;for (int i = 0; i < len; i++) {res += extend(s, i, i, len); // 以i为中心res += extend(s, i, i + 1, len); // 以i和i+1为中心}return res;}public int extend(String s, int i, int j, int n) {int res = 0;while (i >= 0 && j < n && s.charAt(i) == s.charAt(j)) {i--;j++;res++;}return res;}}
*M2.4-516. 最长回文子序列
class Solution {public int longestPalindromeSubseq(String s) {if (s == null || s.length() < 1) return 0;int len = s.length();// dp[i][j]表示s[i,j]所包含的最长回文子序列为dp[i][j]int dp[][] = new int[len][len];// 当i=j时,回文子序列长度为1for (int i = 0; i < len; i++) dp[i][i] = 1;// 注意遍历顺序,因为dp[i][j]需要dp[i+1][j-1]、dp[i+1][j]// 和dp[i][j-1]来计算,所以i需要从后向前遍历,j需要从前往后遍历,// 同时j>i,因为i指向的是子序列的头,j指向子序列的尾for (int i = len - 1; i >= 0; i--) {for (int j = i + 1; j < len; j++) {// dp[i][j]递推分为两种情况// 1. s[i]=s[j] dp[i][j]=dp[i+1][j-1]+2;// 2. s[i]!=s[j] 取只加入左边或者右边大的那一个// dp[i][j]=max(dp[i+1][j],dp[i][j-1])if (s.charAt(i) == s.charAt(j)) {dp[i][j] = dp[i + 1][j - 1] + 2;}else {dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);}}}return dp[0][len - 1];}}
单调栈
什么时候用到单调栈? 通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
*M2.24-739. 每日温度
// 详细写法class Solution {public int[] dailyTemperatures(int[] temperatures) {if (temperatures == null || temperatures.length == 0) return new int[0];int[] res = new int[temperatures.length];Stack<Integer> s = new Stack<>(); // 栈里面放的是下标,温度从栈顶到栈底为递增s.push(0);for (int i = 1; i < res.length; i++) {// 情况1,下标为i的温度小于栈顶下标温度// 情况2,下标为i的温度等与栈顶下标温度if (temperatures[i] <= temperatures[s.peek()]) {s.push(i);}else { //情况3,下标为i的温度大于栈顶下标温度// 先将栈顶下标温度小的全部出栈,同时更新结果while (!s.isEmpty() && temperatures[i] > temperatures[s.peek()]) {int preIndex = s.pop();res[preIndex] = i - preIndex;}s.push(i);}}return res;}}// 精简写法class Solution {public int[] dailyTemperatures(int[] temperatures) {if (temperatures == null || temperatures.length == 0) return new int[0];int[] res = new int[temperatures.length];Stack<Integer> s = new Stack<>(); // 栈里面放的是下标for (int i = 0; i < res.length; i++) {while (!s.isEmpty() && temperatures[i] > temperatures[s.peek()]) {int preIndex = s.pop();res[preIndex] = i - preIndex;}s.push(i);}return res;}}
S2.25-496. 下一个更大元素 I
class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {int[] res = new int[nums1.length];// map用来存储nums2中每个元素第一个比他大的值// key表示当前元素值,value表示nums2中下一个比他大的值Map<Integer, Integer> map = new HashMap<>();// s为单调栈,用来找出第一个比当前元素大的元素,存储的是元素值(不是下标)// 栈顶到栈底为递增Stack<Integer> s = new Stack<>();for (int i = 0; i < nums2.length; i++) {map.put(nums2[i], -1); // 初始化,全为-1}// 单调栈,找nums2中元素右侧第一个比当前元素大的元素for (int i = 0; i < nums2.length; i++) {// 如果栈顶元素小于入栈元素,则找到了第一个比他大的元素// 出栈,然后更新map中的记录while (!s.isEmpty() && s.peek() < nums2[i]) {map.put(s.pop(), nums2[i]);}s.push(nums2[i]);}// 因为nums1是nums2的子集,所指直接从map中取结果即可for (int i = 0; i < nums1.length; i++) {res[i] = map.get(nums1[i]);}return res;}}
M2.26-503. 下一个更大元素 II
// 遍历两遍数组class Solution {public int[] nextGreaterElements(int[] nums) {if (nums == null || nums.length == 0) return null;int size = nums.length;int[] res = new int[size];Arrays.fill(res, -1);Stack<Integer> s = new Stack<>(); // 单调栈,存储下标,栈顶到栈底递增// 直接遍历两遍数组即可for (int i = 0; i < size * 2; i++) {while (!s.isEmpty() && nums[i % size] > nums[s.peek()]) {res[s.pop()] = nums[i % size];}s.push(i % size);}return res;}}// 先遍历数组 再遍历栈class Solution {public int[] nextGreaterElements(int[] nums) {if (nums == null || nums.length == 0) return null;int[] res = new int[nums.length];Arrays.fill(res, -1);Stack<Integer> s = new Stack<>(); // 单调栈,存储下标,栈顶到栈底递增// 首先找右边第一个比当前值大的,普通单调栈的找法就可以for (int i = 0; i < nums.length; i++) {while (!s.isEmpty() && nums[i] > nums[s.peek()]) {res[s.pop()] = nums[i];}s.push(i);}// 如果找完右边大的元素,栈中元素个数大于1,则表示需要循环搜索if (s.size() > 1) {for (int i = 0; i < nums.length; i++) {// 找到比栈顶元素大的元素,同时要保证栈中的元素必须大于1while (s.size() > 1 && nums[i] > nums[s.peek()]) {res[s.pop()] = nums[i];}// 因为没有比最大元素大的元素,所以栈的长度至少为1,此时搜寻完毕if (s.size() <= 1) break;}}return res;}}
*H2.26-42. 接雨水
三种解法,双指针、动态规划、单调栈;其中双指针跟动态规划的思想是一样的,都是按列来计算水量;单调栈是通过行来计算水量。
// 双指针 时间n^2 空间1class Solution {public int trap(int[] height) {if (height == null || height.length <= 1) return 0;int res = 0;for (int i = 0; i < height.length; i++) {// 第一根柱子跟最后一根柱子不接雨水if (i == 0 || i == height.length - 1) continue;int rH = height[i]; // 记录右边柱子的最高高度int lH = height[i]; // 记录左边柱子的最高高度for (int r = i + 1; r < height.length; r++) {if (height[r] > rH) rH = height[r];}for (int l = i - 1; l >= 0; l--) {if (height[l] > lH) lH = height[l];}// 当前柱子能接的雨水高度=min(左边最高柱子,右边最高柱子)-当前柱子高度int cur = Math.min(rH, lH) - height[i];res += cur;}return res;}}// 动态规划 时间n 空间nclass Solution {public int trap(int[] height) {if (height == null || height.length <= 1) return 0;int res = 0;// 为了避免重复计算左边与右边最高柱子的高度// 使用maxRight和maxLeft数组来存储当前柱子左右侧的最高高度int[] maxRight = new int[height.length];int[] maxLeft = new int[height.length];int len = height.length;// 记录每个柱子左边柱子最大高度maxLeft[0] = height[0];for (int i = 1; i < len; i++) {// 动态规划的思想maxLeft[i] = Math.max(maxLeft[i - 1], height[i]);}// 记录每个柱子右边柱子最大高度maxRight[len - 1] = height[len - 1];for (int i = len - 2; i >= 0; i--) {// 动态规划的思想maxRight[i] = Math.max(maxRight[i + 1], height[i]);}for (int i = 0; i < len; i++) {// 第一根柱子跟最后一根柱子不接雨水if (i == 0 || i == len - 1) continue;// 当前柱子能接的雨水高度=min(左边最高柱子,右边最高柱子)-当前柱子高度int cur = Math.min(maxRight[i], maxLeft[i]) - height[i];res += cur;}return res;}}// 单调栈class Solution {public int trap(int[] height) {if (height == null || height.length <= 2) return 0;int res = 0;// 存着下标,计算的时候用下标对应的柱子高度,栈顶到栈底是单调递增Stack<Integer> s = new Stack<>();s.push(0);for (int i = 1; i < height.length; i++) {// 情况1:当前柱子高度小于栈顶柱子高度,直接入栈if (height[i] < height[s.peek()]) {s.push(i);}// 情况2:当前柱子高度等于栈顶柱子高度,要跟更新栈顶元素,// 因为遇到相相同高度的柱子,需要使用最右边的柱子来计算宽度。else if (height[i] == height[s.peek()]) {s.pop();s.push(i);}// 情况3:当前柱子高度大于栈顶柱子高度,此时会形成凹槽else {while (!s.isEmpty() && height[i] > height[s.peek()]) {int mid = s.pop(); // 记录凹槽柱子的下标if (!s.isEmpty()) {// 计算高度 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度int h = Math.min(height[i], height[s.peek()]) - height[mid];// 计算宽度 雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1// 因为只求中间的宽度int w = i - s.peek() - 1;res += h * w;}}s.push(i);}}return res;}}
*H2.27-84. 柱状图中最大的矩形
这题跟接雨水类似。
单调栈的解法,相当于以每根柱子为高,计算能得到的最大面积,类似于一个穷举?
// 动态规划class Solution {public int largestRectangleArea(int[] heights) {if (heights == null || heights.length == 0) return 0;if (heights.length == 1) return heights[0];int res = 0;int size = heights.length;int[] minRight = new int[size]; // 记录每个柱子 右边第一个小于该柱子的下标int[] minLeft = new int[size]; // 记录每个柱子 左边第一个小于该柱子的下标minLeft[0] = -1; // 第一个柱子左边没有for (int i = 1; i < size; i++) {int t = i - 1; // 从左边第一个柱子开始// 动态规划,寻找左侧第一个小于该柱子的下标// 解释一下t=minLeft[t]: 当第t跟柱子高于该柱子的时候,那么直接找第t根柱子// 左侧的第一根比第t跟柱子小的柱子即可。然后再与该柱子比较while (t >= 0 && heights[t] >= heights[i]) t = minLeft[t];minLeft[i] = t;}// 记录每个柱子 右边第一个小于该柱子的下标minRight[size - 1] = size; // 注意这里初始化,防止下面while死循环for (int i = size - 2; i >= 0; i--) {int t = i + 1;// 动态规划,寻找右侧第一个小于该柱子的下标// t=minRigth[i]的思想跟找左侧的一样while (t < size && heights[t] >= heights[i]) t = minRight[t];minRight[i] = t;}// 求解for (int i = 0; i < size; i++) {int sum = (minRight[i] - minLeft[i] - 1) * heights[i];res = Math.max(sum, res);}return res;}}// 单调栈class Solution {public int largestRectangleArea(int[] heights) {if (heights == null || heights.length == 0) return 0;// 首先需要将原数组扩容,因为最左侧跟最右侧的柱子两侧没有柱子,所以应该是0int[] newHeigths = new int[heights.length + 2];for (int i = 0; i < heights.length; i++) {newHeigths[i + 1] = heights[i];}heights = newHeigths;// 栈顶到栈底的顺序应该是从大到小,跟接雨水不同,接雨水是从小到大// 本题是要找每个柱子左右两边第一个小于该柱子的柱子Stack<Integer> s = new Stack<>();s.push(0);int res = 0;for (int i = 1; i < heights.length; i++) {// 情况1:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况if (heights[i] > heights[s.peek()]) s.push(i);// 情况2:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况else if (heights[i] == heights[s.peek()]) {s.pop();s.push(i);}else {while (heights[i] < heights[s.peek()]) {int mid = s.pop();int left = s.peek(); // 左边第一个比当前元素小的柱子int right = i;int w = right - left - 1;int h = heights[mid];res = Math.max(res, w * h);}s.push(i);}}return res;}}
