文章目录
- 1 哈希
- 2 双指针
- 3 滑动窗口
- 4 子串
- 5 普通数组
- 6 矩阵
- 7 链表
- 8 二叉树
- 8.1 94.二叉树的中序遍历🟢
- 8.2 104.二叉树的最大深度🟢
- 8.3 226.翻转二叉树🟢
- 8.4 101.对称二叉树🟢
- 8.5 543.二叉树的直径🟢
- 8.6 102.二叉树的层序遍历🟡
- 8.7 103.二叉树的锯齿形层序遍历(扩展)🟡
- 8.8 108.将有序数组转换为二叉搜索树🟢
- 8.9 98.验证二叉搜索树🟡
- 8.10 230.二叉搜索树中第 K 小的元素🟡
- 8.11 199.二叉树的右视图🟡
- 8.12 114.二叉树展开为链表🟡
- 8.13 105.从前序与中序遍历序列构造二叉树🟡待做
- 8.14 437.路径总和 III🟡待做
- 8.15 236.二叉树的最近公共祖先🟡
- 8.16 124.二叉树中的最大路径和🔴待做
- 9 图论
- 10 回溯
- 11 二分查找
- 12 栈
- 13 堆
- 14 贪心算法
- 15 动态规划
- 16 多维动态规划
- 17 技巧
- 18 补充
尚未完结,后续会不定时更新
1 哈希
1.1 1.两数之和🟢
题目:给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
链接:1. 两数之和
示例 :
输入:nums = [3,3], target = 6
输出:[0,1]
思路:
两个 for 循环可以解决,但第2个 for 循环可以用哈希表来快速查,不用一个个遍历
代码:
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> hashMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 查表,看看是否有能和 nums[i] 凑出 target 的元素
int need = target - nums[i];
if (hashMap.containsKey(need)) {
return new int[]{hashMap.get(need), i};
}
// 查不到则存入映射,这样只用一次for循环
hashMap.put(nums[i], i);
}
return null;
}
}
1.2 49.字母异位词分组🟡
题目:给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
链接:49. 字母异位词分组
示例 :
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
思路:
难点在于怎么判断哪些单词属于异味词,因为不能直接用 ==
来判断。
观察异位词的特点可以看出他们排序后可以用 ==
来直接判断
代码:
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> hashMap = new HashMap<>();
for (String str : strs) {
// 字符串转换成数组对字符串的字符排序
char[] array = str.toCharArray();
Arrays.sort(array);
String key = new String(array);
// 获取key对应的集合,若不存在则返回一个空集合
List<String> list = hashMap.getOrDefault(key, new ArrayList<String>());
list.add(str);
hashMap.put(key, list);
}
return new ArrayList<>(hashMap.values());
}
}
1.3 128.最长连续序列🟡
题目:给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
链接:128. 最长连续序列
示例 :
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
思路:
利用哈希集和可以快速判断 nums
是否存在某个数字。
遍历 nums
,若存在 num-1
,说明当前数字不是该连续序列的起始值,从而过滤掉一些情况。之后不断用哈希集和判断是否存在序列的下一个数值
代码:
class Solution {
public int longestConsecutive(int[] nums) {
// 转化成哈希集合,不需要HashMap,只关注是否存在
Set<Integer> set = new HashSet<Integer>();
for (int num : nums) {
set.add(num);
}
int res = 0;
for (int num : set) {
// num 不是连续子序列的第一个,跳过
if (set.contains(num - 1)) {
continue;
}
// num 是连续子序列的第一个,开始向后计算连续子序列的长度
int curNum = num;
int curLen = 0;
while (set.contains(curNum)) {
curNum += 1;
curLen += 1;
}
// 更新最长连续序列的长度
res = Math.max(res, curLen);
}
return res;
}
}
2 双指针
2.1 283.移动零🟢
题目:给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
链接:283. 移动零
示例 :
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
思路:
快指针遍历数组,慢指针记录数组下一个不为0元素的位置。
快指针遇到不为0的元素时,与慢指针的位置进行交换即可
代码:
class Solution {
public void moveZeroes(int[] nums) {
int left = 0, right = 0;
while (right < nums.length) {
if (nums[right] != 0) {
// 这里也可以用nums[left] = nums[right]
// 然后把left及其后面的元素赋为0
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
2.2 15.三数之和🟡
题目:给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
链接:15. 三数之和
题解详细解释:三数之和
示例 :
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
思路:
3个 for
循环会超时,所以要优化。有重复元素,用哈希表也会比较麻烦
可以一个 for
循环遍历第1个数,剩余两个数不能用 for
循环的话,可以用双指针优化查找时间。
事先排序后,左右各一个指针。计算当前3个数字的和比目标值0大还是小,进而移动左指针(和会变大)或右指针(和会变小)。这样就可以减少循环次数,但要注意去重
代码:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组
if (nums[i] > 0) {
return res;
}
// 去重a
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重b和c,应该放在找到一个三元组之后
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return res;
}
}
2.3 11.盛最多水的容器🟡
题目:给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明: 你不能倾斜容器。
链接:11. 盛最多水的容器
示例 :
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
思路:
最直观的是两个 for 循环遍历所有结果,但这样会超时,所以要进行优化
用双指针,一个指向左边,一个指向右边。最大水量取决于宽度和高度。而高度取决于 left
和 right
中较小的那个,比如 left
较小,那么移动 right
只会让高度不变或者更小,所以要移动高度较低的那条线,这样虽然宽度减小,但是高度有可能增大,容量才有可能变大
抽象成二维数组,因为每次移动都会排除一行或一列,所以不会遗漏
代码:
class Solution {
public int maxArea(int[] height) {
int res = 0, left = 0, right = height.length - 1;
while (left < right) {
// 每次移动高度最短的那条线
if (height[left] < height[right]) {
res = Math.max(res, (right - left) * height[left]);
left++;
} else {
res = Math.max(res, (right - left) * height[right]);
right--;
}
}
return res;
}
}
2.4 42.接雨水🔴
题目:给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
链接:42. 接雨水
示例 :
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
思路:
可以只关注某一个位置能接到多少雨水,应该是 min(左边最高的柱子,右边最高的柱子)-当前高度
但是计算某一侧最高的柱子要是每次都一个个遍历太耗时,可以使用备忘录的方式先提前算出所有位置两侧最高柱子的高度
不过上述题解要定义数组,空间复杂度为 O(n),可以使用双指针进一步优化。思路还是对于每个位置用上面的公式计算当前位置能接多少雨水,不过是左右两个指纹向内移动。用两个变量取代之前的两个备忘录数组。
代码:
1.动态规划解法
class Solution {
public int trap(int[] height) {
if (height.length == 0) {
return 0;
}
int n = height.length;
int res = 0;
// 数组充当备忘录
int[] l_max = new int[n];
int[] r_max = new int[n];
// 初始化 base case
l_max[0] = height[0];
r_max[n - 1] = height[n - 1];
// 从左向右计算 l_max
for (int i = 1; i < n; i++)
l_max[i] = Math.max(height[i], l_max[i - 1]);
// 从右向左计算 r_max
for (int i = n - 2; i >= 0; i--)
r_max[i] = Math.max(height[i], r_max[i + 1]);
// 计算答案 当前位置能接到的雨水取决于min(左,右最高的柱子)-当前高度
for (int i = 1; i < n - 1; i++)
res += Math.min(l_max[i], r_max[i]) - height[i];
return res;
}
}
2.双指针
class Solution {
int trap(int[] height) {
int left = 0, right = height.length - 1;
int lMax = 0, rMax = 0;
int res = 0;
while (left < right) {
lMax = Math.max(lMax, height[left]);
rMax = Math.max(rMax, height[right]);
// res += min(lMax, rMax) - height[i]
if (lMax < rMax) {
res += lMax - height[left];
left++;
} else {
res += rMax - height[right];
right--;
}
}
return res;
}
}
3 滑动窗口
滑动窗口就两步:
- 右指针不断右移,每一次右移要不要做些额外处理
- 判断左侧窗口是否要收缩,若收缩应该怎么处理
无论右移还是收缩都是更改字符在window 对应的数值并更新指针
3.1 3.无重复字符的最长子串🟡
题目:给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
思路:
子串问题那就要用双指针,这题更准确点是用双指针维护一个滑动窗口。
对于当前待添加的元素,若当前滑动窗口含有该元素,则需要删除滑动窗口左侧元素。直到滑动窗口不再包含该元素后,再把该元素添加到滑动窗口,每次添加后判断是否更新 res
代码:
HashSet
:添加前先收缩
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> window = new HashSet<>();
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.length()) {
char cur = s.charAt(right);
// 无重复,窗口右指针右移增大窗口
if (!window.contains(cur)) {
window.add(cur);
right++;
// 更新最长不重复子串的长度
res = Math.max(res, window.size());
// 有重复,左指针右移缩小窗口,直到无重复
} else {
char d = s.charAt(left);
window.remove(d);
left++;
}
}
return res;
}
}
2.模板:先添加,再收缩
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.length()) {
// 窗口右指针不断右移
char cur = s.charAt(right);
window.put(cur, window.getOrDefault(cur, 0) + 1);
right++;
// 判断左侧窗口是否要收缩
while (window.get(cur) > 1) {
char d = s.charAt(left);
window.put(d, window.get(d) - 1);
left++;
}
// 在这里更新答案
res = Math.max(res, right - left);
}
return res;
}
}
3.2 438.找到字符串中所有字母异位词🟡
题目:给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 :
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
思路:
子串的处理仍然是用滑动窗口解决。本题要找的子串的长度是固定的,也就是可以看作是一个固定长度的滑动窗口不断右移,判断当前窗口的子串是否满足条件即可。
因为只有小写字母,所以可以用长度为26的数组记录每个字母出现的个数。然后判断两个字符串是否满足条件就可以转换为判断两个数组内的值是否相等,需要用 Arrays.equals(arr1,arr2)
,注意不能用 arr1.equals(arr2)
用模板则是用一个 count
来记录有效数量是否达到要求,右移的时候判断是否要 ++
,左侧收缩时判断是否要 --
。
注意要做两步判断,首先是判断当前字符是否在目标字符串 p
中,即使在还要判断当前字符的个数是否和 p
的相同。例如,abb 的第3位 b 虽然也在 abc 中,但 b 的数量不同
代码:
1.滑动窗口-数组版
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
if (sLen < pLen) {
return new ArrayList<Integer>();
}
List<Integer> ans = new ArrayList<Integer>();
int[] sCount = new int[26];
int[] pCount = new int[26];
for (int i = 0; i < pLen; ++i) {
++sCount[s.charAt(i) - 'a'];
++pCount[p.charAt(i) - 'a'];
}
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s.charAt(i) - 'a'];
++sCount[s.charAt(i + pLen) - 'a'];
if (Arrays.equals(sCount, pCount)) {
ans.add(i + 1);
}
}
return ans;
}
}
2.滑动窗口-模板版
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : p.toCharArray())
need.put(c, need.getOrDefault(c, 0) + 1);
int left = 0, right = 0;
int count = 0;
List<Integer> res = new ArrayList<>();
while (right < s.length()) {
// 1.窗口右指针不断右移
char cur = s.charAt(right);
right++;
// 右移的时候判断是否更新count
if (need.containsKey(cur)) {
window.put(cur, window.getOrDefault(cur, 0) + 1);
// abb 的第3位b虽然也在abc中,但b的数量不同
if (window.get(cur).equals(need.get(cur)))
count++;
}
// 2.判断左侧窗口是否要收缩
while (right - left >= p.length()) {
// 当窗口符合条件时,把起始索引加入 res
if (count == need.size())
res.add(left);
// 收缩时的处理
char d = s.charAt(left);
left++;
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d)))
count--;
window.put(d, window.get(d) - 1);
}
}
}
return res;
}
}
4 子串
4.1 560.和为 K 的子数组🟡
题目:给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。其中, -1000 <= nums[i] <= 1000
。
示例 :
输入:nums = [1,2,3], k = 3
输出:2
思路:
因为 nums 里有负数,所以没法用滑动窗口。最简单的思路就是从当前数开始向后算出所有的子数组。
子数组是连续的,某一段位置 [j,...i]
的和若为 k,那不就是当前位置 i
的前缀和减去位置 j-1
的前缀和等于 k。这里我们只关系次数,并不关心这个 j
到底是多少,所以可以用 HashMap
存储前缀和,值为前缀和出现的次数。
从下标0到当前位置前缀和为 preSum, 若当前找到了 preSum - k
,说明从0到当前位置分成了两部分:preSum - k 和 k,那就说明找到了合适的子数组
代码:
public class Solution {
public int subarraySum(int[] nums, int k) {
int res = 0, preSum = 0;
// 前缀和为键,出现次数为对应的值
Map<Integer, Integer> mp = new HashMap<>();
// 后面get的时候,若preSum等于k会get(0),所以0对应的值为1
mp.put(0, 1);
for (int num : nums) {
preSum += num;
// 从下标0到当前位置前缀和为preSum
// 若当前找到了preSum - k
// 说明从0到当前位置分成了两部分:preSum - k 和 k
if (mp.containsKey(preSum - k)) {
res += mp.get(preSum - k);
}
mp.put(preSum, mp.getOrDefault(preSum, 0) + 1);
}
return res;
}
}
4.2 239.滑动窗口最大值🔴待做
题目:给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
链接:239. 滑动窗口最大值
示例 :
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
思路:
单调队列
代码:
4.3 76.最小覆盖子串🔴待做
题目:给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
链接:76. 最小覆盖子串
示例 :
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
思路:
滑动窗口
代码:
5 普通数组
5.1 53.最大子数组和🟡
题目:给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
链接:53. 最大子数组和
示例 :
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
思路:
子数组是连续的,所以后面的结果和前面的结果有关。定义 dp[i]
表示以 i
结尾的元素的子数组和,那么其就等于 max(和前面组成子数组,自己组成新的子数组)
。而我们要的结果就是所有子数组中和最大的那个。
由于当前的 dp[i]
只和 dp[i-1]
有关,所以可以用变量 pre
代替 dp
数组
代码:
1.动态规划(易理解版)
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int res = nums[0];
for (int i = 1; i < n; i++) {
// 和前面组成子数组,还是自己成为新的子数组
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
}
2.动态规划(压缩空间版)
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, res = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);
res = Math.max(res, pre);
}
return res;
}
}
5.2 56.合并区间🟡
题目:以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
链接:56. 合并区间
示例 :
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
思路:
先按区间 start
升序排列,这样只用考虑 end
的关系。下一个区间的 start
小于等于上一个区间的 end
时,则要合并。注意合并后的区间的 end
是两个区间 end
取最大。
List<int[]>
怎么转换成二维数组要留意下。也可以直接用二维数组存结果,不过由于无法确定结果的个数,这样就要用一个变量记录二维数组中有效的结果。
代码:
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new ArrayList<>();
// 按区间的 start 升序排列
Arrays.sort(intervals, (a, b) -> (a[0] - b[0]));
res.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
int[] curr = intervals[i];
// res 中最后一个元素的引用
int[] last = res.getLast();
if (curr[0] <= last[1]) {
last[1] = Math.max(last[1], curr[1]); // 合并end
} else {
// 处理下一个待合并区间
res.add(curr);
}
}
return res.toArray(new int[0][0]);
}
}
5.3 189.轮转数组🟡
题目:给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
进阶:你可以使用空间复杂度为 O(1)
的 原地 算法解决这个问题吗?
链接:189. 轮转数组
示例 :
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
思路:
先整体反转得到 [7,6,5,4,3,2,1]
,再反转前 k 个元素得到 [5,6,7,4,3,2,1]
,再反转后面的即可
速记:三次反转:整体、前 k、后面的
代码:
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n; // k可能大于数组长度,所以要取模
reverse(nums, 0, n - 1); // 整体反转
reverse(nums, 0, k - 1); // 前k反转
reverse(nums, k, n - 1); // 后面反转
}
// 反转nums数组的[l,r]
public void reverse(int[] nums, int l, int r) {
while (l < r) {
int tmp = nums[l];
nums[l] = nums[r];
nums[r] = tmp;
l++;
r--;
}
}
}
5.4 238. 除自身以外数组的乘积🟡
题目:给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法, 且在 O(_n_)
时间复杂度内完成此题。
示例 :
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
思路:
若不考虑空间复杂度,定义前缀乘数组和后缀乘数组, nums[i] = pre[i] * back[i]
优化:前缀乘数组可以先直接放到结果数组 res
中,再从后向前遍历更新 res
即乘上后缀乘 back
速记:
res
先放前缀乘,从后遍历乘后缀乘更新res
代码:
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] res = new int[n];
res[0] = 1;
// 先存前缀乘
for (int i = 1; i < n; i++) {
// 前缀乘利用res[i-1],无需定义前缀乘变量
res[i] = res[i - 1] * nums[i - 1];
}
// 从后向前遍历乘上 后缀乘
int back = 1;
for (int i = n - 1; i >= 0; i--) {
res[i] = res[i] * back;
back *= nums[i];
}
return res;
}
}
5.5 41. 缺失的第一个正数🔴待做
题目:给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
链接:41. 缺失的第一个正数
示例 :
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
思路:
将在 [1-n]
范围内的元素 x
交换到 x-1
位置
代码:
6 矩阵
6.1 73.矩阵置零🟡
题目:给定一个 m x n
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
链接:73. 矩阵置零
示例 :
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
思路:
比较直观的是用两个数组,分别记录哪些行和哪些列应该置0,然后根据结果将对应的行和列置0
可以使用数组的第一行和第一列代替上面的两个数组,但是要额外用两个变量记录下第一行和第一列是否有0
代码:
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean flagCol0 = false, flagRow0 = false;
// 第一行或第一列是否有0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
flagCol0 = true;
}
}
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
flagRow0 = true;
}
}
// 标记该行和该列是否有0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}
// 如果当前元素所在行或所在列有0,则将当前位置置0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 将第一行和第一列置0
if (flagCol0) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
if (flagRow0) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
}
}
6.2 54.螺旋矩阵🟡
题目:给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
链接:54. 螺旋矩阵
示例 :
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
思路:
四个变量分别记录上下左右边界,按照顺时针即从左到右、从上到下、从右到左和从下到上的方向遍历。每遍历一行或一列就更新对应的边界,注意更新边界后就要判断下是否超过了边界。
速记:四个变量记录四个边界,遍历一个方向就更新边界并判断是否 break
代码:
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int top = 0, bottom = m - 1, left = 0, right = n - 1;
List<Integer> res = new ArrayList<>();
while (true) {
// 从左到右
for (int i = left; i <= right; i++) {
res.add(matrix[top][i]);
}
// top这一行已经遍历完,所以++,并判断是否达到边界条件
// 注意先++,再比较,因为top改变了才可能会达到边界条件
if (++top > bottom) {
break;
}
// 从上到下
for (int i = top; i <= bottom; i++) {
res.add(matrix[i][right]);
}
// right这一列已经遍历完,所以--,并判断是否达到边界条件
if (--right < left) {
break;
}
// 从右到左
for (int i = right; i >= left; i--) {
res.add(matrix[bottom][i]);
}
// bottom这一行已经遍历完,所以--,并判断是否达到边界条件
if (--bottom < top) {
break;
}
// 从左到右
for (int i = bottom; i >= top; i--) {
res.add(matrix[i][left]);
}
// left这一列已经遍历完,所以++,并判断是否达到边界条件
if (++left > right) {
break;
}
}
return res;
}
}
6.3 48.旋转图像🟡
题目:给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
链接:48. 旋转图像
示例 :
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
思路:
若不要求原地,也就是可以使用额外空间的话,可以使用 matrix_new[j][n - i - 1] = matrix[i][j]
先放到一个新数组,再把新数组的值赋给旧数组即可。
如果不使用额外空间,这时候一般要么是两两交换,要么是多次反转。两两交换太绕,这里记住可以使用上下反转再对角线反转即可。
速记:先上下反转再对角线反转
代码:
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 先上下反转
for (int i = 0; i < n / 2; i++) { // 注意边界
for (int j = 0; j < n; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n - i - 1][j];
matrix[n - i - 1][j] = tmp;
}
}
// 再对角线反转
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) { // 注意边界
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
}
}
6.4 240.搜索二维矩阵 II🟡
题目:编写一个高效的算法来搜索 m x n
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 :
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
思路:
常规遍历都是从左上角开始查找。这里若从左上角开始找,遇到一个更大的数无法确定是向右还是向下去找。原因是向右和向下都是升序的。那我们可以从右上角开始找,这样从右往左是递减的,从上到下是递增的,这样就可以确定是向左还是向下了。
速记:两变量记录行和列,右上角向左下查找
代码:
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
// 初始化在右上角
int i = 0, j = n - 1;
while (i < m && j >= 0) {
if (matrix[i][j] == target) {
return true;
}
if (matrix[i][j] < target) {
// 需要大一点,往下移动
i++;
} else {
// 需要小一点,往左移动
j--;
}
}
// while 循环中没有找到,则 target 不存在
return false;
}
}
7 链表
7.1 160.相交链表🟢
题目:给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
链接:160. 相交链表
示例 :
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
思路:
因为两个链表长度可能不一致,所以同时遍历可能无法到达相交节点。
可以先算出哪个链表较长,让其向后移动两个链表长度的差值,这样两个指针同时遍历就可以同时到达相交节点
上面思路简单但代码比较多。若遍历完当前链表再遍历另一个链表,这样也会同时到达相交节点,或者同时指向 null
如上图中,pA 走了2+3+3到了节点8,pB 走了3+3+2同样到了节点8
代码:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
// pA走到末尾转到B链表,PB走到末尾转到A链表
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
7.2 206.反转链表🟢🔥
题目:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
链接:206. 反转链表
示例 :
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
思路:
用一个指针 cur 用来遍历单链表,再用另一个指针 prev 指向 cur 的前面一个节点。例如 cur=2时,原本是1->2,现在要变成2->1。直接让 cur->next=pre 就可以实现,但这样的话,后面的3就没法遍历到了。所以要用一个临时指针 tmp 先指向 cur->next,再让 cur->next=pre。
到这里,就实现了1和2的反转,如果想继续遍历下去,只需让 pre 和 cur 都向后移一位。
代码:
1.迭代法(双指针)
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head;
ListNode pre = null, next = null;
while (cur != null) {
next = cur.next; // 更新 next
cur.next = pre; // 反转操作
pre = cur; // 更新 pre
cur = next; // 更新 cur
}
return pre;
}
}
2.递归(也要会)
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(head, null);
}
public ListNode reverse(ListNode cur, ListNode pre) {
if (cur == null) {
return pre;
}
ListNode next = cur.next;
cur.next = pre;
return reverse(next, cur);
}
}
7.3 92.反转链表 II(扩展)🟡
题目:给你单链表的头指针 head
和两个整数 left
和 right
,其中 left <= right
。请你反转从位置 left
到位置 right
的链表节点,返回 反转后的链表 。
链接:92. 反转链表 II
示例 :
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
思路:
- 首先根据
left
找到待反转链表的位置 - 然后利用反转链表的逻辑将待反转链表反转后
- 再把反转后的链表和前后区间的链表相连即可。
代码:
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0, head), p0 = dummy;
for (int i = 0; i < left - 1; ++i) {
p0 = p0.next; // 指向待反转链表的前一个节点
}
ListNode cur = p0.next;
ListNode pre = null, next = null;
// 反转之后 pre 是反转区间新的头节点,cur 是下个区间的 head
for (int i = 0; i < right - left + 1; ++i) {
// 和反转链表题目一样的逻辑
next = cur.next;
cur.next = pre; // 执行right - left + 1次反转操作
pre = cur;
cur = next;
}
// tail是反转后区间的末尾,指向下个区间的head
ListNode tail = p0.next;
tail.next = cur;
// 反转区间的前一个节点指向反转区间新的头节点
p0.next = pre;
return dummy.next;
}
}
7.4 25.K 个一组翻转链表🔴🔥
题目:给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 :
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
思路:
和反转链表 II 类似,只不过要反转多个区间。
- 首先统计节点个数来确定要进行多少个区间的反转操作
- 然后利用反转链表的逻辑将待反转链表反转后
- 再把反转后的链表和前后区间的链表相连,同时更新待反转区间的前一个节点
p0
,循环下一个区间的反转操作。
视频讲解:反转链表_哔哩哔哩_bilibili
代码:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// 统计节点个数
ListNode dummy = new ListNode(-1, head), p0 = dummy;
int n = 0;
ListNode cur = head;
while (cur != null) {
n++;
cur = cur.next;
}
cur = head;
ListNode pre = null, next = null;
while (n >= k) {
n -= k;
// 翻转之后 pre 是反转区间新的头节点,cur 是下个区间的 head
for (int i = 0; i < k; ++i) {
// 和反转链表一样的逻辑
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// tail是反转后区间的末尾,指向下个区间的head
ListNode tail = p0.next;
tail.next = cur;
// 反转区间的前一个节点指向反转区间新的头节点
p0.next = pre;
// p0来到当前区间的末尾,也就是后一个区间的前一个节点
p0 = tail;
}
return dummy.next;
}
}
7.5 234.回文链表🟢
题目:给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
链接:234. 回文链表
示例 :
输入:head = [1,2,2,1]
输出:true
进阶: 你能否用 O(n)
时间复杂度和 O(1)
空间复杂度解决此题?
思路:
因为单链表没法从后向前遍历,所以没法用左右双指针遍历判断。但可以先把值放到一个数组中,这样就可以用左右指针向中间遍历来判断。
为了优化空间,可以先找到链表的中间位置,将后面的链表反转。用两个指针分别指向前半部分的链表和后半部分的链表,从而避免创建数组。
代码:
1.值复制到数组然后双指针
class Solution {
public boolean isPalindrome(ListNode head) {
List<Integer> list = new ArrayList<>();
ListNode cur = head;
while (cur != null) {
list.add(cur.val);
cur = cur.next;
}
// 问题变为判断数组中的元素是否是回文
int left = 0, right = list.size() - 1;
while (left < right) {
if (!list.get(left).equals(list.get(right))) {
return false;
}
left++;
right--;
}
return true;
}
}
2.快慢指针
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 链表长度为奇数,slow向后一步左右两边子链表长度一致
if (fast != null)
slow = slow.next;
ListNode left = head;
// 后半部分反转
ListNode right = reverse(slow);
while (right != null) {
if (left.val != right.val)
return false;
left = left.next;
right = right.next;
}
return true;
}
ListNode reverse(ListNode head) {
ListNode pre = null, cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
// 详细解析参见:
// https://labuladong.online/algo/slug.html?slug=palindrome-linked-list
7.6 141.环形链表🟢
题目:给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
链接:141. 环形链表
示例 :
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
思路:
判断有没有环,也就是判断遍历到的当前节点是否之前也遍历到过,所以可以用 HashSet
来快速判断当前节点是否在之前遍历过的节点中
也可以用快慢指针,慢指针走一步,快指针走两步。若是存在环,快指针一定会在某一节点追上慢指针
代码:
public class Solution {
public boolean hasCycle(ListNode head) {
// 快慢指针初始化指向 head
ListNode slow = head, fast = head;
// 快指针走到末尾时停止
while (fast != null && fast.next != null) {
// 慢指针走一步,快指针走两步
slow = slow.next;
fast = fast.next.next;
// 快慢指针相遇,说明含有环
if (slow == fast) {
return true;
}
}
// 不包含环
return false;
}
}
7.7 142.环形链表 II🟡
题目:如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
链接:142. 环形链表 II
示例 :
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
思路:
这题和上题一样,依然可以用 HashSet
快速判断当前节点是否之前遍历过,而且第一次重复的节点就是环的入口。
如果想优化空间,上题的解法快慢指针相遇的地方不一定是环的入口,如例子中会在 -4
相遇。相遇时,只需让一个 tmp
指针从 head
出发,和 slow
一样每次都是走一步,相遇的地方就是环的入口。
代码:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// 快慢指针相遇,说明有环
if (slow == fast) {
// tmp从head出发,和slow相遇的节点就是入口
ListNode tmp = head;
while (slow != tmp) {
slow = slow.next;
tmp = tmp.next;
}
return slow;
}
}
return null;
}
}
7.8 28.合并两个有序链表🟢🔥
题目:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
链接:21. 合并两个有序链表
示例 :
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
思路:
两个指针分别指向两个链表,谁的节点值小下一个就用谁的,最后连接上非空的那个链表即可。为了方便操作和最终返回头结点,这里要设一个虚拟头结点
代码:
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
cur.next = list2;
list2 = list2.next;
} else {
cur.next = list1;
list1 = list1.next;
}
cur = cur.next;
}
// 拼接把剩下的链表
if (list1 != null) {
cur.next = list1;
} else {
cur.next = list2;
}
return dummyHead.next;
}
}
7.9 2.两数相加🟡
题目:给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
链接:2. 两数相加
示例 :
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
思路:
两数相加和平常计算两个数字相加一样,不过是遍历两条链表逐渐相加。主要是存在进位的情况,需要一个变量记录上次相加的进位。
这题需要注意的是如果用 while (l1 != null || l2 != null)
来判断会漏掉最后两个数相加有进位的情况,所以要加上 || 进位 > 0
的条件
代码:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 在两条链表上的指针
ListNode p1 = l1, p2 = l2;
// 虚拟头结点(构建新链表时的常用技巧)
ListNode dummy = new ListNode(-1);
// 指针 p 负责构建新链表
ListNode p = dummy;
// 记录进位
int carry = 0;
// 开始执行加法,两条链表走完且没有进位时才能结束循环
while (p1 != null || p2 != null || carry > 0) {
// 先加上上次的进位
int val = carry;
if (p1 != null) {
val += p1.val;
p1 = p1.next;
}
if (p2 != null) {
val += p2.val;
p2 = p2.next;
}
// 处理进位情况
carry = val / 10;
val = val % 10;
// 构建新节点
p.next = new ListNode(val);
p = p.next;
}
// 返回结果链表的头结点(去除虚拟头结点)
return dummy.next;
}
}
7.10 19.删除链表的倒数第 N 个结点🟡
题目:给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 :
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
思路:
最简单的方法就是先遍历一次链表算出链表长度,然后再遍历一次走到待删节点前一个进行删除操作。
之所以要两次遍历是无法确定 len - n
的值,那我们可以让一个指针先走 n
步,此时让另一个节点从头出发,他们俩同时向后遍历,从而确定 len - n
的值
注意可能会删除 head 节点引发空指针异常,所以要设置一个 dummy
节点
代码:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode fast = dummy, slow = dummy;
// 因为指针初始是在dummy,所以这里多走一步
for (int i = 0; i < n + 1; i++) {
fast = fast.next;
}
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
7.11 31-24.两两交换链表中的节点🟡
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 :
输入:head = [1,2,3,4]
输出:[2,1,4,3]
思路:
链表题目都是涉及到指针移动的问题,最好在纸上画画,防止绕晕。
可以先不考虑怎么移动指针,先考虑交换两个链表节点应该怎么做。
cur.next = cur.next.next
即 1.next=3
,然后我们想让 2.next=1
或者 0.next=2
的时候,会发现此时的 2
因为和 1
的连接断开了, 已经找不到了。所以就要用一个临时节点提前把 2
存起来
交换过两个节点后,现在是 0 2 1 3
,cur=1,prev=0
,按照对 cur
和 prev
的定义,下一次交换时 cur=3,prev=1
,所以对应的代码就是 prev = cur;cur = cur.next;
代码:
1.迭代解法
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(-1, head);
ListNode prev = dummy, cur = head;
// 下一个待反转的为null 或者 下一对待反转的只有1个元素
while (cur != null && cur.next != null) {
ListNode tmp = cur.next;
cur.next = tmp.next;
prev.next = next;
tmp.next = cur;
// 更新两个指针
prev = cur;
cur = cur.next;
}
return dummy.next;
}
}
2.递归解法
能看懂,但下次遇到想不到,起码把迭代解法搞明白就可以了
class Solution {
// 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转,
// 返回翻转后的链表头结点
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode first = head;
ListNode second = head.next;
ListNode others = head.next.next;
// 先把前两个元素翻转
second.next = first;
// 利用递归定义,将剩下的链表节点两两翻转,接到后面
first.next = swapPairs(others);
// 现在整个链表都成功翻转了,返回新的头结点
return second;
}
}
7.12 138.随机链表的复制🟡待做
题目:给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
链接:138. 随机链表的复制
示例 :
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
思路:
第一次遍历用哈希表克隆节点,第二次遍历克隆节点指针
代码:
7.13 148.排序链表🟡待做
题目:给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
链接:148. 排序链表
示例 :
输入:head = [4,2,1,3]
输出:[1,2,3,4]
思路:
对链表归并排序
代码:
7.14 23.合并 K 个升序链表🔴
题目:给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 :
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
思路:
如果是合并两个单链表,那可以设两个指针不断比较哪个值较小即可。但现在链表的数量不固定,无法直接设置对应的指针数量。
最小堆:我们设多个指针的目的是为了判断哪个指针指向的元素较小。可以用最小堆存放每个链表的元素,这样堆顶就是当前几个链表中最小的元素,取出来一个元素后,再把其后面的元素添加到堆中,直到堆为空也就是遍历完所有元素。
分治法:我们还可以从前到后两两合并链表,不过可以使用分治法优化。一分为二,先合并左边的,再合并右边的,再把左右合成最终的链表。对于左边链表的合并,可以继续一分为二,直到无法拆分。
代码:
1.最小堆
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 优先级队列,最小堆
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> (a.val - b.val));
// 将 k 个链表的头结点加入最小堆
for (ListNode head : lists) {
if (head != null) { // 注意head可能为null
pq.add(head);
}
}
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while (!pq.isEmpty()) {
// 获取最小节点,接到结果链表中
ListNode node = pq.poll();
cur.next = node;
if (node.next != null) {
pq.add(node.next);
}
// cur 指针不断前进准备连接下一个节点
cur = cur.next;
}
return dummy.next;
}
}
2.分治法
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
public ListNode merge(ListNode[] lists, int l, int r) {
if (l == r) {
return lists[l]; // 无需合并,直接返回
}
if (l > r) {
return null;
}
int mid = l + (r - l) / 2;
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
cur.next = list2;
list2 = list2.next;
} else {
cur.next = list1;
list1 = list1.next;
}
cur = cur.next;
}
// 拼接剩下的链表
cur.next = list1 != null ? list1 : list2;
return dummyHead.next;
}
}
7.15 146.LRU 缓存🟡🔥
华为一面
题目:请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
链接:146. LRU 缓存
示例 :
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
思路:
get
和 put
要求 O(1)
,那显然是 HashMap
,但没有先后顺序,所以可以使用哈希链表 LinkedHashMap
。
不过面试时一般要求不能直接使用 LinkedHashMap
,要根据哈希表和双向链表来实现。主要就是哈希表存放 key
和对应的节点,节点之间组成双向链表的结构,节点内部存放 key
和 value
。
代码:
1.LinkedHashMap
解法(要会)
class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int capacity;
public LRUCache(int capacity) {
// 容量、负载因子、按照访问顺序迭代(默认false是按插入顺序迭代)
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
// 判断调用put()方法时是否移除键值对
// 构造函数设为true表明移除最老的(最早访问)
// LinkedHashMap中新访问的会放到链表最后,也就是会移除首元素
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return super.size() > capacity;
}
}
2.哈希表+双向链表解法(面试要求)
class LRUCache {
class Node {
int key, value;
Node pre, next;
Node(int k, int v) {
this.key = k;
this.value = v;
}
}
Map<Integer, Node> map = new HashMap<>();
Node dummy = new Node(-1, -1);
int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
dummy.next = dummy;
dummy.pre = dummy;
}
public int get(int key) {
Node node = getNode(key);
return node == null ? -1 : node.value;
}
public void put(int key, int value) {
Node node = getNode(key);
// 有节点就更新
if (node != null) {
node.value = value;
return;
}
// 不存在这个节点就新建
node = new Node(key, value);
// 新建的节点不会被getNode添加到前面,所以手动添加到前面
pushFront(node);
map.put(key, node); // 唯一一次map新加元素
// 是否超容量
if (map.size() > capacity) {
Node d = dummy.pre;
remove(d);
map.remove(d.key); // 唯一一次map删除元素
}
}
// 根据key取节点,若节点存在则把该节点添加到前面
public Node getNode(int key) { // -1 1 (2) 3
Node node = map.get(key);
if (node == null) {
return null;
}
remove(node); // 删除链表中的节点
pushFront(node); // 添加节点到头部
return node;
}
// 删除链表中的节点
public void remove(Node node) { // 1 (2) 3
node.pre.next = node.next;
node.next.pre = node.pre;
}
// 节点添加到链表头部
public void pushFront(Node node) { // -1 1 (2) 3
node.pre = dummy;
node.next = dummy.next;
dummy.next = node;
node.next.pre = node;
}
}
8 二叉树
8.1 94.二叉树的中序遍历🟢
题目:给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
链接:94. 二叉树的中序遍历
示例 :
输入:root = [1,null,2,3]
输出:[1,3,2]
思路:
递归解决,二叉树的前中后序指的是根节点的位置,并且左节点一定在右节点前边。所以是先递归左,再处理根节点也就是加到 res
,再递归右。
速记:左中后序指的是根节点的位置
代码:
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
traverse(root);
return res;
}
public void traverse(TreeNode root) {
if (root == null) {
return;
}
traverse(root.left);
// 中间处理就是中序
res.add(root.val);
traverse(root.right);
}
}
8.2 104.二叉树的最大深度🟢
题目:给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 :
输入:root = [3,9,20,null,null,15,7]
输出:3
思路:
先看局部怎么解决,若是只有两层,如图中 20、15、7
,根节点的最大深度就是子节点的深度加1。所以要求二叉树的最大深度就是求以 root
为根节点的最大深度,就要知道其左右子树的最大深度,所以就是后序遍历(自底向上),先访问左右子节点再访问根节点。
速记:最大深度就是左右子树最大深度取最大+1,所以后序遍历
代码:
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left); // 左子树的深度
int right = maxDepth(root.right); // 右子树的深度
return Math.max(left, right) + 1; // 根节点深度就是 左右子树最大的深度 + 1
}
}
8.3 226.翻转二叉树🟢
题目:给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
链接:226. 翻转二叉树
示例 :
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
思路:
交换左右子树,从局部看,如 2,1,3
就是交换 1
和 3
。用递归翻转左子树,再翻转右子树,再交换左右子树即可。
速记:翻转左得
left
,翻转右得right
,左右互换即可
代码:
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode left = invertTree(root.left); // 翻转左子树
TreeNode right = invertTree(root.right); // 翻转右子树
// 交换左右子树
root.left = right;
root.right = left;
return root;
}
}
8.4 101.对称二叉树🟢
题目:给你一个二叉树的根节点 root
, 检查它是否轴对称。
链接:101. 对称二叉树
示例 :
输入:root = [1,2,2,3,4,4,3]
输出:true
思路:
如上图,第二层不止值要相等,第三层左的左和右的右,左的右和右的左也要相等。所以重新定义一个函数 isValid
传入第二层的 2,2
,方便判断
速记:新定义函数传
left
和right
,检查左的左和右的右,左的右和右的左
代码:
class Solution {
public boolean isSymmetric(TreeNode root) {
return isValid(root.left, root.right);
}
public boolean isValid(TreeNode left, TreeNode right) {
// 左右都为空则对称
if (left == null && right == null) {
return true;
}
// 左右有一个为空就不对称
if (left == null || right == null) {
return false;
}
// 左右都不为空但值不等也不对称
if (left.val != right.val) {
return false;
}
// 检查图中3 4 | 4 3是否对称
return isValid(left.left, right.right) && isValid(left.right, right.left);
}
}
8.5 543.二叉树的直径🟢
题目:给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
链接:543. 二叉树的直径
示例 :
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
思路:
求直径其实就是根节点左右儿子的深度相加。所以可以套用求深度的代码,在求得左右儿子的深度后更新 res
速记:直径就是左右儿子的深度相加取最大
代码:
class Solution {
int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return res;
}
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left); // 左儿子的深度
int right = maxDepth(root.right); // 右儿子的深度
res = Math.max(res, left + right); // 左儿子加右儿子的深度 取最大
return Math.max(left, right) + 1;
}
}
8.6 102.二叉树的层序遍历🟡
题目:给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 :
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
思路:
二叉树没法很好的一层一层直接遍历,一层一层想象成一个队列放一层,然后所有队列连接起来,从前往后就是层序遍历。
关键是怎么从上一层元素获取到下一层元素,遍历上一层的时候,把当前元素放到 level
中,顺便把他的左右子节点放到队列中,这样遍历完这一层后队列存的刚好就是下一层的元素。
代码:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if (root == null) {
return res;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// while 循环控制从上向下一层层遍历
while (!q.isEmpty()) {
int len = q.size(); // for循环中q.size()会变,所以先存起来
// 记录这一层的节点值
List<Integer> level = new LinkedList<>();
// for 循环控制每一层从左向右遍历
for (int i = 0; i < len; i++) {
TreeNode cur = q.poll();
level.add(cur.val);
if (cur.left != null)
q.offer(cur.left);
if (cur.right != null)
q.offer(cur.right);
}
res.add(level);
}
return res;
}
}
8.7 103.二叉树的锯齿形层序遍历(扩展)🟡
题目:给你二叉树的根节点 root
,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
示例 :
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
思路:
和层序遍历类似,只不过多了一个变量 isLeft
判断遍历方向。若是从左往右每层遍历的时候插入 level
的尾部就是一般的层序遍历,而每次都插入头部就可以实现从右向左遍历。每遍历一层就对 isLeft
取非也就是改变方向。
速记:从右向左层序遍历,那就每次
addFirst
插头部
代码:
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
boolean isLeft = true; // 是否从左往右
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
int sz = q.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < sz; i++) {
TreeNode node = q.poll();
// 根据isLeft判断插入尾部还是插入头部实现不同方向遍历
if (isLeft) {
level.addLast(node.val);
} else {
level.addFirst(node.val);
}
if (node.left != null) {
q.add(node.left);
}
if (node.right != null) {
q.add(node.right);
}
}
res.add(level);
isLeft = !isLeft; // 下一层切换方向
}
return res;
}
}
8.8 108.将有序数组转换为二叉搜索树🟢
题目:给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
示例 :
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
思路:
二叉搜索树是左比根小,右比根大,那么根节点其实就可以用有序数组中间节点。对于偶数的数组,中间节点有俩,选哪个都行。而根节点的左右儿子节点依然是剩余的区间取中间节点,所以函数就要传入区间的左右范围,递归创建。
速记:取中间节点为根节点,传左右区间递归创建左右儿子
代码:
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
public TreeNode build(int[] nums, int left, int right) {
if (left > right) {
return null;
}
// 总是选择中间位置左边的数字作为根节点
int mid = left + (right - left) / 2;
// 构建根节点,递归构建根的左和根的右
TreeNode root = new TreeNode(nums[mid]);
root.left = build(nums, left, mid - 1);
root.right = build(nums, mid + 1, right);
return root;
}
}
8.9 98.验证二叉搜索树🟡
题目:给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
提示: -2^31 <= Node.val <= 2^31 - 1
链接:98. 验证二叉搜索树
示例 :
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
思路:
这题很容易只比较左儿子比根小,右儿子比根大。注意要比较左边所有的节点都比根小,右边所有的节点都比根大。所以传参的时候要传最小值和最大值,判断 root.val
是否满足最小值和最大值的情况。
注意题目中说了节点的最小值和最大值范围是 int
类型的最小值和最大值范围内,所以初始化的时候要初始化成 long
类型的最小值和最大值。
速记:根比
min
大,比max
小,递归检查左右子树
代码:
class Solution {
public boolean isValidBST(TreeNode root) {
return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValid(TreeNode root, long min, long max) {
if (root == null) {
return true;
}
// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if (root.val <= min || root.val >= max) {
return false;
}
// 限定左子树的最大值是 root.val,右子树的最小值是 root.val
return isValid(root.left, min, root.val) && isValid(root.right, root.val, max);
}
}
8.10 230.二叉搜索树中第 K 小的元素🟡
题目:给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
个最小元素(从 1 开始计数)。
示例 :
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
思路:
二叉搜索树的中序遍历结果是递增的,所以找第 k
小就是中序遍历遇到的第 k
个元素
速记:中序遍历遇到的第
k
个元素
代码:
class Solution {
int res = 0;
int rank = 0; // 记录当前元素的排名
public int kthSmallest(TreeNode root, int k) {
traverse(root, k);
return res;
}
public void traverse(TreeNode root, int k) {
if (root == null) {
return;
}
traverse(root.left, k);
// 中序遍历代码位置
rank++;
if (k == rank) {
// 找到第 k 小的元素
res = root.val;
return;
}
traverse(root.right, k);
}
}
8.11 199.二叉树的右视图🟡
题目:给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
链接:199. 二叉树的右视图
示例 :
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
思路:
BFS 比较容易想到,每一层遇到的最后一个就是右视图看到的节点。
DFS 是先递归右子树,保证每次首先遇到的都是这一层的最右边的节点。当深度和当前结果个数一致时说明这时刚到达这一层,把该节点存到结果中。
速记:层序遍历每层最后一个就是右视图的结果
代码:
1.BFS
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode node = q.poll();
// 这一层的最后一个加入res
if (i == sz - 1) {
res.add(node.val);
}
if (node.left != null) {
q.add(node.left);
}
if (node.right != null) {
q.add(node.right);
}
}
}
return res;
}
}
2.DFS
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> ans = new ArrayList<>();
dfs(root, 0, ans);
return ans;
}
private void dfs(TreeNode root, int depth, List<Integer> ans) {
if (root == null) {
return;
}
// 这个深度首次遇到
if (depth == ans.size()) {
ans.add(root.val);
}
// 先递归右子树,保证首次遇到的一定是最右边的节点
dfs(root.right, depth + 1, ans);
dfs(root.left, depth + 1, ans);
}
}
8.12 114.二叉树展开为链表🟡
题目:给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 :
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
思路:
先递归拉平左右子树,再将左子树接到右子树的位置,原来的右子树接到下面
速记:递归拉平左右子树,左接到右,右接到下
代码:
class Solution {
// 定义:将以 root 为根的树拉平为链表
public void flatten(TreeNode root) {
if (root == null) {
return;
}
// 先递归拉平左右子树
flatten(root.left);
flatten(root.right);
/**** 后序遍历位置 ****/
// 1、左右子树已经被拉平成一条链表
TreeNode left = root.left;
TreeNode right = root.right;
// 2、将左子树作为右子树
root.left = null;
root.right = left;
// 3、将原先的右子树接到当前右子树的末端
TreeNode cur = root;
while (cur.right != null) {
cur = cur.right;
}
cur.right = right;
}
}
8.13 105.从前序与中序遍历序列构造二叉树🟡待做
题目:
链接:
示例 :
思路:
速记:
代码:
8.14 437.路径总和 III🟡待做
题目:
链接:
示例 :
思路:
速记:
代码:
8.15 236.二叉树的最近公共祖先🟡
题目:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
示例 :
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
思路:
二叉树搜索题目,使用递归解决。如果遇到了 null
或 p
或 q
则应该 return
。分别往左子树和右子树去查找最近公共祖先。然后结果一层层向上 return
。
最终对于 root
,它的左右两个子树返回了查找到的结果。左子树没查到,说明结果在右子树中;右子树没查找,说明结果在左子树中;左右子树都查到,那 root
就是最近公共祖先。
代码:
class Solution {
// 函数功能: 1.p q都能找到 返回最近公共祖先 2. p q找到一个,返回p q 3. 都没找到 返回null
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 如果根节点为空,或者根节点是p或q中的一个,直接返回根节点的值
if (root == null || root == p || root == q) {
return root;
}
// 在左子树中查找最近公共祖先
TreeNode left = lowestCommonAncestor(root.left, p, q);
// 在右子树中查找最近公共祖先
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 如果左子树为空,说明p和q都不在左子树中,返回右子树的结果
if (left == null) {
return right;
}
// 如果右子树为空,说明p和q都不在右子树中,返回左子树的结果
if (right == null) {
return left;
}
// 如果左子树和右子树都返回了节点,说明p和q分别在root的两侧,root即为最近公共祖先
return root;
}
}
8.16 124.二叉树中的最大路径和🔴待做
题目:
链接:
示例 :
思路:
速记:
代码:
9 图论
9.1 200.岛屿数量🟡
题目:给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
链接:200. 岛屿数量
示例 :
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
思路:
遍历二维网格,遇到陆地时,同时将其和相邻的陆地用 dfs
都淹没掉(置为 0
)。这样一次遍历遇到的 1
的数量就是所有岛屿的数量。
二维矩阵的 DFS 和二叉树的遍历类似
代码:
class Solution {
// 主函数,计算岛屿数量
public int numIslands(char[][] grid) {
int res = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1') {
res++; // 每发现一个岛屿,岛屿数量加一
dfs(grid, i, j); // 然后使用 dfs 将其相邻的岛屿淹了
}
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(char[][] grid, int i, int j) {
// 超出索引边界
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
return;
}
// 已经是海水了
if (grid[i][j] == '0') {
return;
}
// 将 (i, j) 变成海水
grid[i][j] = '0';
// 淹没上下左右的陆地
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
}
9.2 994.腐烂的橘子🟡待做
题目:在给定的 m x n
网格 grid
中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格; - 值
1
代表新鲜橘子; - 值
2
代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1
。
链接:994. 腐烂的橘子
示例 :
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
思路:
腐烂橘子放进队列,然后广搜
代码:
9.3 207. 课程表🟡待做
题目:你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
链接:207. 课程表
示例 :
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
思路:
构建成图,判断有没有环
代码:
9.4 208.实现 Trie (前缀树)🟡待做
题目:Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
示例 :
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
思路:
定义前缀树节点(class
),用 next
数组存放下一个节点
代码:
10 回溯
10.1 46.全排列🟡
题目:给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
链接:46. 全排列
示例 :
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
思路:
当路径 path
个数为 nums
时添加到 res
,但注意要 new
一个 path
,不然加的是 path
的引用,后面 path
的改变会影响到 res
排列问题可以往前选,所以循环从 0
开始,但从 0
开始,排列选出的元素又不能重复,所以要用 used
数组来判断之前选过了没。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
backtrack(nums, used);
return res;
}
private void backtrack(int[] nums, boolean[] used) {
// 到达叶子节点
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
// 注意从0开始,可以往回选
// [1,2]中可以选择[1,2]也可以是[2,1]
for (int i = 0; i < nums.length; i++) {
// 因为从0开始,所以会重复选择元素,用used去重
if (used[i])
continue;
path.add(nums[i]);
used[i] = true;
backtrack(nums, used);
path.removeLast();
used[i] = false;
}
}
}
10.2 78.子集🟡待做
题目:给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
链接:78. 子集
示例 :
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
思路:
代码:
10.3 17.电话号码的字母组合🟡待做
题目:给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 :
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
思路:
代码:
10.4 39.组合总和🟡待做
题目:给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
链接:39. 组合总和
示例 :
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
思路:
代码:
10.5 22. 括号生成🟡待做
题目:数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
链接:22. 括号生成
示例 :
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
思路:
两个变量记录左括号和右括号可用数量,递归
代码:
10.6 79.单词搜索🟡待做
题目:给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
链接:79. 单词搜索
示例 :
思路:
代码:
10.7 131.分割回文串🟡待做
10.8 51.N 皇后🔴待做
11 二分查找
11.1 35. 搜索插入位置🟢
题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
链接:35. 搜索插入位置
示例 :
输入: nums = [1,3,5,6], target = 2
输出: 1
思路:
有序数组找位置显然是用二分查找。我个人习惯是用闭区间,所以注意 while
循环中 left==right
时也是有效循环,不然不就漏掉 left==right
的情况了吗
至于为什么最后没找到返回的是 left
。下次遇到这种情况直接举个例子试试。
如 target=2
,现在找到了 1 3
,left=0,right=1
,那 mid=0
。target>nums[mid]
,所以 left=right=mid=1
。此时 target<nums[mid]
,所以 right=0,left=1
,所以 left=1
便是要插入的位置。
代码:
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 闭区间,所以left=right也是有效值
while (left <= right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
return mid;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
11.2 74. 搜索二维矩阵🟡
题目:给你一个满足下述两条属性的 m x n
整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target
,如果 target
在矩阵中,返回 true
;否则,返回 false
。
链接:74. 搜索二维矩阵
示例 :
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
思路:
二维矩阵每一行都是递增的,可以看做是一维递增的数组。一维递增数组查找元素那就可以用二分查找来查,只需要根据一维数组的 mid
求出二维数组对应的 mid
元素即可。
速记:二分查找,根据一维的
mid
转换成二维对应元素
代码:
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int l = 0, r = m * n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
// 根据mid求出二维数组的mid位置元素
int x = matrix[mid / n][mid % n];
if (x < target) {
l = mid + 1;
} else if (x > target) {
r = mid - 1;
} else {
return true;
}
}
return false;
}
}
11.3 34.在排序数组中查找元素的第一个和最后一个位置🟡
题目:给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 :
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
思路:
找到开始位置也就是找到第一个 = target
的位置,而找到结束位置也就是找到第一个 >= target+1
的位置再 -1
就是结束位置。所以写二分查找函数时返回的是第一个 >= target
的位置。
速记:结束位置就是查找第一个
>= target+1
的位置再-1
代码:
class Solution {
public int[] searchRange(int[] nums, int target) {
int start = lowerBound(nums, target);
// 所有数都 < taregt 或者 第一个 >= target的元素不是target
if (start == nums.length || nums[start] != target) {
return new int[]{-1, -1};
}
// 如果 start 存在,那么 end 必定存在
int end = lowerBound(nums, target + 1) - 1;
return new int[]{start, end};
}
// 返回满足 nums[i] >= target 的最左边的i
private int lowerBound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// mid = left-1 即 nums[left-1] < target
left = mid + 1;
} else {
// mid = right+1 即 nums[right+1] >= target
right = mid - 1;
}
}
return left; // 返回right + 1也可以
}
}
11.4 33. 搜索旋转排序数组🟡
题目:整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
链接:33. 搜索旋转排序数组
示例 :
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
思路:
若不考虑时间复杂度,遍历一遍挨个比较就行。时间复杂度为 O(log n)
,那还是要用二分。但由于并不是有序,不能直接用。
可以注意到 mid
分成的两边,肯定有一边是有序的;如果 target
刚好也在这个有序的中间,那可以用二分查找,否则就去另一边继续划分;另一边划分后,肯定也有一边是有序的。
代码:
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
return mid;
}
// 左半有序 注意带等于号 对应 l=mid r=l+1
if (nums[l] <= nums[mid]) {
// target在左半区间内
if (nums[l] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
// 右半有序,且target在右半区间内
if (nums[mid] < target && target <= nums[r]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
}
11.5 153.寻找旋转排序数组中的最小值🟡
题目:已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 :
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
思路:
限制了时间复杂度,所以应该用二分查找。以最后一个元素 nums[n-1]
为基准值,在 [0,n-2]
区间内搜索。若 nums[mid] < nums[n - 1]
说明 [mid,n-1]
是递增的,既然这部分区间都递增了,那最小值肯定在 mid
左区间。否则,如 3,4,5,1,2
中 4 > 2
,说明旋转点在 mid
右边,也就是最小值 1
在 mid
右区间。最后的返回值可以举例试一下。
速记:
nums[n-1]
作为基准值,在[0,n-2]区间搜索缩小区间
代码:
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
int l = 0, r = n - 2; // 在[0,n-2]区间搜索
while (l <= r) {
int mid = l + (r - l) / 2;
// 以最右侧的值为基准值
// 说明[mid,n-1]都是递增的
if (nums[mid] < nums[n - 1]) {
// 最小值在左区间
r = mid - 1;
} else {
// 最小值在右区间
l = mid + 1;
}
}
return nums[l];
}
}
11.6 4.寻找两个正序数组的中位数🔴待做
12 栈
12.1 20.有效的括号🟢
题目:给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
链接:20. 有效的括号
示例 :
输入:s = "([}}])"
输出:false
思路:
括号问题就用栈做。遇到左括号就入栈,遇到右括号就和栈顶元素比较下。如果当前都不是有效的左右括号,那肯定不是有效括号,可直接输出 false
。如果当前是有效左右括号,就弹出栈顶的左括号。
代码:
class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) {
return false;
}
Map<Character, Character> map = new HashMap<>();
map.put(')', '(');
map.put(']', '[');
map.put('}', '{');
Deque<Character> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
char ch = s.charAt(i);
// 不是右括号就push
if (map.containsKey(ch)) {
// 栈空了或者当前右括号和栈顶不匹配 可判断是无效括号
if (stack.isEmpty() || stack.peek() != map.get(ch)) {
return false;
}
// 当前是有效括号,pop左括号
stack.pop();
} else {
stack.push(ch);
}
}
return stack.isEmpty();
}
}
12.2 155.最小栈🟡待做
12.3 394.字符串解码🟡待做
12.4 739.每日温度🟡待做
12.5 84.柱状图中最大的矩形🟡待做
12.6 232.用栈实现队列(扩展)🟢待做
13 堆
13.1 215.数组中的第 K 个最大元素🟡🔥
题目:给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 :
输入: [3,2,1,5,6,4], k = 2
输出: 5
思路:
要求的是第 k 个最大,可以用堆来实现。用最小堆找出前 k 个最大,这样堆顶就是第 k 个最大。
代码:
1.优先队列
class Solution {
public int findKthLargest(int[] nums, int k) {
// 默认是小顶堆,堆顶是最小元素
Queue<Integer> queue = new PriorityQueue<>();
for (int num : nums) {
// 前k个直接添加进去
if (queue.size() < k) {
queue.offer(num);
} else {
// 后面的比堆顶大再添加进去
if (num > queue.peek()) {
queue.poll();
queue.offer(num);
}
}
}
// 堆中是前k个最大元素,堆顶是最小的那个,即第 k 个最大元素
return queue.peek();
}
}
2.快速选择
class Solution {
public int findKthLargest(int[] nums, int k) {
int target = nums.length - k;
// 第k大也就是下标为target的元素
return quickSelect(nums, 0, nums.length - 1, target);
}
public static int partition(int[] nums, int left, int right) {
// 随机在nums[left...right]的范围中, 选择一个数作为pivot
swap(nums, left, (int) (Math.random() * (right - left + 1)) + left);
int pivot = nums[left];
int i = left + 1; // 左侧指针的初始位置
int j = right; // 右侧指针的初始位置
while (true) {
// 左侧指针移动,直到找到一个大于基准值的元素
while (i <= right && nums[i] < pivot) {
i++;
}
// 右侧指针移动,直到找到一个小于基准值的元素
while (j >= left + 1 && nums[j] > pivot) {
j--;
}
// 两边已经分区好了,break
if (i >= j) {
break;
}
// 交换左右指针所指的元素
swap(nums, i, j);
i++;
j--;
}
// j指向的是最后一个小于pivot的元素,所以和j交换
swap(nums, left, j);
return j;
}
public static void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
public static int quickSelect(int[] nums, int left, int right, int target) {
if (left >= right) {
return nums[left];
}
int pivotIndex = partition(nums, left, right);
if (target == pivotIndex) {
return nums[target];
} else if (target < pivotIndex) {
return quickSelect(nums, left, pivotIndex - 1, target);
} else {
return quickSelect(nums, pivotIndex + 1, right, target);
}
}
}
和快速排序相比就 quickSelect
函数不同,快排函数如下
public static void quickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
// 获取划分子数组的位置
int pivotIndex = partition(nums, left, right);
// 对左半部分进行快速排序
quickSort(nums, left, pivotIndex - 1);
// 对右半部分进行快速排序
quickSort(nums, pivotIndex + 1, right);
}
13.2 347.前 K 个高频元素🟡🔥
题目:给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 :
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
思路:
前 k 个高频元素,可以先用 HashMap
统计一下每个元素对应的频率,然后问题转换为求前 k 个最大元素。
与第 k 个最大元素不同,可以直接用大顶堆,把所有元素都添加进去,最终堆里的元素就是前 k 个最大元素。不过这样每个元素都要过一遍堆,可以用小顶堆优化下,不用把每个元素都过一遍导致每次都调整堆
注意代码中队列怎么存放 Map,以及怎么定义 Map 类型的小顶堆
代码:
1.小顶堆
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 小顶堆
Queue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>
((a, b) -> a.getValue() - (b.getValue()));
for (Map.Entry<Integer, Integer> entry : count.entrySet()) {
if (pq.size() < k) {
pq.offer(entry);
} else {
if (entry.getValue() > pq.peek().getValue()) {
pq.poll();
pq.offer(entry);
}
}
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = pq.poll().getKey();
}
return res;
}
}
13.3 295.数据流的中位数🔴待做
14 贪心算法
14.1 121.买卖股票的最佳时机🟢
题目:给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 :
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
思路:
想赚钱,那肯定是低价买入,高价抛出。所以用一个 minPrice
记录最小价格,向后遍历时计算当前价格减 minPrice
得到当前要是抛出赚的钱,求其中的最大值即可。
代码:
class Solution {
public int maxProfit(int[] prices) {
int minPrice = prices[0], res = 0;
for (int i = 1; i < prices.length; i++) {
// 更新价格最小值
if (prices[i] < minPrice) {
minPrice = prices[i];
} else if (prices[i] - minPrice > res) {
// 计算利润最大值
res = prices[i] - minPrice;
}
}
return res;
}
}
14.2 55.跳跃游戏🟡待做
14.3 45.跳跃游戏 II🟡待做
14.4 763.划分字母区间🟡待做
15 动态规划
15.1 70. 爬楼梯🟢
题目:假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
链接:70. 爬楼梯
示例 :
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
思路:
后面节点的状态可以由前面的推导出来,所以要用动态规划。设 dp[n]
为 n 阶时的方法个数,那它其实就等于先爬一步,后面有 dp[i-1]
种方法 + 先爬两步,后面有 dp[i-2]
种方法
代码:
class Solution {
public int climbStairs(int n) {
if (n <= 2)
return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
// 先爬一步,后面有dp[i-1]种方法
// 先爬两步,后面有dp[i-2]种方法
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
15.2 118.杨辉三角🟢
题目:给定一个非负整数 numRows
,生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
链接:118. 杨辉三角
示例 :
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
思路:
下一行的结果可以由前一行的结果推导出来,并且能看出每一行的第一个和最后一个都是 1
。对于非首末元素的值就等于左上角和右上角的和,直接看图可能不直观,可以把杨辉三角旋转一下,如下
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
这样就很明显能看出推导公式为dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
代码:
class Solution {
public List<List<Integer>> generate(int numRows) {
// 初始化动态规划数组
Integer[][] dp = new Integer[numRows][];
// 遍历每一行
for (int i = 0; i < numRows; i++) {
// 注意:初始化当前行
dp[i] = new Integer[i + 1];
// 每一行的第一个和最后一个元素总是 1
dp[i][0] = dp[i][i] = 1;
// 计算中间元素
for (int j = 1; j < i; j++) {
// 中间元素等于上一行的相邻两个元素之和
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}
}
// 将动态规划数组转换为结果列表
List<List<Integer>> res = new ArrayList<>();
for (Integer[] row : dp) {
res.add(Arrays.asList(row));
}
// 返回结果列表
return res;
}
}
15.3 198.打家劫舍🟡
题目:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
链接:198. 打家劫舍
示例 :
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
思路:
只有一间屋时没得选,dp[0] = nums[0]
。两间屋时,选最大的那个。之后对于每个位置有两种选择,偷还是不偷当前位置,从中选择最大的那个。
- 要是偷当前位置,这样就不能偷前一个,此时和为
dp[i - 2] + nums[i]
- 若是不偷当前位置,那和就与前一个位置相同,
dp[i - 1]
代码:
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
}
int[] dp = new int[length];
dp[0] = nums[0]; // 只有1间屋
dp[1] = Math.max(nums[0], nums[1]); // 两间屋选最大的那个
for (int i = 2; i < length; i++) {
// 偷当前位置 和 不偷当前位置 取最大
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
}
因为结果只和前两间房屋的最高金额有关,没必要记录所有位置的最高金额,可以用两个变量代替数组
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int first = nums[0], second = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
扩展:213. 打家劫舍 II:这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。
代码:
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
} else if (length == 2) {
return Math.max(nums[0], nums[1]);
}
// 偷第1家 和 不偷第1家取最大
return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
public int robRange(int[] nums, int start, int end) {
int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
15.4 279.完全平方数🟡待做
15.5 322.零钱兑换🟡待做
15.6 139.单词拆分🟡待做
15.7 300. 最长递增子序列🟡
题目:给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列
链接:300. 最长递增子序列
示例 :
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
思路:
当前位置的结果能由前面的元素的结果推导出来,所以可以用动态规划。
但如果 dp[i]
表示的是最终结果,不选的话直接用前一个的结果,但选的话还要满足递增,那比前面的哪个大算递增呢。如果从前往后一个个比较,但当前位置比前面的大,不代表直接能用前面的结果加1。因为前面的结果可能是前面那个位置的元素没有选的结果。如 1,5,9,4,6
,dp[3]=3
时4就没有选, dp[4]!=dp[3]+1
关键点在于我们无法确定前面的 dp[i]
的结果中 nums[i]
有没有选,那我们可以把 dp[i]
定义成 nums[i]
一定被选时的结果,然后求出所有的 dp[i]
的最大值就是最终结果。
代码:
class Solution {
public int lengthOfLIS(int[] nums) {
// dp数组表示0..i时的结果,且nums[i]必须被选择
int[] dp = new int[nums.length];
// 初始化每个dp都是1即只选当前元素
Arrays.fill(dp, 1);
int res = 1; // 解决nums只有1个元素的情况,也可以下面i从0开始:res=0
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
// 因为必须选择nums[i],所以nums[i]必须大于nums[j]才能满足递增
if (nums[i] > nums[j]) {
// 对于每个j,dp[i]=dp[j]+1,求所有j中最大的dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 所有dp里面的最大值就是res
res = Math.max(res, dp[i]);
}
return res;
}
}
15.8 152.乘积最大子数组🟡待做
15.9 32.最长有效括号🟡待做
16 多维动态规划
16.1 62.不同路径🟡待做
16.2 64.最小路径和🟡待做
16.3 5.最长回文子串🟡
题目:给你一个字符串 s
,找到 s
中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
链接:5. 最长回文子串
示例 :
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
思路:
动态规划:如果一个字符串是回文,那么它左右添加一个相同的字符仍然是回文。可以用 dp
数组记录 [l,r]
是否是回文,先遍历右边界,这样就可以复用已经得到结果的右边界对应的 dp
中心扩散:对于一个字符,若两边字符相同,则不断向两边扩散,记录能扩散的最大值即可。不过要注意中心字符可能是一个如 aca
或两个如 abba
代码:
1.动态规划
public class Solution {
public String longestPalindrome(String s) {
int strLen = s.length();
int maxStart = 0; // 最长回文串的起点
int maxEnd = 0; // 最长回文串的终点
int maxLen = 1; // 最长回文串的长度
// 记录是否是回文子串的DP数组
boolean[][] dp = new boolean[strLen][strLen];
for (int r = 1; r < strLen; r++) { // 从第二个位置开始遍历
for (int l = 0; l < r; l++) { // 遍历r之前的所有位置
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
// 当s[l]等于s[r] 且 l到r的距离<=2或l+1到r-1是回文串时
// 则s.substring(l, r + 1)是回文串,更新dp[l][r]为true
dp[l][r] = true;
// 如果当前回文串的长度大于最大回文串的长度
if (r - l + 1 > maxLen) {
maxLen = r - l + 1; // 更新最大回文串的长度
maxStart = l; // 更新最大回文串的起点
maxEnd = r; // 更新最大回文串的终点
}
}
}
}
return s.substring(maxStart, maxEnd + 1); // 返回最长回文串
}
}
2.中心扩散(推荐)
class Solution {
public String longestPalindrome(String s) {
String res = "";
for (int i = 0; i < s.length(); i++) {
// 以 s[i] 为中心的最长回文子串
String s1 = palindrome(s, i, i);
// 以 s[i] 和 s[i+1] 为中心的最长回文子串
String s2 = palindrome(s, i, i + 1);
// res = longest(res, s1, s2)
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
}
return res;
}
String palindrome(String s, int l, int r) {
// 防止索引越界
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
// 向两边展开
l--;
r++;
}
// 返回以 s[l] 和 s[r] 为中心的最长回文串
return s.substring(l + 1, r);
}
}
16.4 1143.最长公共子序列🟡
题目:给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 :
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
思路:
最长公共子序列是典型的二维动态规划问题,后面的状态可以由前面的状态推出。
当两个指针指向的两个字符串的字符相同时,那当前长度就等于两个指针都向前退一位后的长度加1。
若不等,要么 text1
退1位,要么 text2
退1位,求两者最大。
注意 dp 的长度要设为字符串长度+1。如果是用0表示字符串第1个位置,那就要初始化
dp[0][...]
和dp[...][0]
。但是这样要判断第1个字符是不是在另一个字符串里面,初始化比较麻烦。所以dp[i]
代表的是0...i-1
,这样dp[0]
就是0,不用再写初始化的代码了
代码:
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
// 定义:text1[0..i-1] 和 text2[0..j-1] 的 lcs 长度为 dp[i][j]
int[][] dp = new int[m + 1][n + 1];
// 目标:text1[0..m-1] 和 text2[0..n-1] 的 lcs 长度,即 dp[m][n]
// base case: dp[0][..] = dp[..][0] = 0
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 当前两字符若相等,i 和 j 从 1 开始,所以要减一
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
// 两个字符串都去除最后一个字符的lcs长度 + 1
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
// text2减一个字符后和text1的lcs长度
// 与text1减1个字符后和text2的长度 取最大
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return dp[m][n];
}
}
16.5 72.编辑距离🟡待做
17 技巧
17.1 136.只出现一次的数字🟢
题目:给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 :
输入:nums = [4,1,2,1,2]
输出:4
思路:
最简单的是 HashMap
统计次数,看哪个是一次就输出哪个。也可以用 HashSet
遍历,存在就删除,不存在就添加,最后 HashSet
剩的就是结果。还可以用 HashSet
存所有元素,计算集和中元素和的两倍减去数组中元素的和就是只出现一次的元素。
不过上述都要 O(n)
的空间,这里使用异或运算。已知任何数与 0 异或,都还是原来的数。任何数和自身异或,都是 0。那么所有元素做异或,偶数次的元素异或得到 0,异或后的结果奇数次的元素。
代码:
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for (int n : nums) {
res ^= n;
}
return res;
}
}
17.2 169.多数元素🟢
题目:给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
进阶: 尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
链接:169. 多数元素
示例 :
输入:nums = [2,2,1,1,1,2,2]
输出:2
思路:
使用摩尔投票法,设置一个 queen
,遇到和 queen
相同的元素,则投票 +1
,遇到不同的则投票 -1
,若投票为 0 则选取当前元素为新的 queen
。相当于消消乐,遍历完一轮,最后的 queen
就是次数超过一半的那个。
速记:设置
queen
,相同则投票+1
,不同投票-1
,投票为 0 时选举新的queen
代码:
class Solution {
public int majorityElement(int[] nums) {
int count = 0, queen = 0;
for (int i = 0; i < nums.length; i++) {
// 投票为0,重新选举queen
if (count == 0) {
queen = nums[i];
}
// 遇到相同的+1,不同的-1
count += (nums[i] == queen) ? 1 : -1;
}
return queen;
}
}
17.3 75.颜色分类🟡待做
17.4 31.下一个排列🟡待做
17.5 287.寻找重复数🟡待做
18 补充
18.1 单例模式
1.饿汉式
public class Singleton {
// 1、私有化构造⽅法
private Singleton() {
}
// 2、定义⼀个静态变量指向⾃⼰类型
private final static Singleton instance = new Singleton();
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
return instance;
}
}
2.懒汉式-双重检查锁
public class Singleton {
// 1、私有化构造⽅法
private Singleton() {
}
// 2、定义⼀个静态变量指向⾃⼰类型
private volatile static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
// 第⼀重检查是否为 null
if (instance == null) {
// 使⽤ synchronized 加锁
synchronized (Singleton.class) {
// 第⼆重检查是否为 null
if (instance == null) {
// new 关键字创建对象不是原⼦操作
instance = new Singleton();
}
}
}
return instance;
}
}