算法从入门到精通——双指针

文章目录

◆ 博主名称:此生决int

大家好,欢迎来到我的博客~

⭐ 个人专栏:快速复习系列

⭐ 热门专栏:算法基础到精通系列

上期回顾

上期我们主要介绍了

大家好,我是此生决int,欢迎来到今天的算法专题

今天我们正式学习算法道路上的第一个经典算法------双指针算法。双指针为什么能把原本 O(n²) 的暴力解法优化到 O(n)?它又为什么会成为面试中的高频考点?我们接着往下看吧!

文章概要

本篇文章将从"左右指针""快慢指针""滑动窗口"等经典模型出发,结合 LeetCode 高频真题,带你彻底理解双指针的核心思想与解题套路。不仅会讲清楚"怎么写",更会带你理解"为什么这样写"。
  无论你是算法初学者,还是正在准备面试,都能快速建立属于自己的"双指针思维"!全程通俗易懂,包教包会,轻松拿下双指针

双指针

本章算法题的简单总结(建议最后看)

1,数组原地操作

1,移动0,2,复写0

2,判环

3,快乐数,11,链表判环

3,单调性

3.1,左右指针指向数的特点

4,盛水最多的容器,5,有效三角形的个数

3.2,两数之和及其变式

6,两数之和,7,三数之和,8,四数之和

链表相关题目

9,中间节点,10,倒数第K节点,11,判环

1,移动0⭐️

题目链接:

移动0

解题思路:

模拟一下过程可以发现,在移动的过程中,用两个指针可以把数组分为三个区域,已经归位的------0------没有访问的 ,这个就是双指针算法的一个特点。

解题代码:

cpp 复制代码
/*
 * 题目:移动零 (LeetCode 283)
 * 给定一个数组 nums,将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
 * 要求:必须在不复制数组的情况下原地对数组进行操作。
 *
 * 解题思路:双指针 (快慢指针)
 * - dest:慢指针,指向已处理好的非零元素序列的最后一个位置(初始 -1)。
 * - cur:快指针,用于遍历整个数组(初始 0)。
 * - 遍历过程中:
 *   1. 若 nums[cur] != 0,说明遇到非零元素,需要将其交换到前面。
 *      先将 dest 后移一位 (++dest),然后交换 nums[cur] 与 nums[dest],
 *      最后 cur 后移一位 (cur++)。
 *   2. 若 nums[cur] == 0,则不做交换,只将 cur 后移一位,跳过该零元素。
 * - 整个过程保证了非零元素的相对顺序,且所有零被"挤"到数组末尾。
 * - 时间复杂度 O(n),空间复杂度 O(1)。
 */
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int dest = -1; // 慢指针,指向已处理好的非零序列末尾
        int cur = 0;   // 快指针,用于扫描数组
        while (cur < nums.size()) {
            if (nums[cur] != 0) {
                // 当前元素非零:dest向前移动,然后与cur交换,将非零元素归位
                swap(nums[cur++], nums[++dest]);
            } else {
                // 当前元素为零:仅移动快指针,跳过
                cur++;
            }
        }
    }
};

2,复写0⭐️⭐️

题目链接:

复写0

解题思路:

首先我们想到就是新建一个数组来存储模拟,但是这里要求必须在原数组上修改,所以不能创建新的数组,然后,有人从前向后模拟会发现会改变数组原来的值,又不行!再试试从后向前完成复写,我们发现我们要先找到要复写的最后一个复写的数!!

解题代码1:

cpp 复制代码
/*
 * 题目:复写零 (LeetCode 1089)
 * 给你一个长度固定的整数数组 arr,请你将该数组中出现的每个零都复写一遍,
 * 并将其余的元素向右平移。注意:不要超过数组长度写入元素,必须原地修改。
 *
 * 解题思路:两次遍历,双指针(快慢指针模拟 + 反向填充)
 * 1. 第一次遍历(模拟复写):
 *    - slow 指针逐个扫描原数组元素,fast 指针模拟在结果数组中的下标。
 *    - 当 fast < n 时,若 arr[slow] == 0,fast 前进两步(复写两个零),
 *      否则 fast 前进一步;同时 slow 前进一步。
 *    - 循环结束后,slow 指向原数组中最后一个会被保留的元素之后的位置。
 * 2. 边界处理:
 *    - 如果 fast == n+1,说明原数组中最后一个零在复写时会超出数组右边界,
 *      只能保留一个零。此时将数组最后一个元素直接置为 0,
 *      并回退指针:fast 指向 n-1,slow 回退一位。
 * 3. 第二次遍历(从后向前原地填充):
 *    - 利用 slow 从后向前遍历原数组,fast 从结果数组末尾向前填充。
 *    - 若 arr[slow-1] == 0,则在 arr[fast-1] 和 arr[fast-2] 处放两个 0,
 *      fast 减 2,slow 减 1。
 *    - 否则将 arr[slow-1] 复制到 arr[fast-1],fast 和 slow 各减 1。
 *    - 从后向前操作保证了未处理的元素不会被覆盖。
 * - 时间复杂度 O(n),空间复杂度 O(1)。
 */
