数组题目中有两种双指针的应用:左右指针和快慢指针。
目录如下:
双指针(左右)
双指针(快慢)
相关题目有:
167,两数之和Ⅱ
344,反转字符串
26,删除排序数组中的重复项
27,移除元素
283,移动零
485,最大连续1的个数
540,有序数组中的单一元素
209,长度最小的子数组
双指针(左右)
左右指针在数组中实际是指两个索引值,一般初始化为left = 0, right = nums.length - 1
。循环条件为 while(left < right)
。
在上篇文章二分查找中,凸出了双指针特性。在下面题目中体会。
看到题目要求有【原地修改】的要求,一般都采用双指针。
167,两数之和Ⅱ,easy
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
返回的下标值(index1
和 index2
)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
题解
只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节left
和right
可以调整sum
的大小
代码
class Solution {
public int[] twoSum(int[] numbers, int target) {
int n = numbers.length;
int left = 0;
int right = n - 1;
while(left < right){
if(numbers[left] + numbers[right] == target)
return new int[]{left + 1, right + 1};
else if(numbers[left] + numbers[right] < target)
left++;
else
right--;
}
return new int[]{0,0};
}
}
344,反转字符串,easy
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
代码
class Solution {
public void reverseString(char[] s) {
if(s == null) return;
int left = 0;
int right = s.length - 1;
while(left < right){
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
}
双指针(快慢)
快慢指针在数组中实际是指两个索引值,快指针始终在慢指针的前面,一般初始化为 slow = 0, fast = 0 或 1
。循环条件为 while(fast < nums.length)
。
框架
public int f&s(int[] nums) {
if(nums.length == 0 || nums == null) return 0;
int slow = 0;
int fast = 0 或 1;
while(fast < nums.length){
//当前元素值不是所找的
if(nums[fast] != ...){
//看slow、fast具体含义
...
slow = ...
}
//否则,fast继续向前找
fast++;
}
return ...;
}
26,删除排序数组中的重复项,easy
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,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。
题解
快慢指针初始指向索引0和1,用快指针探路,慢指针保存没有重复元素的数组。
快指针向前,直到不和 当前慢指针指向元素 重复,慢指针索引+1,指向元素为新的数字。
代码
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length == 0) return 0;
int slow = 0;
int fast = 1;
while(fast < nums.length){
if(nums[slow] != nums[fast]){
slow++;
// 维护 nums[0..slow] 无重复
nums[slow] = nums[fast];
}
fast++;
}
// 数组长度为索引 + 1
return slow + 1;
}
}
**注:**类似题目:83.删除排序链表中的重复元素
27,移除元素,easy
你一个数组 nums
和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
题解
快慢指针初始都指向首个元素,快指针用于探路,如果不等于 val,则可以进行覆盖;如果等于 val,继续向前。
代码
class Solution {
public int removeElement(int[] nums, int val) {
if(nums.length == 0 || nums == null) return 0;
int slow = 0;
int fast = 0;
while(fast < nums.length){
//当前元素值不等于val
if(nums[fast] != val){
//建立新数组(覆盖在原数组上)
nums[slow] = nums[fast];
slow++;
}
//否则,fast继续向前找等于val的元素
fast++;
}
return slow;
}
}
细节
上一题要求是删除重复元素,fast 移到下一个不重复元素时,索引slow++
,并将fast 指向的值赋给 nums[slow]
,最后得到数组长度为 索引数slow
+ 1。
这里是先给nums[slow]
赋值然后再给slow++
,这样可以保证nums[0..slow-1]
是不包含值为val
的元素的,最后的结果数组长度就是slow
。
283,移动零,easy
给定一个数组 nums
,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
题解
与上一题类似,只是删除操作改成了移动操作。
代码
class Solution {
public void moveZeroes(int[] nums) {
if(nums.length == 0 || nums == null) return;
int slow = 0;
int fast = 0;
while(fast < nums.length){
if(nums[fast] != 0){
//交换
int temp = nums[fast];
nums[fast] = nums[slow];
nums[slow] = temp;
slow++;
}
fast++;
}
}
}
另一种解法
遍历此数组,如果当前元素不为0,就赋给慢指针所在位置的元素,遍历结束再将慢指针之后的元素赋为 0。可以看成建立了新的数组,但是是在原数组上改变的。
class Solution {
public void moveZeroes(int[] nums) {
if(nums.length == 0) return;
int index = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] != 0)
nums[index++] = nums[i];
}
for(int i = index; i < nums.length; i++){
nums[i] = 0;
}
}
}
485,最大连续1的个数,easy
给定一个二进制数组, 计算其中最大连续1的个数。
示例 1:
输入: [1,1,0,1,1,1]
输出: 3
解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3.
注意:
输入的数组只包含 0 和1。
输入数组的长度是正整数,且不超过 10,000。
题解
快慢指针初始都指向首个元素,快指针移至最后一个非 0 元素,fast - slow
即为当前连续1 的个数。当 nums[fast] = 0
时,slow
移至 fast
之后再开始统计。重复上述步骤直至 fast
遍历到最后一个元素。
代码
class Solution {
public int findMaxConsecutiveOnes(int[] nums) {
if(nums.length == 0 || nums == null) return 0;
int slow = 0;
int fast = 0;
int res = 0;
while(fast < nums.length){
if(nums[fast] != 1){
res = Math.max(res, fast - slow);
slow = fast + 1;
}
fast++;
}
//数组全1的情况
return Math.max(res, fast - slow);
}
}
细节
注意考虑数组全为 1 的情况,返回值为 fast - slow
。
另一种解法
一次遍历。两个计数器 count
、maxCount
作为计数指针 和 当前连续1 的最大个数。
class Solution {
public int findMaxConsecutiveOnes(int[] nums) {
if(nums.length == 0 || nums == null) return 0;
int count = 0;
int maxCount = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] != 0){
count++;
maxCount = Math.max(count, maxCount);
}else{
count = 0;
}
}
return maxCount;
}
}
下面两题稍有不同,但总体思想大致一致。
540,有序数组中的单一元素,medium
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: [3,3,7,7,10,11,11]
输出: 10
代码:
class Solution {
public int singleNonDuplicate(int[] nums) {
int slow = 0;
int fast = 1;
while(fast < nums.length){
//检查这一对数,如果不同返回第一个数
if(nums[slow] < nums[fast])
return nums[slow];
//否则,检查下一对数
if(nums[slow] == nums[fast]){
slow = slow + 2;
fast = fast + 2;
}
}
//nums仅有一个元素的特殊情况
return nums[slow];
}
}
209,长度最小的子数组,medium
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
题解
快慢指针来维护一个滑动窗口,相当于数组元素入队出队。具体过程可以用队列来理解,但代码实现用快慢指针。
代码
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int slow = 0;
int fast = 0;
int sum = 0;
int min = Integer.MAX_VALUE;
while(fast < nums.length){
//当前窗口中元素和 小于 s,加入元素
sum += nums[fast];
fast++;
//窗口内的元素和 大于等于 s 时,更新最小长度,并缩短窗口
while(sum >= s){
min = Math.min(min, fast - slow);
sum -= nums[slow];
slow++;
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
}