双指针的使用范围
对于暴力解法的时间复杂度来说,双指针一般可以将暴力解法的时间复杂度降低一个量级.
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针.
快慢指针
⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
对撞指针
⼀般⽤于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼
近。
• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循
环),也就是:
• left == right (两个指针指向同⼀个位置)
• left > right (两个指针错开)
练习题目
快慢指针
移动零
283. 移动零 - 力扣(LeetCode)
思路
cur从0开始,dest从-1开始,cur往后走,如果nums[cur]不为0, 则将nums[cur]和nums[++dest]交换
---->交换
后面的非0的值都会与前面存在的0进行交换,最终达到将0放到后面的效果.
代码
void moveZeroes(vector<int>& nums)
{
for(int cur = 0, dest = -1; cur < nums.size(); cur++)
if(nums[cur])
swap(nums[cur], nums[++dest]);//前加加是用原值加一后的值进行操作
}
注意dest是前++,这样才能对dest+1后的值进行操作!
复写零
1089. 复写零 - 力扣(LeetCode)
思路
这题双指针的做法是cur与dest从右向左,因为从左向右会导致后面需要后移的值被覆盖.由题意得4为最后一个复写的数是4,故可得(后续会提出计算cur的方法)
如果cur对应值不为零,则cur和dest同时向左移动,并且将cur对应值赋给dest,如图:
如果cur对应值为0,则dest对应值赋为0,且往左移动,再将移动后的对应值也赋为零,然后再往左移动.
------>
寻找cur开始所处的位置(最后复写的数):
cur和dest同时向右移动,如果cur对应值为0,则dest移动两步,直到dest到数组末尾为止.
防止dest越界
dest一次走两步可能走到nums[n],我们要防止这种情况发生:将最后一位置零cur左走一步,越界的dest往左走两步.
代码
void duplicateZeros(vector<int>& arr)
{
int cur = 0, dest = -1, n = arr.size();
//找到cur起点
while(cur < n)
{
if(arr[cur]) dest++;
else dest += 2;
if(dest >= n - 1) break;
cur++;
}
//dest出界的特殊判断
if(dest == n)
{
arr[n - 1] = 0;
cur--, dest -= 2;
}
//倒着走双指针
while(cur >= 0)
{
if(arr[cur]) arr[dest--] = arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
快乐数
202. 快乐数 - 力扣(LeetCode)
思路
这个题的数组下标不是简单的加加减减,而是需要进行位数平方和相加,所以我们需要对此操作封装一个函数
int intSum(int n)
{
int sum = 0;
while(n)
{
int t = n % 10;
sum += t * t;
n /= 10;
}
return sum;
}
是快乐数的例子:19
不是快乐数的例子:2
我们取快慢指针slow fast.
若该数是快乐数,则fast先变为1后不再改变,slow一定能相遇;
若该数不是快乐数,则进入循环,则fast相对slow速度为1,故也一定会相遇。
代码
int intSum(int n)
{
int sum = 0;
while(n)
{
int t = n % 10;
sum += t * t;
n /= 10;
}
return sum;
}
bool isHappy(int n)
{
int slow = n, fast = intSum(n);
while(slow != fast)
{
slow = intSum(slow);
fast = intSum(intSum(fast));
}
return slow == 1 ? true : false;
}
以上就是快慢指针的练习题,不难发现这些题中移动数组元素的情况比较多,或者是数组的循环追及情况比较多。
对撞指针
盛水最多的容器
11. 盛最多水的容器 - 力扣(LeetCode)
思路
我们不妨直接取左右指针left right,当左边高度小于右边时,因为最左边和最右边的底边长是最长的,其容积记为V.当right往左边走,无论其高是多少,容积一定比V小。所以我们应该让left往右走,这样容积可能增大;相反,当右边高度小于左边时,应该让right往左边走
如图:left对应值(高度)小于右边,应该left++
---->
计算每次移动后围成的容积,取其中的最大值即可.
代码
int maxArea(vector<int>& height)
{
int n = height.size();
int left = 0, vol = 0, maxi = 0, right = n - 1;
while(left != right)
{
vol = min(height[left], height[right]) * (right - left);
maxi = max(maxi, vol);//取容积最大值
//if(vol > maxi) maxi = vol;
if(height[left] < height[right]) left++;
else right--;
}
return maxi;
}
有效三角形的个数
611. 有效三角形的个数 - 力扣(LeetCode)
思路
固定最右边的数
对于这类比大小的题,我们需要先对其进行排序,并且这样也可以避免每条边都比较一次;再通过左右指针相加和与固定的值(fixed)相比较,若left+right大于fixed,则left继续右移也和一定大于fixed,所以应该进行righ--,并且left右移的几个值都可以组成三角形,所以ret += right - left。
代码
int triangleNumber(vector<int>& nums)
{
int n = nums.size(), ret = 0;
sort(nums.begin(), nums.end());
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;
}
两数之和
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
思路
左值加右值大于target,则left再加只会更大,所以应该right左移;
左值加右值小于target,则right再减只会更小,所以应该left右移;
代码
vector<int> twoSum(vector<int>& price, int target)
{
int n = price.size();
int left = 0, right = n - 1;
while(left < right)
{
int sum = price[left] + price[right];
if(sum < target) left++;
else if(sum > target) right--;
else
{
//c++的隐式类型转换
return {price[left], price[right]};
}
}
return {-1, -1};
}
leetcode在else那里若没有返回完会报错,所以我们可以在最后加一个return {-1, -1};
三数之和
15. 三数之和 - 力扣(LeetCode)
思路
固定最左边的数
此题与判断三条边那题类似,固定一个值i,再判断剩下两数,总体思路与两数之和类似
注意
值得注意的是本题需要去重,我们分为两重去重:第一重是left与right的去重;第二重是i的去重
并且要防止越界问题!
此时便是fixed代表值-1连续出现了两个,需要去重;
while (i < n && nums[i] == nums[i - 1]) i++;
left代表值1连续出现两个,需要去重;
right代表值4连续出现两个,需要去重。
while (nums[left - 1] == nums[left] && left < right) left++;
while (nums[right + 1] == nums[right] && left < right) right--;
代码
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> ret;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n; )
{
if (nums[i] > 0) break;
int left = i + 1, right = n - 1, res = -nums[i];
while (left < right)
{
int sum = nums[left] + nums[right];
if (sum > res) right--;
else if (sum < res) left++;
else
{
ret.push_back({ nums[i], nums[left], nums[right] });
left++, right--;
//left right去重
while (nums[left - 1] == nums[left] && left < right) left++;
while (nums[right + 1] == nums[right] && left < right) right--;
}
}
//i去重
i++;
while (i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
如果三数之和看懂了,还可以尝试一下四数之和!
18. 四数之和 - 力扣(LeetCode)
综上,对撞指针多为判定值的和或判定值之间的关系。