class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int n = arr.size();
        int fast = 0; // 快指针,模拟复写零后在结果数组中的下标
        int slow = 0; // 慢指针,遍历原数组

        // 第一遍:模拟复写,找到原数组中最后一个会被保留的元素
        while (fast < n) {
            if (arr[slow] == 0) {
                fast += 2; // 遇到0,结果中会放两个0,快指针前进两步
            } else {
                fast += 1; // 非0元素,结果中放一个,快指针前进一步
            }
            slow++; // 原数组指针前进一步
        }

        // 边界情况:模拟结束后 fast == n+1,说明最后的0复写会越界一个位置
        if (fast == n + 1) {
            arr[n - 1] = 0;   // 数组最后一个元素强制置0(只能放下一个0)
            fast = n - 1;     // 修正 fast 指针到最后一个有效下标
            slow--;           // slow 回退,跳过这个导致越界的0
        }

        // 第二遍:从后向前填充,避免覆盖未处理的元素
        while (slow > 0) {
            if (arr[slow - 1] == 0) {
                // 原数组中是0,结果中连续放两个0
                arr[fast - 1] = 0;
                arr[fast - 2] = 0;
                fast -= 2;     // 填充了两个位置
            } else {
                // 非0元素直接复制
                arr[fast - 1] = arr[slow - 1];
                fast -= 1;     // 填充了一个位置
            }
            slow--; // 原数组指针前移
        }
    }
};

没懂?看看下面的大神代码!!

大神解题代码

cpp 复制代码
void duplicateZeros(vector<int>& arr)
{
    // 1. 先找到最后一个数
    int cur = 0, dest = -1, n = arr.size();
    while(cur < n)
    {
        if(arr[cur]) dest++;
        else dest += 2;
        if(dest >= n - 1) break;
        cur++;
    }

    // 2. 处理一下边界情况
    if(dest == n)
    {
        arr[n - 1] = 0;
        cur--; dest -=2;
    }

    // 3. 从后向前完成复写操作
    while(cur >= 0)
    {
        if(arr[cur]) arr[dest--] = arr[cur--];
        else
        {
            arr[dest--] = 0;
            arr[dest--] = 0;
            cur--;
        }
    }
}

代码分析:

主流程步骤:

  1. 先判断 cur 位置的值
  2. 决定 dest 向后移动一步或者两步
  3. 判断一下 dest 是否已经到结束为止
  4. cur++
    补充步骤:
    1.5 处理一下边界情况

3,快乐数⭐️⭐️⭐️

题目链接:

快乐数

解题思路:

乍一看没有任何思路,但不管是通过模拟还是自己发现,假设一个很大的数,10位,每位上全是9,但他经过一次变换后最大也就是109 9,一下就变小了,也就是说,最终会坍缩在一个很小的范围里面。那我们要怎么判断一个数不是快乐数呢?没错,就是进入了循环!(题目也说了,要么变成1,要么有循环)

那么我们要怎么判断他是不是有环呢?那就是双指针 !!也叫快慢指针常用!!!

解题代码1:

cpp 复制代码
/*
 * 题目:快乐数 (LeetCode 202)
 * 编写一个算法判断一个数 n 是不是快乐数。
 * 快乐数定义:每次将数字替换为其各位数字的平方和,重复这个过程,
 * 如果最终能得到 1,则是快乐数;如果陷入无限循环且始终得不到 1,则不是。
 *
 * 解题思路:快慢指针 (Floyd 判圈算法)
 * - 由于求平方和的过程要么最终到 1,要么进入一个不包含 1 的循环。
 *   问题转化为检测链表中是否存在环(且环点是否为 1)。
 * - 使用两个指针 slow 和 fast,初始都指向 n。
 *   - slow 每次走一步(调用一次 change)
 *   - fast 每次走两步(调用两次 change)
 * - 如果过程中任一指针达到 1,说明是快乐数,返回 true。
 * - 如果快慢指针相遇(相等)且值不为 1,说明进入了无 1 的循环,返回 false。
 * - 时间复杂度 O(log n) 量级,空间复杂度 O(1)。
 */
