本文总结了LeetCode热题中的子串相关题目,包括滑动窗口和前缀和等解法。重点介绍了三个典型问题:1)和为K的子数组,通过前缀和+哈希表统计满足条件的子数组个数;2)滑动窗口最大值,使用优先队列或单调队列高效获取每个窗口的最大值;3)最小覆盖子串,通过滑动窗口和哈希表寻找覆盖目标字符串的最小子串。这些题目考察了数组处理、滑动窗口优化等技巧,在笔试面试中频繁出现,具有重要参考价值。
本文最初发布于 CSDN ,现迁移至本站并做格式整理。内容保留原始观点与发布时间。
子串 10. 和为K的子数组 题目 : 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。 子数组是数组中元素的连续非空序列。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public: int subarraySum (vector <int >& nums, int k) { unordered_map <int , int > mp; int ans=0 ; int pre=0 ; mp[0 ]=1 ; for (int i=0 ; i<nums.size(); i++){ pre += nums[i]; if (mp.find(pre-k) != mp.end()) ans += mp[pre-k]; mp[pre]++; } return ans; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int subarraySum (int [] nums, int k) { HashMap <Integer, Integer> map = new HashMap <>(); map.put(0 , 1 ); int pre = 0 ; int ans = 0 ; for (int x : nums){ pre += x; if (map.containsKey(pre-k)){ ans += map.get(pre-k); } map.put(pre, map.getOrDefault(pre, 0 )+1 ); } return ans; } }
分析 :
两个节点j, i, p r e i pre_i prei用于记录含当前节点i之前的所有元素和, p r e j pre_j prej用于记录含当前节点j之前的所有元素和
若[j, ..., i]这一段的和为k,则满足 p r e i − p r e j − 1 = = k pre_i - pre_{j-1} == k prei−prej−1==k,即 p r e i − k = = p r e j − 1 pre_i - k== pre_{j-1} prei−k==prej−1
比较隐藏的一个点:因为数组中存在负数,这个 p r e j − 1 pre_{j-1} prej−1可能出现多次,也就是在不同的j处, p r e j − 1 pre_{j-1} prej−1这个数可能是相同的,表示从不同的起始点j到当前点的子数组和为k的不同情况
举个特殊例子:[1,-1,1,1], k=2 对于这个例子,当i=3的时候,可取的j=0,2,也就是nums[0-3],nums[2-3]满足题目要求总结
mp.find(pre-k) != mp.end()记住找不到是这样表示的
11. 滑动窗口最大值 hard 题目 : 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。
代码:优先队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public: vector <int > maxSlidingWindow (vector <int >& nums, int k) { priority_queue <pair <int , int >> q; for (int i=0 ; i<k; i++) q.emplace(nums[i], i); vector <int > ans; ans.push_back(q.top().first); for (int i=k; i<nums.size(); i++){ q.emplace(nums[i],i); while (q.top().second <= i-k) q.pop(); ans.push_back(q.top().first); } return ans; } };
分析 :
维护一个优先队列,存储元素和其对应的下标,元素利用优先队列堆的性质找出窗口中的最大值,下标用于判断当前队首(堆顶)的元素是否还在窗口之内
代码:单调队列
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 class Solution {public: vector <int > maxSlidingWindow (vector <int >& nums, int k) { deque <int > q; int n=nums.size(); vector <int > ans; for (int i=0 ; i<k; i++){ while (!q.empty() && nums[i]>=nums[q.back()]) q.pop_back(); q.push_back(i); } ans.push_back(nums[q.front()]); for (int i=k; i<n; i++){ while (!q.empty() && nums[i]>=nums[q.back()]) q.pop_back(); q.push_back(i); while (q.front()<= i-k) q.pop_front(); ans.push_back(nums[q.front()]); } return ans; } };
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 [] maxSlidingWindow (int [] nums, int k) { int n = nums.length; Deque <Integer> q = new ArrayDeque <>(); int [] ans = new int [n-k+1 ]; for (int i = 0 ; i<n; i++){ while (!q.isEmpty() && nums[i]>nums[q.getLast()]){ q.removeLast(); } q.addLast(i); int left = i-k+1 ; if (q.getFirst() < left){ q.removeFirst(); } if (left>=0 ){ ans[left] = nums[q.getFirst()]; } } return ans; } }
分析 :
一道可以联系生活实际的题目:窗口代表公司需要的这么些人,这些人站一队,年纪大的在前面,年纪小的在后面(年纪代表数组下标),秋招开始了,当新面试的人能力比队列中后面那几个年轻人能力还强的时候,后面的被裁,新面试的人入队,当前面的年纪大的超出年纪限制(不在窗口内)的时候,虽然你可能很强,但是对不起,也要强制离场
具体细节不再深究了,毕竟现实世界就是这样的
总结
vivo笔试、字节一面出现过的题,我们在优化算法的同时也正被资本优化着,但是无论如何,加油吧!
12. 最小覆盖子串 hard 题目 : 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。代码:滑动窗口
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 class Solution { bool is_covered (int cnts[], int cntt[]) { for (int i='a' ; i<='z' ; i++) if (cnts[i] < cntt[i]) return false ; for (int i='A' ; i<='Z' ; i++) if (cnts[i] < cntt[i]) return false ; return true ; } public: string minWindow (string s, string t) { int m=s.size(); int cnts[128 ]{}; int cntt[128 ]{}; for (char c : t) cntt[c]++; int ansl=-1 , ansr=m-1 ; int left=0 ; for (int right=0 ; right<m; right++){ cnts[s[right]]++; while (is_covered(cnts, cntt)){ if (ansr-ansl > right-left){ ansl=left; ansr=right; } cnts[s[left]]--; left++; } } return ansl==-1 ? "" : s.substr(ansl, ansr-ansl+1 ); } };
分析 :
两个cnt数组用于存储t子串每个字母出现的次数和当前子串每个字母出现的次数
右指针往后走为了使目标子串出现,左指针往右收缩为了获取当前子串下的最小长度
当获得了第一个可能的最小长度子串后,重复上述步骤,这个过程中根据子串长度更新ansl,ansr
代码:优化滑动窗口
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 class Solution {public: string minWindow (string s, string t) { int m=s.size(); int ansl=-1 , ansr=m; int cnt[128 ]{}; int less=0 ; for (char c:t){ if (cnt[c] ==0 ) less++; cnt[c]++; } int left=0 ; for (int right=0 ; right<m; right++){ char c=s[right]; cnt[c]--; if (cnt[c] == 0 ) less--; while (less == 0 ){ if (ansr-ansl>right-left){ ansl = left; ansr = right; } char x = s[left]; if (cnt[x] == 0 ) less++; cnt[x]++; left++; } } return ansl==-1 ? "" : s.substr(ansl, ansr-ansl+1 ); } };
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 class Solution { public String minWindow (String s, String t) { int m = s.length(); int ansl = -1 , ansr = m; int [] cnt = new int [128 ]; int less = 0 ; for (char c : t.toCharArray()) { if (cnt[c] == 0 ) { less++; } cnt[c]++; } int left = 0 ; for (int right = 0 ; right < m; right++) { char c = s.charAt(right); cnt[c]--; if (cnt[c] == 0 ) { less--; } while (less == 0 ) { if (ansr - ansl > right - left) { ansl = left; ansr = right; } char x = s.charAt(left); if (cnt[x] == 0 ) { less++; } cnt[x]++; left++; } } return ansl == -1 ? "" : s.substring(ansl, ansr + 1 ); } }
分析 :
优化cnt,初始化时和上面的cntt进行一样的操作,记录每个目标字母出现的次数;less初始用于记录字符串t中有多少个不同的字母,或者说是当前子串中还有多少个字母的数量不满足要求;具体而言,后续right每向右移动的过程中,如果移动进来的是t中的字母,则消化掉cnt对应位置的一个元素,当新来的x字母不属于t中,cnt[x]--就变成负的了,当新来的x字母属于t中,cnt[x]--可能变为0或者大于0(因为t中相同字母可能重复出现),当变成0的时候说明当前子串字母x的个数和目标串字母x的个数相等,此时less--,表示当前子串中还差less个才能满足要求
less==0后,根据条件压缩边界,右边移动完了现在收缩左边,当左侧字母x的cnt[x]==0,说明这个字母移动出去之后当前子串不满足了,也就是还差一个满足,遂less++并将cnt[x]++还原回去
总结
字节一面、深信服笔试中的题,上来就给hard,以后真的要坚持刷题
普通数组 13. 最大子数组和 中等 题目 : 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。代码:动态规划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public: int maxSubArray (vector <int >& nums) { int n=nums.size(); vector <int > dp (n) ; dp[0 ] = nums[0 ]; int ans=nums[0 ]; for (int i=1 ; i<n; i++){ dp[i] = max(dp[i-1 ]+nums[i], nums[i]); ans = max(ans, dp[i]); } return ans; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int maxSubArray (int [] nums) { int ans = nums[0 ]; int pre = 0 ; for (int x : nums){ pre = Math.max(pre+x, x); ans = Math.max(ans, pre); } return ans; } }
分析 :
求解动态规划最难的在于如何将子问题定义出来?题目要求的是最大子数组和,那么子数组如此之多,我们究竟选哪一个呢,或者说从什么特征的数组呢,或者说,我们应该如何选择子数组呢?
对了,首先要解决的就是怎么选数组,暴力的解法是遍历所有子数组,比方说现在数组有四个元素,我们就需要遍历4+3+2+1个子数组并求和获得最大值,但这样做似乎会超时,那怎么办?
这时候就需要分析问题本身的特点,给定的数组正负数都有,假如都是正数,那么和最大的子数组一定是整个数组,但是但是,数组中存在负数,如果当前nums[i]是负数,那么之前的任意一段以nums[i-1]结尾的子数组 加上nums[i]都不会更大,等等!!之前的任意一段以nums[i-1]结尾的子数组 ?到这里好像有点动态规划的感觉了,从上面的分析可以看出,我们好像是可以计算以每个元素结尾的子数组的最大长度,当计算下一个元素结尾的最大长度时只需要看下一个元素值的正负,正的则在上一个基础上加上当前值,负的则和上一个最大长度相等
我们可以用dp[i]表示以nums[i]结尾所有子数组的最大长度,dp[0]当然就是nums[0],以后的每次比较中dp[i] = max(dp[i-1]+nums[i], nums[i]),这里的max可以保证我们将之前的某一段出现的最大值保留下来
总结
之前上课的时候老师直接讲用dp[i]记录以nums[i]结尾子数组的最大长度,而没有讲明白为什么非得是以nums[i]结尾子数组,前面的刷题还是有点用处的,分析题目的特点,根据题目的一些规律,或者说找出题目可以让我们投机取巧的部分(下划线那一部分)是解题的关键
14. 合并区间 中等 题目 : 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public: vector <vector <int >> merge(vector <vector <int >>& intervals) { if (intervals.size()==0 ) return {}; sort(intervals.begin(), intervals.end()); vector <vector <int >> ans; for (int i=0 ; i<intervals.size(); i++){ int l=intervals[i][0 ], r=intervals[i][1 ]; if (!ans.size() || l>ans.back()[1 ]) ans.push_back({l, r}); else ans.back()[1 ] = max(r, ans.back()[1 ]); } return ans; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public int [][] merge (int [][] intervals) { Arrays.sort(intervals, (p, q) -> p[0 ]-q[0 ]); List <int []> ans = new ArrayList <>(); for (int [] p : intervals){ int m = ans.size(); if (m>0 && p[0 ] <= ans.get(m-1 )[1 ]){ ans.get(m-1 )[1 ] = Math.max(p[1 ], ans.get(m-1 )[1 ]); }else { ans.add(p); } } return ans.toArray(new int [ans.size()][]); } }
分析 :
每个子区间有序的情况下,只要左段的右端小于右段的左端就不能合并,这时直接将右段push进去就好
其余的情况都能合并,将左段的右端替换更新就好
总结
快手日常实习一面手撕,终于遇到了一道简单题
vector是back,push_back,队列、栈、优先队列优化构造对象的时候用emplace
15. 轮转数组 简单 题目 : 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。代码:数组翻转
1 2 3 4 5 6 7 8 9 class Solution {public: void rotate (vector <int >& nums, int k) { k %= nums.size(); reverse(nums.begin(), nums.end()); reverse(nums.begin(), nums.begin()+k); reverse(nums.begin()+k, nums.end()); } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public void rotate (int [] nums, int k) { int n = nums.length; k %= n; reverse(nums, 0 , n-1 ); reverse(nums, 0 , k-1 ); reverse(nums, k, n-1 ); } private void reverse (int [] nums, int i, int j) { while (i < j){ int t = nums[i]; nums[i++] = nums[j]; nums[j--] = t; } } }
分析 :
空间为 O ( n ) O(n) O(n)的方法是放到开辟的新数组里面再复制回去:newArr[(i + k) % n] = nums[i];
空间 O ( 1 ) O(1) O(1)数组翻转:用类似分治的方法先整体反转,再从k位置将原数组分成俩子数组分别反转;假设给定数组为[1, 2, 3, 4, 5], k=2 ——> [5, 4, | 3, 2, 1] ——> [4, 5, | 1, 2, 3]
总结
指定位置的时候是左闭右开左闭右开左闭右开,sort的也是
连着两天简单题,挺开心
刚发现2022年竟然做过这道题,欣喜也意外…
16. 除自身以外数组的乘积 简单 题目 : 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度内完成此题。代码:前缀和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public: vector <int > productExceptSelf (vector <int >& nums) { int n=nums.size(); vector <int > L (n) ; vector <int > R (n) ; vector <int > ans (n) ; L[0 ] = 1 ; R[n-1 ] = 1 ; for (int i=1 ; i<n; i++) L[i] = L[i-1 ]*nums[i-1 ]; for (int i=n-2 ; i>-1 ; i--) R[i] = R[i+1 ]*nums[i+1 ]; for (int i=0 ; i<n; i++) ans[i] = L[i]*R[i]; return ans; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int [] productExceptSelf (int [] nums) { int n = nums.length; int [] ans = new int [n]; ans[n-1 ] = 1 ; for (int i = n-2 ; i>=0 ; i--){ ans[i] = nums[i+1 ]*ans[i+1 ]; } int L = 1 ; for (int i = 0 ; i<n; i++){ ans[i] *= L; L *= nums[i]; } return ans; } }
分析 :
能上来想到的简单方法:获得数组内所有元素的乘积再除以nums[i],但是数组中出现0这一招就不管用了
类似上一题的分段,将问题拆解为左右来看,分别记录nums[i]左边和右边所有元素的乘积L[i],R[i],然后ans[i]=L[i]*R[i],初始化的时候需要注意位置0左边和位置n-1右边的乘积应该是1;时间空间都是 O ( n ) O(n) O(n)
空间 O ( 1 ) O(1) O(1)的做法:ans定义完以后可以利用起来记录单边L[i],因为ans[i]=L[i]*R[i] = ans[i]*R[i] = ans[i]*R;每个ans[i]在被覆盖以前已经被利用,每个位置的R又可以从后往前迭代获得。
总结
轮转数组和这道题都是需要找一个分解的位置,轮转数组分解的位置是k,这道题是每个nums[i]处,然后左右分别来看就好了
有点感觉了,加油加油
17. 缺失的第一个正数 hard 题目 : 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public: int firstMissingPositive (vector <int >& nums) { int n = nums.size(); for (int i = 0 ; i < n; ++i) { while (nums[i] > 0 && nums[i] < n && nums[i] != nums[nums[i] - 1 ]) { swap(nums[i], nums[nums[i] - 1 ]); } } for (int i = 0 ; i < n; ++i) { if (nums[i] != i + 1 ) return i + 1 ; } return n + 1 ; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int firstMissingPositive (int [] nums) { int n = nums.length; for (int i = 0 ; i<n; i++){ while (nums[i]>=1 && nums[i]<=n && nums[i]!=nums[nums[i]-1 ]){ int j = nums[i]-1 ; int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } } for (int i = 0 ; i<n; i++){ if (nums[i] != i+1 ){ return i+1 ; } } return n+1 ; }
分析 :
简单思路:因为找的是缺失的第一个正数,我们可以以此查看1,2,3…n在不在nums数组中,返回第一个不在的值,如果都在返回n+1
但是这样复杂度不满足要求,每查看一个数在不在数组中都要遍历一遍数组,我们能不能将数组中的元素映射到1,2,3…n的位置呢?映射之后的数组如果位置1的元素是1,说明1在数组中,位置2的元素不是2,说明2不在数组中
原地哈希:假设下标从1开始的话,当nums[i]不在索引之内,我们不进行处理,在索引之内,那就将nums[i]放到位置nums[nums[i]]上,为了避免覆盖,这里做交换即可
这样从前往后依次遍历,第一个位置i的元素值不是i的i,就是ans,每个元素位置都能对起来的话ans=n+1
总结
其实就是不同大小的萝卜放到不同大小的坑里,坑的数目是有限的,超出范围的萝卜我们不关心,范围内的萝卜放到对应坑大小的位置,最后从左到右看那个坑没放和坑大小对应的萝卜
矩阵 18. 矩阵置零 简单 题目 : 给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public: void setZeroes (vector <vector <int >>& matrix) { int m = matrix.size(); int n = matrix[0 ].size(); vector <int > mm (m) , nn (n) ; for (int i=0 ; i<m; i++) for (int j=0 ; j<n; j++) if (matrix[i][j]==0 ) mm[i] = nn[j] = 1 ; for (int i=0 ; i<m; i++) for (int j=0 ; j<n; j++) if (mm[i] || nn[j]) matrix[i][j]=0 ; } };
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 class Solution { public void setZeroes (int [][] matrix) { int m = matrix.length; int n = matrix[0 ].length; boolean flag1 = false ; boolean flag2 = false ; for (int i = 0 ; i<m; i++){ if (matrix[i][0 ] == 0 ){ flag1 = true ; } } for (int j = 0 ; j<n; j++){ if (matrix[0 ][j] == 0 ){ flag2 = true ; } } 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 ; } } } 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 ; } } } if (flag1){ for (int i = 0 ; i<m; i++){ matrix[i][0 ] = 0 ; } } if (flag2){ for (int j = 0 ; j<n; j++){ matrix[0 ][j] = 0 ; } } } }
分析 :
matrix[i][j]==0时,记录i, j说明i行j列的元素应该置零,再次遍历矩阵的时,查i, j有没有被记录过选择性清零,空间复杂度 O ( m + n ) O(m+n) O(m+n)
我想的是用set记录i, j,平均空间复杂度 O ( m + n 2 ) O(\frac{m+n}{2}) O(2m+n),嗯,也算进步?
空间 O ( 2 ) O(2) O(2):用矩阵的第一行第一列替代上面的标记数组,也就是将m-1 * n-1这个小矩阵有零的情况记录在第一行第一列,比方说matrix[i][j]==0时,反正所在行列是要被清零的,直接matrix[i][0] = matrix[0][j] = 0记录下来,根据记录的情况将小矩阵清零;只剩下第一行第一列没人管了,找两个变量记录这两部分单独处理就好了。 因为 O ( 1 ) = O ( 2 ) O(1)=O(2) O(1)=O(2),真 O ( 1 ) O(1) O(1)就不看了886~
总结
19 螺旋矩阵 简单 题目 : 给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public: vector <int > spiralOrder (vector <vector <int >>& matrix) { int m=matrix.size(); int n=matrix[0 ].size(); int l=0 , r=n-1 , t=0 , b=m-1 ; vector <int > ans; while (ans.size() != m*n){ for (int j=l; j<=r && ans.size()<m*n; j++) ans.push_back(matrix[t][j]); t++; for (int i=t; i<=b && ans.size()<m*n; i++) ans.push_back(matrix[i][r]); r--; for (int j=r; j>=l && ans.size()<m*n; j--) ans.push_back(matrix[b][j]); b--; for (int i=b; i>=t && ans.size()<m*n; i--) ans.push_back(matrix[i][l]); l++; } return ans; } };
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 class Solution { public List <Integer> spiralOrder (int [][] matrix) { int m = matrix.length; int n = matrix[0 ].length; int l = 0 ; int r = n-1 ; int t = 0 ; int b = m-1 ; List <Integer> ans = new ArrayList <>(); int total = m*n; while (ans.size() != total){ for (int j = l; j<=r&&ans.size()<total; j++){ ans.add(matrix[t][j]); } t++; for (int i = t; i<=b&&ans.size()<total; i++){ ans.add(matrix[i][r]); } r--; for (int j = r; j>=l&&ans.size()<total; j--){ ans.add(matrix[b][j]); } b--; for (int i = b; i>=t&&ans.size()<total; i--){ ans.add(matrix[i][l]); } l++; } return ans; } }
分析 :
压缩遍历:由于是顺时针往内海螺一样的遍历,首先想到的就是怎么遍历最外圈?
看起来像是:从左上角开始先遍历第一行,再从右上角往下遍历第最后一列,再从右下角往左遍历最后一行,再从左下角往上遍历第一列回到左上角,看起来实现了这些就能循环起来了; 遍历完第一行/列,这一行/列就可以看做是新的边界,那就更新记录四个边界(上t下b左l右r)防止越界访问,每个方向遍历的时候用对应的边界就ok
为了防止遍历完最后一个元素后还有for没执行造成越界,每个for里面都判断是否遍历完
总结
20 旋转图像 简单 题目 : 给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。代码1:观察法
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public: void rotate (vector <vector <int >>& matrix) { int size = matrix.size(); for (int i = 0 ; i < size; i++) for (int j = 0 ; j < i; j++) swap(matrix[i][j], matrix[j][i]); for (int i = 0 ; i < size; i++) for (int j = 0 ; j < size / 2 ; j++) swap(matrix[i][j], matrix[i][size - 1 - j]); } };
分析 :
代码2:推公式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public: void rotate (vector <vector <int >>& matrix) { int n=matrix.size(); for (int i=0 ; i<n/2 ; i++){ for (int j=0 ; j<(n+1 )/2 ; j++){ int temp = matrix[i][j]; matrix[i][j] = matrix[n-1 -j][n-1 -(n-1 -i)]; matrix[n-1 -j][n-1 -(n-1 -i)] = matrix[n-1 -i][n-1 -j]; matrix[n-1 -i][n-1 -j] = matrix[j][n-1 -i]; matrix[j][n-1 -i] = temp; } } } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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+1 )/2 ; j++){ int t = matrix[i][j]; matrix[i][j] = matrix[n-1 -j][n-1 -(n-1 -i)]; matrix[n-1 -j][n-1 -(n-1 -i)] = matrix[n-1 -i][n-1 -j]; matrix[n-1 -i][n-1 -j] = matrix[j][n-1 -i]; matrix[j][n-1 -i] = t; } } } }
分析 :
不难发现matrix[i][j]旋转完会落到matrix[j][n-1-i],由于是正方形旋转跳四次后必然回到起点成环,也就是matrix[i][j]有三个好朋友,他们四个元素顺时针走一步,原地算法的话用temp防覆盖
好朋友怎么找?给定模版matrix[j][n-1-i] = matrix[i][j]的朋友也很好找了就:
1 2 3 4 matrix[j][n-1 -i] = matrix[i][j] matrix[n-1 -i][n-1 -j] = matrix[j][n-1 -i]; matrix[n-1 -j][n-1 -(n-1 -i)] = matrix[n-1 -i][n-1 -j] matrix[n-1 -(n-1 -i)][n-1 -(n-1 -j)] = matrix[n-1 -j][n-1 -(n-1 -i)]
需要遍历那些元素?其实可以将矩阵四分块,每块包含上面等号右边的四个公式所代表的元素,n为偶数时,显然平均分,n为奇数时,矩阵最中间的元素不需要处理,画个3x3的找一下感觉吧~
总结
评论区说temu社招一面要求用代码2的方式实现,上午脑子晕乎乎的,一点不想看方法2,晚上脑子清醒了发现其实不难
21. 搜索二维矩阵 II 题目: 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
代码: Z 字形查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public: bool searchMatrix (vector <vector <int >>& matrix, int target) { int m=matrix.size(); int n=matrix[0 ].size(); int x=0 , y=n-1 ; while (x<m && y>-1 ){ if (matrix[x][y]==target) return true ; else if (matrix[x][y]>target) y--; else x++; } return false ; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public boolean searchMatrix (int [][] matrix, int target) { int m = matrix.length; int n = matrix[0 ].length; int i = 0 ; int j = n-1 ; while (i<m && j>=0 ){ if (matrix[i][j] == target){ return true ; } else if (matrix[i][j] < target){ i++; } else { j--; } } return false ; } }
分析:
从右上角开始查找,每次向左或者向下移动:
当matrix[x][y] > target,因为升序,这一列的当前位置及下面都要大于目标值,这一列跳过,y--
当matrix[x][y] < target,说明这一行的当前位置及左边都要小于目标值,遍历下一行x++
总结:
难在不知道要从右上角开始找,左上右下是最大最小,往哪个方向走都是相同的单调性
举一反一:试了一下左下角也可以作为起点