雙指針算法
什么是雙指針
嚴格的來說,雙指針只能說是是算法中的一種技巧。
雙指針指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針)或者相反方向(對撞指針)的指針進行掃描,從而達到相應的目的。最常見的雙指針算法有兩種:一種是,在一個序列里邊,用兩個指針維護一段區間;另一種是,在兩個序列里邊,一個指針指向其中一個序列,另外一個指針指向另外一個序列,來維護某種次序。

模板
for (int i = 0, j = 0; i < n; i ++ ) // j從某一位置開始,不一定是0
{
while (j < i && check(i, j)) j ++ ;
// 具體問題的邏輯
}
常見問題分類:
(1) 對於一個序列,用兩個指針維護一段區間,比如快排的划分過程
(2) 對於兩個序列,維護某種次序,比如歸並排序中合並兩個有序序列的操作
雙指針算法的核心思想(作用):優化
在利用雙指針算法解題時,考慮原問題如何用暴力算法解出,觀察是否可構成單調性,若可以,就可采用雙指針算法對暴力算法進行優化.
當我們采用朴素的方法即暴力枚舉每一種可能的情況,時間復雜度為O(n*n)
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
//具體邏輯
}
}
而當我們使用雙指針算法時通過某種性質就可以將上述O(n*n)的操作優化到O(n)

例題
例題01、
先看這樣一個例子:輸入一個字符每個子串之間有一個空格,讓你輸出每一個空格后的子串。
輸入
abc def hij輸出
abc def hij
【參考代碼】
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str;
getline(cin, str);
int n = str.size();
for(int i = 0; i < n; i++)
{
int j = i;
while(str[j] != ' ') j++;
// cout<<j;
for(int k = i; k < j; k++) cout<<str[k];
cout<<endl;
i = j; //循環體執行完后for()中的i才 i++即,下一次開始時 i就到了上一次空格(位置j)的下一位
}
return 0;
}

例題02、
【AcWing 799. 最長連續不重復子序列 】
給定一個長度為 n 的整數序列,請找出最長的不包含重復的數的連續區間,輸出它的長度。
輸入格式
第一行包含整數 n。
第二行包含 n 個整數(均在 0∼105 范圍內),表示整數序列。
輸出格式
共一行,包含一個整數,表示最長的不包含重復的數的連續區間的長度。
數據范圍
1≤n≤105
輸入樣例:
5
1 2 2 3 5輸出樣例:
3
思路:
使用雙指針算法,根據觀察發現,當使用i,j兩個快慢指針表示當前的指針移動到i的最長不重復序列時候,具有單調性,即i向后移動,j必然向右或者不動,不可能向左移動,這一單調性質導致可以使用雙指針算法。
在雙指針算法中,一個指針掃描整個數組而移動,關鍵如何找到對應的另一個指針移動的位置,在本題中,我們定義i為塊指針,j為慢指針,j的位置定義為i對應的最長不重復序列的j的位置,因為不重復,i和j元素都不重復,出現次數都為一,因此我們使用一個數組s來記錄各個元素出現的次數,i,j不斷移動,數組及時更新,每次i更新,便更新j確保j,i區間元素都只出現一次,代碼如下
【參考代碼】
#include<iostream>
using namespace std;
const int N = 100000+10;
int a[N],s[N];// s[N]用來記錄數據出現的次數
int main()
{
int n;
cin>>n;
int res = 0;
for(int i = 0; i < n; i++) cin>>a[i];
for(int i = 0, j = 0; i < n; i++)
{
s[a[i]]++; // 記錄數值a[i]出現的次數
// i快指針,j 慢指針
while(j <= i && s[a[i]] > 1) // 若出現重復的數值。j <= i不要也行
{
s[a[j]]--;
j++;
}
//更新的不包含重復的數的連續區間的最大長度
res = max(res, i - j +1);
}
cout<<res;
return 0;
}
圖解輔助理解:

