【第七课】双指针算法(acwing-799最长连续不重复子序列 思路详解 c++代码)

目录

引例--分行输出字符串中的单词 

直接遍历整个字符串

双层循环嵌套实现 

双指针算法

acwing--799最长连续不重复子序列 

错误的思路和写法(!!可跳) 

双指针算法

思想

代码如下 

代码思路的解释


引例--分行输出字符串中的单词 

在进入本节例题之前,先看一个双指针算法解决问题的简单的例子。

要求输入一段字符串,用一个空格隔开每个单词,分行输出每个单词。这里默认用户按要求输入字符串。

直接遍历整个字符串

我最开始想到的是直接遍历整个字符串,如果是空格就换行,不是空格就一直输出。思路比较简单。

#include<iostream>
 using namespace std;
 int main()
 {
    string a;
    getline(cin,a);
    for(int i=0;i<a.size();i++)
    {
        if(a[i]!=' ')printf("%c",a[i]);
        else printf("\n");
    }
    return 0;
}

这里注意要保证输入的字符串的方式不会遇到空格就自动判定字符串输入完毕。刚开始学做这种相关需要输入空格的字符串的时候,总出现这个问题还看不出哪错(hh),像是c语言里scanf输入,c++里直接cin输入,都会导致这个问题。学到这里对于这个问题就有警惕了,会自然想到输入方式。这里我使用了getline函数,它可以读取整行,包括空格,直到遇到换行符,其第一个元素表示输入流,第二个元素表示字符串。 (具体的我没有详细查,遇到复杂问题了再说)

当然这个方法和我们要讲的双指针没什么关系(笑哭),好嘛,这只是我看到这个题的思路啦。

双层循环嵌套实现 

下面用双层循环嵌套实现这道题。(很复杂写的好像也有点没必要,只是为了对应后面双指针算法的写法)

#include<iostream>
using namespace std;
int main()
{
    string a;
    getline(cin,a);
    for(int i=0;i<a.size();i++)
    {
        if(a[i]==' ')continue;
        for(int j=i;j<a.size() && a[j]!=' ';j++)
        {
            cout<<a[j];
        }
        cout<<endl;
        while(i<a.size() && a[i]!=' ')i++;//j在其自身循环内部定义,循环结束之后j就被销毁,所以我们要重新找
    }
    return 0;
}

 这里我们的时间复杂度是O(n*n), 对于每一个 i 在内层循环里 j 都会执行n次,i 自身又要执行n次。这个我们比较熟悉了。

双指针算法

下面使用双指针算法来实现。

// 双指针
#include <iostream>
using namespace std;
int main()
{
    string a;
    getline(cin, a);
    for (int i = 0; i < a.size(); i++)
    {
        int j = i; // j指针每次从i指针的位置开始
        while (j < a.size() && a[j] != ' ')
            j++; // 只要j仍有效,并且不为空格,就++,这样就记录下了每个单词的位置,刚好i作为单词的开始,j作为单词的末尾

        for (int k = i; k < j; k++)
            cout << a[k];
        cout << endl;

        i = j; // 更新i为该单词末尾,刚好最外层for循环会给i++,i指向空格位置,在下一次for循环内部不满足条件,未执行,直接再次i++,使得i指向了新单词的开头
    }
    return 0;
}

我们来看看这段代码是如何执行的。主要看循环部分

这里外层以 i 为指针,从字符串开头开始,循环内部定义第二个指针 j ,将其赋值为 i ,其作用是找到当前单词的末尾,我们知道这里 i 的功能是指向该单词的头部。我们让 j 指针往后走,条件是当 j 指针在合法范围内(虽然 i 有限定范围,j最初又被赋值为 i ,但是while循环的执行会让 j 的值增加,所以这里 j 的限定范围的条件不可省略),且其值不为空格。j 指向该单词末尾时,我们将该单词for循环输出即可。最终将 i 指向 j ,也就是当前单词的末尾。由于外层for循环每次为 i++,所以在输出一个单词之后,i的值实际上更新为了该单词后空格的位置,所以此时的这一轮循环并没有执行内部的代码(因为条件不符合),于是i再次++,进入到下一轮循环,此时 i 所指的便是新一个单词的开头啦。这里单说可能容易晕,可以想一下代码执行过程。

感觉下面的分析真的很重要了。 

我们来分析一下该算法的时间复杂度。我们的 i 每次标记一个位置,j 刚好每次读取并未被 i 读取过的元素(这里可以思考一下哈可以理解),下面的定义变量k遍历输出的循环由于其起始位置是确定的从 i 遍历到 j ,所以是O(1)时间复杂度

所以 i 和 j 指针的移动像是互补的( 重点!!核心思想)字符串中每个元素都只被处理过一次,因此其时间复杂度为O(n),取决于字符串的长度

好啦,有了上面的简单引入,我们来深入看一下双指针算法。

acwing--799最长连续不重复子序列 

错误的思路和写法(!!可跳) 

在学习双指针算法之前,还是先自己写了一下这道题。还是想了挺久的emm错啦。

这里说一下我的思路(万一有和我想法一样的呢hh)

在其进行遍历输入的时候,就同时判断其是否与前一个数相同,定义一个变量len,记录重复的连续的序列的长度,如果遇到了和前一个数相同的,那这个序列就不算了,于是len重新定义为1,这里定义为1是因为我们在判断该数与其前面的数不同的时候,len++,如果符合,那算上其自身和其前一个数,len应该为2,所以len初值定义为1,同时注意由于数组中第一个元素如果按照上面的思路,a[1]与a[0]比较,我们将a数组定义在主函数外部,也就将其初始化为0了,a[0]是0,但其实我们根本就不需要和它比较,所以我们 len++ 的执行要除去 i=1 的情况。如果我们遍历到了数组最后一个元素都没有与前一个元素重复的,那就会一直++,但是我们的ans赋值在else语句里,最后一次统计符合条件的话其实并没有被赋值给ans,所以为了避免遗漏最后一次计算符合条件的情况,我们在循环外部在再ans进行正确的赋值之后在输出

