前言
双指针
常见的双指针有两种形式,一种是对撞指针,⼀种是左右指针。
对撞指针:一般用于顺序结构中,也称左右指针。
- 对撞指针从两端向中间移动。一个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
- 对撞指针的终止条件一般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
-
- left == right (两个指针指向同一个位置)
-
- left > right (两个指针错开)
快慢指针:又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。
这种方法对于处理环形链表或数组非常有用。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。
快慢指针的实现方式有很多种,最常用的⼀种就是:
- 在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢。
1. 有效三角形的个数(medium)
题目描述:
给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。
示例 1:
输入: nums = [2,2,3,4]
输出: 3
解释:有效的组合是:
2,3,4 (使用第一个 2)
2,3,4 (使用第二个 2)
2,2,3
示例 2:
输入: nums = [4,2,3,4]
输出: 4
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
2. 解法
- 解法一(暴力解法,会超时):
算法思路:
三层 for 循环枚举出所有的三元组,并且判断是否能构成三角形。
虽然说是暴力求解,但是还是想优化一下:
判断三角形的优化:
- 如果能构成三角形,需要满足任意两边之和要大于第三边。但是实际上只需让较小的两条边之和大于第三边即可。
- 因此我们可以先将原数组排序,然后从⼩到⼤枚举三元组,⼀⽅⾯省去枚举的数量,另一方面便判断是否能构成三角形。
算法代码:
class Solution {
public:
int triangleNumber(vector<int>& nums) {
// 1. 排序
sort(nums.begin(), nums.end());
int n = nums.size(), ret = 0;
// 2. 从⼩到⼤枚举所有的三元组
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
// 当最⼩的两个边之和⼤于第三边的时候,统计答案
if (nums[i] + nums[j] > nums[k])
ret++;
}
}
}
return ret;
}
};
- 解法二(双指针 - 对撞指针):
算法流程(附带算法分析,为什么可以使用对撞指针):
1.分析:
首先明确计算规则:从示例 1 可以知道,对于三元组 (2,3,4)(2,3,4)(2,3,4) 和 (4,3,2)(4,3,2)(4,3,2),我们只统计了其中的 (2,3,4)(2,3,4)(2,3,4),并没有把 (4,3,2)(4,3,2)(4,3,2) 也统计到答案中,所以题目意思是把这两个三元组当成是同一个三元组,我们不能重复统计。
既然有这样的规则,那么不妨规定三角形的三条边 a,b,c 满足:
1 ≤ a ≤ b ≤ c
这可以保证我们在统计合法三元组 (a,b,c)(a,b,c)(a,b,c) 的个数时,不会把 (c,b,a)(c,b,a)(c,b,a) 这样的三元组也统计进去。
由于三角形两边之和大于第三边,我们有
- a+b>c
- a+c>b
- b+c>a
上式中的a+c>b 是必然成立的,因为 a+c≥a+b>b(注意 a 至少是 1)。
同样的,b+c>a也必然成立,因为 b+c≥a+a=2a>a(注意 a 至少是 1)。
所以只需要考虑第一个式子,那么问题变成,从 nums 中选三个数,满足 1≤a≤b≤c 且 a+b>c 的方案数。
2.算法思路:
先将数组排序。
根据「解法一」中的优化思想,我们可以固定⼀个「最长边」,然后在比这条边小的有序数组中找出⼀个二元组,使这个而元组之和大于这个最长边。由于数组是有序的,我们可以利用「对撞指针」来优化。
设最长边枚举到 i 位置,区间 [left, right] 是 i 位置左边的区间(也就是比它小的区间):
- 如果 nums[left] + nums[right] > nums[i] :
- 说明 [left, right - 1] 区间上的所有元素均可以与 nums[right] 构成比nums[i] 大的二元组
- 满足条件的有 right - left 种
- 此时 right 位置的元素的所有情况相当于全部考虑完毕, right-- ,进⼊下⼀轮判断
- 如果 nums[left] + nums[right] <= nums[i] :
- 说明 left 位置的元素是不可能与 [left + 1, right] 位置上的元素构成满足条件
的⼆元组- left 位置的元素可以舍去, left++ 进入下轮循环
C++ 算法代码:
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(),nums.end());
int sum=0;
for(int i=nums.size()-1;i>1;i--)
{
int left=0,right=i-1;
while(left<right)
{
int s=nums[left]+nums[right];
if(s>nums[i])
{
sum+=right-left;
right--;
}
else
left++;
}
}
return sum;
}
};
Java 算法代码:
class Solution
{
public int triangleNumber(int[] nums)
{
// 1. 优化:排序
Arrays.sort(nums);
// 2. 利⽤双指针解决问题
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;
}
}
3.LCR 179. 查找总价格为目标值的两个商品
题目描述:
购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况,返回任一结果即可。
示例 1:
输入:price = [3, 9, 12, 15], target = 18
输出:[3,15] 或者 [15,3]
示例 2:
输入:price = [8, 21, 27, 34, 52, 66], target = 61
输出:[27,34] 或者 [34,27]
提示:
1 <= price.length <= 10^5
1 <= price[i] <= 10^6
1 <= target <= 2*10^6
4. 解法
- 解法一(暴力解法,会超时):
算法思路:
两层 for 循环列出所有两个数字的组合,判断是否等于目标值。
◦ 外层 for 循环依次枚举第 一个数 a ;
◦ 内层 for 循环依次枚举第二个数 b ,让它与 a 匹配;
ps : 这里有个魔鬼细节:我们挑选第二个数的时候,可以不从第⼀个数开始选,因为 a 前
⾯的数我们都已经在之前考虑过了;因此,我们可以从 a 往后的数开始列举。
然后将挑选的两个数相加,判断是否符合目标值
算法代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n = nums.size();
for (int i = 0; i < n; i++) { // 第⼀层循环从前往后列举第⼀个数
for (int j = i + 1; j < n; j++) { // 第⼆层循环从 i 位置之后列举第⼆个数
if (nums[i] + nums[j] == target) // 两个数的和等于⽬标值,说明我们已经找到结果了
return { nums[i], nums[j] };
}
}
return { -1, -1 };
}
};
2.算法思路:
a. 初始化 left , right 分别指向数组的左右两端(这里不是我们理解的指针,而是数组的下
标)
b. 当 left < right 的时候,一直循环
- 当 nums[left] + nums[right] == target 时,说明找到结果,记录结果,并且返回;
- 当 nums[left] + nums[right] < target 时:
- 对于 nums[left] 而言,此时 nums[right] 相当于是 nums[left] 能碰到的最大值(别忘了,这里是升序数组哈~)。如果此时不符合要求,说明在这个数组里面,没有别的数符合 nums[left] 的要求了(最大的数都满足不了你,你已经没救了)。因此,我们可以大胆舍去这个数,让 left++ ,去比较下⼀组数据;
- 那对于 nums[right] 而言,由于此时两数之和是小于目标值的, nums[right]还可以选择比nums[left] 大的值继续努力达到目标值,因此 right 指针我们按兵不动;
- 当 nums[left] + nums[right] > target 时,同理我们可以舍去nums[right] (最小的数都满足不了你,你也没救了)。让 right-- ,继续⽐较下⼀组数据,而left 指针不变(因为他还是可以去匹配比 nums[right] 更小的数的)。
C++ 算法代码:
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
int left = 0, right = nums.size() - 1;
while (left < right)
{
int sum = nums[left] + nums[right];
if (sum > target) right--;
else if (sum < target) left++;
else return { nums[left], nums[right] };
}
// 照顾编译器
return { -4941, -1 };
}
};
Java 算法代码:
class Solution
{
public int[] twoSum(int[] nums, int target)
{
int left = 0, right = nums.length - 1;
while (left < right)
{
int sum = nums[left] + nums[right];
if (sum > target) right--;
else if (sum < target) left++;
else return new int[] {nums[left], nums[right]};
}
// 照顾编译器
return new int[] {0};
}
}