基础算法-双指针算法

一、双指针算法详解

1. 双指针算法介绍

  • 双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的。
  • 在前文所介绍的快速排序和归并排序也是双指针算法的一种。
  • 每当遇到双指针问题时,都可以先通过暴力方法尝试解决问题,然后发现其中存在的一些性质,再用双指针算法进行优化。

2. 双指针算法常见套路

  • 双指针的初始位置。根据双指针的分类,有两种可能。
  • 双指针的移动方法。根据双指针的分类,有两种可能。
  • 遍历的结束条件。根据双指针的分类,有两种可能。

3. 双指针算法作用

  • 朴素算法每次在第二层遍历的时候,是会从新开始( j 会回溯到初始位置),然后再遍历下去。(假设i是终点, j 是起点)
  • 双指针算法:由于具有某种单调性,每次在第二层遍历的时候,不需要回溯到初始位置(单调性),而是在满足要求的位置继续走下去或者更新掉。
  • 因此,双指针算法主要起优化的功能,例如当我们使用朴素方法即暴力枚举每一个可能的情况时,时间复杂度为 O(n*n)
    基础算法-双指针算法_第1张图片

此时,我们使用双指针算法利用就可以将 O(n*n) 优化为 O(n)
基础算法-双指针算法_第2张图片

4. 具体应用

4.1 数组里的双指针

用暴力解法一定可解,双重循环得出结果。使用双指针的方法,可以借助一个额外变量,实现降维优化。

(1)相反方向运动
  • 两个指针在数组的头和尾,都往中间移动,直到相遇。
  • 两个指针在数组的中间,往数组的两端移动,直到到达边界。
  • 每次循环时,我们值比较当前的两个元素,在这两个元素当中,求出对应的结果。
(2)相同方向运动
  • 两个指针均在数组的一端,都往另一端移动,直到满足条件为止。
  • 两个指针的移动速度不同,类似于滑动窗口问题,快指针一直往前遍历,走到尾部则循环结束,慢指针视条件进行移动。
  • 每次循环时,看子区间是否满足某个条件,子区间是由双指针框起来, 输出的是子区间的变形。

4.2 双指针算法与前缀和

  • 二者均是对子区间进行操作,因此有一定的结合可能。
  • 前缀和是需要用到子区间和时,通过借助一个数组,去存储前缀和。

4.3 链表里的双指针

  • 以快慢指针为主,快指针一次 2 步,慢指针一次 1 步等等。
  • 典型例题:在链表当中找一个环,有环的话,快慢指针必定会相遇,若无环的话,则快指针就直接走到结尾。

5. 举例说明

关于具体算法应用大家可以看我的快速排序和归并排序,都是非常典型的双指针问题。

  • 快速排序
  • 归并排序

二、双指针算法例题

1. 最长连续不重复子序列

题目描述

给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

输入格式

输入共两行。
第一行包含整数 n。
第二行包含 n 个整数(均在 0∼1e5 范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围

1≤n≤100000

输入样例

5
1 2 2 3 5

输出样例

3

具体实现

实现思路

基础算法-双指针算法_第3张图片

  • 绿色指针表示他最左可以到什么位置,使得绿色指针和红色指针没有重复数字,每次将红色指针向右移动一个位置,然后重新求绿色指针最靠左的话可以再什么位置,每次都要进行匹配。
  • 每当红色指针向右走时,绿色指针也必定向右走。假如说红色指针往右走一个位置时,绿色指针往左走就会矛盾,因为,移动后的绿色指针和红色指针没有重复元素的话,那么在红色指移动之前绿色指针所处的位置就不是最左端,一个没有重复区间内部的子区间内部都是没有重复元素的
  • 只需要每次枚举红色指针,通过绿色指针的位置在红色位置的左边和检查绿色指针和红色指针之间有没有重复元素,然后判断绿色指针是否需要向右移动。
  • 时间复杂度从 O(n*n) 优化为 O(n)
辅助图解

基础算法-双指针算法_第4张图片

代码注解
  • s[N] 用来记录每个数据出现的次数,要确保所求区间内部每个元素都只出现一次。
  • i 可看作上述思路的红色指针,j 可看作上述的绿色指针。
实现代码
#include 
using namespace std;

const int N = 100010;
int n;
int q[N], s[N];
int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ )
    {
        cin>>q[i];
    }
    int res = 0;
    for (int i = 0, j = 0; i < n; i ++ )
    {
        s[q[i]] ++ ;
        while (j <= i && s[q[i]] > 1) 
        {
            s[q[j]]--;
            j++;
        }
        res = max(res, i - j + 1);
    }
    cout << res << endl;
    system("pause");
    return 0;
}

