双指针算法

文章目录

  • 概念
  • [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. 快慢指针(同向指针)
  • 定义:两个指针从同一端出发,以不同 "速度" 同向移动(快指针遍历更快,慢指针标记有效边界)。
  • 核心作用:划分 "已处理区间" 和 "待处理区间",常用于筛选、分离元素或检测周期。
  1. 左右指针(首尾指针)
  • 定义:两个指针分别从数据结构的两端出发,向中间移动(可同向也可反向)。
  • 核心作用:利用数据的有序性(或对称性),快速缩小查找范围,常用于二分查找、配对问题。
  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]))。

我们用左右指针分别初始在数组的两端(此时宽度最大),然后向中间收缩,每次只移动较矮的那一侧指针。这样做的原因是:

  1. 初始状态:宽度最大
    初始时 left = 0,right = n-1,此时水平距离(宽度)是所有可能中最大的。这是 "宽度" 的最优起点。
  2. 移动指针的选择:舍弃 "较矮的边界"
    假设当前 height[left] < height[right](左垂线更矮):
  • 如果移动右指针(right--):宽度会减小(right - left 变小),而高度由 min(左垂线, 新右垂线) 决定 ------ 新高度不会超过原来的左垂线(因为左垂线更矮)。此时 "宽度减小 + 高度不增",面积必然变小。
  • 如果移动左指针(left++):宽度会减小,但可能遇到更高的左垂线,使得新的高度增大,从而面积有可能变大。
    同理,若 height[right] < height[left](右垂线更矮),则移动右指针。
  1. 迭代优化:持续探索更大面积
    每次移动指针后,重新计算当前面积,并与 "历史最大面积" 比较,保留较大值。直到左右指针相遇,遍历结束。
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;
    }
};
相关推荐
郝学胜-神的一滴3 小时前
矩阵的奇异值分解(SVD)及其在计算机图形学中的应用
程序人生·线性代数·算法·矩阵·图形渲染
电子_咸鱼8 小时前
LeetCode——Hot 100【电话号码的字母组合】
数据结构·算法·leetcode·链表·职场和发展·贪心算法·深度优先
仰泳的熊猫8 小时前
LeetCode:785. 判断二分图
数据结构·c++·算法·leetcode
rit84324998 小时前
基于MATLAB实现基于距离的离群点检测算法
人工智能·算法·matlab
my rainy days11 小时前
C++:友元
开发语言·c++·算法
haoly198911 小时前
数据结构和算法篇-归并排序的两个视角-迭代和递归
数据结构·算法·归并排序
微笑尅乐11 小时前
中点为根——力扣108.讲有序数组转换为二叉搜索树
算法·leetcode·职场和发展
im_AMBER12 小时前
算法笔记 05
笔记·算法·哈希算法
夏鹏今天学习了吗12 小时前
【LeetCode热题100(46/100)】从前序与中序遍历序列构造二叉树
算法·leetcode·职场和发展