从我上面的叙述可以看出,我错在只比较了该元素与其前面的元素是否重复,如果我们读取了一段1 2 3 4,下一个元素为2,我只比较了2和4不相同就进行了赋值,显然是有问题的。要想代码正确,就要重新写一下比较函数。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int a[N],n,len;

/*这是想在源代码基础上修改正确,需要补充的比较函数。我这里不在修改了。
bool compare(int a[],int x,int i)
{}
*/

int main()
{
    int ans=0;
    len=1;//初始状态为 1 是为了计算上其自身
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        if(a[i]!=a[i-1] && i!=1)len++;//考虑不全的写法,但代码是可以执行
//if(compare(a,a[i],i) && i!=1)len++;//将前一个条件修改一下才对
        else{
            ans=max(len,ans);
            len=1;}
    }
    ans=max(len,ans);
    cout<<ans;
    return 0;
}

下面介绍双指针算法

双指针算法

思想

核心思想:两个指针在遍历过程中的协同作用,即当一个指针移动时,另一个指针也相应地做出调整,从而将两层循环遍历优化为一层的算法。刚刚引例里的“两个指针互补”就反映了该思想。

取自bing:

通常有一个指针作为主指针(如 i),另一个指针(如 j)则根据主指针的位置和算法的目标进行移动。这种移动方式使得 i 和 j 在整个数组或链表中的移动像是互补的:当 i 向前移动时,j 也会向前移动,直到满足特定的条件。这种互补的移动方式使得算法能够在 O(n) 的时间复杂度内解决问题。

格式一般是

for (int i = 0, j = 0; i < n; i++) {
    while (j < i && check(i, j)) j++;
    // 每道题目的具体逻辑
}

在大多数双指针算法中,i 和 j 两个指针都是从数组的开始位置出发,然后 i 指针向后移动,j 指针在 i 指针的前面或者和 i 指针在同一位置。

 (由于做题不多,下面这两点体会还没有很深)

1.使用双指针的一个情况是:单调性:当你发现问题中存在一种单调性,即当一个指针向后移动时,另一个指针只能向一个方向移动。

2.我们一般先写出两层循环O(n*n)的写法,再按照格式进行修改。

(感觉这个不太好想啊,要多在题目里应用帮助理解。 

代码如下 

两层for循环实现:这里是错误的,希望解答,非常感谢

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int a[N],n,res,len;

//符合条件的情况
bool check(int l,int r)
{
    if(l>=r)return 0;
    for(int i=l;i<r;i++)
    {
        if(a[i]==a[r])return 0;
    }
    return 1;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<i;j++)//j指针最左能在哪
        {
            if(check(j,i))
            {
                res=max(res,i-j+1);//在符合条件的情况里找最大的
            }
        }
    }
    cout<<res;
    return 0;
}

双指针算法实现  

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N], s[N], n;
int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    int res = 0;
    for (int i = 0, j = 0; i < n; i++)
    {
        s[a[i]]++;//s数组记录每一个数出现的次数
        while (s[a[i]] > 1)//读入了i之后,有了重复元素,所以我们j指针往后移一位,
        {
            s[a[j]]--;
            j++;
        }
        res = max(res, i - j + 1);
    }
    cout << res;
    return 0;
}
代码思路的解释

关于这道题应用双指针算法的思路。像上面引例一样,

我们让 i 指针记录序列的第一个元素并不断向后移动(作为右端点)以遍历找到最大可能的序列,j 指针本来处在当前所处理子序列的开始位置,其作用是  找到  (!注意是找到,并不是本来就是)  第一个使得序列里有重复元素的数(作为左端点)。这点非常重要!!!!着重理解一下。这里的找到都是动词

解释一下循环代码。

起初 i j 均处在第一个元素的位置,创建一个数组s,存放每个元素出现的次数,每输入数字,就s[a[i]]++,这里 i 指针走的是比 j 快的,原因就是我们定义 i 指针的功能就是不断在向序列后寻找,尽量找到最长的符合要求的序列。

只有当第一次出现重复元素的时候,才会进入while循环,这是while循环的条件,同时也要求我们要不断移动 j 指针,使得删掉重复元素第一次出现之前的所有数字出现的记录(这是因为出现了第二个该元素,所以第一个该元素前面所有的元素都已经不符合我们想要的答案了),使得我们 j 指向重复元素第一次出现的位置,实现它该有的功能,这里好好理解一下!!!

比如:2 3 4 5 4,这个序列,我们i指向4最后一个4的时候,进入while循环,j此时指向2,由于出现了第二个4所以第一个4前面所有的元素都已经不符合我们想要的答案了,所以我们将第一个4之前的元素都删掉,j指针不断向后,使得其处在他该在的位置上

每次处理一段序列(可能执行了好几次外层循环才执行一次while循环)之后要更新一下我们的答案, res = max(res, i - j + 1), i为右端点 j 为左端点,期间长度就是 i-j+1 

在这段代码中没有明确地限制 j < i,但是我们仍然可以确保 j 指针始终不会超过 i 指针。这是因为我们始终在处理一个连续子序列,而 j 指针始终指向这个连续子序列的开始位置


好啦,这道题就写完啦。

双指针算法也只是浅浅了解了一下,感觉不像之前的算法那样明确的有一套相似的流程(也可能是我做题不够多哈hh),有点懵懵的感觉。

如果哪里说法有问题欢迎指出,非常感谢!!

也欢迎交流和建议哦。

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值