class Solution {
    // 辅助函数:计算一个数各位数字的平方和
    int change(int n) {
        int ret = 0;
        while (n) {
            int x = n % 10;      // 取出最后一位
            ret += (x * x);      // 累加平方
            n /= 10;             // 去掉最后一位
        }
        return ret;
    }
public:
    bool isHappy(int n) {
        int fast = n;   // 快指针,每次走两步
        int slow = n;   // 慢指针,每次走一步
        while (true) {
            // 途中任一指针到达 1,说明是快乐数
            if (fast == 1 || slow == 1)
                return true;
            
            // 快指针移动两步
            fast = change(fast);
            fast = change(fast);
            // 慢指针移动一步
            slow = change(slow);
            
            // 若两指针相遇且不为 1,说明进入了无限循环且不是快乐数
            if (fast == slow && fast != 1)
                return false;
        }
    }
};

没懂?看看大神的代码!!

大神解题代码

cpp 复制代码
// 解题思路:
// 快乐数的判定存在两种情况:最终变为1,或进入不含1的无限循环
// 采用快慢指针法(龟兔赛跑)检测循环:慢指针每次计算1次平方和,快指针每次计算2次
// 若两指针相遇时值为1,则是快乐数;否则说明陷入循环,不是快乐数

class Solution
{
public:
    // 计算数字n每一位的平方和
    int bitSum(int n) 
    {
        int sum = 0;
        while(n) // 循环提取每一位数字
        {
            int t = n % 10; // 取当前个位数字
            sum += t * t;   // 累加平方和
            n /= 10;        // 去掉个位,处理下一位
        }
        return sum;
    }

    bool isHappy(int n)
    {
        // 初始化快慢指针:慢指针从n开始,快指针先走一步(避免初始状态相等)
        int slow = n, fast = bitSum(n);
        // 循环直到快慢指针相遇(检测循环)
        while(slow != fast)
        {
            slow = bitSum(slow);          // 慢指针:每次走1步(计算1次平方和)
            fast = bitSum(bitSum(fast));  // 快指针:每次走2步(计算2次平方和)
        }
        // 相遇时的值为1,说明是快乐数;否则说明陷入非1循环
        return slow == 1;
    }
};

4,盛水最多的容器⭐️⭐️⭐️

题目链接:

盛水最多的容器

解题思路:

1,暴力

枚举左右两个边,每次都算出可以成的水,

2,双指针

我们先模拟一下会发现

这就是这道题的特性。

解题代码

这边建议直接看后面大神的代码

cpp 复制代码
/*
 * 解题思路:对撞双指针 + 贪心跳过无效状态
 * - 初始时,左右指针分别指向数组两端,计算当前容水量。
 * - 每次移动较短的那一侧指针,并跳过所有高度不超过该侧原高度的柱子,
 *   因为宽度减小的情况下高度不增,面积必然不会更大,可以直接排除。
 * - 移动后重新计算面积,更新最大值,直至两指针相遇。
 * - 时间复杂度 O(n),空间复杂度 O(1)。
 */
class Solution {
public:
    int maxArea(vector<int>& height) {
        int l = 0, r = height.size() - 1; // 左右指针
        int h = min(height[l], height[r]); // 当前短板高度
        int ret = h * (r - l);             // 初始面积

        while (l < r) {
            if (height[l] < height[r]) {
                // 左边为短板,尝试右移左指针
                int ll = l + 1;
                // 跳过所有高度不大于原左边高度的柱子
                while (height[ll] < height[l] && ll < r) {
                    ll++;
                }
                l = ll; // 更新左指针
                int v = (r - l) * min(height[l], height[r]);
                ret = max(v, ret);
            } else {
                // 右边为短板(或等高),尝试左移右指针
                int rr = r - 1;
                // 跳过所有高度不大于原右边高度的柱子
                while (height[rr] < height[r] && rr > l) {
                    rr--;
                }
                r = rr; // 更新右指针
                int v = (r - l) * min(height[l], height[r]);
                ret = max(v, ret);
            }
        }
        return ret;
    }
};

