当前位置:   article > 正文

《算法笔记》总结No.8——双指针

《算法笔记》总结No.8——双指针

去年发表过较简单的双指针案例,建议先行阅读~

C++实现双指针算法_c++ 双指针排序-CSDN博客文章浏览阅读154次。本贴介绍双指针的入门典例~_c++ 双指针排序https://jslhyh32.blog.csdn.net/article/details/129829790


 一.定义

相比说双指针是一种算法,他更倾向是一种编程技巧,话不多说直接看一个引例:

1.正整数和

如下,给定递增序列【1,3,5,7,9】,寻找到两个相加为16的元素。如果使用暴力的思想,相当于是一个双层循环:外层下标i对应的A[i]和内层下标为j的A[j]之和为16,则算一个合法的答案。

然而双层循环的复杂度为N方,当N特别大时,这显然是无法接受的。

但是仔细想一想不难发现——其实在这个暴力过程中出现了很多无用功

  • 当A[i]+A[j]>16时,由于序列是递增的,所以A[i]+A[j+1]是必定大于16的,因此后面的查找是多余的
  • 当A[i]+A[j]>16时,还是由于递增的性质,显然也没必要比较A[i+1]+A[j]的值~

        不难发现,上面两种导致多余查找的原因在于,i和j的枚举是相互牵制的,因此要想到一种可以同时考虑i和j的算法——就有了接下来双指针的思想~ 


令下标i的值为0,而j的值为数组的长度n-1,然后根据 A[i]+A[j]和目标值M的大小关系进行3种不同的选择,直到j>i为止:

  • 如果A[i]+A[j]=M:说明找到了一种方案,此时无论是A[i+1]+A[j]>M还是A[i]+A[j-1]<M均成立,但是A[i+1]+A[j-1]与M的关系却未知,因此要i+1且j-1
  • 如果A[i]+A[j]>M:此时A[i+1]+A[j]>M必然成立,因此只能移动j,j=j-1
  • 如果A[i]+A[j]<M:此时A[i]+A[j-1]<M必然成立,因此只能移动i,i=i-1

代码如下:

  1. #include<iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. using namespace std;
  5. struct item{
  6. int i=0,j=0;
  7. };
  8. int count(vector<int> V,vector<item>& I,int x)
  9. {
  10. int ans=0;
  11. int i=0,j=V.size()-1;
  12. while(i<j)
  13. {
  14. if(V[i]+V[j]==x)
  15. {
  16. ans++;
  17. item temp;
  18. temp.i=i;
  19. temp.j=j;
  20. I.push_back(temp);
  21. i++;
  22. j--;
  23. }
  24. else if(V[i]+V[j]>x)
  25. j--;
  26. else if(V[i]+V[j]<x)
  27. i++;
  28. }
  29. return ans;
  30. }
  31. int main(){
  32. int num=0;
  33. cout<<"请输入数组个数:"<<endl;
  34. cin>>num;
  35. cout<<"请依次输入元素:"<<endl;
  36. vector<int> V;
  37. vector<item> I;
  38. for(int i=1;i<=num;i++)
  39. {
  40. int temp=0;
  41. cin>>temp;
  42. V.push_back(temp);
  43. }
  44. sort(V.begin(),V.end());
  45. int x=0;
  46. cout<<"请输入待查询值:"<<endl;
  47. cin>>x;
  48. int ans=count(V,I,x);
  49. cout<<"符合要求的答案有"<<ans<<"个:"<<endl;
  50. for(int a=0;a<=I.size()-1;a++)
  51. cout<<I[a].i<<" "<<I[a].j<<endl;
  52. }

别树一帜的写法,大家自行品味~

返回的值是下标~

此外一个新手很容易晕的小细节:

int  count(vector<int> V,vector<item>& I,int x)

第二个数组I是引用传递!因为要用到返回的结果,而返回值只是一个int,为了不让结果为空,一定要引用传递!!!

2.序列合并

存在两个递增序列A和B,请把他们按序合并到新的数组C中~

这个比较简单,还是使用双指针,按需扫描A和B即可,用每次扫出来的A[i]和B[j]做比较,这样排进C中的数据一定就是有序的~

代码如下:

  1. #include<iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. using namespace std;
  5. void count(vector<int> A,vector<int> B,int x,int y)
  6. {
  7. int i=0,j=0;
  8. while(i<x&&j<y) //当有某一个序列遍历完之后结束
  9. {
  10. if(A[i]<=B[j]){
  11. cout<<A[i]<<endl;
  12. i++;
  13. }
  14. else{
  15. cout<<B[j]<<endl;
  16. j++;
  17. }
  18. }
  19. //将未遍历完的那一个按序输出~
  20. while(i<x)
  21. {
  22. cout<<A[i]<<endl;
  23. i++;
  24. }
  25. while(j<y)
  26. {
  27. cout<<B[j]<<endl;
  28. j++;
  29. }
  30. }
  31. int main(){
  32. int num1=0,num2=0;
  33. vector<int> A;
  34. vector<int> B;
  35. cout<<"请输入A数组个数:"<<endl;
  36. cin>>num1;
  37. cout<<"请依次输入元素:"<<endl;
  38. for(int i=1;i<=num1;i++)
  39. {
  40. int temp=0;
  41. cin>>temp;
  42. A.push_back(temp);
  43. }
  44. sort(A.begin(),A.end());
  45. cout<<"请输入B数组个数:"<<endl;
  46. cin>>num2;
  47. cout<<"请依次输入元素:"<<endl;
  48. for(int i=1;i<=num2;i++)
  49. {
  50. int temp=0;
  51. cin>>temp;
  52. B.push_back(temp);
  53. }
  54. sort(B.begin(),B.end());
  55. count(A,B,num1,num2);
  56. }

几个点需要注意一下:

  • 这里博主调用了sort函数,保证序列在调用函数的时候肯定是有序的,这个其实是多此一举的操作,为了方便可以乱序输入,实际上题干中给的数据已经是有序的
  • 注意上面的while循环存在条件——两个数组都没有被遍历完,因此不难得到其否命题为有个已经被遍历完了。要注意,此处双指针的作用是比较两个数组最小元素的大小,如果有一个全部元素都比完了都没找到一个比另一个大的,则说明这个另一个中的剩余元素一定均大于之前遍历过的,因此直接按序输出即可~

测试一下,没什么bug:

(返回值类型也可以是vector<int> 数组,但是要注意负责存放排序后的值的那个数组一定要引用传参! ) 

二.归并排序

这里介绍最简单的2-路归并排序:

假设现有数列【66,22,33,57,64,27,18】,排序的过程如下:

  • 第一步,将所有元素两两划分,然后组内单独排序:【22,66】,【33,57】,【27,64】,【18】
  • 第二步,合并相邻两个序列并排序:【22,33,57,66】、【18,27,64】
  • 第三步,继续合并并排序【18,22,27,33,57,64,66】——得到答案

不难发现,核心在于如何将两个有序序列合并为一个有序序列——这一操作在上面已经实现~

此外,归并排序的复杂度为N*logN——对于限制N方的题目来说,这是非常好的排序手段!

1.递归实现

首先我们要对上面将有序序列合并的函数做出修改——使其可以在规定的区间内完成有序~

一步一步来,首先将上述修改成返回一个int型vector的函数,本质上没有发生任何改变:

  1. vector<int> count(vector<int> A,vector<int> B)
  2. {
  3. int i=0,j=0;
  4. int x=A.size(),y=B.size();
  5. vector<int> C;
  6. while(i<x&&j<y) //当有某一个序列遍历完之后结束
  7. {
  8. if(A[i]<=B[j]){
  9. C.push_back(A[i]);
  10. i++;
  11. }
  12. else{
  13. C.push_back(B[j]);
  14. j++;
  15. }
  16. }
  17. while(i<x)
  18. {
  19. C.push_back(A[i]);
  20. i++;
  21. }
  22. while(j<y)
  23. {
  24. C.push_back(B[j]);
  25. j++;
  26. }
  27. return C;
  28. }

然后需要注意,这时候参数要改了这里只是要实现:将某个数组中两个无序的区间合成为同一个有序的区间(数组),因此需要传入的参数应该是5个:目标数组,然后是4个区间端点!

合并函数如下:

  1. vector<int> merge(vector<int> V,int x1,int y1,int x2,int y2)
  2. {
  3. int i=x1-1,j=x2-1;//下标和位序相差一,这里符合惯性思维的赋值方式~
  4. vector<int> V1;
  5. while(i<=y1-1&&j<=y2-1) //当有某一个序列遍历完之后结束
  6. {
  7. if(V[i]<=V[j]){
  8. V1.push_back(V[i]);
  9. i++;
  10. }
  11. else{
  12. V1.push_back(V[j]);
  13. j++;
  14. }
  15. }
  16. while(i<=y1-1)
  17. {
  18. V1.push_back(V[i]);
  19. i++;
  20. }
  21. while(j<=y2-1)
  22. {
  23. V1.push_back(V[j]);
  24. j++;
  25. }
  26. return V1;
  27. }

在main函数测试一下效果:

  1. int main(){
  2. int num1=0,num2=0;
  3. vector<int> A;
  4. vector<int> B;
  5. cout<<"请输入A数组个数:"<<endl;
  6. cin>>num1;
  7. cout<<"请依次输入元素:"<<endl;
  8. for(int i=1;i<=num1;i++)
  9. {
  10. int temp=0;
  11. cin>>temp;
  12. A.push_back(temp);
  13. }
  14. int x1=0,x2=0,y1=0,y2=0;
  15. cout<<"请依次输入4个区间:";
  16. cin>>x1>>y1>>x2>>y2;
  17. vector<int> C;
  18. C=merge(A,x1,y1,x2,y2);
  19. for(int i=0;i<=C.size()-1;i++)
  20. cout<<C[i]<<" ";
  21. }

想起来英语有一个单词叫做perfect~ 

        上面的区间就是我们高中学的数列中正常的位序——博主在代码中已经做了换算,大家直接输入位序即可!

        不过细心的同学可能会发现——如果我输入的A先入为主是有序的,还需要归并排序干嘛?这里实现的是两个区间,区间是人为给定好的,但是在归并排序里面,或者说这种2路归并排序,实际上参数值是某种固定的顺序。因此通过递归,将这个数组的区间不断细分,细分到只剩下一个元素的时候,实际上一下子就能比较出来,然后层层递进回来,上一层递归返回来的数组本身就是一个有序序列!

 因此来写我们的递归函数:

  1. void mergeSort(vector<int> &V,int left,int right)
  2. {
  3. if(left<right)
  4. {
  5. int mid =(left+right)/2;
  6. mergeSort(V,left,mid);
  7. mergeSort(V,mid+1,right);
  8. merge(V,left,mid,mid+1,right);
  9. }
  10. }

参数类型不同的时候代码可能会有不同的结果,大家自行尝试~ 

2.非递归实现 

非递归的大家自己看一下思路吧,博主能力有限,由于返回值类型的有效性,目前还没有较完美的实现~

三.快速排序 

依旧是一个复杂度为N*logN的排序算法。


这部分理论先放一下,直接上实现代码:

  1. #include <stdio.h>
  2. #include <iostream>
  3. #include <math.h>
  4. #include <algorithm>
  5. using namespace std;
  6. int part(int* r, int low, int hight) //划分函数
  7. {
  8. int i = low, j = hight, pivot = r[low]; //基准元素
  9. while (i < j)
  10. {
  11. while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
  12. {
  13. j--;
  14. }
  15. if (i < j)
  16. {
  17. swap(r[i++], r[j]); //r[i]和r[j]交换后 i 向右移动一位
  18. }
  19. while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
  20. {
  21. i++;
  22. }
  23. if (i < j)
  24. {
  25. swap(r[i], r[j--]); //r[i]和r[j]交换后 i 向左移动一位
  26. }
  27. }
  28. return i; //返回最终划分完成后基准元素所在的位置
  29. }
  30. void Quicksort(int* r, int low, int hight)
  31. {
  32. int mid;
  33. if (low < hight)
  34. {
  35. mid = part(r, low, hight); // 返回基准元素位置
  36. Quicksort(r, low, mid - 1); // 左区间递归快速排序
  37. Quicksort(r, mid+1, hight); // 右区间递归快速排序
  38. }
  39. }
  40. int main()
  41. {
  42. int a[10001];
  43. int N;
  44. cout << "请输入要排序的数据的个数: " << endl;
  45. cin >> N;
  46. cout << "请输入要排序的数据: " << endl;
  47. for (int i = 0; i < N; i++)
  48. {
  49. cin >> a[i];
  50. }
  51. cout << endl;
  52. Quicksort(a, 0, N - 1);
  53. cout << "排序后的序列为: " << endl;
  54. for (int i = 0; i < N; i++)
  55. {
  56. cout << a[i] << " ";
  57. }
  58. cout << endl;
  59. return 0;
  60. }

写在最后:单说思想本身,个人感觉归并快排已经是初学者除了DP最难的算法了,实现起来考虑到返回值类型什么的会更加困难一些。但考虑到比N方还要低的复杂度,这两种算法的性价比相当之高!这里先留个坑位,之后理论部分补充一些王道408的部分会更好一些~

对于基础的双指针算法,没什么理解起来的难度,需要注意的是等号的选取范围,以及while循环执行下去的条件,不同题目可能不尽相同。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号