双指针算法

双指针,算法书上称为尺取法,用来解决序列的区间问题,操作简单,容易编程。如果区间是单调的,也常常可以用二分法求解,所以很多问题双指针和二分法都行。

双指针的概念

什么是双指针?为什么双指针能用来优化?考虑下面的应用背景:

(1)给定一个序列,有时需要它是有序的,先排序;

(2)问题和序列的区间有关,且需要操作两个变量,可以用两个下标(指针)i 和 j 扫描区间。

对于上面的应用,一般的做法是用 i 和 j 分别扫描区间,有二重循环,复杂度为O(n^{2})。以反向扫描(即 i 与 j 的方向相反)为例,代码如下:

for(int i = 0; i < n; i++)
{
	for(int j = n - 1; j > 0; j--)
	{
		//....
	}
} 

下面用双指针来优化上面的算法。实际上,双指针就是把一个二重循环变为一个循环,在这个循环中同时处理 i 和 j 。复杂度从O(n^{2})变为O(n)。具体代码如下:

//用while实现
int i = 0, j = n - 1;
while(i < j)
{//i和j在中间相遇,这样做还能防止i和j越界 
	//...
	i++;//i从头扫到尾 
	j--;//j从尾扫到头 
}

//用for循环实现
for(int i = 0, j = n - 1; i < j; i++, j--)
{
	//...	
} 

在双指针中,i 和 j 有以下两种扫描方式:

(1)反向扫描。i 和 j 方向相反,i 从头到尾,j 从尾到头,在中间相会。

(2)同向扫描。i 和 j 方向相同,都是从头到尾,速度不同,如让 j 跑在 i 前面。

把同向扫描的 i、j 指针称为“快慢指针”,把反向扫描的 i、j 指针称为“左右指针”,更加形象。其中,“快慢指针”在序列上产生了一个大小可变的“滑动窗口”,有灵活的应用,如寻找区间、数组去重、多指针问题。

反向扫描

我们用几个例子来说明反向扫描的编码:

(1)找指定和的整数对

问题描述:输入 n (n <= 100000)个整数,放在数组 a[ ] 中。找出其中两个数,它们之和等于整数 m(假设肯定有解)。所有的整数都为 int 型。

说明:输入样例的第一行是数组 a[ ] ,第二行是 m = 28。输出样例的 5 和 23,相加得28。

输入样例:

21 4 5 6 13 65 32  9 23

28

输出样例:

5 23

我们用双指针来实现,首先对数组从小到大进行排序;然后设置两个变量 i 和 j ,分别指向头和尾,i 初值为 0,j 初值为 n - 1,然后让 i 和 j 逐渐向中间移动,检查 a[ i ] + a[ j ],如果大于 m,就让 j 减 1,如果小于 m,就让 i 加 1,直到 a[ i ] + a[ j ] = m。排序的复杂度为O(nlogn),检查的复杂度为O(n),总复杂度为O(nlogn)。

