面试专题训练之“双指针”
一、需要思考的问题包括以下几点:
双指针是什么,什么时候需要用到双指针
通用的模板是什么
实现过程中需要注意的细节有哪些
常见的双指针题型有哪些
二、模板整理
三、专题训练
1 class Solution { 2 public: 3 void moveZeroes(vector<int>& nums) { 4 int n = nums.size(); 5 int j = 0; 6 for (int i = 0; i < n; i++) { 7 if (nums[i] != 0) nums[j++] = nums[i]; 8 } 9 while (j < n) { 10 nums[j++] = 0; 11 } 12 } 13 };
题意:给定一个数组,把 0 移动到末尾的位置,剩下非 0 的数的相对位置保持不变。
题解:相当于有两个指针,都是从头开始。一个指向为 0 的值,一个指向非 0 的值。然后进行交换。
class Solution { public: int numberOfSubarrays(vector<int>& nums, int k) { vector<int> ind; int n = nums.size(), ans = 0; ind.push_back(-1); for (int i = 0; i < n; i++) { if (nums[i] % 2 == 1) ind.push_back(i); } ind.push_back(n); int m = ind.size(); for (int i = 1; i+k-1 < m-1; i++) { int l = ind[i]; int r = ind[i+k-1]; int leftGap = l - ind[i-1]; int rightGap = ind[i+k] - r; ans += leftGap * rightGap; } return ans; } };
1 class Solution { 2 public: 3 int numberOfSubarrays(vector<int>& nums, int k) { 4 return atMost(nums, k) - atMost(nums, k-1); 5 } 6 7 int atMost(vector<int>& nums, int k) { 8 int ans = 0, l = 0, n = nums.size(); 9 for (int r = 0; r < n; r++) { 10 k -= (nums[r] & 1); 11 while (k < 0) { 12 k += (nums[l++] & 1); 13 } 14 ans += (r - l + 1); 15 } 16 return ans; 17 } 18 };
题意:给定一个长度为 n 的数组,求有多少个子数组满足其中包含 k 个奇数。
题解:
1:将所有奇数的下标保存起来,目的在于能够快速找到恰好包含 k 个奇数的子数组(/窗口),然后将该窗口向左右延申,但是不能破坏之前所满足的条件,在延申的过程中的窗口都是答案。
2:添加函数 atMost 表示对于给定的数组,最多包含 k 个奇数的子数组有多少。这样原问题的答案就变成了 atMost(k) - atMost(k-1)。在统计子数组个数时,设置两个指针,右指针一直往右延申,每次延申一格,然后看当前的子数组 [l, r] 是否满足条件,如果不满足条件,则右移左指针直至符合条件,然后更新答案。
1 class Solution { 2 public: 3 vector<int> intersect(vector<int>& nums1, vector<int>& nums2) { 4 unordered_map<int, int> mp; 5 for (int num : nums1) mp[num]++; 6 vector<int> ans; 7 int n = nums2.size(); 8 for (int i = 0; i < n; i++) { 9 int num = nums2[i]; 10 if (mp[num] > 0) { 11 ans.push_back(num); 12 mp[num]--; 13 } 14 } 15 return ans; 16 } 17 };
题意:给定两个数组,求两个数组的交集
题解:
方法1:用 unordered_map 存一个数组的数据,然后遍历另外一个数组去判断 map 中是否存在那个数。
方法2:先对两个数组进行排序,然后对于每个数组设置指针指向头部,比较并后移。
1 class Solution { 2 public: 3 int threeSumClosest(vector<int>& nums, int target) { 4 sort(nums.begin(), nums.end()); 5 int n = nums.size(); 6 int ans = nums[0] + nums[1] + nums[2]; 7 for (int i = 0; i < n-2; i++) { 8 int j = i+1, k = n-1; 9 while (j < k) { 10 int sum = nums[i] + nums[j] + nums[k]; 11 if (abs(target-sum) < abs(target-ans)) ans = sum; 12 if (sum < target) j++; 13 else if (sum == target) return sum; 14 else k--; 15 } 16 } 17 return ans; 18 } 19 };
题意:给定一个数组 nums,从中取出三个数,使得取出的三个数的和最接近给定的 target 值,问该值是多少。
题解:“三指针”,用 for i = [0 ~ n-2] 先确定第一个指针的位置,然后剩下两个指针,分别设定为 l = i+1, r = n-1。在根据其nums[i]+nums[l]+nums[r] 的结果更新答案。
1 class Solution { 2 public: 3 void sortColors(vector<int>& nums) { 4 int n = nums.size(); 5 int one, two, three; 6 one = two = three = 0; 7 for (int i = 0; i < n; i++) { 8 if (nums[i] == 0) { 9 nums[three++] = 2; 10 nums[two++] = 1; 11 nums[one++] = 0; 12 } else if (nums[i] == 1) { 13 nums[three++] = 2; 14 nums[two++] = 1; 15 } else { 16 nums[three++] = 2; 17 } 18 } 19 } 20 };
1 class Solution { 2 public: 3 void sortColors(vector<int>& nums) { 4 int n = nums.size(); 5 int i, j, k; 6 i = j = 0, k = n - 1; 7 while (j <= k) { 8 if (nums[j] == 0) swap(nums[i++], nums[j++]); 9 else if (nums[j] == 1) j++; 10 else swap(nums[j], nums[k--]); 11 } 12 } 13 };
题意:给定一个只包含数字 1 2 3 的数组 nums,只遍历一遍实现原地更新。
题解:
1.记值为 0 的数字个数为 a,值为 1 的数字个数为 b,值为 2 的数字个数为 c. 这样可以保证 [0, a) 的位置的值全为 0,[a, a+b) 的位置的值全为 1,[a+b, a+b+c) 的位置的值全为 2。我们用变量 one 代表值 <= 0 的数的个数(/位置),用变量 two 代表值 <= 1 的数的个数,用变量 three 代表值 <= 2 的数的个数。这样每次访问到 nums[i] == 0 时,那么我们需要按照 three -> two -> one 的顺序依次更新各个值。 每次访问到 nums[i] == 1 时,那么我们需要按照 three -> two 的顺序依次更新各个值。 每次访问到 nums[i] == 2 时,那么我们需要按照 three 的顺序依次更新各个值。 这样可以保证先前暂时被写到前面位置的大值被覆盖。
2.“三指针”,用 i 代表值为 0 的数的位置,j 代表值为 1 的数的位置,k 代表值为 2 的数的位置。初始时 i = j = 0, k = n-1. 在整个过程中移动 j 的值,遇到 0,就交换 nums[i++] 和 nums[j++] 确保值 0 一定可以放到前面。遇到 1,则 j++。遇到 2,就交换 nums[j] 和 nums[k--],确保值 2 一定放在最后。即 0 是从开头往后增长,2 是从最后往前增长,而 1 是根据 0 和 2 的位置调整变动的。
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode { 4 * int val; 5 * ListNode *next; 6 * ListNode(int x) : val(x), next(NULL) {} 7 * }; 8 */ 9 class Solution { 10 public: 11 ListNode* removeNthFromEnd(ListNode* head, int n) { 12 if (n == 0) return head->next; 13 ListNode* fast = head; 14 ListNode* slow = head; 15 for (int i = 0; i < n; i++) fast = fast->next; 16 if (fast == NULL) return slow->next; 17 while (fast->next != NULL) { 18 fast = fast->next; 19 slow = slow->next; 20 } 21 slow->next = slow->next->next; 22 return head; 23 } 24 };
题意:给定一个链表,去掉倒数第 k 个数。
题解:这里只给出了 one pass 的解法。因为要将倒数第 k 个数删掉,这里我们采用“快慢指针” 的做法,让快指针先走 k 个数。这样就能保证当快指针走到末尾时,满指针所在的位置就是要去掉的位置。
1 class Solution { 2 public: 3 bool check(string &s, int k, int num) { 4 int mp[26]; 5 memset(mp, 0, sizeof(mp)); 6 int cnt = 0; 7 int n = s.size(); 8 for (int i = 0; i < n; i++) { 9 mp[s[i]-'A']++; 10 if (i >= num) mp[s[i-num]-'A']--; 11 for (int j = 0; j < 26; j++) { 12 cnt = max(cnt, mp[j]); 13 } 14 if (i >= num-1 && cnt+k >= num) return true; 15 } 16 return false; 17 } 18 19 int characterReplacement(string s, int k) { 20 int n = s.size(); 21 if (n <= k) return n; 22 int l = k, r = n+1; 23 while (l < r) { 24 int mid = l + (r-l)/2; 25 if (mid == l) { 26 if (check(s, k, mid+1)) l = mid+1; 27 break; 28 } 29 if (check(s, k, mid)) l = mid; 30 else r = mid-1; 31 } 32 return l; 33 } 34 };
1 class Solution { 2 public: 3 int characterReplacement(string s, int k) { 4 int n = s.size(); 5 if (n <= k) return n; 6 int i = 0, ans = 0, maxx = 0; 7 unordered_map<int, int> mp; 8 for (int j = 0; j < n; j++) { 9 maxx = max(maxx, ++mp[s[j]]); 10 if (j-i+1 <= maxx+k) ans = j-i+1; 11 else mp[s[i++]]--; 12 } 13 return ans; 14 } 15 };
题意:给定一个字符串 s,和一个整数 k,k 表示可以对字符串任意位置进行最多 k 次的改变。求经过改变后能够得到的最长子串,其中子串中所有字符一样。
题解:
1.二分答案,然后用固定长度的窗口去判断二分的结果是否满足条件。
2.只需要遍历一遍,移动右指针,当不满足条件时移动左指针,注意窗口只能增大不减小,所有左指针每次最多只能向右边移动一格。
1 class Solution { 2 public: 3 struct cmp 4 { 5 bool operator()(pair<int, int> a, pair<int, int> b) 6 { 7 if (a.first == b.first) return a.second > b.second; 8 return a.first > b.first; 9 } 10 }; 11 12 vector<int> smallestRange(vector<vector<int>>& nums) { 13 int n = nums.size(); 14 vector<int> cnt(n); 15 vector<int> ind(n); 16 priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> pq; 17 while (!pq.empty()) pq.pop(); 18 int l = INT_MAX, r = INT_MIN; 19 for (int i = 0; i < n; i++) { 20 int m = nums[i].size(); 21 cnt[i] = m-1; 22 ind[i] = 0; 23 pq.push(make_pair(nums[i][0], i)); 24 l = min(l, nums[i][0]); 25 r = max(r, nums[i][0]); 26 } 27 int ans = r - l; 28 vector<int> res{l, r}; 29 while (true) { 30 pair<int, int> p = pq.top(); 31 pq.pop(); 32 int pos = p.second; 33 if (ind[pos] < cnt[pos]) { 34 int val = nums[pos][++ind[pos]]; 35 if (val > r) r = val; 36 pq.push(make_pair(val, pos)); 37 l = pq.top().first; 38 if (r-l < ans) { 39 ans = r-l; 40 res[0] = l; 41 res[1] = r; 42 } 43 } else { 44 break; 45 } 46 } 47 return res; 48 } 49 };
题意:给定 k 个已经排好序的数组,求一个范围最小的区间 [a, b],使得每个数组都有数落在区间 [a, b] 内。
题解:先从每个数组中取出最小的数,按照 (value, position) 这样的组合(其中 value 代表值,position 代表该值属于第几个数组)存入到优先队列中。每次取出 value 最小的 pair,将该 pair 所处数组中 下一个位置的值加入到该优先队列中,同时每次将当前优先队列中最大值和最小值的差值用来更新最后的答案。直到任意一个数组取完为止。
10.Leetcode1234
1 class Solution { 2 public: 3 int change(char c) { 4 if (c == 'Q') return 0; 5 else if (c == 'W') return 1; 6 else if (c == 'E') return 2; 7 return 3; 8 } 9 int balancedString(string s) { 10 int n = s.size(), ans = INT_MAX; 11 vector<int> cnt(4, 0); 12 int target = n / 4; 13 for (int i = 0; i < n; i++) { 14 cnt[change(s[i])]++; 15 } 16 int need = 4; 17 for (int i = 0; i < 4; i++) { 18 cnt[i] -= target; 19 if (cnt[i] <= 0) need--; 20 } 21 if (need == 0) return 0; 22 for (int i = 0, j = 0; i <= j && j < n; j++) { 23 if ((--cnt[change(s[j])]) == 0) need--; 24 while (i < j) { 25 if ((cnt[change(s[i])]+1) <= 0) { 26 cnt[change(s[i++])]++; 27 } else { 28 break; 29 } 30 } 31 if (need == 0) ans = min(ans, j-i+1); 32 } 33 return ans; 34 } 35 };
1 class Solution { 2 public: 3 int balancedString(string s) { 4 int balancedString(string s) { 5 unordered_map<int, int> count; 6 int n = s.length(), res = n, i = 0, k = n / 4; 7 for (int j = 0; j < n; ++j) { 8 count[s[j]]++; 9 } 10 for (int j = 0; j < n; ++j) { 11 count[s[j]]--; 12 while (i < n && count['Q'] <= k && count['W'] <= k && count['E'] <= k && count['R'] <= k) { 13 res = min(res, j - i + 1); 14 count[s[i++]] += 1; 15 } 16 } 17 return res; 18 } 19 } 20 };
题意:给定一个只包含 "Q" "W" "E" "R" 四个字符的字符串,选取其中一个子串,将子串进行相同长度的字符替换。使得最后得到的字符串每个字符的数量相同。求满足条件的最小的子串的长度。
题解:记最后每个字符的数量为 k,维护一个滑动窗口,需要保证滑动窗口之外的字符他们各自的数量 < k。因为求最小的子串长度,所有右指针每次移动一位,而左指针移动到刚好满足条件为止。
11.Leetcode209
1 class Solution { 2 public: 3 int minSubArrayLen(int s, vector<int>& nums) { 4 int n = nums.size(); 5 int ans = INT_MAX; 6 int sum = 0; 7 for (int i = 0, j = 0; i <= j && j < n; j++) { 8 sum += nums[j]; 9 while (sum >= s && i <= j) { 10 if (sum >= s) ans = min(ans, j-i+1); 11 sum -= nums[i++]; 12 } 13 } 14 return ans == INT_MAX? 0 : ans; 15 } 16 };
题意:给定一个正整数数组和一个正整数 s,选取位置连续子数组,使得子数组中数的和 >= s,求当满足条件时,子数组最短的长度是多少。
题解:双指针。维护的窗口满足 sum >= s。因为求最小值,每次右数组移动一位,左数组移动到直到不满足条件为止。
12.Leetcode845
1 class Solution { 2 public: 3 int longestMountain(vector<int>& A) { 4 int n = A.size(); 5 vector<int> vi; 6 vi.push_back(-1); 7 for (int i = 1; i < n-1; i++) { 8 if (A[i] > A[i-1] && A[i] > A[i+1]) vi.push_back(i); 9 } 10 vi.push_back(n); 11 int m = vi.size(); 12 int ans = 0; 13 for (int i = 1; i < m-1; i++) { 14 int l = vi[i-1], r = vi[i+1]; 15 int k = vi[i]-1, j = vi[i]+1; 16 while (k > l) { 17 if (A[k] < A[k+1]) k--; 18 else break; 19 } 20 while (j < r) { 21 if (A[j] < A[j-1]) j++; 22 else break; 23 } 24 ans = max(ans, j-k-1); 25 } 26 return ans; 27 } 28 };
1 class Solution { 2 public: 3 int longestMountain(vector<int>& A) { 4 int n = A.size(), ans = 0; 5 vector<int> up(n, 0), down(n, 0); 6 for (int i = n-2; i >= 0; i--) { 7 if (A[i] > A[i+1]) down[i] = down[i+1] + 1; 8 } 9 for (int i = 1; i < n; i++) { 10 if (A[i] > A[i-1]) up[i] = up[i-1] + 1; 11 if (up[i] != 0 && down[i] != 0) ans = max(ans, up[i]+down[i]+1); 12 } 13 return ans; 14 } 15 };
1 class Solution { 2 public: 3 int longestMountain(vector<int>& A) { 4 int n = A.size(), ans = 0, down = 0, up = 0; 5 for (int i = 1; i < n; i++) { 6 if ((down && A[i-1] < A[i]) || A[i-1] == A[i]) down = up = 0; 7 down += A[i] < A[i-1]; 8 up += A[i] > A[i-1]; 9 if (up && down) ans = max(ans, up+down+1); 10 } 11 return ans; 12 } 13 };
题意:给定一个数组,求最长的 “mountain” 长度。
题解:
1. 将所有满足 a[i] > a[i-1] && a[i] > a[i+1] 的位置 i 保存起来,从位置 i 向两侧延申。
2. 维护两个数组 up, down。从后往前扫一次,更新 down 数组,if (A[i] > A[i+1]) down[i] = donw[i+1] + 1;从前往后扫一次,更新 up 数组,if (A[i] > A[i-1]) up[i] = up[i-1] + 1。up[i] + down[i] + 1 表示以 i 为峰顶满足条件的 “mountain” 长度。
3. one way 做法。维护两个整数变量 up 和 down,和上面所表示的含义一样。在指针向右移动的过程中同时维护 down 和 up 变量,同时更新答案。当遇到新的“mountain” 时重置 up 和 down 的值。
13.Leetcode713
1 class Solution { 2 public: 3 int numSubarrayProductLessThanK(vector<int>& nums, int k) { 4 if (k == 0) return 0; 5 int n = nums.size(), i = 0, res = 0; 6 long long ans = 1; 7 for (int j = 0; j < n; j++) { 8 ans *= nums[j]; 9 if (ans >= k) { 10 while (i <= j && ans >= k) ans /= nums[i++]; 11 } 12 res += (j-i+1); 13 } 14 return res; 15 } 16 };
题意:给定一个数组 nums,求有多少个连续的子数组满足子数组中所有元素的和小于给定的数 k
题解:右指针每次向右移动一格,左指针移动到恰好满足条件的位置。假设当前右指针的位置为 j, 左指针的位置为 i。则所有 [k, j] (i <= k < j) 的位置都满足条件。