【算法题】双指针(一)

一、移动零

题目描述:

给定一个数组 nums,需原地操作将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。

示例

  • 输入 nums = [0,1,0,3,12],输出 [1,3,12,0,0]
  • 输入 nums = [0],输出 [0]

解题思路:

本题核心是原地整理非零元素,让0自然'沉淀'到末尾 ,采用双指针法实现:

  • 定义指针 dest:标记已处理的非零元素的最后位置 (初始为 -1,表示暂无非零元素)。
  • 定义指针 cur:遍历整个数组,寻找非零元素。
  • 遍历过程:
    1. nums[cur] 是 0,直接跳过(留到后面)。
    2. nums[cur] 非零,将 dest 向后移动一位(指向新的非零元素位置),然后交换 nums[dest]nums[cur]
  • 最终,dest 及之前的位置都是非零元素(保持原顺序),dest 之后的位置自然都是 0。

完整代码:

cpp 复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int dest = -1, cur = 0;
        for(cur = 0; cur < nums.size(); cur++)
        {
            if(nums[cur] == 0)
            {
                continue;
            }
            else
            {
                swap(nums[++dest], nums[cur]);
            }
        }
    }
};

复杂度分析:

  • 时间复杂度 :O(n)O(n)O(n)。仅遍历数组一次,每个元素最多被交换一次。
  • 空间复杂度 :O(1)O(1)O(1)。仅使用常数级额外变量,完全满足"原地操作"要求。

二、复写零

题目描述:

给你一个长度固定的整数数组 arr,将每个出现的零复写一遍,并右移其余元素。要求:

  • 不能在数组长度外写入元素
  • 必须原地修改数组,不能使用额外数组

示例

输入:arr = [1,0,2,3,0,4,5,0]

输出:[1,0,0,2,3,0,0,4](最后一个0因数组长度限制无法复写)

解题思路:

如果直接从前往后遍历复写零,会覆盖后续未处理的元素;如果用额外数组存储结果,又不符合"原地修改"的要求。

因此我们采用先找边界、再从后往前填充的双指针策略:

  1. 第一步:确定有效元素的边界

    • 用指针 cur 遍历原数组,dest 模拟"复写零后"的数组长度(遇到非零元素,dest+1;遇到零,dest+2)。
    • dest 超过数组长度时,停止遍历。此时 cur 左侧的元素是最终数组需要包含的元素。
  2. 第二步:处理边界溢出情况

    • dest == n(数组长度),说明最后一个元素是零且复写后超出长度,此时只需要写一个零到数组末尾。
  3. 第三步:从后往前填充数组

    • cur 位置倒序遍历,将元素写入 dest 位置:非零元素直接写,零则写两次(注意 dest 指针同步左移)。

完整代码:

cpp 复制代码
class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int cur = 0;    // 遍历原数组的指针
        int dest = -1;  // 模拟复写零后的数组长度指针
        int n = arr.size();

        // 第一步:找到有效元素的边界(cur左侧元素是最终数组需要包含的元素)
        while (cur < n) {
            if (arr[cur] != 0) {
                dest++;  // 非零元素,dest+1
            } else {
                dest += 2; // 零元素,复写后占两个位置,dest+2
            }
            if (dest >= n - 1) { // 当dest到达数组末尾(或超出),停止遍历
                break;
            }
            cur++;
        }

        // 第二步:处理dest刚好等于n的情况(最后一个零复写后超出数组长度)
        if (dest == n) {
            dest--;          // 回退到数组最后一个位置
            arr[dest--] = 0; // 只写一个零
            cur--;           // cur指针回退
        }

        // 第三步:从后往前填充数组
        while (cur >= 0) {
            if (arr[cur] != 0) {
                arr[dest--] = arr[cur]; // 非零元素直接填充
            } else {
                arr[dest--] = 0;        // 零元素填充两次
                arr[dest--] = 0;
            }
            cur--;
        }
    }
};

复杂度分析:

  • 时间复杂度:O(n)。只需要遍历数组两次(一次找边界,一次填充),n是数组长度。
  • 空间复杂度:O(1)。只使用了常量级别的额外变量,完全原地修改。

三、快乐数

题目描述:

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例

  • 输入 n = 19,输出 true(过程:12+92=82→82+22=68→62+82=100→12+02+02=11^2+9^2=82 → 8^2+2^2=68 → 6^2+8^2=100 → 1^2+0^2+0^2=112+92=82→82+22=68→62+82=100→12+02+02=1)
  • 输入 n = 2,输出 false

解题思路:

快乐数的核心矛盾是:过程要么终止于 1,要么陷入循环 (因为数字的平方和范围有限,比如 3 位数的最大平方和是 92+92+92=2439^2+9^2+9^2=24392+92+92=243,不可能无限增大)。

因此,我们可以用 快慢指针算法 来判断是否存在循环:

  • 慢指针 slow:每次走 1 步(计算 1 次平方和)
  • 快指针 fast:每次走 2 步(计算 2 次平方和)
  • 若过程中出现 slow == fast,说明进入循环:
    • 若相遇时的值是 1 → 是快乐数
    • 否则 → 不是快乐数

完整代码:

cpp 复制代码
class Solution {
public:
    int Sum(int n){
        int sum = 0;
        while(n){
            int t = n % 10;
            sum += t * t;
            n = n / 10;
        }
        return sum;
    }

    bool isHappy(int n) {
        int slow = n, fast = Sum(n);
        while(slow != fast){
            slow = Sum(slow);
            fast = Sum(Sum(fast));
        }
        return slow == 1;
    }
};

复杂度分析:

  • 时间复杂度 :O(log⁡n)O(\log n)O(logn)。每次计算平方和的时间是数字的位数(即 log⁡10n\log_{10}nlog10n),而快慢指针相遇的次数是有限的(因为平方和范围固定)。
  • 空间复杂度 :O(1)O(1)O(1)。仅使用了常数级别的额外变量,比哈希集合法(空间 O(log⁡n)O(\log n)O(logn))更优。
相关推荐
952361 小时前
二叉平衡树
java·数据结构·学习·算法
AIpanda8882 小时前
AI营销软件系统是什么?主要有哪些功能与优势?
算法
Rock_yzh2 小时前
LeetCode算法刷题——53. 最大子数组和
java·数据结构·c++·算法·leetcode·职场和发展·动态规划
阿_旭2 小时前
LAMP剪枝的基本原理与方法简介
算法·剪枝·lamp
前端小L2 小时前
回溯算法专题(六):双重剪枝的艺术——「组合总和 III」
算法·剪枝
leoufung2 小时前
103. 二叉树的锯齿形层序遍历(LeetCode 103)
算法·leetcode·职场和发展
程序员东岸2 小时前
《数据结构——排序(上)》从扑克牌到分治法:插入排序与希尔排序的深度剖析
数据结构·笔记·算法·排序算法
Yolo_TvT2 小时前
数据结构:队列
数据结构
客梦2 小时前
数据结构-哈希表
java·数据结构·笔记