双指针的妙用-算法通关村
1.双指针思想
- 所谓的双指针其实就是两个变量,不一定真的是指针。
- 例:删除重复元素 [1,2,2,2, 3, 3,3,5,5,7,8],重复元素中保留一个。删除后的结果为 [1,2,3,5,7,8]。
- 使用双指针可以方便解决这个问题:
- 首先我们定义两个指针slow、fast。slow表示当前位置之前的元素都是不重复的,而fast则一直向后找,直到找到与slow位置不一样的,找到之后就将slow向后移动一个位置,并将arrlfast」复制给arr[slow],之后fast继续向后找,循环执行。找完之后slow以及之前的元素就都是单一的了。
- 这种一个在前一个在后的方式也称为快慢指针,从两端向中间走,这种就称为对撞型指针或者相向指针。还有一种比较少见的就是从中间向两边走。
2.删除元素专题
2.1原地移除所有数值等于 val 的元素
-
LeetCode27: 给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。要求:不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
-
例子1: 输入:nums = [3,2,2,3],val = 3 输出:2,nums = [2,2] 例子2: 输入:nums = [0,1,2,2,3,0,4,2],val = 2 输出:5,nums = [0,1,4,0,3](第二种方法结果) -
在删除的时候,从删除位置开始的所有元素都要向前移动,所以这题的关键是如果有很多值为val的元素的时候,如何避免反复向前移动呢?
第一种:快慢指针
-
整体思想就是定义两个指针slow和fast,初始值都是0。Slow之前的位置都是有效部分,fast表示当前要访问的元素。这样遍历的时候,fast不断向后移动:
- 如果num[fast]的值不为val,则将其移动到numslslow++J处。
- 如果nums[fast]的值为val,则fast继续向前移动,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]; slow++; } } return slow; }
第二种:对撞双指针
-
对撞指针,有的地方叫做交换移除,核心思想是从右侧找到不是val的值来顶替左侧是val的值。我们以nums =[0,1,2,2,3,0,4,2], val = 2为例:
-
public static int removeElement3(int[] nums, int val){ int left = 0, right = nums.length-1; for(left = 0; left <= right; ){ if((nums[left] == val) && (nums[right] != val)){ int temp = nums[left]; nums[left] = nums[right]; nums[right] = temp; } if(nums[left] != val) left++; if(nums[right] == val) right--; } return left; }
-
拓展:本题还可以进一步融合上面两种方式创造出:“对撞双指针+覆盖”法。当nums[left]等于val的时候,我们就将nums[right] 位置的元素覆盖nums[left],继续循环,如果nums[left] 等于val就继续覆盖,否则才让left++,这也是双指针方法的方法。
-
public static int removeElement4(int[] nums, int val){ int right = nums.length-1; for(int left = 0; left <= right; ){ if(nums[left] == val){ nums[left] = nums[right]; right--; }else{ left++; } } return right+1; }
2.2删除有序数组中的重复项
-
LeetCode26:给你一个有序数组nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组 并在使用O(1) 额外空间的条件下完成。
-
示例1: 输入:nums = [1,1,2] 输出:2,nums = [1,2] 解释:函数应该返回新的长度 2,并且原数组 nums 的前两个元素被修改为 1, 2。不需要考虑数组中超出新长度后面的元素。 例子2: 输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5,nums = [0,1,2,3,4] 解释:函数应该返回新的长度 5,并且原数组 nums 的前两个元素被修改为 0, 1, 2, 3, 4。不需要考虑数组中超出新长度后面的元素。 -
public static int removeDuplicates(int[] nums){ //slow表示可以放入新元素的位置 int slow = 1; //循环起到了快指针的作用 for(int fast = 0; fast < nums.length; fast++){ if(nums[fast] != nums[slow-1]){ nums[slow] = nums[fast]; slow++; } } return slow; }
3.元素奇偶移动专题
-
LeetCode905,按奇偶排序数组。给定一个非负整数数组 A,返回一个数组,在该数组中,A的所有偶数元素之后跟着所有奇数元素。你可以返回满足此条件的任何数组作为答案。
-
例如: 输入:[3, 1, 2, 4] 输出:[2, 4, 3, 1] 输出 [4, 2, 3, 1] [2, 4, 1, 3] [4, 2, 1, 3] 也会被接受。 -
可以采用对撞双指针方法:
-
维护两个指针 left=0 和 right=arr.length-1,left从0开始逐个检查每个位置是否为偶数,如果是则跳过,如果是奇数则停下来。然后right从右向左检查,如果是奇数则跳过偶数则停下来。然后交换arraylleftl和arraylright]。之后再继续巡循环,直到left>=right。
-
public static int[] sortArrayByParity(int[] arr){ int left = 0, right = arr.length-1; while(left < right){ if(arr[left] %2 > arr[right] %2){ int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } if(arr[left] %2 == 0) left++; if(arr[right] %2 == 1) right--; } return arr; }
4.数组轮转问题
-
LeetCode189: 给你一个数组,将数组中的元素向右轮转K个位置,其中K是非负数。
-
例如: 输入:nums = [1, 2, 3, 4, 5, 6, 7],k = 3 输出:[5, 6, 7, 1, 2, 3, 4] 解释: 向右轮转 1 步:[7, 1, 2, 3, 4, 5, 6] 向右轮转 2 步:[6, 7, 1, 2, 3, 4, 5] 向右轮转 3 步:[5, 6, 7, 1, 2, 3, 4] -
两轮反转:
- 首先对整个数组实行翻转,例如 [1,2,3,4,5,6,7] 我们先将其整体翻转成 [7,6,5.4,3,2,1]。
- 从k处分隔成左右两个部分,这里就是根据k将其分成两组 [7,6,5] 和 [4,3,2,1]。
- 最后将两个再次翻转就得到 [5,6,7] 和 [1,2,3,4] ,最终结果就是 [5,6,7,1,2,3,4]。
-
public void rotate(int[] nums, int k){ k %= nums.length; reverse(nums, 0, nums.length-1); reverse(nums, 0, k-1); reverse(nums, k, nums.length-1); } public void reverse(int[] nums, int start, int end){ while(start < end){ int temp = nums[start]; nums[start] = nums[end]; nums[end] = temp; start++; end--; } }
5.数组的区间问题
-
LeetCode228.给定一个无重复元素的有序整数数组nums。返回恰好覆盖数组中所有数字的最小有序区间范围列表。也就是说,nums的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字x。列表中的每个区间范围 [a,b] 应该按如下格式输出:“a->b”,如果 a != b"a",如果 a==b。
-
示例1: 输入:nums = [0, 1, 2, 4, 5, 7] 输出:[“0->2”,“4->5”,“7”] 解释:区间范围是: [0, 2]—> “0->2” [4, 5]—>“4->5” [7, 7]—>“7” -
示例2: 输入:nums = [0, 2, 3, 4, 6, 8, 9] 输出:[“0”,“2->4”,“6”,“8->9”] 解释:区间范围是: [0, 0]—>“0” [2, 4]—>“2->4” [6, 6]—>“6” [8, 9]—>“8->9” -
慢指针指向每个区间的起始位置,快指针从慢指针位置开始向后遍历直到不满足连续递增(或快指针达到数组边界),则当前区间结束;然后将 slow指向更新为 fast +1,作为下一个区间的开始位置,fast继续向后遍历找下一个区间的结束位置,如此循环,直到输入数组遍历完毕。
-
public static List<String> summaryRanges(int[] nums){ List<String> res = new ArrayList(); //slow 初始指向第一个区间的起始位置 int slow = 0; for(int fast = 0; fast < nums.length; fast++){ //fast 向后遍历,直到不满足连续递增 //或者,fast 到达数组边界 if(fast +1 == nums.length || nums[fast]+1 == nums[fast+1]){ //将当前区间 [slow, fast] 写入结果列表 StringBuilder sb = new StringBuilder(); sb.append(nums[slow]); if(slow != fast){ sb.append("->").append(nums[fast]); } res.add(sb.toString()); //将 slow 指向更新为 fast+1,作为下一个区间的起始位置 slow = fast+1; } } return res; }
6.字符串替换空格问题
-
剑指offer中的题目:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
-
public String replaceApace(StringBuffer str){ StringBuilder res = new StringBuilder(); for(int i = 0; i < str.length(); i++){ char c = str.charAt(i); if(c == ' '){ res.append("%20"); }else{ res.append(c); } } return res.toString(); }
-
比较好的方式是可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,每替换一个空格,长度增加2,即替换之后的字符串长度为:
- 新串的长度=原来的长度+2*空格数目
- 接下来从字符串的尾部开始复制和替换,用两个指针fast和slow分别指向原始字符串和新字符串的末尾,然后:slow不动,向前移动fast:
• 若指向的不是空格,则将其复制到slow位置,然后fast和slow同时向前一步;
• 若fast指向的是空格,则在slow位置插入一个%20,fast则只移动一步。
循环执行上面两步,便可以完成替换。详细过程如下:
-
public String replaceSpace(StringBuffer str){ if(str == null){ return null; } int numOfBlank = 0;//空格数量 int len = str.length(); for(int i = 0; i < len; i++){//计算空格数量 if(str.charAt(i) == ' '){ numOfBlank++; } } str.setLength(len + 2*numOfBlank); int fast = len-1; int slow = (len + 2*numOfBlank) -1; while(fast > 0 && slow > fast){ char c = str.charAt(fast); if(c == ' '){ fast--; str.setCharAt(slow--, '0'); str.setCharAt(slow--, '2'); str.setCharAt(slow--, '%'); }else{ str.setCharAt(slow, c); fast--; slow--; } } return str.toString(); }