本节内容
977.有序数组的平方
209.长度最小的子数组
59.螺旋矩阵II
总结
977.有序数组的平方※ 题目建议 : 本题关键在于理解双指针思想
题目链接:https://leetcode.cn/problems/squares-of-a-sorted-array/ 文章讲解:https://programmercarl.com/0977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9.html 视频讲解: https://www.bilibili.com/video/BV1QB4y1D7ep
题目分析
双指针,从两个方向向内查找,数值从后向前生成。两个指针比较自己的绝对值(或者平方) 结束条件: 循环次数达到 N (或者 左指针 大于 右指针)
方案一 双指针法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int [] sortedSquares(int [] nums) { int [] result = new int [nums.length]; int left = 0 , right = nums.length -1 ; for (int i = right; i >= 0 ; i--) { if (nums[left] * nums[left] > nums[right] * nums[right]) { result[i] = nums[left] * nums[left]; left++; } else { result[i] = nums[right] * nums[right]; right--; } } return result; } }
结果 解答成功: 执行耗时:1 ms,击败了100.00% 的Java用户 内存消耗:44 MB,击败了14.91% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( n )
代码随想录 https://programmercarl.com/0977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9.html#%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95
方案一 其于 上述方案一致, 具体代码请看方案一。
209.长度最小的子数组※ 题目建议 : 本题关键在于理解滑动窗口,这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。 拓展题目可以先不做。
题目链接:https://leetcode.cn/problems/minimum-size-subarray-sum/ 文章讲解: https://programmercarl.com/0209.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.html 视频讲解:https://www.bilibili.com/video/BV1tZ4y1q7XE
https://www.yuanql.top/2023/04/04/02_leetcode/209.%20%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/
题目分析
思考一: 尝试使用双指针找去除短板的方式去找最小的长度,但是其可能会导致去除的时候失去最小的长度,即在向内移动的时候失去了最优解。
思考二:滑动窗口 :滑动窗口详解 使用滑动窗口可以从左向右进行遍历,这样就不会导致莫名失去最优的
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Solution { public int minSubArrayLen (int target, int [] nums) { int sum = 0 , left = 0 , right = 0 , length = nums.length, flag = 0 ; while (sum < target) { if (right == length) { return flag; } sum += nums[right++]; } flag = right - left; for (; right < length; right++) { sum += nums[right]; while (true ) { sum -= nums[left]; if (sum < target) { sum += nums[left]; break ; } left++; } if (flag > (right - left + 1 )) { flag = right - left + 1 ; } } if (flag == length) { while (true ) { sum -= nums[left]; if (sum < target) { sum += nums[left]; break ; } left++; } if (flag > (right - left)) { flag = right - left; } } return flag; } }
以前做过的方案 :https://www.yuanql.top/2023/04/04/02_leetcode/209.%20%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/#%E8%87%AA%E5%B7%B1%E7%BC%96%E5%86%99
相同的思想,经过一段时间后,实现的具体逻辑就不同了,直接自己思考出的方案,距离最优的方案还是有区别的。
结果 解答成功: 执行耗时:1 ms,击败了99.80% 的Java用户 内存消耗:52 MB,击败了66.20% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( 1 )
代码随想录 https://programmercarl.com/0209.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.html
方案一:滑动窗口
在本题中实现滑动窗口,主要确定如下三点:
窗口内是什么?
如何移动窗口的起始位置?
如何移动窗口的结束位置?
窗口内: 窗口内的所有数值相加大于 target; 起始位置: 窗口内的数据大于 target 的时候,尝试左移,判断还是否大于 target ,一直移动知道无法在向左移动; 结束位置:遍历整个数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int minSubArrayLen (int s, int [] nums) { int left = 0 ; int sum = 0 ; int result = Integer.MAX_VALUE; for (int right = 0 ; right < nums.length; right++) { sum += nums[right]; while (sum >= s) { result = Math.min(result, right - left + 1 ); sum -= nums[left++]; } } return result == Integer.MAX_VALUE ? 0 : result; } }
904. 水果成篮
官方题目链接: https://leetcode.cn/problems/fruit-into-baskets/
题目分析
滑动窗口,需要窗口之内的水果种类为2种,找出可以装下最多的水果个数等价于滑动窗口最大。
方案一 具体的思考逻辑可见代码的注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution { public int totalFruit (int [] fruits) { int left = 0 , right = 1 , temp = left, flag = -1 , result = 1 ; for (; right < fruits.length; right++) { if (fruits[right] != fruits[left]) { if (flag == -1 ) { flag = fruits[right]; temp = right; } else { if (fruits[right] == flag) { if (fruits[right] != fruits[temp]) { temp = right; } } else { left = temp; flag = fruits[right]; temp = right; } } } else { if (fruits[right] != fruits[temp]) { temp = right; } } if (result < (right - left + 1 )) { result = right - left + 1 ; } } return result; } }
结果 解答成功: 执行耗时:5 ms,击败了96.64% 的Java用户 内存消耗:54.5 MB,击败了6.61% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( 1 )
官方题解 - 滑动窗口加哈希表
使用 Hash表,比上文中一顿判断猛如虎 轻松的多,也更容易思考。
对于有多个种类需要进行区分的情况,可以考虑使用hash去做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution { public int totalFruit (int [] fruits) { int n = fruits.length; Map<Integer, Integer> cnt = new HashMap <Integer, Integer>(); int left = 0 , ans = 0 ; for (int right = 0 ; right < n; ++right) { cnt.put(fruits[right], cnt.getOrDefault(fruits[right], 0 ) + 1 ); while (cnt.size() > 2 ) { cnt.put(fruits[left], cnt.get(fruits[left]) - 1 ); if (cnt.get(fruits[left]) == 0 ) { cnt.remove(fruits[left]); } ++left; } ans = Math.max(ans, right - left + 1 ); } return ans; } } 作者:LeetCode-Solution 链接:https: 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
76. 最小覆盖子串
官方题目链接: https://leetcode.cn/problems/minimum-window-substring/
题目分析
方法一 :首先将字符串 t 中的所有数据放入到 HashMap,然后通过滑动窗口的方式去一步步减少HashMap中的数据,直到哈希表为空的时候,说明全部搞定,此时就记录一下其生成的字符串并对比。此方法时间复杂度较高。
方法二:在方法一的基础上进行优化,还是使用HaspMap存储字符串 t 的相关信息,滑动窗口的前进和缩小方式发生了改变。在方法一中,滑动窗口的缩小直接缩小到最左节点,此方法中拟采用左格逐步向右移动的方式来缩小滑动窗口。 设置一个标志位,用于记录HashMap中有多少个key对于的数值大于1,只要标志位flag大于1,则意味着目前滑动窗口中还未完全包含字符串 t 中的所有字符,在滑动窗口膨胀的过程中,允许HashMap在key对应的数值小于0,不再对key进行移除操作。 当标志位为0的时候,要对滑动窗口进行缩小,此时是判断HashMap在key对应的数值什么时候大于0,允许有一个数值大于0,即允许flag为1,但是不允许flag为2,当碰到flag将要变为2的时候,就要对滑动窗口进行扩大了。 在滑动窗口扩大和缩小的前夕,对滑动窗口内的字符串长度与结果中的字符串长度向对比,如果小于result的长度,则滑动窗口的数据覆盖result结果中的数据。
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class Solution { public String minWindow (String s, String t) { int s_length = s.length(), t_length = t.length(), left = 0 , right = 0 ; if (s_length < t_length) { return "" ; } if (s.equals(t)) { return s; } Boolean flag = false ; StringBuffer result = new StringBuffer (), resultTemp = new StringBuffer (); HashMap<Character, Integer> hashMap = new HashMap <Character, Integer>(); for (int i = 0 ; i < t_length; i++) { hashMap.put(t.charAt(i), hashMap.getOrDefault(t.charAt(i), 0 ) + 1 ); } HashMap<Character, Integer> clonMap = new HashMap <>(hashMap); for (; right < s_length; right++) { if (clonMap.containsKey(s.charAt(right))) { if (!flag) { flag = true ; left = right; } if (clonMap.get(s.charAt(right)) == 1 ) { clonMap.remove(s.charAt(right)); } else { clonMap.put(s.charAt(right), clonMap.get(s.charAt(right)) - 1 ); } if (clonMap.isEmpty()) { resultTemp.append(s.charAt(right)); if (result.length() > resultTemp.length() || result.length() == 0 ) { result = resultTemp; } resultTemp = new StringBuffer (); right = left; flag = false ; clonMap = new HashMap <>(hashMap); } } if (flag) { resultTemp.append(s.charAt(right)); } } return result.toString(); } }
结果 运行超时265 / 267 个通过测试用例
分析 时间复杂度: O( n * n ) <– 极端情况下
空间复杂度: O( n ) <– 极端情况下
方案二 在方案一的基础上进行优化,还是使用HaspMap存储字符串 t 的相关信息,滑动窗口的前进和缩小方式发生了改变。在方法一中,滑动窗口的缩小直接缩小到最左节点,此方法中拟采用左格逐步向右移动的方式来缩小滑动窗口。 设置一个标志位,用于记录HashMap中有多少个key对于的数值大于1,只要标志位flag大于1,则意味着目前滑动窗口中还未完全包含字符串 t 中的所有字符,在滑动窗口膨胀的过程中,允许HashMap在key对应的数值小于0,不再对key进行移除操作。 当标志位为0的时候,要对滑动窗口进行缩小,此时是判断HashMap在key对应的数值什么时候大于0,允许有一个数值大于0,即允许flag为1,但是不允许flag为2,当碰到flag将要变为2的时候,就要对滑动窗口进行扩大了。 在滑动窗口扩大和缩小的前夕,对滑动窗口内的字符串长度与结果中的字符串长度向对比,如果小于result的长度,则滑动窗口的数据覆盖result结果中的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 class Solution { public String minWindow (String s, String t) { if (s.length() < t.length()) { return "" ; } if (s.equals(t)){ return s; } HashMap<Character, Integer> tHashMap = new HashMap <Character, Integer>(); for (int i = 0 ; i < t.length(); i++) { tHashMap.put(t.charAt(i), tHashMap.getOrDefault(t.charAt(i), 0 ) + 1 ); } StringBuilder result = null , temp = new StringBuilder (); int left = 0 , right = 0 , tFlag = tHashMap.size(); for (; right < s.length(); right++) { if (tHashMap.containsKey(s.charAt(right))) { tHashMap.put(s.charAt(right), tHashMap.get(s.charAt(right)) - 1 ); if (tHashMap.get(s.charAt(right)) == 0 ) { tFlag--; } if (tFlag == 0 ) { temp.append(s.charAt(right)); if (result == null || result.length() > temp.length()) { result = new StringBuilder (temp); } while (tFlag == 0 || tFlag == 1 ) { if (tHashMap.containsKey(s.charAt(left))) { tHashMap.put(s.charAt(left), tHashMap.get(s.charAt(left)) + 1 ); if (tHashMap.get(s.charAt(left)) == 1 ) { tFlag++; if (tFlag == 1 && result.length() > temp.length()) { result = new StringBuilder (temp); } } else if (tHashMap.get(s.charAt(left)) == 2 ) { left++; tFlag++; if (temp.length() > 1 ) { temp.deleteCharAt(0 ); } else { temp.deleteCharAt(0 ); break ; } break ; } } left++; if (temp.length() > 1 ) { temp.deleteCharAt(0 ); } else { temp.deleteCharAt(0 ); break ; } } left--; temp.insert(0 , s.charAt(left)); tHashMap.put(s.charAt(left), tHashMap.get(s.charAt(left)) - 1 ); tFlag--; temp.deleteCharAt(temp.length() - 1 ); } } temp.append(s.charAt(right)); } return result == null ? "" : result.toString(); } }
结果 解答成功: 执行耗时:44 ms,击败了34.25% 的Java用户 内存消耗:44.3 MB,击败了5.03% 的Java用户
分析 时间复杂度: O( n + m )
空间复杂度: O( m + n ) <– 极端情况下
官方题解 https://leetcode.cn/problems/minimum-window-substring/solution/zui-xiao-fu-gai-zi-chuan-by-leetcode-solution/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution { Map<Character, Integer> ori = new HashMap <Character, Integer>(); Map<Character, Integer> cnt = new HashMap <Character, Integer>(); public String minWindow (String s, String t) { int tLen = t.length(); for (int i = 0 ; i < tLen; i++) { char c = t.charAt(i); ori.put(c, ori.getOrDefault(c, 0 ) + 1 ); } int l = 0 , r = -1 ; int len = Integer.MAX_VALUE, ansL = -1 , ansR = -1 ; int sLen = s.length(); while (r < sLen) { ++r; if (r < sLen && ori.containsKey(s.charAt(r))) { cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0 ) + 1 ); } while (check() && l <= r) { if (r - l + 1 < len) { len = r - l + 1 ; ansL = l; ansR = l + len; } if (ori.containsKey(s.charAt(l))) { cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0 ) - 1 ); } ++l; } } return ansL == -1 ? "" : s.substring(ansL, ansR); } public boolean check () { Iterator iter = ori.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Character key = (Character) entry.getKey(); Integer val = (Integer) entry.getValue(); if (cnt.getOrDefault(key, 0 ) < val) { return false ; } } return true ; } } 作者:LeetCode-Solution 链接:https: 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
59.螺旋矩阵II※ 题目建议 : 本题关键还是在转圈的逻辑,在二分搜索中提到的区间定义,在这里又用上了。
题目链接:https://leetcode.cn/problems/spiral-matrix-ii/ 文章讲解:https://programmercarl.com/0059.%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5II.html 视频讲解:https://www.bilibili.com/video/BV1SL4y1N7mV/
题目分析
思考一 : 定义参数: flage = 0, // 定义下一步加入数据的前进方向,0:向右 1:向下 2:向左 3:向上 up = 1, // 上节点的阈值 down = n - 1, // 下节点的阈值 left = 0, //左节点的阈值 right = n - 1, // 右节点的阈值 x = 0, // 当前所在节点的 x 轴坐标 y = 0; // 当前所在节点的 y 轴坐标
循环螺旋判断。
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class Solution { public int [][] generateMatrix(int n) { int [][] ints = new int [n][n]; int flage = 0 , up = 1 , down = n - 1 , left = 0 , right = n - 1 , x = 0 , y = 0 ; for (int i = 1 ; i <= n * n; i++) { ints[x][y] = i; switch (flage) { case 0 : if (y == right) { flage = 1 ; x++; right--; } else { y++; } break ; case 1 : if (x == down) { flage = 2 ; y--; down--; } else { x++; } break ; case 2 : if (y == left) { flage = 3 ; x--; left++; } else { y--; } break ; case 3 : if (x == up) { flage = 0 ; y++; up++; } else { x--; } break ; } } return ints; } }
曾经的做法:https://www.yuanql.top/2023/04/13/02_leetcode/59.%20%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5%20II/
结果 解答成功: 执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:39 MB,击败了99.52% 的Java用户
分析 时间复杂度: O( n * n )
空间复杂度: O( n * n )
代码随想录 https://programmercarl.com/0059.%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5II.html
方案一 这道题目可以说在面试中出现频率较高的题目,本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution { public int [][] generateMatrix(int n) { int loop = 0 ; int [][] res = new int [n][n]; int start = 0 ; int count = 1 ; int i, j; while (loop++ < n / 2 ) { for (j = start; j < n - loop; j++) { res[start][j] = count++; } for (i = start; i < n - loop; i++) { res[i][j] = count++; } for (; j >= loop; j--) { res[i][j] = count++; } for (; i >= loop; i--) { res[i][j] = count++; } start++; } if (n % 2 == 1 ) { res[start][start] = count; } return res; } }
54. 螺旋矩阵 #未完成
官方题目链接: https://leetcode.cn/problems/spiral-matrix/
题目分析
方案一
结果 分析 时间复杂度: O( )
空间复杂度: O( )
方案二
结果 分析 时间复杂度: O( )
空间复杂度: O( )
总结※ 题目建议 :希望大家 也做一个自己 对数组专题的总结
文章链接:https://programmercarl.com/%E6%95%B0%E7%BB%84%E6%80%BB%E7%BB%93%E7%AF%87.html
数组常用到的算法 二分法 二分法的使用条件: 见到有顺序的数组(有些无序的数组也可以通过排序变成有序的),就要想到是否可以使用二分法,都来碰碰瓷。但是当数组中有重复数据的时候,,二分查找返回的数据将是不唯一的,所以此时要慎用二分法。
双指针法 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
滑动窗口 其属于双指针法的变种。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。