算法介绍
双指针是一种非常重要的基础算法.
常见的双指针有两种形式,⼀种是对撞指针,⼀种是快慢指针.
对撞指针:⼀般用于顺序结构中,也称左右指针.特点:从两端向中间夹逼,直到相遇.
快慢指针:两个移动速度不同的指针在数组或链表等序列结构上移动.特点:处理循环往复的情况.
习题讲解
1. 移动零
题目解析
难点:
这题的难点在于原地去操作数组,把所有0移动到所有非0元素的后面.
算法应用:
实际上,在操作过程中,数组就可以分为两部分,左边一部分全部是非0元素,右边一部分全部是0,这就可以使用双指针进行处理.cur指针用来遍历数组,dest指针用来标记最后一个非0元素的位置,确保dest是非0元素与0的分界点.
操作细节:
cur指针初始化在0下标位置,但是dest要初始化在-1下标位置,因为一开始不知道0位置是否为非0元素.
代码实现:
public void moveZeroes(int[] nums) {
for (int cur=0, dest=-1; cur<nums.length; cur++) {
//找到非0元素,开始交换
if (nums[cur]!=0) {
dest++;//最后一个非0元素位置,再向后一位
//交换cur和dest位置的元素
int tmp = nums[cur];
nums[cur] = nums[dest];
nums[dest] = tmp;
}
}
}
2. 复写零
题目解析
难点:
这题的难点仍在于原地去操作数组,把所有0复写一遍.
算法应用:
这道题在操作过程中,需要注意必须从后往前进行复写,一位从前往后的顺序会覆盖许多元素.那我们就需要先找到最后一个需要复写的0.这个过程中就可以使用双指针,cur指针用来遍历数组,dest指针用来模拟复写的过程.
当cur等于0时,dest向后移动两位;cur不等于0时,dest向后移动一位,dest越界时,进行判断是否越界到n(只有cur指向0时才可能出现越界情况)(若是,则n-1位置的值改为0,cur向前移动一位,dest向前移动两位,说明这个cur所指的0不能完成复写),cur位置所指的数就是最后一个需要复写的数,随后从后往前进行复写.
操作细节:
cur指针初始化在0下标位置,但是dest要初始化在-1下标位置,对于和cur位置下数有关的dest的移动方式,通常将dest初始化在-1位置.
判断dest位置的操作要在cur移向下一个位置之前.
代码实现:
public void duplicateZeros(int[] arr) {
int cur,dest,n = arr.length;
for (cur=0, dest=-1; cur<n; cur++) {
if (arr[cur]!=0) {
dest++;
} else {
dest+=2;
}
//判断操作要在cur移向下一个位置之前
if (dest>=n-1) break;
}
//判断是否越界
if (dest==n) {
arr[n-1] = 0;
cur--;
dest-=2;
}
//复写
while (cur>=0) {
if (arr[cur]==0) {
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
} else {
arr[dest--] = arr[cur--];
}
}
}
3. 快乐数
题目解析
难点:
这题的难点仍在于无限循环地去操作一个数字.
算法应用:
要解决这道题,需要有对于无限循环这个操作的理解.题目中告诉我们是否为快乐数的判断标准是:操作到最后是否为1,无论对1进行多少次操作得到的结果恒为1,这就形成了一个环.问题就转化为了:判断一个(隐式)链表是否有环的问题.就可以使用快慢指针算法,慢指针走一步,快指针走两步,判断这两个指针是否最后在1处相遇,就是判断这个数是否为快乐数的标准.
操作细节:
slow指针初始化为要判断的数n,fast指针初始化为操作过一次的数happy(n).出循环条件是fast与slow指向的数相等,初始化若都指向n,就不会进入循环.
代码实现:
public boolean isHappy(int n) {
int slow = n;
int fast = happy(n);
while(fast!=slow) {
//如果fast能到1,那么slow也必能到1,最后会在1相遇
if(fast==1) {
return true;
}
fast = happy(happy(fast));
slow = happy(slow);
}
return slow==1;
}
public int happy(int n) {
int ret = 0;
while(n!=0) {
ret+=(n%10)*(n%10);
n/=10;
}
return ret;
}
4. 盛水最多的容器
题目解析
难点:
这题的难点在于不能使用暴力枚举的方式来解决,而要找到盛水量与容器左右壁的关系.
算法应用:
要解决这道题,要找到盛水量与容器左右壁的关系----盛水量的多少实际上就是由左右壁中较短的一条决定.(容积 = min(左壁,右壁) * 宽度)这里可以使用对撞指针的思想来处理.
我们假设某种状态下容器左壁小于容器右壁,即nums[left]<nums[right].随后,由于采用对撞指针,不论是left++,还是right--,容器宽度都必然减少.
如果让left++,改变nums[left](较短的那个容器壁),就会导致变化后的盛水高度肯定不大于nums[right],这样我们并不知道容积具体的变化情况;然而,我们如果让right--,改变nums[right](较长的那个容器壁)就会导致变化后的盛水高度肯定不大于nums[left],宽度又是在减小的,就保证了变化后的容积必然小于当前容积,由于要找最大的容积,这种情况应该直接舍去.
经过上述分析,可以得到结论: 采用对撞指针,对于每一种情况先判断是否更新最大值,再移动时应该移动指向较短的一条边的指针.
操作细节:
计算容积时的宽度是right-left.
代码实现:
public int maxArea(int[] height) {
int left = 0,right = height.length-1;
int min = 0,area = 0;
while(left<right) {
min = Math.min(height[left],height[right]);
area = Math.max(area,min*(right-left));
if(height[left]<height[right]) left++;
else right--;
}
return area;
}
5. 有效三角形的个数
题目解析
难点:
这题的难点在于不能使用暴力枚举的方式来解决,而要找到三角形边长间的单调关系.
算法应用:
要解决这道题,要找到三角形边长间的单调关系.
我们规定三角形三边长分别为a,b,c且a<=b<=c,那么要构成有效的三角形,就只要保证a+b>c.
先对这个数组进行排序,便于寻找单调性.我们就可以先固定最长的一条边c,在它的左边的区间范围中找一个a,一个b.这里我们就可以使用对撞指针的思想,让a=nums[left],b=nums[right].
如果a+b>c,即nums[left]+nums[right]>c,说明a无论取nums[left]~nums[right-1]范围内的值(由于数组有序这些值都大于nums[left]),都能与b组合形成有效的三角形(b视作不动).直接计数,随后让right--.
如果a+b<=c,即nums[left]+nums[right]<=c,说明b无论取nums[left+1]~nums[right]范围内的值(由于数组有序这些值都大于nums[right]),都不能与a组合形成有效的三角形(a视作不动),直接让left++.
操作细节:
固定最长边的时候为了防止重复计算,从后往前遍历固定.
计算有效组数时,有效组数应为right-left.
代码实现:
public int triangleNumber(int[] nums) {
Arrays.sort(nums);
int ret = 0,n = nums.length;
for(int i=n-1; i>=2; i--) {
int left = 0,right = i-1;
while(left<right) {
if(nums[left]+nums[right]>nums[i]) {
ret+=right-left;
right--;
}else {
left++;
}
}
}
return ret;
}
6. 两数之和
题目解析
难点:
这道题难点在于不能使用暴力算法,计算两个和为s的数字.
算法应用:
我们要观察到这个数组是一个递增数组,就可以利用单调性+双指针(这里是对撞指针)来解决问题.同样定义两个指针left和right.
如果nums[left]+nums[right]<target,说明nums[left]与nums[left+1]~nums[right]范围内的值相加必定小于target,不能得到target.
如果nums[left]+nums[right]>target,说明nums[right]与nums[left]~nums[right-1]范围内的值都相加必定大于target,不能得到target.
如果相等就直接记录下对应的nums[left]和nums[right],直接构造数组返回.
操作细节:
最后多返回一个空数组,来照顾编译器.
代码实现:
public int[] twoSum(int[] nums, int target) {
int left = 0,right = nums.length-1;
while(left<right) {
if(nums[left]+nums[right]>target) {
right--;
}else if(nums[left]+nums[right]<target) {
left++;
}else {
return new int[]{nums[left],nums[right]};
}
}
return new int[]{0};
}
7. 三数之和
题目解析
难点:
这题相较于上一题,多加了一个元素并且要找出所有不重复的三元组,即这三个元素的和为0.
算法应用:
处理过程中,我们仍然可以使用上一题的思想.但是在此之前,需要先对数组进行排序(这题的条件中没有数组递增).
随后,从前向后,先去固定一个数nums[i],再在剩下的区间中,找到两个数的和为target=-nums[i],使用上一题的策略即可,这里不过多赘述.找到一组对应三元组后,应该继续缩小区间寻找,防止遗漏符合条件的三元组.
操作细节:
由于答案中不能包含重复的三元组,在对nums[i]的固定和left,right指针移动时,都需要考虑去重.因此,i++的自增条件不能直接放入for循环中.
代码实现:
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ret = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for(int i = 0; i < n; )
{
if(nums[i] > 0) break;
int left = i + 1, right = n - 1, target = -nums[i];
while(left < right)
{
int sum = nums[left] + nums[right];
if(sum > target) right--;
else if(sum < target) left++;
else {
ret.add(
new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));
left++; right--;
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
}
i++;
while(i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
8. 四数之和
题目解析
难点:
这道题相较于上一题的条件和要求,仅仅是增加了一个元素,从三元组变成了四元组,并且这四个元素的和又变成了target.
算法应用:
如果你已经掌握了三数之和这道题目,本题只需要在这个双指针操作之外再包一层循环,先固定nums[i],再固定nums[j],随后使用双指针即可.
操作细节:
本题对nums[i],nums[j],nums[left],nums[right]都需要进行去重.
在对计算所要求的nums[left],nums[right]的和aim时,需要将其定义为long类型,因而需要对target强转为long类型.
代码实现:
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> ret = new ArrayList<>();
int n = nums.length;
for(int i=0; i<n;) {
for(int j=i+1; j<n;) {
int left = j+1,right = n-1;
long aim = (long)target-nums[i]-nums[j];
while(left<right) {
if(nums[left]+nums[right]>aim) {
right--;
}else if(nums[left]+nums[right]<aim) {
left++;
}else {
ret.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right]));
left++;right--;
while(left<right&&nums[left] == nums[left-1]) left++;
while(left<right&&nums[right] == nums[right+1]) right--;
}
}
j++;
while(j<n&&nums[j]==nums[j-1]) j++;
}
i++;
while(i<n&&nums[i]==nums[i-1]) i++;
}
return ret;
}