没懂?看看大神的解题代码!!

大神解题代码

cpp 复制代码
class Solution
{
public:
    int maxArea(vector<int>& height)
    {
        // 初始化左右指针(分别指向数组两端)、记录最大水量ret
        int left = 0, right = height.size() - 1, ret = 0;
        // 双指针未相遇时循环
        while(left < right)
        {
            // 计算当前容器水量:较短边高度 × 两指针距离
            int v = min(height[left], height[right]) * (right - left);
            // 更新最大水量
            ret = max(ret, v);

            // 移动指针:只移动较短边,才有可能找到更大水量
            if(height[left] < height[right]) 
                left++;  // 左边更短,左指针右移
            else 
                right--; // 右边更短或相等,右指针左移
        }
        return ret; // 返回最大水量
    }
};

5,有效三角形个数⭐️⭐️⭐️

题目链接:

有效三角形个数

解题思路:

因为有三条边,我们可以先固定一条边,首先,我们要要知道三角形的条件,任意两边之和大于第三边,即两条边大于最大的边即可。我们要先对数组排序,然后,我们先固定最大的一条边,大小为C,那么,

对于区间【0,C-1】(令【a,b】)里面,

如果两边界之和a+b大于C,那么,a之后的所有边与b组合都比C大,所以直接算出这个区间的三角形个数为b-a个,区间更新位【a,b-1】

如果两边界之和a+b小于C,那么,b之前的所有边与a组合都比C小,所以,区间更新为【a+1,b】

解题代码

cpp 复制代码
/*
 * 解题思路:排序 + 双指针(固定最长边)
 * - 首先将数组排序,方便判断三边关系。
 * - 固定最长的边 nums[j](j 从末尾向前遍历),
 *   在 [0, j-1] 区间使用对撞双指针 l, r 寻找两条"短边"
 *   满足 nums[l] + nums[r] > nums[j]。
 * - 若满足,则 l 到 r-1 之间的所有元素都能与 r 和 j 组成三角形,
 *   累加 (r - l) 并右指针左移;否则左指针右移。
 * - 时间复杂度 O(n²),空间复杂度 O(1)。
 */
class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        int l = 0, r = nums.size(); // 预定义双指针(实际会在循环内重置)
        int ret = 0;
        sort(nums.begin(), nums.end()); // 排序,使数组单调递增

        // 固定最长边 j(至少要有两条更短的边,故 j >= 2)
        for (int j = nums.size() - 1; j >= 2; j--) {
            l = 0;
            r = j - 1; // 双指针置于当前最长边的左侧区间
            while (l < r) {
                // 两短边之和大于最长边,可组成三角形
                if (nums[l] + nums[r] > nums[j]) {
                    ret += (r - l); // 从 l 到 r-1 的所有元素都与 r 和 j 满足条件
                    r--;           // 尝试更小的右指针对应值
                } else {
                    l++;           // 和太小,左指针右移增大 sum
                }
            }
        }
        return ret;
    }
};

没懂?看看大神的解题代码!!

大神解题代码

cpp 复制代码
int triangleNumber(vector<int>& nums)
{
    // 1. 对数组升序排序,方便利用三角形三边关系的特性
    sort(nums.begin(), nums.end());

    // 2. 初始化结果计数器,n 为数组长度
    int ret = 0, n = nums.size();
    // 从后往前固定最大边 nums[i],i 至少为 2 才能凑齐三元组
    for(int i = n - 1; i >= 2; i--) 
    {
        // 双指针:left 指向区间左端,right 指向区间右端(最大边的前一个位置)
        int left = 0, right = i - 1;
        while(left < right)
        {
            // 若较小两边之和 > 最大边,说明 [left, right-1] 与 right 组成的三元组都有效
            if(nums[left] + nums[right] > nums[i])
            {
                ret += right - left; // 累加当前 right 对应的所有有效组合数
                right--; // 右指针左移,尝试更小的 right
            }
            else
            {
                // 和不够大,左指针右移,增大较小边的和
                left++;
            }
        }
    }
    return ret;
}

6,两数之和------查找总价格为目标值的两个商品⭐️

题目链接:

查找总价格为目标值的两个商品

