【C++】競程筆記(雙指標)
===
[TOC]
程式碼範例參考:NTUCPC Guide,此筆記僅為個人學習用途。
雙指標(Two-Pointer)
---
雙指標是一種常用於陣列或字串處理的技巧。
顧名思義,雙指標是指同時使用兩個指標(指向資料結構的索引)來遍歷或操作資料。
這種技巧可以減少時間複雜度,能避免掉多重迴圈,常被用在排序陣列或雙向搜尋的情況上。
常見的指標移動方式有:
* 一個從頭開始、另一個從尾開始(雙向靠攏)
* 一個快指標、另一個慢指標(追趕法)
* 同時向同一方向移動但控制條件不同
---
有一個更快理解什麼是雙指標的例子:
假設有個已經排序好的序列:`arr = [1, 3, 4, 5, 7, 10, 11]`。
我們希望能找出兩個數的和是 14。
則雙指標的初始狀態:`left → 1 3 4 5 7 10 11 ← right`
首先走第一遍初始狀態:`arr[left] + arr[right] = 12`,值太小,需要移動左邊的指標,叫他往右,讓值變大一點。
所以第二遍:`arr[left+1] + arr[right] = 14`,剛好就找到 14 了。
像這種就是 Two sum 問題。
### 例題 1
---
Leetcode 209. Minimum Size Subarray Sum:https://leetcode.com/problems/minimum-size-subarray-sum/description/
最開始會想到的一定都會是暴力解,如這題可用三重迴圈去解決,只是時間複雜度會變成 $O(N^3)$ 。具體解法:
1. 窮舉出所有 $(l, r)$
2. 求出所有 $num[l]$ 到 $num[r]$ 的數字總和
3. 看總和有沒有超過 $target$
雙指標做法:
1. 使用兩個指標:l 為左界,r 為右界,用來定義一個動態的區間 `[l, r]`。
2. r 從頭到尾遍歷陣列,每次將 `nums[r]` 加入目前的總和 sum。
3. 每當 `sum >= target` 時,表示目前這個區間 `[l, r]` 是一組解(其子陣列總和符合要求)。
4. 為了找到最短的子陣列和,就要把 l 往右推移,每推一次就更新當前最短長度,直到 `sum < target` 為止。
5. 重複步驟 2~4,直到整個陣列走完為止。
6. 若最後沒有找到任何子陣列和(即最短長度仍為初始最大值),則回傳 0。
範例程式碼:
```cpp=
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size(); // 陣列長度
int l = 0, sum = 0;
int minLen = INT_MAX; // 最短的合法子陣列之長度
for (int r = 0; r < n; r ++){
sum += nums[r];
while (sum >= target){
// 為了找到最小子陣列和
// 所以把 l 右移
minLen = min(minLen, r - l + 1);
sum -= nums[l];
++l;
}
}
return (minLen == INT_MAX) ? 0 : minLen;
// minLen 從來沒變過,都還是 INT_MAX 的話,代表找不到子陣列和,回傳 0
}
```
時間複雜度: $O(n)$ 。
### 例題 2
---
Leetcode 167. Two Sum II - Input Array Is Sorted:https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/
解題思路:
1. 初始化兩個指標:`left = 0`、`right = numbers.size() - 1`。
2. 計算 `numbers[left] + numbers[right]`:
- 若和 `= target`:回傳 `left + 1` 和 `right + 1`(題目要求 1-indexed)。
- 若和 `< target`:表示數字太小,將 `left` 往右移動。
- 若和 `> target`:表示數字太大,將 `right` 往左移動。
3. 因為題目保證有唯一解,故不需考慮無解情況。
程式碼:
```cpp=
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int l = 0, r = numbers.size() - 1;
while (l < r){
int sum = numbers[l] + numbers[r];
if (sum == target){
return {l + 1, r + 1};
}
else if (sum < target){
l++;
}
else{
r--;
}
}
return {};
}
};
```
習題
---
1. Leetcode 3. Longest Substring Without Repeating Characters:https://leetcode.com/problems/longest-substring-without-repeating-characters/description/
解題思路:
1. 初始化雙指標 `l, r`。
2. 用 `unordered_set` 記錄目前的字元。
3. 當 `r` 指到重複的字元,就不斷地從 `l` 開始移除,直到移除掉所有重複的字元。
4. 每次更新最大長度。
```cpp=
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set <char> ch;
int l = 0, r = 0;
int maxLen = 0;
while (r < s.length()){
// find() 若找不到就指向 ch.end(), 代表這個字母沒出現過
if (ch.find(s[r]) == ch.end()){
ch.insert(s[r]);
maxLen = max(maxLen, r - l + 1);
r++;
}
else{
ch.erase(s[l]);
l++;
}
}
return maxLen;
}
};
```
2. Leetcode 977. Squares of a Sorted Array:https://leetcode.com/problems/squares-of-a-sorted-array/description/
解題思路:
原陣列 nums 為非遞減排序,負數平方後可能比正數平方大:
```
nums = [-4, -1, 0, 3, 10]
squares = [16, 1, 0, 9, 100]
```
若直接平方再排序的時間複雜度為 $O(nlogn)$ ,可用雙指標從兩端往中間夾,因為最大的平方數只可能出現在兩端。
1. 建立左右指標 `int l = 0, r = num.size() - 1;`。
2. 建立 result vector,從尾端填入最大的平方值(因為最大平方值在兩端)。
3. 每次比較 `abs(nums[left])` 和 `abs(nums[right])`,取較大者平方後放到 `result[i]`。
```cpp=
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int n = nums.size(); // 陣列長度
int l = 0, r = n - 1; // 左右指標
int index = n - 1; // 作為 result vector 的起始點, 也就是最右邊
// 另外要先塞最大的平方數, 再慢慢遞減下來 ( index-- )
vector<int> result(n);
while(l <= r){
if (abs(nums[l]) > abs(nums[r])){
result[index--] = nums[l] * nums[l];
l ++;
}
else{
result[index--] = nums[r] * nums[r];
r --;
}
}
return result;
}
};
```