例題03、
【acwing 800.數組元素的目標和】
給定兩個升序排序的有序數組 A 和 B,以及一個目標值 xx。
數組下標從 0開始。
請你求出滿足 A[i]+B[j]=x 的數對 (i,j)(i,j)。
數據保證有唯一解。
輸入格式
第一行包含三個整數n,m,x,分別表示 A 的長度,B 的長度以及目標值 x。
第二行包含 n 個整數,表示數組 A。
第三行包含 m 個整數,表示數組 B。
輸出格式
共一行,包含兩個整數 i 和 j。
數據范圍
數組長度不超過 105。
同一數組內元素各不相同。
1≤數組元素≤109輸入樣例:
4 5 6
1 2 4 7
3 4 6 8 9輸出樣例:
1 1
【暴力做法】O(n*n)
#include<iostream>
using namespace std;
const int N = 100000+10;
int a[N],b[N];
int main()
{
int n,m,x;
cin>>n>>m>>x;
for(int i = 0; i < n; i++) scanf("%d",&a[i]);
for(int i = 0; i < m; i++) scanf("%d",&b[i]);
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(a[i]+b[j] == x)
{
cout<<i<<" "<<j<<endl;
}
}
}
return 0;
}
【雙指針算法】O(n + m)
思路:
- 雙指針算法的核心思想是優化,因此可以先寫出暴力做法
- 尋找單調性,雙指針算法進行優化
通過暴力法我們可以知道,對於每一個i都想找到一個j使得a[i]+b[j]==x,由於兩段序列都是單調遞增的,具有單調性,因此我們可以用雙指針算法進行優化。
我們讓j從m-1位置開始(從右往左掃描),根據單調性,一旦a[i] + b[j] > x,當前i位置的下一個a[i]:必定會有a[i] + b[j] > x,那么j就左移j--。當出現a[i] + b[j] == x時輸出結果即可。注:j是從下標m-1位置開始往左移的,即還要滿足j>=0。
#include<iostream>
using namespace std;
const int N = 100000+10;
int a[N],b[N];
int main()
{
int n,m,x;
cin>>n>>m>>x;
for(int i = 0; i < n; i++) scanf("%d",&a[i]);
for(int i = 0; i < m; i++) scanf("%d",&b[i]);
for(int i = 0, j = m - 1; i < n; i++)
{
while(j >= 0 && a[i] + b[j] > x) j--;
if(a[i] + b[j] == x)
{
printf("%d %d\n", i, j);
break;
}
}
return 0;
}
例題04、
【acwing 2816. 判斷子序列】
給定一個長度為 n 的整數序列 a1,a2,…,an 以及一個長度為 m 的整數序列 b1,b2,…,bm。
請你判斷 a 序列是否為 b 序列的子序列。
子序列指序列的一部分項按原有次序排列而得的序列,例如序列 {a1,a3,a5} 是序列 {a1,a2,a3,a4,a5} 的一個子序列。
輸入格式
第一行包含兩個整數 n,m。
第二行包含 n 個整數,表示 a1,a2,…,an。
第三行包含 m 個整數,表示 b1,b2,…,bm。
輸出格式
如果 a 序列是 b 序列的子序列,輸出一行
Yes。否則,輸出
No。數據范圍
1≤n≤m≤1051≤n≤m≤105,
−109≤ai,bi≤109−109≤ai,bi≤109輸入樣例:
3 5
1 3 5
1 2 3 4 5輸出樣例:
3 5
1 3 5
1 2 3 4 5
思路:
- 判斷子序列,順次判斷!
j指針用來掃描整個b數組,i指針用來掃描a數組。若發現a[i]==b[j],則讓i指針后移一位。- 整個過程中,
j指針不斷后移,而i指針只有當匹配成功時才后移一位,若最后若i==n,則說明匹配成功。
【參考代碼】
#include<iostream>
using namespace std;
const int N = 100000+10;
int a[N],b[N];
int main()
{
int n,m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) scanf("%d",&a[i]);
for(int i = 0; i < m; i++) scanf("%d",&b[i]);
int i = 0, j = 0;
while(i < n && j < m)
{
//i只有在匹配成功時才往后移動一位,而j在整個過程中要不斷掃描
if(a[i] == b[j])
{
i++;
j++;
}
else j++;
}
// 最后i == n說明匹配成功
if(i == n) puts("Yes");
else puts("No");
return 0;
}
上述14~24行代碼也可以改成:
//j在整個過程中要不斷掃描,而i只有在匹配成功時才往后移動一位
int i;
for(int j = 0; j < m; j++)
{
if(i < n && a[i] == b[j]) i++;
}
【總結】
針對板子里while什么情況下使用?
當我們遇到像 AcWing 799.最長連續不重復子序列,AcWing 800.數組元素的目標和 這種問題,我們需要先固定一個指針,然后另一個指針去連續的判斷一段區間,需要while()循環。換句話說,
while()循環用來解決連續一段區間的判斷問題,而這道題中我們需要對a數組和b數組的每一位,逐位去進行比較判斷,j指針不斷后移,而i指針只有當匹配成功時才后移一位,它不是連續一段區間的判斷。更重要的還是靈活變通!
例題05、
【acwing 32. 調整數組順序使奇數位於偶數前面】
輸入一個整數數組,實現一個函數來調整該數組中數字的順序。
使得所有的奇數位於數組的前半部分,所有的偶數位於數組的后半部分。
樣例
輸入:[1,2,3,4,5]
輸出: [1,3,5,2,4]
思路:類似於快速排序的划分過程
題目要求:調整數組順序使奇數位於偶數前面。
- 前段:定義
i指針從頭開始遍歷掃描,如果是奇數則指針右移i++,一旦出現偶數則停止 - 后段:定義
j指針,從右往左遍歷掃描,如果是偶數則指針左移j--,一旦出現奇數則停止 - 在
i < j情況下交換停止時的奇偶數,然后接着下一次循環,直到i=j時循環結束
【參考代碼】
class Solution {
public:
void reOrderArray(vector<int> &array) {
int i = 0, j = array.size() - 1;
while(i < j)
{
while(i < j && array[i] % 2 == 1) i++;
while(i < j && array[j] % 2 == 0) j--;
if(i < j) swap(array[i], array[j]);
}
}
};
總結
運用雙指針算法時不僅僅要找到某種性質(解題的關鍵——單調性),同時也別忘了指針i、j的范圍問題!