void find_sum(int a[], int n, int m)
{//注意数组的输入,可以参考我的博文"scanf的返回值" 
	sort(a, a + n);//排序
	int i = 0, j = n - 1; //i指向头,j指向尾 
	while(i < j)
	{
		int sum = a[i] + a[j];
		if(sum > m)
		{
			j--;
		}
		if(sum < m)
		{
			i++;
		}
		if(sum == m)
		{
			cout << a[i] << " " << a[j] <

在这个例子中,双指针不仅效率高,而且不需要额外的空间

(2) 判断回文串(hdu 2029)

Palindromes _easy version

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 84956    Accepted Submission(s): 50935


 
Problem Description
“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。请写一个程序判断读入的字符串是否是“回文”。 
 
Input
输入包含多个测试实例,输入数据的第一行是一个正整数n,表示测试实例的个数,后面紧跟着是n个字符串。 
 
Output
如果一个字符串是回文串,则输出"yes",否则输出"no". 
 
Sample Input
4
level
abcde
noon
haha
Sample Output
yes
no
yes
no
 
Author
lcy
 
Source
C语言程序设计练习(五)

我们用双指针来实现:

#include
using namespace std;

int main()
{
	int n;
	cin >> n;
	
	while(n--)
	{
		string s;
		cin >> s;
		bool ans = true;//默认为true
		int i = 0, j = s.size() - 1;//双指针
		while(i < j)
		{
			if(s[i] != s[j])
			{
				ans = false;
				break;
			}
			i++;
			j--;
		} 
		if(ans)
		{
			cout << "yes" << endl;
		}
		else
		{
			cout << "no" << endl;	
		} 
	}
	
	return 0;
} 

 同向扫描

下面给出几个同向扫描的例子:

(1)寻找区间和

这是双指针法产生滑动窗口的经典例子

问题描述:给定一个长度为 n 的数组 a[ ] 和一个数 s,在这个数组中找到一个区间,使这个区间的数组元素之和等于 s。输出区间的起点和终点位置。

说明:输入样例的第一行是 n = 15,第二行是数组 a[ ],第三行是区间和 s = 6。输出样例共有四种情况。

输入样例:

15

6 1 2 3 4 6 4 2 8 9 10 11 12 13 14

6

输出样例:

0 0

1 3

5 5

6 7

指针 i 和 j (i <= j)都从头向尾扫描,判断区间 [ i , j ] 数组元素是否等于 s。

我们用双指针,具体步骤如下:

        (1)初始值 i = 0,j = 0,即开始都指向第一个元素 a[ 0 ]。定义 sum 是区间 [ i , j ] 数组元素和,初值 sum = a[ 0 ]。

        (2)如果 sum = s,输出一个解。继续。把 sum 减掉元素 a[ i ],并把 i 向后移动一位。

        (3)如果 sum > s,让 sum 减掉元素 a[ i ],并把 i 向后移动一位。

        (4)如果 sum < s,把 j 向后移动一位,并把 sum 的值加上这个新元素。

在上面的步骤中,有两个非常关键的技巧:

(1)滑动窗口的实现。窗口就是区间 [ i , j ] ,随着 i 和 j 从头到尾移动,窗口就“滑动”扫描了整个序列,检索了所有数据。i 和 j 并不是同步增加的,窗口像一只蚯蚓伸缩前进,它的长度是变化的,这个变化正对应了对区间的计算。

(2)sum 的使用。利用 sum,每次移动 i 或 j 时,只需要把 sum 加或减一次,就得到了区间和,复杂度为O(1)。这也是“前缀和”递推思想的应用。

void findsum(int *a, int n, int s)
{
	int i = 0, j = 0;
	int sum = a[0];
	while(j < n)
	{
		if(sum >= s)
		{
			if(sum == s)
			{
				cout << i << " " << j << endl;	
			}
			sum -= a[i];
			i++;
			if(i > j)
			{//防止i超过j 
				sum = a[i];
				j++;	
			}		
		}
		if(sum < s)
		{
			j++;
			sum += a[i];
		} 

	}	
} 
(2)数组去重

数组去重是很常见的操作,方法有很多,这里介绍双指针

        (1)将数组排序,排序后重复的整数就会挤到一起。

        (2)定义双指针 i 和 j,初值都指向 a[ 0 ]。i 和 j 都从头扫描数组 a[ ]。i 指针走得快,逐个遍历整个数组;j 指针走得慢,它始终指向当前不重复部分的最后一个数。也就是说,j 用于获得不重复的数。

        (3)扫描数组。快指针执行 i++,如果此时 a[ i ] 不等于慢指针 j 指向的 a[ j ],就执行 j++,并且把 a[ i ] 复制到慢指针 j 当前的位置 a[ j ]。

        (4)i 扫描结束后,a[ 0 ] ~ a[ j ] 就是不重复数组。

你可能感兴趣的:(算法竞赛--初级,算法)