文章目录
- 概念
- [283. 移动零](#283. 移动零)
- [1089. 复写零](#1089. 复写零)
- [202. 快乐数](#202. 快乐数)
- [11. 盛最多水的容器](#11. 盛最多水的容器)
- [611. 有效三角形的个数](#611. 有效三角形的个数)
- [LCR 179. 查找总价格为目标值的两个商品](#LCR 179. 查找总价格为目标值的两个商品)
- [15. 三数之和](#15. 三数之和)
- [18. 四数之和](#18. 四数之和)
概念
一、核心概念
双指针算法是一种通过设置两个指针(索引)在数据结构(如数组、链表)中移动,协同完成问题求解的策略。
核心思想:通过指针的合理移动,减少无效遍历,将原本需要嵌套循环(时间复杂度 (O(n^2))的问题优化为线性时间复杂度(通常 (O(n)) 或 (O(n log n)),同时多数情况下可实现原地操作(空间复杂度 (O(1))。
二、常见类型
- 快慢指针(同向指针)
- 定义:两个指针从同一端出发,以不同 "速度" 同向移动(快指针遍历更快,慢指针标记有效边界)。
- 核心作用:划分 "已处理区间" 和 "待处理区间",常用于筛选、分离元素或检测周期。
- 左右指针(首尾指针)
- 定义:两个指针分别从数据结构的两端出发,向中间移动(可同向也可反向)。
- 核心作用:利用数据的有序性(或对称性),快速缩小查找范围,常用于二分查找、配对问题。
- 固定间距指针
- 定义:两个指针保持固定距离同向移动(本质是快慢指针的特例)。
- 核心作用:处理 "滑动窗口" 类问题,或定位特定间隔的元素。
三、双指针算法的优势
1.时间效率高:将嵌套循环的 (O(n^2)) 优化为线性遍历的 (O(n));
2.空间复杂度低:多数情况可原地修改数据(无需额外空间);
3.逻辑清晰:通过指针分工简化问题,避免复杂的嵌套判断。
283. 移动零

根据题目,目标是把所有的0移动到数组的最后面。
所以我们可以定义两个同向的快慢指针src、cur
(0,src)是我们处理过的非0数组,(src,cur)是全0数组,(cur,n)是未处理的数组。
下面分析怎么处理:
分类讨论:
cur:
1.(src,cur)放的是全0数组,所以当cur遍历遇到0的时候,符合情况,继续遍历(cur++)不做处理
2.当cur遍历遇到非0的时候,由于我们规定(0,src)存放非0,所以需要交换 src 和cur对应的数。交换完(src++,原来src位置已经符合(0,src)的情况)),继续遍历cur
src:
不需要主动遍历,当cur遇到0时,与src对应的数交换。
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int src=0,cur=0;
for(;cur<nums.size();cur++)
{
if(nums[cur]==0)continue;
else
{
swap(nums[cur],nums[src++]);
}
}
return ;
}
};
1089. 复写零

题意:要求我们把出现的0复写一遍。
我们可以直接根据需求遍历,遇到0就复写,然后用新数组的部分替换旧数组。
不过题目要求我们直接在原数组上进行操作,根据本章的专题。我们要用到双指针(快慢指针)解决问题。
分析:
如果我们直接从前向后遍历:遇到0就要复写,但是直接复写会覆盖掉后面的非0数,而那个数是我们需要保留在原数组中的。所以我们不能从前往后遍历。
试着从后往前复写0,不过再复写前,我们需要知道需要被复写的数组的区间是哪里。
可以定义dest:复写后数组的最后一个数
然后cur,开始遍历,cur对应的数不为0:dest++,为0:dest+=2;直到dest=n-1
最终找到会被处理的最后一个数cur。
然后就是对cur以及之前的数进行复写。
cur非0,dest写入cur
cur=0,dest写入两个0
还需要判断,如果刚好最后一个被处理的数是0,那么dest会超范围,次数最后一个0就不用复写
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur=0,dest=-1,n=arr.size();
while(cur<n)
{
if(arr[cur])dest++;
else dest+=2;
if(dest>=n-1)break;
cur++;
}
if(dest==n)
{
arr[n-1]=0;
dest-=2;cur--;
}
while(cur>=0)
{
if(arr[cur])arr[dest--]=arr[cur--];
else{
arr[dest--];
arr[dest--];
cur--;
}
}
return;
}
};
202. 快乐数
可以看到,符合条件的,最终会进入1的循环。
不符合条件的,最终进入一群数的循环。
所以我们定义快慢指针后只需要判断,当指针重合时,值是否为1
cpp
class Solution {
public:
int sq(int n)
{
if(n<10)return n*n;
else
{int res=0;
while(n)
{
int x=n%10;
res+=x*x;
n/=10;
}
return res;
}
}
bool isHappy(int n) {
int slow=n,fast=sq(n);
while(slow!=fast)
{
slow=sq(slow);
fast=sq(sq(fast));
}
return slow==1;
}
};
11. 盛最多水的容器
容器的面积由两个关键因素决定:
- 宽度:两条垂线的水平距离(即指针的间距 right - left)。
- 高度:两条垂线中较矮的那一条(即 min(height[left], height[right]))。
我们用左右指针分别初始在数组的两端(此时宽度最大),然后向中间收缩,每次只移动较矮的那一侧指针。这样做的原因是:
- 初始状态:宽度最大
初始时 left = 0,right = n-1,此时水平距离(宽度)是所有可能中最大的。这是 "宽度" 的最优起点。 - 移动指针的选择:舍弃 "较矮的边界"
假设当前 height[left] < height[right](左垂线更矮):
- 如果移动右指针(right--):宽度会减小(right - left 变小),而高度由 min(左垂线, 新右垂线) 决定 ------ 新高度不会超过原来的左垂线(因为左垂线更矮)。此时 "宽度减小 + 高度不增",面积必然变小。
- 如果移动左指针(left++):宽度会减小,但可能遇到更高的左垂线,使得新的高度增大,从而面积有可能变大。
同理,若 height[right] < height[left](右垂线更矮),则移动右指针。
- 迭代优化:持续探索更大面积
每次移动指针后,重新计算当前面积,并与 "历史最大面积" 比较,保留较大值。直到左右指针相遇,遍历结束。
cpp
class Solution {
public:
int maxArea(vector<int>& height) {
int l=0,r=height.size()-1,ret=0;
while(l<r)
{
int v=min(height[l],height[r])*(r-l);
ret=max(v,ret);
if(height[l]<height[r])l++;
else r--;
}
return ret;
}
};
611. 有效三角形的个数
一、核心思路
三角形的有效条件(当数组升序排序后,设三边为 (a≤b≤c):(a + b > c)(因为 (a≤b≤c),所以 (a + c > b) 和 (b + c > a) 会自然满足)。
二、具体步骤
1.排序数组:将数组升序排列,利用单调性简化双指针的移动逻辑。
2.固定最大边:遍历数组,将每个元素作为三角形的最大边 c(索引 i 从 2 开始,确保有前两个数作为另外两边 (a, b))。
3.双指针找有效组合:在区间 [0, i-1] 内,用左指针 left(初始为 0)和右指针 right(初始为 i-1)寻找满足 (nums[left] + nums[right] > nums[i]) 的组合:
- 若满足:则 left 到 right-1 之间的所有 left 都能与当前 right 组成有效三角形(数组升序,更小的 left 对应的 a 更小,但和仍满足 (a + b > c)。因此计数增加 right - left,并 right--(尝试更小的 b)。
- 若不满足:则 a 太小, left++(增大 a,使 (a + b) 更大)。
cpp
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(),nums.end());
int res=0;
for(int i=nums.size()-1;i>=2;i--)
{
int l=0,r=i-1;
while(l<r)
{
if(nums[l]+nums[r]>nums[i])
{
res+=(r-l);
r--;
}
else l++;
}
}
return res;
}
};
LCR 179. 查找总价格为目标值的两个商品

cpp
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int l=0,r=price.size()-1;
while(l<r)
{
int sum=price[l]+price[r];
if(sum>target)r--;
else if(sum<target)l++;
else return {price[l],price[r]};
}
return {-1,-1};
}
};
15. 三数之和

上题是求两数之和,这题是三数之和,只需要固定第一个数t,然后在后面的区间求两数和为-t即可。
需要注意的是,因为对顺序没有要求,我们需要对结果去重,可以直接在双指针求两数和的时候 + 固定第一个数的时候 去重,就是把重复的元素跳过
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<vector<int>>ret;
for(int i=0;i<nums.size();i++)
{
int t=-nums[i];
int l=i+1,r=nums.size()-1;
while(l<r)
{
if(nums[l]+nums[r]<t)l++;
else if(nums[l]+nums[r]>t)r--;
else
{
ret.push_back({nums[i],nums[l],nums[r]});
while(l<r&&nums[l+1]==nums[l])l++;
while(l<r&&nums[r-1]==nums[r])r--;
l++,r--;
}
}
while(i+1<nums.size()&&nums[i+1]==nums[i])i++;
}
return ret;
}
};
18. 四数之和

求四数之和,和前面求三数之和类似。固定一个数t,然后求三数为-t。
cpp
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
sort(nums.begin(),nums.end());
int n=nums.size();
for(int i=0;i<n;i++)
{
double tar=target-nums[i];
for(int j=i+1;j<n;j++)
{
double t=tar-nums[j];
int l=j+1,r=n-1;
while(l<r)
{
if(nums[l]+nums[r]<t)l++;
else if(nums[l]+nums[r]>t)r--;
else
{
ret.push_back({nums[i],nums[j],nums[l],nums[r]});
//去重
while(l<r&&nums[l+1]==nums[l])l++;
while(l<r&&nums[r-1]==nums[r])r--;
l++,r--;
}
}
while(j+1<n&&nums[j+1]==nums[j])j++;
}
while(i+1<n&&nums[i+1]==nums[i])i++;
}
return ret;
}
};