解题思路:

有了上面三角形的那个题的思路,我们可以定义两个指针left和right,如果两区间端点的值大于target,right--;小于left++;

解题代码

cpp 复制代码
/*
 * 解题思路:排序 + 双指针(固定最长边)
 * - 首先将数组排序,方便判断三边关系。
 * - 固定最长的边 nums[j](j 从末尾向前遍历),
 *   在 [0, j-1] 区间使用对撞双指针 l, r 寻找两条"短边"
 *   满足 nums[l] + nums[r] > nums[j]。
 * - 若满足,则 l 到 r-1 之间的所有元素都能与 r 和 j 组成三角形,
 *   累加 (r - l) 并右指针左移;否则左指针右移。
 * - 时间复杂度 O(n²),空间复杂度 O(1)。
 */
class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        int l = 0, r = nums.size(); // 预定义双指针(实际会在循环内重置)
        int ret = 0;
        sort(nums.begin(), nums.end()); // 排序,使数组单调递增

        // 固定最长边 j(至少要有两条更短的边,故 j >= 2)
        for (int j = nums.size() - 1; j >= 2; j--) {
            l = 0;
            r = j - 1; // 双指针置于当前最长边的左侧区间
            while (l < r) {
                // 两短边之和大于最长边,可组成三角形
                if (nums[l] + nums[r] > nums[j]) {
                    ret += (r - l); // 从 l 到 r-1 的所有元素都与 r 和 j 满足条件
                    r--;           // 尝试更小的右指针对应值
                } else {
                    l++;           // 和太小,左指针右移增大 sum
                }
            }
        }
        return ret;
    }
};

没懂?看看大神的解题代码!!

大神解题代码

cpp 复制代码
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};
    }
};

7,三数之和⭐️⭐️(会了前面之后)

题目链接:

三数之和

解题思路:

类似三角形那题,先固定一个数,再在那个区间里面使用双指针,注意,这题要去重(关键)

解题代码

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
       sort(nums.begin(),nums.end());
       vector<vector<int>> ret;
       for(int i=nums.size()-1;i>=2;)
       {
            if(nums[i]<0)
            break;
            int left=0;
            int right=i-1;
            while(left<right)
            {
                if(nums[left]+nums[right]+nums[i]>0)
                {
                     right--;
                }
            else if(nums[left]+nums[right]+nums[i]<0)
                {
                    left++;
                }   
            else
                {
                vector<int>tmp;
                tmp.push_back(nums[left]);
                 tmp.push_back(nums[right]);
                  tmp.push_back(nums[i]);
                ret.push_back(tmp);
                left++;
                right--;
                  while(nums[right]==nums[right+1]&&right>left)
                     {
                        right--;
                     }
                    while(nums[left]==nums[left-1]&&left<right)
                    {
                        left++;
                    }
                 }
            }
           i--;
            while(nums[i]==nums[i+1]&&i>=2)i--;
       } 
       //o(N*N)
       return ret;
    }
};

没懂?看看大神的解题代码!!

大神解题代码

cpp 复制代码
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ret;
        // 1. 对数组升序排序,方便双指针操作和去重
        sort(nums.begin(), nums.end());
        int n = nums.size();
        
        // 2. 固定第一个数 nums[i],遍历所有可能的i
        for(int i = 0; i < n; ) 
        {
            // 优化:nums[i] > 0时,后续数均非负,三数之和不可能为0,直接break
            if(nums[i] > 0) break;
            
            int left = i + 1;       // 左指针:从i的下一个位置开始
            int right = n - 1;      // 右指针:从数组末尾开始
            int target = -nums[i];  // 目标和:需找到两数之和等于 -nums[i]
            
            while(left < right)
            {
                int sum = nums[left] + nums[right];
                if(sum > target) 
                    right--;  // 和大于目标,右指针左移,减小和
                else if(sum < target) 
                    left++;   // 和小于目标,左指针右移,增大和
                else 
                {
                    // 找到一组和为0的三元组,加入结果集
                    ret.push_back({nums[i], nums[left], nums[right]});
                    left++;
                    right--;
                    
                    // 对left去重:跳过与前一个值相同的数
                    while(left < right && nums[left] == nums[left - 1]) 
                        left++;
                    // 对right去重:跳过与后一个值相同的数
                    while(left < right && nums[right] == nums[right + 1]) 
                        right--;
                }
            }
            
            // 对i去重:跳过与前一个值相同的数,避免重复三元组
            i++;
            while(i < n && nums[i] == nums[i - 1]) 
                i++;
        }
        return ret;
    }
};

8,四数之和⭐️⭐️(会了前面之后)

题目链接:

四数之和

在这里插入图片描述

解题思路:

就是三数之和的变式,先固定两个数,再用双指针即可。不会的建议先学会三数之和。

解题代码

cpp 复制代码
/*
 * 解题思路:排序 + 固定前两个数 + 双指针
 * - 首先将数组排序,便于后续去重和双指针查找。
 * - 第一层循环固定第一个数 nums[i],第二层循环固定第二个数 nums[j]。
 * - 在剩余子数组 [j+1, n-1] 上使用对撞双指针 left 和 right,
 *   寻找与 nums[i]+nums[j] 相加等于 target 的剩下两个数。
 * - 根据四数之和与 target 的关系移动左右指针,找到解后记录,
 *   同时跳过重复元素避免结果集重复。
 * - 每一层固定数移动时也跳过重复值,保证最终四元组不重复。
 * - 时间复杂度 O(n³),空间复杂度 O(1)(不计返回结果)。
 */
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());       // 排序
        int n = nums.size();
        vector<vector<int>> ret;
        int i = 0, j = 0;

        // 第一层:固定第一个数
        for (i = 0; i < n - 3;) {
            // 第二层:固定第二个数
            for (j = i + 1; j < n - 2;) {
                // 剪枝:当前最小两数和已 >= target,后续只会更大
                if (nums[i] + nums[j] >= target)
                    break;

                int left = j + 1;            // 左指针
                int right = n - 1;           // 右指针

                while (left < right) {
                    int sum = nums[left] + nums[right] + nums[i] + nums[j];
                    if (sum > target) {
                        right--;             // 和偏大,右指针左移
                    } else if (sum < target) {
                        left++;              // 和偏小,左指针右移
                    } else {
                        // 找到一组解,记录
                        ret.push_back({nums[i], nums[j], nums[left], nums[right]});
                        right--;
                        left++;
                        // 跳过左指针重复元素
                        while (left < right && nums[left] == nums[left - 1])
                            left++;
                        // 跳过右指针重复元素
                        while (left < right && nums[right] == nums[right + 1])
                            right++;
                    }
                }
                j++;
                // 跳过重复的第二个数
                while (j < n - 2 && nums[j] == nums[j - 1])
                    j++;
            }
            i++;
            // 跳过重复的第一个数
            while (i < n - 3 && nums[i] == nums[i - 1])
                i++;
        }
        return ret;
    }
};

没懂?看看大神的解题代码!!

大神解题代码

和我的一样

链表相关的双指针题(中点,判环,倒K)

**这些代码这里就没有解析咯!**要看解析可以去看题解或我的链表那期博文哦!!

9,链表的中间节点

题目链接:链表的中间节点

解题代码:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
typedef struct ListNode listnode;

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        // 快慢指针法:快指针速度是慢指针的2倍,快指针到尾时慢指针刚好在中间
        listnode* fast = head;  // 快指针:每次走2步
        listnode* slow = head;  // 慢指针:每次走1步
        
        // 循环条件:fast和fast->next不为空,避免访问空指针,兼容奇偶长度链表
        while (fast && fast->next)
        {
            slow = slow->next;          // 慢指针走1步
            fast = fast->next;          // 快指针先走第1步
            if (fast) fast = fast->next;// 快指针再走第2步(凑够2步,避免空指针)
        }
        return slow; // 循环结束时,slow指向中间结点(偶数长度时返回第二个中间结点)
    }
};

10,倒数第K个节点

题目链接:
返回链表倒数第K个节点

解题代码

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
typedef struct ListNode listnode;

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        // 快慢指针法:快指针速度是慢指针的2倍,快指针到尾时慢指针刚好在中间
        listnode* fast = head;  // 快指针:每次走2步
        listnode* slow = head;  // 慢指针:每次走1步
        
        // 循环条件:fast和fast->next不为空,避免访问空指针,兼容奇偶长度链表
        while (fast && fast->next)
        {
            slow = slow->next;          // 慢指针走1步
            fast = fast->next;          // 快指针先走第1步
            if (fast) fast = fast->next;// 快指针再走第2步(凑够2步,避免空指针)
        }
        return slow; // 循环结束时,slow指向中间结点(偶数长度时返回第二个中间结点)
    }
};

解题代码:

11,判环

题目链接:
判断链表是否有环

解题代码

cpp 复制代码
/**
 * 解题思路:快慢指针法(Floyd 判圈算法)
 * - 定义两个指针:慢指针每次走一步,快指针每次走两步。
 * - 若链表无环,快指针会先到达 nullptr,返回 false。
 * - 若链表有环,快慢指针最终会在环内相遇,返回 true。
 * - 特殊情况:空链表或只有一个节点且无环的直接返回 false。
 * 
 * 时间复杂度 O(n),空间复杂度 O(1)
 */

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
    typedef struct ListNode listnode;
public:
    bool hasCycle(ListNode *head) {
        // 空链表一定无环
        if (head == NULL)
            return false;
        // 只有一个节点且没有自环(next为NULL),无环
        if (head->next == NULL)
            return false;

        // 快慢指针初始化都指向头节点
        listnode *fast = head;
        listnode *slow = head;

        // 当快慢指针均非空时继续移动
        while (fast && slow) {
            slow = slow->next;          // 慢指针走一步
            fast = fast->next;          // 快指针先走一步
            if (fast)                   // 如果快指针未到末尾,再走一步(共两步)
                fast = fast->next;

            // 快慢指针相遇说明存在环
            if (fast == slow)
                return true;
        }

        // 遍历结束未相遇,无环
        return false;
    }
};

双指针算法本质剖析

双指针 = 用两个位置之间的关系,减少重复遍历。不再"穷举所有情况",而是"边移动边排除"。一般可以将O(N*N)的时间复杂度优化为O(N)。双指针的核心本质,双指针是在"利用单调性""来排除一大片不可能情况"

什么时候适合用双指针

1. 数组有序

两数之和

三数之和(四数之和)

2. 涉及"连续区间"(目前还没有找到合适的题目)

最长子串

最短子数组

连续和

3. 需要原地操作

删除元素

移动零

数组去重

4. 链表问题

找中点

判断环

倒数第 k 个节点

题型总结

双指针的题目主要可以分为以下几个类型

1. 左右夹逼型(左右指针)

适用于:

1,有序数组

2,回文判断

3,两数之和

比如我们上面的 1,2,4,5,6,7,8

2. 快慢指针

适用于:

1,链表相关的(中点,判环,倒K)

2,去重和删除元素

3,判环

比如我们上面的 3

3. 滑动窗口

在下期啦!!!

下期预告

双指针的变式------滑动窗口!!!

结语

本期内容就到这里啦,欢迎大家在评论区一起交流讨论

如果你也在为蓝桥杯/ACM备赛头疼,或是准备算法面试找不到系统学习路径,欢迎订阅我的「算法从入门到精通」专栏

这里没有枯燥的理论堆砌,只有完整的算法学习路线

搭配精选梯度习题+清晰思路解析,帮你把每个算法学透、练熟。包教包会的!

我们一起在算法路上稳步进阶!

相关推荐
普马萨特1 小时前
Uber H3:地理网格索引在空间数据分析中的应用
数据结构·算法
alphaTao1 小时前
LeetCode 每日一题 2026/5/11-2026/5/17
算法·leetcode
洛水水1 小时前
【力扣100题】45.零钱兑换
算法·leetcode·职场和发展
Aaron15882 小时前
全频段 SDR干扰源模块解决方案(星链干扰、LORA无人机干扰)
人工智能·算法·fpga开发·硬件架构·硬件工程·无人机·信息与通信
AI科技星2 小时前
全域数学·球面拓扑微扰标准系数η=0.01 应用详解(典籍正式版)
人工智能·算法·数学建模·数据挖掘·机器人
逻辑君2 小时前
物理学研究报告【20260001】
人工智能·算法
AI科技星2 小时前
算法联盟·全域数学公理体系下黑洞标量毛发与LVK引力波O4全维理论、求导、证明、计算、验证、分析
人工智能·线性代数·算法·架构·学习方法·量子计算
谙弆悕博士2 小时前
【附C语言源码】C语言 栈结构 实现及其扩展操作
c语言·开发语言·数据结构·算法·链表·指针·
YuanDaima20482 小时前
图论基础原理与题目说明
数据结构·人工智能·python·算法·图论·手撕代码