2. 数组元素的目标和

题目描述

给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i] + B[j] = x 的数对 (i,j)。
数据保证有唯一解。

输入格式

输入共三行。
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。
第二行包含 n 个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。

输出格式

共一行,包含两个整数 i 和 j。

数据范围

数组长度不超过 1e5。
同一数组内元素各不相同。
1 ≤ 数组元素 ≤ 1e9

输入样例

4 5 6
1 2 4 7
3 4 6 8 9

输出样例

1 1

具体实现1——暴力法

实现思路
  • 通过两个单独的 for 循环对 A 和 B 两个数组内的元素进行输入。
  • 通过两个 for 循环对每个数组当中的每个元素进行遍历,并判断是否等于目标值,若等于,输出此时的 i ,j 坐标。
  • 暴力法可以解决问题,但是会 超时
  • 时间复杂度为 O(mn)
实现代码
#include 
using namespace std;

const int N=100010;
int A[N],B[N];
int n,m,x;
int main()
{
    cin>>n>>m>>x;
    for(int i=0;i<n;i++)
    {
        cin>>A[i];
    }
    for(int j=0;j<m;j++)
    {
        cin>>B[j];
    }
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            if(A[i]+B[j]==x)
            {
                cout<<i<<" "<<j<<endl;
            }
        }
    }
    system("pause");
    return 0;
}

具体实现2——双指针算法

实现思路
  • 寻找单调性,对暴力做法进行优化。
  • 对于每一个 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 时,将 i 右移 i++,此时,j 保持不动,一直移动 i 进行判断,直到 A[i] + B[j] = x 时,输出结果。注:j 是从下标 m-1 位置开始往左移的,即还要满足 j>=0。
  • 就好比两个数组,当大于时移动其中一个,小于时移动其中另一个。
  • 时间复杂度为 O(n)
输入样例演示
i j 判断是否为6
1 9 大于
1 8 大于
1 7 大于
1 6 大于
1 4 小于
2 4 等于
实现代码
#include 
using namespace std;

const int N = 1e5 + 10;
int n, m, x;
int A[N], B[N];
int main()
{
    cin>>n>>m>>x;
    for (int i = 0; i < n; i ++ ) 
    {
        cin>>A[i];
    }
    for (int i = 0; i < m; i ++ )
    {
        cin>>B[i];
    }
    for (int i = 0, j = m - 1; i < n; i ++ )
    {
        while (j >= 0 && A[i] + B[j] > x)
        {
            j -- ;
        }
        if (j >= 0 && A[i] + B[j] == x) 
        {
            cout << i << ' ' << j << endl;
        }
    }
    system("pause");
    return 0;
}

3. 判断子序列

题目描述

给定一个长度为 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 ≤ 1e5
−1e9 ≤ ai , bi ≤ 1e9

输入样例

3 5
1 3 5
1 2 3 4 5

输出样例

Yes

具体实现

实现思路
  • 判断子序列,顺次判断。
  • j指针用来扫描整个 b 数组,i 指针用来扫描 a 数组。若发现 a[i] == b[j] ,则让 i 指针后移一位。
  • 整个过程中,j 指针不断后移,而i指针只有当匹配成功时才后移一位,若最后若i==n,则说明匹配成功。
  • 但凡存在一种匹配方式,那么我们的双指针算法一定可以找到。
实现代码
#include 
using namespace std;

const int N = 100010;
int n, m;
int a[N], b[N];
int main()
{
    cin>>n>>m;
    for (int i = 0; i < n; i ++ ) 
    {
        cin>>a[i];
    }
    for (int i = 0; i < m; i ++ )
    {
        cin>>b[i];
    }
    int i = 0, j = 0;
    while (i < n && j < m)
    {
        if (a[i] == b[j]) 
        {
            i ++ ;
        }
        j ++ ;
    }
    if (i == n) 
    {
        puts("Yes");
    }
    else 
    {
        puts("No");
    }
    system("pause");
    return 0;
}

你可能感兴趣的:(算法与数据结构,算法,数据结构,链表)