雙指針算法


雙指針算法

什么是雙指針

嚴格的來說,雙指針只能說是是算法中的一種技巧。

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

模板

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)

image

例題

例題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;
}

image

例題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;
}

圖解輔助理解:

image

例題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

思路:

  1. 判斷子序列,順次判斷!
  2. j指針用來掃描整個b數組,i指針用來掃描a數組。若發現a[i]==b[j],則讓i指針后移一位。
  3. 整個過程中,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]

思路:類似於快速排序的划分過程

題目要求:調整數組順序使奇數位於偶數前面。

  1. 前段:定義i指針從頭開始遍歷掃描,如果是奇數則指針右移i++,一旦出現偶數則停止
  2. 后段:定義j指針,從右往左遍歷掃描,如果是偶數則指針左移j--,一旦出現奇數則停止
  3. 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的范圍問題!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM