算法通关村系列文章目录
前言
本系列文章是针对于鱼皮知识星球——编程导航中的算法通关村中的算法进行归纳和总结。 该篇文章讲解的是第三关中的白银挑战———双指针思想
在数组,字符串这些存放元素的连续空间中,如果后面的元素想要向前移动,为了保持后面元素的连续性,后面的元素就要整体移动,同样,如果在中间插入一个元素,其身后的元素也要整体向后移动,这就会导致有些算法都需要多轮,大量地移动元素,效率就会比较低,那为解决这一问题,我们来看一个针对于此的方式——双指针
一、双指针介绍
双指针不一定真的是指针,更多的情况下就是两个变量,因为我们经常用两个变量指向我们所要操作的元素,所以说着说着就把变量叫做双指针了。既然是叫双指针,那么一定会是有两个指针的。在双指针中,指针的走向可能有很多情况,比如说两个指针都从头开始走,这样的情况就叫做快慢双指针;如果两个指针分别从数组的两端开始,走向数组的内部,这样的情况就叫做对撞双指针。
不管是哪种类型的双指针,其核心都是一个指针的前方或者后面是符合条件的元素,一个指针去遍历数组元素,并通过一些变化将这些元素也变为符合条件的元素,并且标识符合条件元素指针移动,将这些符合条件的元素全部划分到它 “统治的名下” ,最后两个指针相遇代表着数组中所有的元素都已成为符合条件的元素。
下面我们还是通过LeetCode题目来进行双指针内容的解析
二、原地移除所有数组等于 val 的元素
LeetCode 27题:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
我们就这一道题解析两种双指针的用法
2.1 快慢双指针
在前言中,我们介绍了不管是哪种双指针,都会有一个指针,标记符合条件的元素,有一个指针遍历元素,将不符合条件的元素变得符合条件,以此来增加符合条件元素的数量。
那在快慢双指针中,我们把走的快的指针叫做 fast,把走的慢的指针叫做 slow 。并且slow指针用来标记符合条件的元素,fast指针用来遍历
步骤:
- slow和fast都初始化为0,指向数组的第一个元素,因为这里的数组不是升序或者降序等特殊序列,所以无法判断第一个元素是不是符合条件,所以直接赋值为0
- fast要遍历数组,每遍历一个元素,就要判断,该题是要删除值为 val 的元素。当fast判断该元素符合条件后就不用管,将fast指向的元素赋值给slow指向的元素,因为slow表示的含义是slow-1前的元素都符合条件,slow指向的元素是要进行处理的元素,而fast是进行判断的,fast判断0符合条件,将0赋值给slow指向的元素,这样slow就可以向前移动一位
- 当fast判断该元素不符合条件后,slow指针不动,毕竟现在这个元素不符合条件,也不应该将其包括进去。fast指针继续移动,直到移动到一个符合条件的元素,用该元素的值覆盖掉slow指向的值,这样slow指向的值就是符合条件的,然后slow就可以移动了
public static int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) nums[slow++] = nums[fast];
}
return slow;
}
2.2 对撞双指针
看过同向移动的快慢双指针,我们接下来再看不同向的对撞双指针,对撞双指针就要两侧同时移动了
步骤:
- left指针从0开始,right指针从数组末尾开始,并且left指针前的元素是符合条件的元素,right指针后的元素都是不符合条件的元素
- 当left指向的元素不等于 val 符合条件 left 向后移动一位
- 当right指向的元素等于 val 不符合条件,right向前移动一位
- 当left指向的元素等于 val 不符合条件,同时 right指向的元素 不等于 val,符合条件,这时候,交换left和right所指的元素,这样 left左边就都是符合条件,right右边就都是不符合条件
代码:
public static int removeElementCash(int[] nums, int val) {
int left = 0;
int right = nums.length - 1;
for (left = 0; left <= right; ) {
// 没找到
if ((nums[left] == val) && (nums[right] != val)) {
// 这里一定是交换值,而不是简单的把nums[right]赋值给nums[left],因为当right检测到一个不是val的值时
// 如果不交换,right就一直停留在这个值,不会继续向前检索
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
if (nums[left] != val) left++;
if (nums[right] == val) right--;
}
return left;
}
三、元素奇偶移动
LeetCode 905
给你一个整数数组 nums,将 nums 中的的所有偶数元素移动到数组的前面,后跟所有奇数元素。
返回满足此条件的 任一数组 作为答案:
输入:nums = [3,1,2,4]
输出:[2,4,3,1]
解释:[4,2,3,1]、[2,4,1,3] 和 [4,2,1,3] 也会被视作正确答案。
这种分成两个类型的就十分适合对撞双指针来写,其写法跟上面的对撞双指针大同小异。那这里就不给解析了,直接贴出来代码
public static int[] sortArrayByParity(int[] nums){
int left=0;
int right=nums.length-1;
for (left=0;left<=right;){
if((nums[left]%2!=0&&nums[right]%2==0)){
int temp=nums[right];
nums[right]=nums[left];
nums[left]=temp;
}
if(nums[left]%2==0) left++;
if(nums[right]%2!=0) right--;
}
return nums;
}
四、数组的区间问题
LeetCode 228:
给定一个 无重复元素 的 有序 整数数组 nums 。
返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。
输入:nums = [0,1,2,4,5,7]
输出:[“0->2”,“4->5”,“7”]
解释:区间范围是:
[0,2] --> “0->2”
[4,5] --> “4->5”
[7,7] --> “7”
这道题我们用快慢双指针做
步骤:
- slow和mi之间的元素是符合条件的,fast负责找符合条件的元素 相当于 slow是某个区间的左端点,mi是某个区间的右端点
- 当mi指向的元素+1不等于fast指向的元素,也就是说这时候mi和fast不连续了,那就不符合条件,所以就要把slow-fast之间的元素摘出来,并且,让slow和mi都等于现在都fast
- 最后一个元素当fast遍历完的时候,一定会剩下一个元素或者一个区间没有被摘出来,这时候就需要有一个收尾的工作
三指针代码:
public List<String> summaryRanges(int[] nums) {
List<String> strings = new ArrayList<>();
if(nums.length==0) return strings ;
int slow=0;
int fast=0;
int mi=0;
for ( fast = 1; fast < nums.length; fast++) {
if(nums[mi]+1!=nums[fast]){
if (nums[slow]==nums[mi]) {
strings.add(nums[slow]+"");
}else {
strings.add(nums[slow]+"->"+nums[mi]);
}
slow=fast;
mi=fast;
}else {
mi++;
}
}
if(slow!= nums.length-1) strings.add(nums[slow]+"->"+nums[nums.length-1]);
if(slow== nums.length-1) strings.add(nums[nums.length-1]+"");
return strings;
}
这里研究后,发现fast也可以把mi的事情也做了,我们把这里的fast当作mi,fast+1当作原来的fast,也可以,让slow和fast之间的元素是符合条件的,这样更加清晰
双指针代码:
public static List<String> summaryRanges(int[] nums){
List<String> strings = new ArrayList<>();
int slow=0;
int fast=0;
// int mi=0;
// 这个fast既起到了我们的mi又相当于fast
for ( fast = 0; fast < nums.length; fast++) {
if(fast+1== nums.length||nums[fast]+1!=nums[fast+1]){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(nums[slow]);
if (nums[slow]!=nums[fast]) {
stringBuilder.append("->"+nums[fast]);
}
strings.add(stringBuilder.toString());
slow=fast+1;
// mi=fast;
}
// else {
// mi++;
// }
}
// if(slow!= nums.length-1) strings.add(nums[slow]+"->"+nums[nums.length-1]);
// if(slow== nums.length-1) strings.add(nums[nums.length-1]+"");
return strings;
}
总结
到这里双指针的简单运用就结束了,其本质还是一个指针划分符合条件的元素,一个指针寻找符合条件的元素,还是比较好理解的,后面有时间会将这一关的黄金挑战列出来,也就是双指针一些比较难的一些题目,但是思想还是那个思想,只是在处理过程中需要考虑的事情变多了,仔细研究后还是能相通的