06.02第 013 ~ 025 题(第 05 ~ 08 天)
分析
把数组看成左右两部分,左边部分一定比右半部分值大。
- 如果
nums[mid] == target
,直接返回下标。
- 如果
nums[mid] >= nums[left]
,说明mid在左半部分,此时:
nums[mid] > target && target >= nums[left]
,target在mid左侧,移动right = mid - 1
。
nums[mid] < target
,则target在mid右侧,left = mid + 1
。
target < nums[left]
,target在右半部分,left = mid + 1
。
- 如果
nums[mid] < nums[left]
,说明mid在右半部分,此时:
nums[mid] < target && taret < nums[right]
,则target在mid右侧,left = mid + 1
。
nums[mid] > target
,则target在右半部分,且在mid左侧,right = mid - 1
。
target > nums[right]
,target在左半部分,right = mid-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
| class Solution { public: int search(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; while(left <= right){ int mid = (left + right) / 2; if(target == nums[mid]){ return mid; } if(nums[mid] >= nums[left]){ if(nums[mid] > target && target >= nums[left]){ right = mid - 1; } else{ left = mid + 1; } } else{ if(nums[mid] < target && target <= nums[right]){ left = mid + 1; } else{ right = mid - 1; } } } return -1; } };
|
分析
使用两个指针left和right分别指向数组的第一个元素和最后一个元素。取中间值mid,如果nums[mid] < nums[mid + 1]
,则峰值在右侧,left = mid + 1
;否则峰值在左侧,right = mid
。当left == right
时跳出循环,返回结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Solution { public: int findPeakElement(vector<int>& nums) { int left = 0, right = nums.size() - 1; while(left < right){ int mid = (left + right) / 2; if(nums[mid] < nums[mid+1]){ left = mid + 1; } else{ right = mid; } } return left; } };
|
分析
中位数把数组分割成了左右两部分,并且左右两部分元素个数相等。如果两个数组元素个数为偶数,单侧元素个数为$(n1+n2) / 2$个;否则是$(n1+n2)/2 + 1$个,可以总结为单侧元素个数: $\lfloor \frac{(n1 + n2 + 1)}{2} \rfloor$ 。现在的问题就变为了:如何在两个有序数组中找到前 k 小的元素位置?
- 让left指向nums1的首个元素,right指向最后一个元素,m1为中间元素。
- 则
m2 = k - m1
,然后判断m1和m2位置上元素的关系:
nums1[m1] < nums2[m2]
,说明最多有m1 + m2 - 1 = k - 1
个元素比nums[m1]小,所以左侧的m1个元素都不可能是第k个元素,因此可以将left右移,即left = m1 + 1
。
- 否则,说明m1取值过大,
right = m1
。
- 当
left == right
,循环结束,找到正确的m1和m2.
- 分奇偶情况讨论。
- 如果
m1 <= 0
,说明整个nums1都比nums2大,那第k个元素一定在m2中;反之亦然。
- 如果
m1 >= n1
,则说明整个nums1都小于第k个元素,即第k个元素在m2中;反之亦然。
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: double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) { int n1 = nums1.size(); int n2 = nums2.size(); if (n1 > n2) { return findMedianSortedArrays(nums2, nums1); }
int k = (n1 + n2 + 1) / 2; int left = 0; int right = n1; while (left <= right) { int m1 = (left + right) / 2; int m2 = k - m1;
if (m1 < n1 && m2 > 0 && nums1[m1] < nums2[m2 - 1]) { left = m1 + 1; } else if (m1 > 0 && m2 < n2 && nums1[m1 - 1] > nums2[m2]) { right = m1 - 1; } else { int c1; if (m1 == 0) { c1 = nums2[m2 - 1]; } else if (m2 == 0) { c1 = nums1[m1 - 1]; } else { c1 = max(nums1[m1 - 1], nums2[m2 - 1]); }
if ((n1 + n2) % 2 == 1) { return c1; }
int c2; if (m1 == n1) { c2 = nums2[m2]; } else if (m2 == n2) { c2 = nums1[m1]; } else { c2 = min(nums1[m1], nums2[m2]); }
return (c1 + c2) / 2.0; } }
return 0.0; } };
|
重写的代码怎么也通不过,只能再看之前写的代码。
- 时间复杂度:时间复杂度:O(log min(m,n))
- 空间复杂度:O(1)
分析
z字形查找方法,从矩阵右上角进行搜索:
matrix[i][j] == target
,搜索完成。
matrix[i][j] > target
,j--
。
matrix[i][j] < target
,i++
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public: bool searchMatrix(vector<vector<int>>& matrix, int target) { int m = matrix.size(); int n = matrix[0].size(); int i = 0, j = n - 1; while(i < m && j >= 0){ if(matrix[i][j] == target){ return true; } else if(matrix[i][j] > target){ j--; } else{ i++; } } return false; } };
|
- 时间复杂度:O(m + n),操作过程中,要么i+1,要么j-1,i最多加m次,j最多减n次。
- 空间复杂度:O(1)
分析
可以从0~x的范围,用二分法进行查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: int mySqrt(int x) { int left = 0; int right = x; int res = -1; while(left <= right){ int mid = (left + right) / 2; if((long long)mid * (long long)mid <= x){ res = mid; left = mid + 1; } else{ right = mid - 1; } } return res; } };
|
分析
使用两个指针,slow指向指向处理好的非0数字的尾部,fast指向当前待处理元素。不断向右移动fast,每次移动到非零数,则将左右指针对应的数交换,交换同时将slow右移。最后,slow指针左侧均为处理好的非零数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Solution { public: void moveZeroes(vector<int>& nums) { int slow = 0, fast = 0; while(fast < nums.size()){ if(nums[fast] != 0){ swap(nums[slow], nums[fast]); slow++; } fast++; } } };
|
分析
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
| class Solution { public: string addStrings(string num1, string num2) { int n1 = num1.length(); int n2 = num2.length(); if(n1 < n2){ return addStrings(num2, num1); } string res; int carry = 0; int i = n1-1, j = n2-1; while(i >= 0 && j >= 0){ int t1 = num1[i--] - '0'; int t2 = num2[j--] - '0'; res = to_string((t1 + t2 + carry) % 10 ) + res; carry = (t1 + t2 + carry) / 10; } while(i >= 0){ int t = num1[i--] - '0'; res = to_string((t + carry) % 10) + res; carry = (t + carry) / 10; } if(carry == 1){ res = '1' + res; } return res; } };
|
- 时间复杂度:O(max(len1, len2))
- 空间复杂度:O(1)
优先队列
初始时将前k个元素加入优先队列的二叉堆中。存入优先队列的是数组值与索引构成的元组。优先队列将数组值作为优先级。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。可以将其永久地从优先队列中移除。不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { int n = nums.size(); priority_queue<pair<int, int>> q; for(int i = 0; i < k; i++){ q.push({nums[i], i}); } vector<int> res; res.push_back(q.top().first); for(int i = k; i < n; i++){ q.push({nums[i], i}); while(q.top().second <= i-k){ q.pop(); } res.push_back(q.top().first); } return res; } };
|
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
单调栈
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
| class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { int n = nums.size(); deque<int> q; for(int i = 0; i < k; i++){ while(!q.empty() && nums[i] >= nums[q.back()]){ q.pop_back(); } q.push_back(i); } vector<int> res = {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(); } res.push_back(nums[q.front()]); } return res; } };
|
- 时间复杂度:O(n),每一个下标恰好被放入队列一次,并且最多被弹出队列一次。
- 空间复杂度:O(k),因此「不断从队首弹出元素」保证了队列中最多不会有超过k+1个元素。
分块 + 预处理
将数组所有元素每k个分为一组,最后一组中元素的数量可能会不足 k 个。如果我们希望求出 nums[i] 到 nums[i+k−1] 的最大值,就会有两种情况:
- 如果i是k的倍数,那么此区间恰好是一个分组,只需要预处理出每个分组的最大值即可;
- 如果i不是k的倍数,那么存在一个j,
i<j≤i+k−1
,那么 nums[i] 到 nums[j−1] 就是第一个分组的后缀,nums[j] 到 nums[i+k−1] 就是第二个分组的前缀。如果我们能够预处理出每个分组中的前缀最大值以及后缀最大值,同样可以在 O(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
| class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { int n = nums.size(); vector<int> prefixMax(n), suffixMax(n); for(int i = 0; i < n; i++){ if(i % k == 0){ prefixMax[i] = nums[i]; } else{ prefixMax[i] = max(prefixMax[i-1], nums[i]); } } for(int i = n-1; i >= 0; i--){ if(i == n-1 || (i + 1) % k == 0){ suffixMax[i] = nums[i]; } else{ suffixMax[i] = max(suffixMax[i+1], nums[i]); } } vector<int> res; for(int i = 0; i <= n-k; i++){ res.push_back(max(suffixMax[i], prefixMax[i+k-1])); } return res; } };
|
分析
双指针,维护一个left和一个right,用哈希表记录两个指针的窗口内字符的个数,用一个变量维护不含重复字符的最长子串长度。
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
| class Solution { public: int lengthOfLongestSubstring(string s) { int n = s.length(); int left = 0, right = 0; unordered_map<char, int> hash; int res = 0; while(right < n){ if(hash.find(s[right]) == hash.end()){ hash[s[right]] = 1; } else{ hash[s[right]]++; } while(hash[s[right]] > 1){ hash[s[left]]--; left++; } res = max(res, right - left + 1); right++; } return res; } };
|
分析
用一个变量 less 维护目前子串中有 less 种字母的出现次数小于 t 中字母的出现次数。
- 初始化左右指针,
ansLeft = -1, ansRight = m
,m为s子串的长度。
- 用哈希表或数组记录t中的字符情况,由于也要记录s中的情况,因此可以用一个数组
cnt[] = cntT[] - cntS
来代替,节省空间。
- 初始化less为t中不同字母的个数。
- 枚举右指针,即将右指针所指字符移入窗口内,
cnt[s[right]]--
,如果此时cnt[s[right]] == 0
,说明在当前字符移入窗口后该字符的数量等于t中的数量,因此less可以减1.
- 当less=0时,即s涵盖t,可以不断移动左指针,直到s又无法涵盖t。如果left所指字符在移出窗口前的cnt=0,那么在移出后,s无法涵盖t,所以less要+1.移出字符即
cnt[s[left++]]++
。
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: string minWindow(string s, string t) { int m = s.length(); int ansLeft = -1, ansRight = m; int less = 0; int cnt[123]; 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(right - left < ansRight - ansLeft){ ansLeft = left; ansRight = right; } char x = s[left]; if(cnt[x] == 0){ less++; } cnt[x]++; left++; } } return ansLeft < 0 ? "" : s.substr(ansLeft, ansRight - ansLeft + 1); } };
|
- 时间复杂度:O(m+n+∣Σ∣)
- 空间复杂度:O(∣Σ∣)
分析
- 划分阶段:按照子数组结尾位置进行阶段划分。
- 定义状态:dp[i][j]表示nums1前i个数字和nums2的前j个数字的最长公共子数组长度。
- 状态转移方程:
- 如果nums1[i-1] == nums2[j-1],那么dp[i][j] = dp[i-1][j-1]+1;
- 否则,dp[i][j] = 0。
- 初始条件:dp[0][j] = dp[i][0] = 0
- dp[i][j]中的最大值,可以在循环中用一个变量保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public: int findLength(vector<int>& nums1, vector<int>& nums2) { int m = nums1.size(); int n = nums2.size(); int res = 0; vector<vector<int>> dp(m+1, vector<int>(n+1)); for(int i = 1; i <= m; i++){ for(int j = 1; j <= n; j++){ if(nums1[i-1] == nums2[j-1]){ dp[i][j] = dp[i-1][j-1] + 1; res = max(res, dp[i][j]); } } } return res; } };
|
分析
用两个指针,pre和cur,当cur->val == pre->val
时,删除当前节点,即pre->next = cur->next
。
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: ListNode* deleteDuplicates(ListNode* head) { if(!head || !head->next){ return head; } ListNode* pre = head; ListNode* cur = pre->next; while(cur){ if(pre->val == cur->val){ pre->next = cur->next; } else{ pre = cur; } cur = cur->next; } return head; } };
|
或者只用一个指针:
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
|
class Solution { public: ListNode* deleteDuplicates(ListNode* head) { if(!head){ return head; } ListNode* cur = head; while(cur->next){ if(cur->val == cur->next->val){ cur->next = cur->next->next; } else{ cur = cur->next; } } return head; } };
|
分析
与上一题的不同之处是,上一题会保留重复元素中的一个,而这道题要把重复的元素全部删除。在循环中用一个循环,把所有重复的节点删除,然后删除第一个节点,但是可能会把头节点删除,所以加一个dummy。
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
|
class Solution { public: ListNode* deleteDuplicates(ListNode* head) { if(!head){ return head; } ListNode* dummy = new ListNode(0); dummy->next = head; ListNode* cur = dummy; while(cur->next && cur->next->next){ if(cur->next->val == cur->next->next->val){ int x = cur->next->val; while(cur->next && cur->next->val == x){ cur->next = cur->next->next; } } else{ cur = cur->next; } } return dummy->next; } };
|
总结
第二部分的题目主要涉及二分查找、滑动窗口、双指针等,注意二分查找的变形题目,主要是分析每次计算mid后,如何更新左右指针。滑动窗口的关键在于更新右指针后的操作以及如何更新左指针。在遍历链表时,要注意快慢指针所指向的节点,在有可能更改头节点的时候,要申请一个哑节点。