双指针,算法书上称为尺取法,用来解决序列的区间问题,操作简单,容易编程。如果区间是单调的,也常常可以用二分法求解,所以很多问题双指针和二分法都行。
双指针的概念
什么是双指针?为什么双指针能用来优化?考虑下面的应用背景:
(1)给定一个序列,有时需要它是有序的,先排序;
(2)问题和序列的区间有关,且需要操作两个变量,可以用两个下标(指针)i 和 j 扫描区间。
对于上面的应用,一般的做法是用 i 和 j 分别扫描区间,有二重循环,复杂度为O()。以反向扫描(即 i 与 j 的方向相反)为例,代码如下:
cpp
for(int i = 0; i < n; i++)
{
for(int j = n - 1; j > 0; j--)
{
//....
}
}
下面用双指针来优化上面的算法。实际上,双指针就是把一个二重循环变为一个循环,在这个循环中同时处理 i 和 j 。复杂度从O()变为O(n)。具体代码如下:
cpp
//用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)。
cpp
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] <<endl;//打印一个答案
i++;//可能有多个答案,继续查找
}
}
}
在这个例子中,双指针不仅效率高,而且不需要额外的空间
(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): 50935Problem 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语言程序设计练习(五)
我们用双指针来实现:
cpp
#include<bits/stdc++.h>
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)。这也是"前缀和"递推思想的应用。
cpp
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 ] 就是不重复数组。