《算法笔记》总结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

代码如下:

#include<iostream>
#include <vector>
#include <algorithm> 
using namespace std;

struct item{
	int i=0,j=0;
};

int  count(vector<int> V,vector<item>& I,int x)
{
	int ans=0;
	int i=0,j=V.size()-1;
	while(i<j)
	{
		if(V[i]+V[j]==x)
		{
			ans++;
			item temp;
			temp.i=i;
			temp.j=j;
			I.push_back(temp);
			i++;
			j--; 
		}
		else if(V[i]+V[j]>x)
			j--;
		else if(V[i]+V[j]<x)
			i++;
	}
	return ans;
}

int main(){
	int num=0;
	cout<<"请输入数组个数:"<<endl;
	cin>>num;
	cout<<"请依次输入元素:"<<endl;
	vector<int> V;
	vector<item> I;
	for(int i=1;i<=num;i++)
	{
		int temp=0;
		cin>>temp;
		V.push_back(temp);
	}
	sort(V.begin(),V.end());
	int x=0;
	cout<<"请输入待查询值:"<<endl;
	cin>>x;
	int ans=count(V,I,x);
	cout<<"符合要求的答案有"<<ans<<"个:"<<endl;
	
	for(int a=0;a<=I.size()-1;a++)
		cout<<I[a].i<<" "<<I[a].j<<endl;
}

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

返回的值是下标~

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

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

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

2.序列合并

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

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

代码如下:

#include<iostream>
#include <vector>
#include <algorithm> 
using namespace std;

void count(vector<int> A,vector<int> B,int x,int y)
{
	int i=0,j=0;
	while(i<x&&j<y) //当有某一个序列遍历完之后结束 
	{
		if(A[i]<=B[j]){
			cout<<A[i]<<endl;
			i++;
		}
		else{
			cout<<B[j]<<endl;
			j++;
		} 
	}
	//将未遍历完的那一个按序输出~ 
	while(i<x)
	{
		cout<<A[i]<<endl;
		i++;
	}
	while(j<y)
	{
		cout<<B[j]<<endl;
		j++;
	} 
}

int main(){
	int num1=0,num2=0;
	vector<int> A;
	vector<int> B;
	
	cout<<"请输入A数组个数:"<<endl;
	cin>>num1;
	cout<<"请依次输入元素:"<<endl;
	for(int i=1;i<=num1;i++)
	{
		int temp=0;
		cin>>temp; 
		A.push_back(temp);
	}
	sort(A.begin(),A.end());
	
	cout<<"请输入B数组个数:"<<endl;
	cin>>num2;
	cout<<"请依次输入元素:"<<endl;
	for(int i=1;i<=num2;i++)
	{
		int temp=0;
		cin>>temp;
		B.push_back(temp);
	}
	sort(B.begin(),B.end());	

	count(A,B,num1,num2);

}

几个点需要注意一下:

  • 这里博主调用了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的函数,本质上没有发生任何改变:

vector<int> count(vector<int> A,vector<int> B)
{
	int i=0,j=0;
	int x=A.size(),y=B.size();
	vector<int> C;
	while(i<x&&j<y) //当有某一个序列遍历完之后结束 
	{
		if(A[i]<=B[j]){
			C.push_back(A[i]);
			i++;
		}
		else{
			C.push_back(B[j]);
			j++;
		} 
	}
	while(i<x)
	{
		C.push_back(A[i]);
		i++;
	}
	while(j<y)
	{
		C.push_back(B[j]);
		j++;
	} 
	return C;
}

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

合并函数如下:

vector<int> merge(vector<int> V,int x1,int y1,int x2,int y2)
{
	int i=x1-1,j=x2-1;//下标和位序相差一,这里符合惯性思维的赋值方式~ 
	vector<int> V1;
	while(i<=y1-1&&j<=y2-1) //当有某一个序列遍历完之后结束 
	{
		if(V[i]<=V[j]){
			V1.push_back(V[i]);
			i++;
		}
		else{
			V1.push_back(V[j]);
			j++;
		} 
	}
	while(i<=y1-1)
	{
		V1.push_back(V[i]);
		i++;
	}
	while(j<=y2-1)
	{
		V1.push_back(V[j]);
		j++;
	} 
	return V1;
}

在main函数测试一下效果:

int main(){
	int num1=0,num2=0;
	vector<int> A;
	vector<int> B;
	
	cout<<"请输入A数组个数:"<<endl;
	cin>>num1;
	cout<<"请依次输入元素:"<<endl;
	for(int i=1;i<=num1;i++)
	{
		int temp=0;
		cin>>temp; 
		A.push_back(temp);
	}
	int x1=0,x2=0,y1=0,y2=0;
	cout<<"请依次输入4个区间:";
	cin>>x1>>y1>>x2>>y2;
	vector<int> C;
	C=merge(A,x1,y1,x2,y2);
	for(int i=0;i<=C.size()-1;i++)
		cout<<C[i]<<" ";

}

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

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

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

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

void mergeSort(vector<int> &V,int left,int right)
{
	if(left<right)
	{
		int mid =(left+right)/2;
		mergeSort(V,left,mid);
		mergeSort(V,mid+1,right);
		merge(V,left,mid,mid+1,right);
	}
} 

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

2.非递归实现 

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

三.快速排序 

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


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

#include <stdio.h>
#include <iostream>
#include <math.h>
#include <algorithm>
using namespace std;
int part(int* r, int low, int hight)  //划分函数
{
	int i = low, j = hight, pivot = r[low]; //基准元素
	while (i < j)
	{
		while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
		{
			j--;
		}
		if (i < j)
		{
			swap(r[i++], r[j]);  //r[i]和r[j]交换后 i 向右移动一位
		}
		while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
		{
			i++;
		}
		if (i < j)
		{
			swap(r[i], r[j--]);  //r[i]和r[j]交换后 i 向左移动一位
		}
	}
	return i;  //返回最终划分完成后基准元素所在的位置
}
void Quicksort(int* r, int low, int hight)
{
	int mid;
	if (low < hight)
	{
		mid = part(r, low, hight);  // 返回基准元素位置
		Quicksort(r, low, mid - 1); // 左区间递归快速排序
		Quicksort(r, mid+1, hight); // 右区间递归快速排序
	}
}
int main()
{
	int a[10001];
	int  N;
	cout << "请输入要排序的数据的个数: " << endl;
	cin >> N;
	cout << "请输入要排序的数据: " << endl;
	for (int i = 0; i < N; i++)
	{
		cin >> a[i];
	}
	cout << endl;
	Quicksort(a, 0, N - 1);
	cout << "排序后的序列为: " << endl;
	for (int i = 0; i < N; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
	return 0;
}

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

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

相关推荐

  1. 指针算法笔记

    2024-07-18 14:38:01       37 阅读
  2. 代码随想录day8--字符串总结指针总结

    2024-07-18 14:38:01       58 阅读
  3. 算法训练day10字符串总结指针回顾

    2024-07-18 14:38:01       52 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 14:38:01       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 14:38:01       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 14:38:01       58 阅读
  4. Python语言-面向对象

    2024-07-18 14:38:01       69 阅读

热门阅读

  1. 计算机视觉篇5 图像的位置--边框

    2024-07-18 14:38:01       18 阅读
  2. 大龄程序员的出路在哪里?

    2024-07-18 14:38:01       23 阅读
  3. [ptrade交易实战] 第十六篇 融资融券查询类函数!

    2024-07-18 14:38:01       25 阅读
  4. 项目随笔【环境变量的获取】

    2024-07-18 14:38:01       21 阅读
  5. 15.FreeRTOS_资源管理

    2024-07-18 14:38:01       23 阅读
  6. 牛奶供应(三)

    2024-07-18 14:38:01       17 阅读
  7. 正则表达式

    2024-07-18 14:38:01       22 阅读
  8. AI发展下的伦理挑战,应当如何应对?

    2024-07-18 14:38:01       18 阅读
  9. SDF学习笔记整理

    2024-07-18 14:38:01       24 阅读
  10. 24/07/18数据结构(7.1220)队列实现

    2024-07-18 14:38:01       22 阅读
  11. HOW - SVG 图标组件封装(Lucide React)

    2024-07-18 14:38:01       22 阅读
  12. Linux-快捷键以及vim工具使用

    2024-07-18 14:38:01       20 阅读