双指针
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
- 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
- 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
left == right
(两个指针指向同一个位置)left > right
(两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
- 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
1. 移动零(easy)
「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部分。这种类型的题,⼀般就是使⽤「双指针」来解决。
1. 题目解析
283. 移动零 - 力扣(LeetCode)
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
提示:
1 <= nums.length <= 104
-231 <= nums[i] <= 231 - 1
进阶:你能尽量减少完成的操作次数吗?
2. 算法原理
快排的思想:数组划分区间-数组分两块
算法思路:
在本题中,我们可以⽤⼀个cur指针来扫描整个数组,另⼀个dest指针⽤来记录⾮零数序列的最后⼀个位置。根据cur在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在cur遍历期间,使[0, dest]
的元素全部都是⾮零元素,[dest + 1, cur - 1]
的元素全是零。
- 特点
- 给了一个数组
- 给制定了一个标准或规则
- 在这个规则下把数组划分成若干个区间
- 移动零
把所有的非零元素移到左边,所有的零元素移到右边
因此数组会在这个这个标准之下分成两个部分 - 双指针算法
数组中,用双指针算法的话,利用数组下标来充当指针
可以直接用下标来索引到里面的元素,没有必要真正定义一个指针
在数组区间中0~n-1,定义两个指针,dest(目的地)和cur(当前)
两个指针的作用
- cur:当前的意思,从左往右扫描整个数组,遍历数组
cur在扫描的时候会把数组分成左右两个部分
右边表示待处理的区间
左边表示处理过的区间 - dest:已处理的区间内,非零元素的最后一个位置
把左边处理过的区间又划分为两个小区间
左边小区间表示非零元素
右边小区间表示零元素
相当于分界线
当这两个指针从左往右走的时候,会把整个数组划分为3个区间
[0,dest]:已经处理过的区间,里面全都是非零元素
[dest+1,cur-1]:全都是0元素
[cur,n-1]:待处理的元素
指针是从左往右扫描的,扫描过的最右边就是cur-1
如果dest指针和cur指针在从左向右移动的过程中,一直都能让这三个区间保持这样一个性质的话,当cur指针移动到n位置的时候(把数组从左往右扫描过一遍之后,这个区间就划分成了)
当cur走到n的时候,待处理区间就不见了,不可能存在[n,n-1]
这个区间。转而就变成了,整个区间[0,n-1]
,被dest指针划分成两部分,最右边区间不存在的话,就剩左边两个区间了,左边部分是非零0元素,右边部分是零元素
要做的就是
在这两个指针从左向右移动的过程中,一直让这三个区间保持这样的性质
双指针算法的本质
这两个指针有这样一个作用,三个区间有这样一个性质的时候,就可以完成划分
如何划分
[0,1,0,3,12]
-
第一个指针,cur指针,初始化的时候应该先让它指向下标为0的位置
-
为了让dest指针表示非零元素的最后一个位置,刚开始扫描的时候没有非零元素,先让dest指针指向-1位置
刚开始还没有非零元素,非零元素不存在相当于这个区间不存在
-
接下来两个指针往右移动,dest先别动,因为cur是扫描的,先让cur移动,
cur在从前往后移动的过程中,会遇到两种情况-
遇到零元素,保证三个区间分别时零、非零、待处理,只需让cur指针向右移动一位即可
此时[dest+1,cur-1] = [0]
,只需cur往后移动一位
因此当碰到0元素的时候,不做任何处理,cur直接向后移动一位
-
遇到非零元素
-
要让1元素加入到最左边的区间里,当前最左边的区间在
[dest,0]=[-1]
,想加入它,相当于多了一个元素,dest向后移动一位
-
1要加入这个区间不能直接覆盖到这里,否则相当于把0直接给抹掉了
-
因此,先让dest+1,然后交换dest和cur位置的元素
[1,0,0,3,12]
,交换完之后相当于cur位置的元素已经处理过了,cur+1
- 此时三个区间
[1][0][0,3,12]
-
-
-
接下来碰到0元素不做任何处理,cur++,
[1][0,0][3,12]
-
当遇到3,非零元素的时候,要把这个非零元素加入到dest里面,加入以后,dest往后移动一位
不能直接覆盖,将dest和cur指向的元素互换
交换完以后这个元素就进入到非零元素区间,处理过以后cur直接++即可,
[1,3][0,0][12]
-
最后遇到12
dest++,dest和cur位置的元素交换,cur++,[1,3,12][0,0][]
cur遍历到n,完成了区间划分
算法流程
- 初始化cur = 0(⽤来遍历数组),dest = -1(指向⾮零元素序列的最后⼀个位置。
因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为-1 ) - cur依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:
- 遇到的元素是0,cur直接++。因为我们的⽬标是让
[dest + 1, cur - 1]
内的元素全都是零,因此当cur遇到0的时候,直接 ++ ,就可以让0在cur - 1的位置上,从⽽在[dest + 1, cur - 1]
内; - 遇到的元素不是0,dest++,并且交换cur位置和dest位置的元素,之后让cur++ ,扫描下⼀个元素。
- 因为dest指向的位置是⾮零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元素,那么它的位置应该在dest + 1的位置上,因此dest先⾃增1;
- dest++之后,指向的元素就是0元素(因为⾮零元素区间末尾的后⼀个元素就是0),因此可以交换到cur所处的位置上,实现
[0, dest]
的元素全部都是⾮零元素,[dest + 1, cur - 1]
的元素全是零。
总结
cur从前往后遍历的过程中
- 遇到0元素:
cur++;
- 遇到非0元素:
swap(dest+1, cur);
//先交换dest下一个位置的元素以及cur位置的元素,交换完之后
dest++;
cur++;
在从左往右扫描的过程中,一直这样做,就可以一直保持这三个区间是这样一个性质,当扫描完之后,这个数组就划分完了
快排
- 双指针算法是快速排序里面最核心的一步,数据划分
先给一个数组,选一个基准元素tmp
根据基准元素将数组划分成两个部分- 左边的部分让它全部<=tmp
- 右边的部分让它全部>tmp
- 整个数组分为三个部分
- 小于等于tmp的区间、大于tmp的区间、未处理的区间
- 后面的操作和双指针后面的步骤一模一样
- 如果数据量全都是相同的数的时候,时间复杂度逼近n^2,因此快排不能处理一些比较恶心的数据
后面会有颜色划分一题,把数组划分成三块,用那个思想来解决快排是最优化的解法
3. 编写代码
class Solution
{
public:
void moveZeroes(vector<int>& nums) {
for (int cur = 0, dest = -1; cur < nums.size(); cur++)
{
if (nums[cur]) //处理非0元素
{
swap(nums[++dest], nums[cur]);
//先让dest自增1,之后正好是自己想要的位置
//dest交换的时候没有毛病,dest同时也+1了
}
}
}
};
时间复杂度O(n)
空间复杂度O(1)