热题100
哈希
1. 两数之和
给定: 数组nums 和target
要求:找出两元素之和为target的数组下标
暴力:
1for i in range(len(nums)):
2 for j in range(i+1, len(nums)):
3 if ...
4# O(n^2)
题解:
问题分解 => 给定x 寻找 target-x(这是复杂度的主要部分)
用额外结构记录
1hashtable = dict()
2for i in range(len(nums)):
3 if nums[i] in hashtable:
4 return [i, hashtable[nums[i]]]
5 else:
6 hashtable[nums[i]] = i
7# 保存遍历过的元素,参考过去
⭐思路:target = num1 + num2 => 已知num1 求num2 => 寻找 target - num1
寻找过程:hash存储,这样可以 O(1) 查找 另一个
49. 字母异位词分组
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
思路:condition => […]
HashTable, Key=排序后的Str, Value=List<String>
1HashMap<String, List<String>> hashTable = new HashMap<>();
2for(String str : strs){
3 char[] charArray = str.toCharArray(); // 将字符串转换为字符数组
4 Arrays.sort(charArray); // 对字符数组进行排序
5 String orderStr = new String(charArray);
6 if (hashTable.containsKey(orderStr)) {
7 hashTable.get(orderStr).add(str);
8 }else {
9 hashTable.put(orderStr, new ArrayList<>());
10 hashTable.get(orderStr).add(str);
11 }
12}
13
14List<List<String>> res = new ArrayList<>();
15
16for (Map.Entry<String, List<String>> entry : hashTable.entrySet()) {
17 res.add(entry.getValue());
18}
19return res;
⭐思路:组合在一起 => eat和tea的共同点 => 均含有aet(顺序的)
如何归档呢? 类似于上课怎么知道去哪个教室呢? 可以查看号码咯,这里可以使用hash key为顺序排列的元素。大家进入相同的教室呢!
128. 最长连续序列
1HashSet<Integer> hashSet = new HashSet<>();
2// 去重复,并且查询O(1)
3for (int num : nums) {
4 hashSet.add(num);
5}
6
7List<Integer> start_nums = new ArrayList<>();
8
9// 找所有为起点的数
10for (Iterator<Integer> it = hashSet.iterator(); it.hasNext(); ) {
11 int num = it.next();
12 if (!hashSet.contains(num-1)){
13 start_nums.add(num);
14 }
15}
16
17// 遍历起点x, 遍历 x+1, x+2, ...
18int longest_length = 0;
19for(int start : start_nums){
20 int current_length = 1;
21 int start_i = start;
22 while (hashSet.contains(start_i+1)) {
23 current_length += 1;
24 start_i += 1;
25 }
26 if (current_length > longest_length){
27 longest_length = current_length;
28 }
29}
30return longest_length;
31}
⭐思路:原始:多队列,每个队列挂载比自己小的,错误❌
✔:从小到大找嘛,那我只需要有能力串起来就行 => 串的话呢,主要就是找next,因此使用hashmap进行加速。
这样我们可以遍历
双指针
283. 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序(原始数组位置,非不是重新排序)。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
1if (nums.length == 1){
2 return;
3}
4int i = 0;
5int j = 1;
6for (;j<nums.length;j++) {
7 if (nums[i] == 0 && nums[j] != 0){
8 nums[i] = nums[j];
9 nums[j] = 0;
10 }
11 while (i<=j) {
12 if (nums[i] != 0) {
13 i+=1;
14 } else {
15 break;
16 }
17 }
18}
19for (;i<nums.length;i++) {
20 nums[i] = 0;
21}
⭐思路:两个指针嘛? 模拟一下就好了,
11. 盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
**说明:**你不能倾斜容器。
1// 左右双指针,area = h * w, h限制于最矮的边 <= 漏水
2int i = 0;
3int j = height.length - 1;
4int max = 0;
5while (i < j) {
6 int h = 0;
7 if (height[i] < height[j]) {
8 h = height[i];
9 } else {
10 h = height[j];
11 }
12 int area = h * (j - i);
13 if (area > max) {
14 max = area;
15 }
16 if (height[i] < height[j]) {
17 i += 1;
18 } else {
19 j -= 1;
20 }
21}
22return max;
思路:双指针吧,左右,每次算面积,然后哪边矮,那边向内移动。
15. 三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
⭐ 分析:
先排序,递增
-
固定a => 遍历b 则 c = - a - b
-
1 a 2[-4,-1,-1,0,1,2] 3 b c 4// 若b向右,则c向左,因为 c = - a - b,且nums递增, b增大 => c减小 5 6// e.g. 7 a a 8[-3,-1,0,1,2,3,4,5] => [-3,-1,0,1,2,3,4,5] 9 b c ✔️ b c||| 10c不比从最右侧开始
1.
1// 固定a, b => c !!! 由于 nums[b]<=nums[c] 并且nums递增 c不比每次从末尾开始,因为b向右,则c向左。
2Arrays.sort(nums);
3ArrayList<List<Integer>> res = new ArrayList<>();
4for (int a=0; a<nums.length-2; a++) {
5 if (a-1>=0 && nums[a-1] == nums[a]) {
6 continue;
7 }
8 if (nums[a] > 0){
9 break;
10 }
11 // 最右侧
12 int c = nums.length-1;
13 for (int b=a+1; b<nums.length-1; b++) {
14 if (b-1>a && nums[b] == nums[b-1]) {
15 // b+=1;
16 continue;
17 }
18 int target = -nums[a] - nums[b];
19
20 while (b < c && target <= nums[c]) {
21 if (nums[a] + nums[b] + nums[c] == 0) {
22 List<Integer> temp = new ArrayList<>();
23 temp.add(nums[a]);
24 temp.add(nums[b]);
25 temp.add(nums[c]);
26 res.add(temp);
27 break;
28 }
29 c -= 1;
30 }
31 }
32}
33return res;
⭐思路:num1 + num2 + num3 = 0 (全部+去重)
for num1 :
能不能一步到位查看 对应的 num2 + num3 呢, 使得三者之和为0
构造所有的num2+num3的Map<Integer, Record(num2, num3)>
好像能行但是效率不高❌❌
✔️:
11. 先排个序 ⭐预处理
2for num : nums:
3 if num > 0: 后面不可能组合出负数来抵消
4 if 后面的数和当前值一样,且大于0: 3 3 3 ;跳过这部分
5 // 双指针
6 // num [num+1, ..., -1]
7 // 找符合的三元组
8 // 注意left|right可能跳过重复的数值(去重复)
42. 接雨水
局部化 => 每个单位的储水量
- 暴力 |_| 查每个单位的左右最大边,看看这个地方是否有存水的资格
1// 暴力 => 分解为局部某小块[单位长度]聚水量
2if (height.length < 3)
3 return 0;
4int ans = 0;
5for (int i=1; i<height.length-1; i++) {
6 int max_left = 0;
7 // 左侧最大值
8 for (int left=0; left<i; left++) {
9 if (max_left < height[left])
10 max_left = height[left];
11 }
12 if (max_left < height[i]) {
13 continue;
14 }
15 int max_right = 0;
16 // 右侧最大值
17 for (int right=height.length-1; right>i; right--) {
18 if (max_right < height[right])
19 max_right = height[right];
20 }
21 if (max_left>height[i] && max_right>height[i]){
22 ans += Math.min(max_left, max_right) - height[i];
23 }
24}
25return ans;
预先计算,避免重复。 预先把每个位置的左右最大高计算出来,直接判断是否被两高边夹住 => 能存水
1if (height.length < 3)
2 return 0;
3// 预先存储每个位置的左右最大高,判断该位置是否能蓄水
4int[] per_pos_left_max = new int[height.length];
5per_pos_left_max[0] = 0;
6int[] per_pos_right_max = new int[height.length];
7per_pos_right_max[height.length-1] = 0;
8
9for (int i=1; i<height.length; i++) {
10 per_pos_left_max[i] = Math.max(per_pos_left_max[i-1], height[i-1]);
11}
12for (int i=height.length-2; i >= 0; i--) {
13 per_pos_right_max[i] = Math.max(per_pos_right_max[i+1], height[i+1]);
14}
15int ans = 0;
16for (int i=0; i<height.length; i++) {
17 int min = Math.min(per_pos_left_max[i], per_pos_right_max[i]);
18 if (min > height[i])
19 ans += min - height[i];
20}
21
22return ans;
⭐ 思路呢:
整体的蓄水量 => 分而治之:单位的聚水量再聚合结果
单位的聚水量取决于左右两侧的最高边:思路已成
左右两侧的最高边 => 左右遍历
滑动窗口
3. 无重复字符的最长子串
滑动窗口 => a[???bca]bcbb => a???b[cab]cbb
左边界直接滑动窗口内第一个重复位置的右侧【左边界右滑一大步】
1char[] chars = s.toCharArray();
2HashSet<Character> hashSet = new HashSet<>();
3int res = 0;
4for(int left=0, right=0; right<s.length(); right++) {
5 char ch = chars[right];
6
7 while(hashSet.contains(ch)) {
8 hashSet.remove(chars[left]);
9 left+=1;
10 }
11
12 hashSet.add(ch);
13 res = Math.max(res, right - left + 1);
14}
15
16return res;
⭐思路:Yes:(双指针)遍历一遍,使用一个Map(字母,下标)存储窗口内的元素,并累计数据,如果重复了,左边界可以右划到重复元素下一个的位置哦。
438. 找到字符串中所有字母异位词
描述: 给定s,和目标p,找出在s中所有由p随机组合的字串起点
思路:
- 【暴力】遍历数组s,每个元素作为起始点,sortStr([start, start+target.length()-1]).equals(sortStr(target))
- 【滑动窗口】 DNA转录? 1️⃣ 还是遍历整个s数组 2️⃣ 比较方法变为 当前窗口元素出现次数的词频表
1List<Integer> ans = new ArrayList<>();
2
3// 意外情况
4if (s.length() < p.length()) {
5 return ans;
6}
7
8// 词频表 index-value == alpha-count
9int[] sCount = new int[26];
10int[] pCount = new int[26];
11for (int i = 0; i < p.length(); i++) {
12 sCount[s.charAt(i) - 'a'] += 1;
13 pCount[p.charAt(i) - 'a'] += 1;
14}
15// 首个异构词
16if (Arrays.equals(sCount, pCount)) {
17 ans.add(0);
18}
19
20int left = 0;
21int right = p.length()-1;
22for(int i=0; i<s.length()-p.length(); i++) {
23 // 左侧出界
24 sCount[s.charAt(left) - 'a'] -= 1;
25 left += 1;
26 // 右边界添加新元素
27 right += 1;
28 sCount[s.charAt(right) - 'a'] += 1;
29
30 if (Arrays.equals(sCount, pCount)) {
31 ans.add(left);
32 }
33}
34
35return ans;
⭐:1. 遍历比较整个p字串(顺序化)。
比较过程可以使用元素出现次数体现
字串
560. 和为 K 的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
思路:
统计个数而非位置 => 前缀和
[[???]—?] 利用前缀和-前缀和的差 代表之前的子列和
例如: nums=[[1,2,3,4,5,6],-2,2,6], k=6
preSum=[0,1,3,6,10,15,21,[19,21],27] , 利用HashMap存储子列前缀和和其次数,如次数大于0,说明子列1与子列2前缀和相等=>它们之间的和为0.
当preSum=27时,次数num=6, 本身[6]为子列,并且[-2,2,6]也是
1public static int subarraySum(int[] nums, int k) {
2 int count = 0;
3 // preSum -> count
4 HashMap<Integer, Integer> preSum = new HashMap<>();
5 preSum.put(0, 1);
6 int sum = 0;
7 for (int i = 0; i < nums.length; i++) {
8 sum += nums[i];
9 if (preSum.containsKey(sum - k)) {
10 count += preSum.get(sum - k);
11 }
12 preSum.put(sum, preSum.getOrDefault(sum, 0) + 1);
13 }
14
15 return count;
16}
239.滑动窗口中的最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路 =>
- ?[???]?? 维护一个最大堆,每次删除&添加,记录结果
- for(数组)=>{for(窗口大小)=>{挑选出最大的元素}} – 暴力遍历
- 维护一个单调队列: [max…min] 每次滑动窗口
- 循环(将右侧新元素与队列min比较,大了,则删除Last_Min),简单插入排序=> 因为只要窗口内的最大值,那么小的值没有必要维护,删了还减轻比较开销。
- 将最左侧的元素出队
1int n = nums.length;
2int ans_idx = 0;
3int[] ans = new int[n-k+1];
4
5Deque<Integer> deque = new LinkedList<>(); // 存储下标就行
6// 第一个窗口
7for (int i = 0; i < k; i++) {
8 while (!deque.isEmpty() && nums[i] > nums[deque.peekLast()]) {
9 deque.removeLast();
10 }
11 deque.offerLast(i);
12}
13
14ans[ans_idx++] = nums[deque.peekFirst()];
15// 开始移动
16for (int i = k; i < n; i++) {
17 while (!deque.isEmpty() && nums[i] > nums[deque.peekLast()]) {
18 deque.removeLast();
19 }
20 deque.offerLast(i);
21
22 while (deque.peekFirst() < i-k+1) {
23 deque.removeFirst();
24 }
25 ans[ans_idx++] = nums[deque.peekFirst()];
26}
27return ans;
[X待写X]76. 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。 # 唯一
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
扩张[右边界]与收缩[左边界]
1️⃣利用HashMap进行判断是否重合
1public boolean isCovered(Map<Character, Integer> targetMap, Map<Character, Integer> curMap) {
2 Set<Character> keySet = targetMap.keySet();
3 for (Character key : keySet) {
4 Integer tgt = targetMap.getOrDefault(key, 0);
5 Integer crt = curMap.getOrDefault(key, 0);
6 if (tgt > crt)
7 return false;
8 }
9 return true;
10}
11
12public String minWindow(String s, String t) {
13 Map<Character, Integer> targetMap = new HashMap<>();
14 Map<Character, Integer> curMap = new HashMap<>();
15
16 for (int i = 0; i < t.length(); i++) {
17 char c = t.charAt(i);
18 targetMap.put(c, targetMap.getOrDefault(c, 0)+1);
19 }
20 int minLen = Integer.MAX_VALUE; int startLoc = 0;
21 int left = 0, right = 0;
22 for (right = 0; right < s.length(); right++) {
23 char c = s.charAt(right);
24 curMap.put(c, curMap.getOrDefault(c, 0)+1);
25 while (isCovered(targetMap, curMap)) {
26 if (minLen > (right - left)){
27 minLen = right - left;
28 startLoc = left;
29 }
30 char ch = s.charAt(left);
31 curMap.put(ch, curMap.get(ch)-1);
32 left += 1;
33 }
34 }
35
36 if (minLen < Integer.MAX_VALUE) return s.substring(startLoc, startLoc+minLen+1);
37 else return "";
38}
⭐先扩展右边界,再收缩左边界
2️⃣ 每次记录符合个数,避免 重复的对两字串进行判断复合关系
1public String minWindow(String s, String t) {
2 Map<Character, Integer> targetMap = new HashMap<>();
3 Map<Character, Integer> curMap = new HashMap<>();
4
5 for (int i = 0; i < t.length(); i++) {
6 char c = t.charAt(i);
7 targetMap.put(c, targetMap.getOrDefault(c, 0) + 1);
8 }
9 int minLen = Integer.MAX_VALUE; int startLoc = 0;
10 int curLen = 0; int tgtLen = targetMap.keySet().size();
11 int left = 0, right;
12
13 for (right = 0; right < s.length(); right++) {
14 char c = s.charAt(right);
15 if (targetMap.containsKey(c)) {
16 curMap.put(c, curMap.getOrDefault(c, 0)+1);
17 if (targetMap.get(c).equals(curMap.get(c))) {
18 curLen += 1;
19 }
20 }
21 while (curLen == tgtLen) {
22 if (minLen > (right-left)){
23 minLen = right-left;
24 startLoc = left;
25 }
26 char ch = s.charAt(left);
27 if (curMap.containsKey(ch)) {
28 curMap.put(ch, curMap.get(ch)-1);
29 if (curMap.get(ch) < targetMap.get(ch))
30 curLen -=1;
31 }
32 left += 1;
33 }
34 }
35
36 if (minLen < Integer.MAX_VALUE) return s.substring(startLoc, startLoc+minLen+1);
37 else return "";
38}
普通数组
53. 最大子数组和
线段树做法
1// 例 [-2,4,5,-1][-1,2,3,-4]
2// l:{
3// iSum=6,lSum=7,rSum=8,mSum=9
4// }
5// r:{
6// iSum=0,lSum=4,rSum=1,mSum=5
7// }
8public class Status {
9 public int iSum, lSum, rSum, mSum;
10
11 public Status(int iSum, int lSum, int rSum, int mSum) {
12 this.iSum = iSum; // 区间总和
13 this.lSum = lSum; // 区间从左边界开始最大和
14 this.rSum = rSum; // 区间从右边界开始最大和
15 this.mSum = mSum; // 区间最大和
16 }
17}
18
19public Status getMaxSum(int[] nums, int start, int end) {
20 if (start == end) {
21 return new Status(nums[start], nums[start], nums[start], nums[start]);
22 }
23 int mid = (start + end) >> 1;
24 Status l = getMaxSum(nums, start, mid);
25 Status r = getMaxSum(nums, mid+1, end);
26 return popPush(l, r);
27}
28
29//
30public Status pushUp(Status l, Status r) {
31 int iSum = l.iSum + r.iSum; // 区间总和
32 // 1. [x?][??] or [xx][x?]
33 int lSum = Math.max(l.lSum, l.iSum+r.lSum);
34 // 2. [??][?x] or [?x][xx]
35 int rSum = Math.max(r.rSum, l.rSum+r.iSum);
36 // 1. [?xx?][???] 2. [???][?xx?] 3. [?xx][xx?]
37 int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum+r.lSum);
38 return new Status(iSum,lSum,rSum,mSum);
39}
40
41public int maxSubArray(int[] nums) {
42 return getMaxSum(nums, 0, nums.length-1).mSum;
43}
41. 缺失的第一个正数
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。
### 缺失的最小的正整数,给定数组nums, 那么目标一定在[1, N+1]中。
例子
1. [x?????]? => 1
2. [???x??]? =>[-1<x, ...]
3. [??????]x =>N+1 # 给定的数组刚好占满[1-N],那么最小的正整数为N+1
暴力:
将数组放入HashSet(查询时间O(1)=>空间换时间)中,然后检查[1-N]是否在HashSet中,不在返回i,全在返回N+1;
思路2✔️.
构建hashMap,映射条件为 i=>i+1, 给定下标i,则元素值为i+1;
例:
nums = [3,4,-1,1]
=> i: 0,1, 2,3
=>nums = [-1,4,3,1] i 0, 1,2,3
=>nums = [-1,1,3,4] => [1,-1,3,4] => i=1处异常 => ans=i+1=2
JAVA语法:
1// 传递给方法的是引用的拷贝,而不是引用本身
2public void rotate(int[] nums, int k) {
3 int n = nums.length;
4 int[] ans = new int[n];
5 // ...
6 nums = ans; //❌❌❌ nums的引用是原数组引用的拷贝
7}
8
9例: a -> [???]
10 rotate(a) => rotate(nums) {
11 // nums->[???]<-a
12}
13// nums != a
矩阵
73. 矩阵置零
1使用额外标记数组 col,row 标识某列\行是否存在0,刷新为0的col或row
2 col: 0,1,0 row 判断 mark col[0][j]==0 || row[i][0]==0
3输入:matrix = [[1,1,1], 0
4 [1,0,1], 1
5 [1,1,1]] 0
6额外空间=> O(M+N)
7输出:[[1,0,1],
8 [0,0,0],
9 [1,0,1]]
10
11利用第一行和第一列充当标记数组,并且使用两个变量标识第一列行是否刷为0 空间复杂度O(1)
12 1 1 1
13 输入:matrix = 1[0,1]
14 1[1,1]
54. 螺旋矩阵
1思路: 定义四个边界 top,bottm,left,right
2while() {
3 1.for ➡️ 并且更新边界 + 判断是否退出
4 2.for ⬇️
5 3.for ⬅️
6 4.for ⬆️
7}
48. 旋转图像
11 2 3 4
22 y z 5
33 x k 6
45 6 7 8
5
6分步 先旋转外围 再逐层往内进行旋转
7x ➡️ x
8⬆️ ⬇️
9x ⬅️ x
1int len = matrix.length;
2int count = len - 2 > 0?len - 2:1;
3for (int i = 0; i < count; i++) {
4 int left=i, top=i, right=len-1-i, bottom=len-1-i;
5 for (int j = i; j < len-i-1; j++) {
6 int temp1, temp2;
7 // 1. matrix[top][left]
8 temp1 = matrix[top][len-1-i];
9 matrix[top][len-1-i] = matrix[i][left];
10
11 // 2. matrix[top][right]
12 temp2 = matrix[len-1-i][right];
13 matrix[len-1-i][right] = temp1;
14
15 // 3. matrix[bottom][right]
16 temp1 = matrix[bottom][i];
17 matrix[bottom][i] = temp2;
18
19 // 4. matrix[bottom][left]
20 matrix[i][left] = temp1;
21
22 // move
23 left += 1;
24 top += 1;
25 right -= 1;
26 bottom -= 1;
27 }
28}
or
123 147 741
456 =.T> 258 =filp> 852
789 369 963
240. 搜索二维矩阵 II
编写一个高效的算法来搜索 *m* x *n* 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
有序=>二分查找O(logn)
for int[]row in nums:
binarySearch(row)
Z字查询 – 二分查找树
1// small<-medium->large
2[1,4,7,11,{15⭐}] small<- medium
3[2,5,8,12,19] ⬇
4[3,6,9,16,22] large
链表
⭐多指针⭐
142. 环形链表 II
找环的入口 =>
1head => 环入口 记为x
2第一次相遇点 => 环入口 记为z
3环入口 => 第一次相遇点 记为y
4
5快慢指针
6slow(单步): x+y
7fast(双步): x+y + n(y+z) // 多绕了n圈
8
9通过步数建立等式
10
112(x+y) = x+y + n(y+z)
12x+y = n(y+z)
13x = n(y+z) - y
14x = (n-1)(y+z) + z # 当只绕一圈相遇时 n=1
15x = z
16
17第一次相遇点到入口的距离 == 起点到入口的距离 ⭐⭐⭐
1public ListNode detectCycle(ListNode head) {
2 ListNode fast = head;
3 ListNode slow = head;
4 while (fast != null && fast.next != null) {
5 fast = fast.next.next;
6 slow = slow.next;
7
8 if (fast == slow) {
9 ListNode index1 = head;
10 ListNode index2 = fast;
11 while (index1 != index2) {
12 index1 = index1.next;
13 index2 = index2.next;
14 }
15
16 return index1;
17 }
18 }
19 return null;
20}
160. 相交链表
任务:找到第一个相交点
1// 暴力
2// for(nodeA: A)
3// for(nodeB: B) => nodeA?nodeB
4// 利用空间替换时间 => 查找时间利用hashSet O(1)
5// All nodeA => hashSet
6// for(nodeB:B) nodeB?hashSet
填充长度,=> 相等长度
1X-?-?-√-√-√
2?-?-?-√-√-√
3同时遍历
4or
5从尾部开始? 知道第一个不相等的Node?
交叉遍历
1-lenA-
2 -lenC-
3-lenB-
4// A+C+B => 循环后 可视为 B+A+C
5// B+C+A A+B+C
234. 回文链表
判断对称
暴力:
读出来=> array => left,right=> 进行判断
⭐ 快慢指针
1// 快慢指针:快一次走一步;慢一次走两步
2head: 1-2-3-[3-2-1]-null 偶数
3 p q
4head: 1-2-3-4-[3-2-1]-null 奇数
5 p q
6
7head - [1-2-3]
8newHead - [1-2-3] // 头插,逆序
9判断完
10头插回去,进行复原
翻转链表
- 尾插法:顺序构建链表
- 头插法:逆序构建链表
可以构建空的头结点 => 无需判断第一个节点还是后续节点
138. 随机链表的复制
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
⭐利用hash,空间换时间⭐
思路:
1. 顺序构建深拷贝链表,利用hashMap (oldNode) -> (new Node)
2. 填充random point
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) 的平均时间复杂度运行。
## 分析:
- 函数
get和put必须以O(1)的平均时间复杂度运行 => 直接定位 要么数组要么hash - 数量超过
capacity,则应该 逐出 最久未使用的关键字
## 做法
- hashMap <Key => ObejectAddress> O(1)定位能力
- 自定义双向链表 head(used)<=>tail(not use)
1class LRUCache {
2 class BiLinkedList {
3 int key,val;
4 BiLinkedList prev,next;
5
6 public BiLinkedList() {
7 }
8
9 public BiLinkedList(int key, int val) {
10 this.key = key;
11 this.val = val;
12 }
13 }
14
15 private Map<Integer, BiLinkedList> map = new HashMap<>();
16 private int capacity; // total
17 private int size; // current
18 private BiLinkedList head, tail;
19
20 public LRUCache(int capacity) {
21 this.capacity = capacity;
22 this.size = 0;
23 head = new BiLinkedList();
24 tail = new BiLinkedList();
25 head.next = tail;
26 tail.prev = head;
27 }
28
29 public int get(int key) {
30 // 访问 => 放表头
31 if (map.containsKey(key)) {
32 BiLinkedList node = map.get(key);
33 move2Head(node);
34 return node.val;
35 } else {
36 return -1;
37 }
38 }
39
40 public void put(int key, int value) {
41 // 修改 => 放表头
42 if (map.containsKey(key)) {
43 map.get(key).val = value;
44 move2Head(map.get(key));
45 }else {
46 // 新增
47 if (size >= capacity) {
48 BiLinkedList node = deleteTail();// map 也需要删除啊
49 map.remove(node.key);
50 size -= 1;
51 }
52 // 插入到表头
53 BiLinkedList node = insert2Head(key, value);
54 map.put(key, node);
55 size += 1;
56 }
57 }
58
59 public void move2Head(BiLinkedList node) {
60 node.prev.next = node.next;
61 node.next.prev = node.prev;
62
63 node.next = head.next;
64 node.prev = head;
65 head.next.prev = node;
66 head.next = node;
67 }
68
69 public BiLinkedList deleteTail() {
70 BiLinkedList node = tail.prev;
71 tail.prev.prev.next = tail;
72 tail.prev = tail.prev.prev;
73 return node;
74 }
75
76 public BiLinkedList insert2Head(int key, int value) {
77 BiLinkedList node = new BiLinkedList(key, value);
78 node.next = head.next;
79 node.prev = head;
80 head.next.prev = node;
81 head.next = node;
82 return node;
83 }
84}
二叉树
翻转二叉树
思路:
- 递归遍历 - 自上而下
- 每次遍历某个节点后,将其左右儿子指针调换
对称二叉树
判断二叉树中心轴对称
递归遍历 - 自上而下
双指针:
- 左左 | 右右
- 左右 | 右左
1TreeNode q=p=root;
2
3# func start
4eq1 = q.val==p.val
5eq2 = check(q.left, p.right);
6eq3 = check(q.right, p.left);
7
8eq1&&eq2&&eq3
9# func end
二叉树直径
任意两结点的边数
思路:
考虑每个结点为枢纽,左侧距离+右侧距离+本身
利用深度
1
2int deep(TreeNode node, int deep):
3 if(node == null):
4 return 0;
5 int leftDeep = deep(node.left);
6 int rightDeep = deep(node.right);
7 if(...) // 保存最大深度 leftDeep+rightDeep+1
8 ...
9 return max(leftDeep, rightDeep) + 1;
二叉树层序遍历
利用队列
1queue.add(root)
2while(!queue.isEmpty) {
3 node = queue.poll();
4 ...
5
6}
验证二叉搜索树
node.left.val < node.val < node.right.val
思路:
// 递归 => 当前结点 + 左子树 + 右子树
该结点大于左子树最大
小于右子树最小
1public boolean isValidBST(TreeNode node, long leftMax, long rightLower) {
2 if (node == null)
3 return true;
4 if (node.val <= leftMax || node.val >= rightLower)
5 return false;
6
7 return isValidBST(node.left, leftMax, node.val) && isValidBST(node.right, node.val, rightLower);
8 }
230. 二叉搜索树中第 K 小的元素
思路:二叉搜索树
二叉搜索树中序遍历是升序结果
中序遍历
199. 二叉树的右视图
先右子树=>再访问根结点 =>左子树
=> 使用辅助数组标记每层的输出情况
114. 二叉树展开为链表
每次把左子树插在右子树上,进行递归
105. 从前序与中序遍历序列构造二叉树
思路:前序第一个是Root
按照root把中序分成左子树结点和右子树结点
437. 路径总和 III
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
遍历Tree,并且有target和node.val,存储一下preSum,往下找target-preSum结点
1public int dfs(TreeNode node, long preSum, int targetSum) {
2 int ret = 0;
3 if (node != null) {
4 int val = node.val;
5 if (val+preSum == targetSum) {
6 ret++;
7 }
8 ret += dfs(node.left, val+preSum,targetSum);
9 ret += dfs(node.right, val+preSum,targetSum);
10 return ret;
11 }
12 return ret;
13}
14
15public int pathSum(TreeNode root, int targetSum) {
16 int ans = 0;
17 if (root == null){
18 return 0;
19 }
20 int val = root.val;
21 ans += dfs(root, 0, targetSum);
22 ans += pathSum(root.left, targetSum);
23 ans += pathSum(root.right, targetSum);
24 return ans;
25}
236. 二叉树的最近公共祖先
三种情况(局部化):
- pq分布在左右子树
- root有个是q||p, 左or右子树存在另一个
1// 三种
2boolean leftok = find(node.left)
3boolean rightok = find(node.right)
4if (leftok&&rightok)||(node.val==p||node.val==q)&&(leftok||rightok)
1private TreeNode ans = null;
2
3// 如何控制最近组先 => DFS, 优先从底部开始搜索
4// 当最近组先被发现后,次一级组先由于(pq均在一侧),故不满足if判断。
5public boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
6 if (root == null)
7 return false;
8 boolean l = dfs(root.left, p, q);
9 boolean r = dfs(root.right, p, q);
10 if ((l&&r) || ((root == p||root==q)&&(l||r)))
11 ans = root;
12 return l || r || (root==q || root==p);
13}
14
15// 暴力 => 查询某个节点是否包括俩目标节点 ❌ 最近!!!
16public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
17 dfs(root, p, q);
18 return ans;
19}
124. 二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
- 考虑枢纽结点为中心,两侧子树的正向贡献
1int dfs(TreeNode node) {
2 if (node == null)
3 return 0;
4 int lv = max(dfs(node.left), 0);
5 int rv = max(dfs(node.right), 0);
6 if(lv+r+rv > ans)
7 // save
8 return max(lv+r, rv+r);
9}
图论
200. 岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
考点
=> 邻接矩阵的连通分量(有几个不相连的子图)
BFS
1// BFS 判断连通性 => answer == 连通子图个数
2public void colorMap(char[][] grid, int i, int j) {
3 int h = grid.length;
4 int w = grid[0].length;
5 if (i<0 || j<0 || i>=h || j>=w || grid[i][j]==0)
6 return;
7
8 grid[i][j] = '0';
9 colorMap(grid, i-1, j);
10 colorMap(grid, i+1, j);
11 colorMap(grid, i, j-1);
12 colorMap(grid, i, j+1);
13}
14
15public int numIslands(char[][] grid) {
16 int ans = 0;
17 int row = grid.length;
18 int col = grid[0].length;
19 for (int i = 0; i < row; i++) {
20 for (int j = 0; j < col; j++) {
21 if (grid[i][j] == 1){
22 ans++;
23 colorMap(grid, i, j);
24 }
25 }
26 }
27 return ans;
28}
994. 腐烂的橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值
0代表空单元格; - 值
1代表新鲜橘子; - 值
2代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
⭐ 注意每个烂橘子同一时刻均开始
=> queue : [111], => [2222] => [3333]
BFS遍历
207. 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
- 例如,先修课程对
[0, 1]表示:想要学习课程0,你需要先完成课程1。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
方法一:
- 检查拓扑序列是否存在 => 入度表 + map<edge, List<neighborEdge»
方法二:
- DFS,状态0:未访问过,1:遍历到未收录(用来判断是否重复遍历到),2:遍历到且收入囊中;
208. 实现 Trie (前缀树)
字典树
1class TreeNode {
2 boolean isEnd;
3 TreeNode[] children;
4}
5
6children => [abcd..xyz]
7 => [abcd..xyz]
回溯
组合问题 – 使用树型结构分析所有可能情况
46. 全排列
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
1public void dfs(int[] nums, int depth, Deque<Integer> path, Set<Integer> used, List<List<Integer>> ans) {
2 if (depth == nums.length) {
3 ans.add(new ArrayList<>(path));
4 return;
5 }
6 for (int i = 0; i < nums.length; i++) {
7 if (used.contains(nums[i])) {
8 continue;
9 }
10 path.addLast(nums[i]);
11 used.add(nums[i]);
12 dfs(nums, depth+1, path, used, ans);
13 // 回溯
14 path.removeLast();
15 used.remove(nums[i]);
16 }
17}
78. 子集
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
从{}开始,遍历所有元素,分加入或者不加入
1public void recur(int i, int[] nums, Deque<Integer> path) {
2 if (i == nums.length){
3 ans.add(new ArrayList<>(path));
4 return;
5 }
6 path.addLast(nums[i]);
7 recur(i+1, nums, path);
8 path.removeLast();
9 recur(i+1, nums, path);
10}
17. 电话号码的字母组合
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
考点:多个集合的随机组合(每个集合使用一次)
1// 思路
2
3{a => {e => {? => {# =>
4b f ? #
5c} g} ?} #}
6
7// 递归出口 num.len == path.len
1public void recur(int i, String str, String path) {
2 if (i == str.length()-1) {
3 for (char c : num2Letter.get(str.charAt(i))) {
4 ans.add(path.concat(""+c));
5 }
6 return;
7 }
8 // 遍历集合
9 for (char c : num2Letter.get(str.charAt(i))) {
10 path = path.concat(""+c); // 追加下一个位置的集合
11 recur(i+1, str, path);
12 path = path.substring(0, path.length()-1); // 撤销追加的元素
13 }
14}
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
要去重复
1public void help(int[] candidates, int start, int target, List<Integer> path) {
2 if (target < 0)
3 return;
4 else if (target == 0) {
5 ans.add(new ArrayList<>(path));
6 return;
7 }
8 // 利用i控制范围,只看自己及右侧
9 for (int i = start; i < candidates.length; i++) {
10 int num = candidates[i];
11 path.add(num);
12 help(candidates, i, target - num, path);
13 path.remove(path.size()-1);
14 }
15}
22. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
1代表( 0代表)
构建选择二叉树
{} => {1} => {11} => {111}
=>{110} => {1101}
=> {1100} => {11001}…
再补充右括号
1public void help(int num_1, int num_0, int n, String str) {
2 if (num_1 > n || num_1 < num_0)
3 return;
4 if (num_1 == n) {
5 ans.add(str);
6 return;
7 }
8 help(num_1+1, num_0, n, str+"(");
9 help(num_1, num_0+1, n, str+")");
10}
131. 分割回文串
1注意: result.add(new ArrayList<>(path)); // 重新克隆一份,避免浅拷贝的对象地址
2
3path.addLast path.removeLast path.push path.pop
4 a 0 a 0 b 0 a 0
5 a 1 a 1 a 1 a 1
6 b 2 a 2
1private Deque<String> path = new ArrayDeque<>();
2 private List<List<String>> result = new ArrayList<>();
3
4 public List<List<String>> partition(String s) {
5 backtrack(s, 0);
6 return result;
7 }
8
9 public void backtrack(String s, int startIdx) {
10 if (startIdx >= s.length()) {
11 result.add(new ArrayList<>(path)); // 收集叶子节点的结果
12 }
13 for (int i = startIdx; i < s.length(); i++) {
14 if (!isValid(s.substring(startIdx, i+1)))
15 continue;
16 path.addLast(s.substring(startIdx, i+1));
17
18 backtrack(s, i+1);
19
20 path.removeLast();
21 }
22
23 }
24
25 private boolean isValid(String substring) {
26 int len = substring.length();
27 for (int i = 0; i < len/2; i++) {
28 if (substring.charAt(i) != substring.charAt(len-i-1))
29 return false;
30 }
31 return true;
32 }
79. 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
1public boolean exist(char[][] board, String word) {
2 int row = board.length;
3 int col = board[0].length;
4 for (int i = 0; i < row; i++) {
5 for (int j = 0; j < col; j++) {
6 if (ans==false&&board[i][j] == word.charAt(0)) {
7 board[i][j] = '#';
8 help(board, i, j, word.substring(1));
9 board[i][j] = word.charAt(0); // 恢复
10 }
11 }
12 }
13 return ans;
14}
15
16// 深度搜索 + 回溯
17private void help(char[][] board, int i, int j, String word) {
18 if (word.equals("")||word.length()==0){
19 ans = true;
20 return;
21 }
22 int row = board.length;
23 int col = board[0].length;
24 char target = word.charAt(0);
25 int[][] offsets = {{1,0},{-1,0},{0,1},{0,-1}};
26 for (int[] offset : offsets) {
27 int r = i + offset[0];
28 int c = j + offset[1];
29 if (r<0||c<0||r>=row||c>=col)
30 continue;
31 if (board[r][c] == target&&board[r][c]!='#'){
32 board[r][c] = '#';
33 help(board, r, c, word.substring(1));
34 board[r][c] = target; // 恢复
35 }
36 }
37}
51. N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
1{[????]}
2 ⬇️ check
3{[????]}
4...
1public static void backtracking(char[][] chessboard, int n, int row) {
2 if (row == n){
3 ans.add(convert(chessboard));
4 return;
5 }
6 for (int i = 0; i < n; i++) {
7 if (isValid(chessboard, row, i, n)) {
8 chessboard[row][i] = 'Q';
9 backtracking(chessboard, n, row+1);
10 chessboard[row][i] = '.';
11 // break;
12 }//else
13 }
14}
15
16private boolean isValid(char[][] chessboard, int r, int c, int n) {
17 // check column
18 for (int i = 0; i < r; i++) {
19 if (chessboard[i][c] == 'Q') {
20 return false;
21 }
22 }
23 // check 45 degree
24 for (int i=r-1,j=c-1; i>=0&&j>=0; i--,j--) {
25 if (chessboard[i][j] == 'Q') {
26 return false;
27 }
28 }
29 // check 135 degree
30 for (int i=r-1,j=c+1; i>=0&&j<n; i--,j++) {
31 if (chessboard[i][j] == 'Q') {
32 return false;
33 }
34 }
35 return true;
36}
37
38public List<List<String>> solveNQueens(int n) {
39 ans = new ArrayList<>();
40 char[][] chessboard = new char[n][n];
41 for (char[] chars : chessboard) {
42 Arrays.fill(chars, '.');
43 }
44 backtracking(chessboard, n, 0);
45
46 return ans;
47}
二分查找
153. 寻找旋转排序数组中的最小值
输入:nums = [3,4,5,1,2]
输出:1
1 l r
2[345 12] // 大 小 // 且每段均递增
3 m
41. m落在左段 => 往右找
52. m落在右端 => 往左字段找
63. 终止条件 => left-mid-right 有序
1public int findMin(int[] nums) {
2 int left = 0, right = nums.length-1;
3 while (left<=right) {
4 int mid = (left+right)/2;
5 if (nums[left]<=nums[mid] && nums[mid]<=nums[right]) {
6 return nums[left];
7 }else if(nums[left]<=nums[mid]) {
8 left = mid+1;
9 }else {
10 right = mid;
11 }
12 }
13 return 0;
14}
33. 搜索旋转排序数组
1 l r
2[345 12] // 大 小 // 且每段均递增
3 m
4
51. num[m] == target 终止条件
62. m落在左段上
7 => if(target < num[m]) => [left, m-1]
8 else => [m+1,right]
93. m落在右段上
10 => if(target < num[m]) => [left, m-1]
11 else => [m+1,right]
1public int search(int[] nums, int target) {
2 int left = 0, right = nums.length-1;
3 while (left <= right) {
4 int mid = (left + right) / 2;
5 if (nums[mid] == target) {
6 return mid;
7 }
8
9 if (nums[mid] >= nums[left]) {
10 if (nums[left]<=target && nums[mid]>target) {
11 right = mid-1;
12 }else {
13 left = mid+1;
14 }
15 }else {
16 if (nums[right]>=target && target>nums[mid]) {
17 left = mid+1;
18 }else {
19 right = mid-1;
20 }
21 }
22 }
23 return -1;
24}
34. 在排序数组中查找元素的第一个和最后一个位置
第一次查左边界 => 第二次查右边界
递增序列
1输入:nums = [5,7,7,8,8,10], target = 8
2输出:[3,4]
3
4注意边界包括情况
1public int[] searchRange(int[] nums, int target) {
2 if (nums.length == 0)
3 return new int[]{-1, -1};
4 int[] ans = new int[2];
5 Arrays.fill(ans, -1);
6 int a = searchFirstLoc(nums, target);
7 if (a != -1) {
8 ans[0] = a;
9 int b = searchSecondLoc(nums, target);
10 ans[1] = b;
11 }
12 return ans;
13}
14
15// 求右边界
16private int searchSecondLoc(int[] nums, int target) {
17 int left = 0;
18 int right = nums.length-1;
19 while (left<right) {
20 int mid = (left+right+1) / 2;
21 if (nums[mid] == target) {
22 // [mid, right]
23 left = mid;
24 }
25 else if (nums[mid] > target) {
26 // [left, mid-1]
27 right = mid-1;
28 }else {
29 // [mid+1, right]
30 left = mid+1;
31 }
32 }
33 return left;
34}
35
36// 计算左边界
37private int searchFirstLoc(int[] nums, int target) {
38 int left = 0;
39 int right = nums.length-1;
40 while (left<right) {
41 int mid = (left+right) / 2;
42 if (nums[mid] == target) {
43 // [left, mid]
44 right = mid;
45 }
46 else if (nums[mid] > target) {
47 // [left, mid-1]
48 right = mid-1;
49 }else {
50 // [mid+1, right]
51 left = mid+1;
52 }
53 }
54 if (nums[left]!=target)
55 return -1;
56 return left;
57}
74. 搜索二维矩阵
给你一个满足下述两条属性的 m x n 整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
坐标i,j => 转换为一维左边 i*col+j
1private int coordinate2Index(int i, int j, int col){
2 return i*col+j;
3}
4public boolean searchMatrix(int[][] matrix, int target) {
5 int row = matrix.length;
6 int col = matrix[0].length;
7 int left_i = 0, left_j = 0;
8 int right_i = row-1, right_j = col-1;
9 while (coordinate2Index(left_i, left_j, col)<=coordinate2Index(right_i,right_j,col)){
10 int mid_i = (coordinate2Index(left_i, left_j, col)+coordinate2Index(right_i,right_j,col))/col;
11 int mid_j = (coordinate2Index(left_i, left_j, col)+coordinate2Index(right_i,right_j,col))%col;
12 if (matrix[mid_i][mid_j] == target)
13 return true;
14 else if (matrix[mid_i][mid_j] > target) {
15 if (mid_j > 0) {
16 right_i = mid_i;
17 right_j = mid_j - 1;
18 }else {
19 right_i = mid_i-1;
20 right_j = col-1;
21 }
22 }else {
23 if (mid_j < col-1) {
24 left_i = mid_i;
25 left_j = mid_j+1;
26 }else {
27 left_i = mid_i+1;
28 left_j = 0;
29 }
30 }
31 }
32 return false;
33}
寻找两个正序数组的中位数
两个有序数组,寻找中位数 => 第K小的元素
删删删,每次删掉一部分[K/2个] => 找第K-K/2小的元素。
⭐分双数与偶数情况
1public double findMedianSortedArrays(int[] nums1, int[] nums2) {
2 int n = nums1.length;
3 int m = nums2.length;
4
5 if ((n+m) % 2 == 0){ // 偶数 => (left + right)/2
6 int a = getKth(nums1, 0, n-1, nums2, 0, m-1, (n+m)/2);
7 int b = getKth(nums1, 0, n-1, nums2, 0, m-1, (n+m)/2+1);
8 return (a+b)/2.0;
9 }else
10 return getKth(nums1, 0, n-1, nums2, 0, m-1, (n+m+1)/2);
11
12}
13
14private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
15 int len1 = end1 - start1 + 1;
16 int len2 = end2 - start2 + 1;
17
18 if (len1 > len2)
19 return getKth(nums2, start2, end2, nums1, start1, end1, k);
20
21 if (len1 == 0)
22 return nums2[start2 + k - 1];
23
24 if (k == 1)
25 return Math.min(nums1[start1], nums2[start2]);
26 // 忽略之前删除的 // 防止越界
27 int i = start1 + Math.min(len1, k / 2) - 1; // 下标从0开始
28 int j = start2 + Math.min(len2, k / 2) - 1; // 下标从0开始
29
30 if (nums1[i] > nums2[j]) { // 那么nums2中 包含j及其之前的元素,都不可能是第k小的元素,直接逻辑删除
31 return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
32 }
33 else {
34 return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
35 }
36}
逻辑:
- 分偶数与单数, 偶数的话,分别求 第K个和K+1个元素; 单数就求第K个元素就好
- 让nums1是元素数量小的那个
- 如果nums1为空,返回nums2中的第k小的元素
- 如果k等于1(递归入口),返回两个数组中,头部最小的元素。
栈
155 最小栈
思路: 1. 普通栈 2. 记录最小值的栈(元素复用)
Stack: 2431
minStack: 2221 (单调栈)
394 字符串解码
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
NumStack: 存储次数
StrStack: 存储字符
分"[" 和 “]“时机
739. 每日温度
下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
(单调栈)
index: [ 0, 1, 2, 3, 4, 5, 6, 7]
temp = [73, 74, 75, 71, 69, 72,76,73]
element:[74, 75, 76, 72,72, 76,-1,-1] 从前往后 (一起找老爸)
target :[ 1, 2, 6, 5, 5, 6, -1, -1]
output : [ 1, 1, 4, 2, 1, 1, 0, 0]
过程:
preElstack: [73] 73<74 => [74] 74<75 => [75,71,69] 71,69<72=> [75,72] 75,72<76 => [76] …
res:[74(1),75(2), 76(6), 72(5),72(5), 76(6), x, x]
从该点出发 => 向左找左边界 >=该点
从该点出发 => 向右找右边界 >=该点
S=wxh
堆
215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
1# 思路:
2维护一个最小堆
3然后每次比较,如比(之前最大的K个元素的)最小的大,则Poll堆顶,add该元素
347. 前 K 个高频元素
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
1# 思路:
21. 先统计词频
32. 词频排序 <= 只维护需要的k个最值元素即可, 最小堆<=
295. 数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]的中位数是3。 - 例如
arr = [2,3]的中位数是(2 + 3) / 2 = 2.5。
1# 思路:
2O(1)时间计算中位数,=> 固定其位置,O(1)查询 => 堆顶
3
4左堆(最大堆) : 右堆(最小堆)
5
6# 插入情况
71. 偶数时, 保持 中位数 => 左堆顶
8 # 判断插入位置, 插左边:左堆.add; 插右边: 右最小值=>左堆,左堆.add;
92. 奇数时(左堆比右堆多一个元素), 保持 中位数 => (左堆顶 + 右堆顶) /2
10 # 判断插入位置, 插左边:左最大值=>右堆,左堆.add; 插右边: 右堆.add;
贪心
55. 跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
思路:
1nums = [2,3,1,1,4]
2 1. | . .|
3 2. | . . .|
4
5# 查看从当前位置,是否能够突出重围(覆盖范围) max(cover, i+nums[i]) <= 尽可能的往远跳
25/3/18 第二次思考
45. 跳跃游戏 II
跳跃游戏问题的最小步数
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
1# 思路
2nums = [2,3,1,1,4]
3 | . .|
4 | . . .| √ <= 下一轮往这跳 # 每轮尽可能跳的远
5 | .| ×
6
7curRoundDist = 0;
8nextRoundDist = 0;
9count = 0;
10for(int i=0; i<nums.length; i++) {
11 nextRoundDist = max(nextRoundDist, i+nums[i])
12 if(i == curRoundDist){ # 此轮结束
13 curRoundDist = nextRoundDist
14 count += 1;
15 if(nextRoundDist >= nums.length-1)
16 break;
17 }
18}
763. 划分字母区间
示例 1:
输入:s = "[ababcbaca][defegde][hijhklij]"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
###每个字母最多出现在一个片段中。###
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
1# 思路, 最简单情况,左端点 => 最远右端点
2# 其中有元素进行扩展右边界 => 更新最远右端点 <= ###
3
4int[] farthestIdx = new int[26]
5for(int i=0; i<s.length; i++) :
6 char c = s.charAt(i);
7 farthestIdx[c - 'a'] = i
8
9left=0; right=0;
10for(int i=0; i<s.length; i++) :
11 char c = s.charAt(i);
12 right = max(right, farthestIdx[c-'a']) # 贪心处
13 if(right == i){
14 # save ans
15 left = right+1;
16 }
动态规划
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
1# 思路
2
3考虑当前处于第i处台阶,那么如何才能到达现在的i号台阶呢?
4依赖关系如下:
51. 从i-1阶 走一步
62. 从i-2阶 走两步
7
8=> dp[i]表示走到i号台阶的所有可能方法,i表示台阶
9
10递推关系:
11dp[i] = dp[i-1] + dp[i-2]
118. 杨辉三角
1# 思路
2
3preRow = [1, 2, 1]
4curRow = [1]
5for(i of preRow)
6 curRow.add(preRow[i] + preRow[i+1])
7curRow.add(1)
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
# 不能相邻的选择
1# 思路
2dp[i]表示包含第i号房间所能偷取的最大金额, i表示房间号
3
4# 考虑当前偷到第i号房间
5# 选择可能方案
61. 偷 => money[i] + dp[i-2]
72. 不偷 => dp[i-1]
8
9dp[i] = max(money[i] + dp[i-2], dp[i-1])
[322. 零钱兑换]
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
# 最小组合个数装满
1# dp[i]表示装满金额i需要的金币数量,i表示金额
2
3# 考虑当前 要装满的金额为i
4# 那么当前能够选择的方案如下:
51. dp[i-CoinA] + 1 # 跳跃
62. dp[i-CoinB] + 1
73. dp[i-CoinC] + 1
8
9dp[i] = min(1., 2., 3.)
279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
元素: [1, 4, 9, 16 …]
# 题目考点如上
1# dp[i]表示的是给与的整数i所需最少数量的完全平方数,i表示该整数。
2
3# 考虑当前的整数为i
4# 那么当前能够凑到该整数的方案如下:
51. dp[i-e1*e1]+1
61. dp[i-e2*e2]+1 # 最少步数跳跃,o.O
71. dp[i-e2*e2]+1
8
9dp[i] = min(1. 2. 3.)
139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
1# 思路
2
3# dp[i]表示长度为i的字串是否能够被凑出来,i表示该字串
4
5# 考虑当前的字串的能够选择凑出来的可能方案如下:
61. dp[i-len(word1)]==True && subString(i-len(word1),i) in wordDict
7...
⭐⭐⭐ 总体思路:概率当前位置的所有可能方案,建立线性的依赖关系。
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
1# 思路
2
3# 考虑当前元素 与先前 最长严格递增子序列 们 的关系。
4
5# dp[i] 表示,第0-i个元素的最长严格递增子序列长度, i表示该元素。 相对于以i结尾的最长严格递增子序列长度
6
7# 初始化全为1,只包含自身。
8if[nums[i] > nums[j]] # 递增
9dp[i] = max(dp[i], dp[j]+1)
10
11
12for(int i=1; i<nums.length; i++)
13 for(int j=0; j<i; j++)
14 if(nums[i] > nums[j])
15 dp[i] = max(dp[i], dp[j]+1) # 动态选择,跳跃
152. 乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
# 连续。元素相邻 ✔️!= 元素值相邻❌
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
1# 由于有正有负
2# 当前值若为负,与先前最大负数相乘 => -- => +
3
4# Min_dp[i] 表示 i号元素及其之前 最小的乘积和
5# Max_dp[i] 表示 i号元素及其之前 最大的乘积和
6
7Min_dp[i] = Min(nums[i], Min_dp[i-1]*nums[i], Max_dp[i-1]*nums[i]) # M??_dp[i-1]*nums[i] 保证连续
8Max_dp[i] = Max(nums[i], Min_dp[i-1]*nums[i], Max_dp[i-1]*nums[i]) # 统一正负
9
10result = Max(Max_dp[0,...,n])
0-1背包问题 (2维)
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
dp[i][j] 表示, 任取{0,i}物品,放至容量j的背包中的 总价值。
dp状态转移方程:
- 不放物品i: dp[i][j] = dp[i-1][j]
- 放物品i : dp[i][j] = dp[i-1][j-weight[i]] + value[i] # 从任意放置{0,…,i-1}的状态中进行迁移,要求其背包容量足够装下物品i
1for (int i = 1; i < n; i++) { // 物品
2 for (int j = 0; j <= bagweight; j++) { // 背包
3 if (j < weight[i]) { // 装不下 => 不装
4 dp[i][j] = dp[i - 1][j];
5 } else {
6 dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); // 装或者不装中价值最大
7 }
8 }
9}
遍历顺序=> dp[i][j] 依赖于 上一行 dp[i-1][0,…,j]
0-1背包问题 (1维)
1 j 0, 1, 2, 3, 4 # 背包容量
2dp[] = [0,15,15,15,15] # 当前价值, 初始化放置物品1
3 [0,15,15,|20,35] # 放置物品2 [✔️✔️] max(不放,放)
4 [0,15,15,20,|35] # 放置物品3 [❌]
5
6dp[j] = max(dp[j], dp[j-weight[i]]+value[i]) # 1. 不放置 2.放置
7
8
9
10# 遍历
11for(int i = 0; i < weight.size(); i++) { // 遍历物品(关键是重量),只使用一次
12 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 从大到小
13 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
14
15# 当背包容量 j 小于当前物品的重量 weight[i] 时,根本不可能放入该物品,因此不需要继续更新 dp[j]。我们应该跳过这些容量小于 weight[i] 的情况
分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
0-1背包问题: 和相等 => 堆积sum的一半作为目标
1# 1. 求和 sum => target == sum//2
2
3# dp[j] 表示j容量的背包的最大价值
4int[] dp = new int[target+1];
5# 背包容量==0-target , 目标装满target => dp[target] == target
6for(int i=0; i<nums.length; i++)
7 for(int j=target; j>=nums[i]; j--) # 背包容量小于当前物品,后续容量的背包没有必要再装了
8 dp[j] = max(dp[j], dp[j-nums[i]]+nums[i]) # 不装,装
9
10if(dp[target] == target)
11 return true
32 最长有效括号
1// 栈的例子
2-1 0 1 2 3 4 5
3 ( ( ) ( ) )
4 x |.| Step: 1
5 x |.....| Step: 2
6 x |.........| Step: 3
7
8-1 0 1 2 3 4 5
9 ( ) ( ( ) )
10 x |.|
11 x |.|
12 x |.........|
1public int longestValidParentheses(String s) {
2 Stack<Integer> stack = new Stack<>();
3 int maxLen = 0;
4 stack.push(-1);
5
6 for (int i = 0; i < s.length(); i++) {
7 char c = s.charAt(i);
8 if (c == '(') {
9 stack.push(i);
10 }else {
11 stack.pop(); // 这个可能是 相匹配的 "(" or 基准边界
12
13 if (!stack.isEmpty()) { // 相匹配的 "("
14 maxLen = Math.max(maxLen, i - stack.peek());
15 }else {
16 stack.push(i); // 更新基准边界
17 }
18 }
19 }
20 return maxLen;
21}
动态规划做法
dp[i] 子串长度为i的 字符串 的 最长有效括号
1# 条件转移方程:
2 1. if s[i] == ")"
3 if s[i-1] == "(" dp[i-2]+2
4 dp[i] = dp[i-2] + 2 [xxx]()
5 else == ")" dp[i-dp[i-1]-2]+dp[i-1]+2
6 dp[i] = dp[i-dp[i-1]-2]+dp[i-1]+2 [xxx] ( [xxx] )
7 idx i-dp[i-1]-1
8
9情况1: [xxx]() dp[i]=2+dp[i-2]
10情况2: [xxx]([xxx]) dp[i]=2+dp[i-1] + dp[i-2-dp[i-1]]
11 (的idx为i-dp[i-1]-1
1public int longestValidParentheses(String s) {
2 int[] dp = new int[s.length()+1];
3 Arrays.fill(dp, 0);
4 int maxLen = 0;
5 for (int i = 1; i < s.length(); i++) {
6 char c = s.charAt(i);
7 if (c == ')') {
8 if (s.charAt(i-1) == '(' && i-2 >= 0)
9 dp[i] = dp[i-2] + 2;
10 else if (i-dp[i-1]-1 >= 0 && s.charAt(i-dp[i-1]-1) == '('){
11 dp[i] = dp[i-1] + 2 + (i-2-dp[i-1]>=0 ? dp[i-2-dp[i-1]]:0);
12
13 }
14 if (dp[i] > maxLen)
15 maxLen = dp[i];
16 }
17 }
18 return maxLen;
19}
技巧题
只出现一次的数字
异或
1binary: 8 4 2 1
2eg. 4 2 2 1 1
3=> 0100 0010 0010 0001 0001
4java => ^ (XOR )
5
60100
70010 => 0110
8
90110
100010 => 0100
11
120100
130001 => 0101
14
150101
160001 => 0100
17
18# 思想:对偶同归于尽
多数元素
枚举当前可能的候选人 (摩尔投票)
1nums = [2,2,1,1,1,2,2]
2
3count = [1,2,1,0,1,0,1] # 拥举票数
4candidate=[2,2,2,2,1,1,2] # 当前最有可能的候选元素
颜色分类
荷兰国旗问题
[1,2,0,1,0,2] => [0,0,1,1,2,2] 元素分组
双指针法:
[0,…,0]P0
P0[1,…,1]P2
P2[2,…,2]
判断i当前的元素视角。进行交换
下一个排序
数字组合递增
# 1 树状结构⭐⭐⭐❗❗❗
1# 1,2,3
2 []
3 1 2 3
4 2 3 1 3 1 2
5 3 2 3 1 2 1
找[23541]下一个排序 [24135]
1 定位 1号元素 [从后往前遍历,非递增的第一个元素] => 3
[2]3[541]
2 [541]中找比3大的最小元素 => 4 && 交换
[2]4[531] => 逆序
3 => [2]4[135]
1 .2]
2 .3 4]
3 1 4 .5 1]
4 [...] .4 3]
5 .1 5]
287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
1idx: [0,1,2,3,4]
2input: [1,3,4,2,2] // 顺序表,数组作为链表
等价于找环的入口。
1public int findDuplicate(int[] nums) {
2 int fast = 0; int slow = 0;
3 while(true) {
4 fast = nums[nums[fast]];
5 slow = nums[slow];
6 if(fast == slow) {
7 int idx1 = 0;
8 int idx2 = fast;
9 while(idx1 != idx2) {
10 idx1 = nums[idx1];
11 idx2 = nums[idx2];
12 }
13 return idx1;
14 }
15 }
16}
原理:由于存在的重复的数字 target,因此 target 这个位置一定有起码两条指向它的边,因此整张图一定存在环,且我们要找到的 target 就是这个环的入口 ❗❗❗仅有一个重复。 重复位置 入度为2 => 环的入口位置
多维动态规划
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
dp[i][j]表示到该位置的可能的路径数,仅可以往右和下移动(这个限制,创建了转移方程的条件)
dp[i][j] = dp[i-1][j] + dp[i][j-1] \\ 仅从上面和左边到该位置
初始化
1 1 1 1
21
31
41
64. 最小路径和
给定一个包含非负整数的 *m* x *n* 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
dp[i][j]: 到该位置最小的数字和(最小的代价)
=Min(dp[i-1][j], dp[i][j-1]) + current_grid_value
初始化
1 s0 s1 s2 // 累加
2t0
3t1
4t2
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的 回文 子串。
dp[i][j]: 字串i-j是否为回文字串。
转移条件{
如果字串为 ?? 或者 ?x? 且首尾字符相等 => True,
如果字串为?xxx? 且首尾字符相等 且 dp[i+1][j-1]=True => True
}
初始化
1s = "caddab"
2
3 0 1 2 3 4 5
40 T
51 T
62 T
73 T
84 T
95 T
10
11T ca cad cadd cadda caddab
12 T ad add a[dd]a addab <=
13 T [dd] dda ddab <=
14 T da dab
15 T ab
16 T
17 ^
18 / 这就是为什么为[i+1][j-1]的原因,利用之前的状态
最长重复子数组
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
子数组为连续的部分
dp[i][j] 含义, (0 - i-1)和(0 - j-1)的数组的最长重复子数组
转移方程:
if nums1[i-1] == nums2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 // 在之前的基础上最长子数组+1
else:
0 // 因为已经不连续了
1 "" "1" "12" "123" "1232" "12321"
2 "" 0 0 0 0 0 0
3 "3" 0 0 0 1 0 0
4 "32" 0 0 1 0 [2] 0
5 "321" 0 1 0 0 0 [3]
6 "3214" 0 0 0 0 0 0
7"32147" 0 0 0 0 0 0
dp[i][j] = dp[i-1][j-1] + 1 // 在之前的基础上最长子数组+1⭐⭐⭐
1143. 最长公共子序列
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
子序列,相对位置保持不变就行,不要求连续
dp[i][j] 含义, (0 - i-1)和(0 - j-1)的数组的最长公共子序列
转移方程:
if nums1[i-1] == nums2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 // 在之前的基础上最长子数组+1
else:
dp[i][j] = Max(dp[i-1][j], dp[i][j-1]) // 不要求连续
初始化:
1 "" "a" "ab" "abc" "abcd" "abcde"
2 "" 0 0 0 0 0 0
3 "a" 0 1 1 1 1 1
4 "ac" 0 1 1 2 2 2
5"ace" 0 1 1 2 2 3
72. 编辑距离
只能执行 新增,删除,替换三个操作。
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
dp[i][j] 含义, 将(0 - i-1) 变为 (0 - j-1)的最小操作。
状态转移:
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1] // 什么都不需要做,忽略掉这个单词
else:
dp[i][j] = Min(Min( dp[i-1][j], dp[i][j-1] ), dp[i-1][j-1])
从上or右,新增一个元素,or左上角修改一个元素
递推方向:
1i-1,j-1 i-1,j
2 ↘️ ⬇️
3i,j-1 ➡️ i,j
4
5⬇️➡️: 新增一个元素 以匹配
6↘️: 修改一个元素,以匹配
初始化:
1 "" "h" "ho" "hor" "hors" "horse"
2 "" 0 1 2 3 4 5
3 "r" 1
4 "ro" 2
5"ros" 3