【算法】【学习笔记】双指针

双指针

双指针是一种十分常用的算法方法,一般有滑动窗口、快慢指针等使用方式。

在下文就根据题目详细解释下。

❔题目

1. 环形链表

【点击跳转】环形链表——力扣

解题思路

是否有环是十分经典的快慢指针题目,就像龟兔赛跑一样。

只需要一个步长为1的slow指针,和一个步长为2的fast指针即可解决。

  • 只要fast能走到终点,说明无环;
  • 假如fast和slow相遇,说明有环。

证明:
假设有环,进入环后,fast相对于slow每轮靠近1个节点,
因此一定能够追上。

实现

function hasCycle(head: ListNode | null): boolean {
    let slow = head, fast = head;
    while(fast !== null && fast.next !== null){
        slow = slow!.next; // TS类型需要非空断言,算法上slow一定存在
        fast = fast.next.next;
        if(slow === fast){
            return true;
        }
    }
    return false;
};
public class Solution {
    public bool HasCycle(ListNode head) {
        var slow = head;
        var fast = head;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast){
                return true;
            }
        }
        return false;
    }
}
class Solution {
public:
    bool hasCycle(ListNode *head) {
        auto slow = head, fast = head;
        while(fast != NULL && fast->next != NULL){
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast){
                return true;
            }
        }
        return false;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
    无环时快指针遍历 \(N\) 个节点;有环时快指针追上慢指针最多多走一圈(环的长度),总步数依然在 \(O(N)\) 级别。
  • 空间复杂度\(O(1)\)
    仅使用了 slow 和 fast 两个节点指针,占用常数级别额外空间。

2. 最接近的三数之和

【点击跳转】最接近的三数之和——力扣

解题思路

一开始思考是否可以用滑动窗口,不过此题不需要连续,所以不可以用滑动窗口。

这时候可以用排序+双指针。

当定好第一个数 \(x\) 时,问题就转变为了 \(l+r\) 要尽可能接近 \(target-x\)

首先,进行排序,排序的目的就是利用单调性,使双指针移动具有方向性。

其中,\(l\)\(x\) 往后的第一个数,\(r\) 为数组的最后一个数:

  • \(l+r+x < target\) ,说明数值需要增大,由于已经排序了,即 \(l\) 往右移即可;
  • \(l+r+x > target\) ,说明数值需要缩小,由于已经排序了,即 \(r\) 往左移即可;
  • \(l+r+x = target\) ,说明已经是最优解,返回即可。

实现

function threeSumClosest(nums: number[], target: number): number {
    // 先排序了,定好一个数字,然后双指针找最近target的
    nums.sort((a,b)=>a-b);
    let t = nums[0] + nums[1] + nums[2];
    const n = nums.length;
    for(let i = 0; i < n-2; i++){
        const x = nums[i];
        // 当当前数与上次数一样,效果不会有变化的,跳过
        if (i > 0 && x === nums[i - 1]) continue;
        let l = i+1, r = n-1;
        while(l < r){
            const cur = nums[l] + nums[r] + nums[i];
			if(cur === target){
				return target;
			}
            if(Math.abs(target - t) > Math.abs(target - cur)){
                t = cur;
            }
            if(cur < target){
                l++;
            }else{
                r--;
            }
        }
    }
    return t;
};
public class Solution {
    public int ThreeSumClosest(int[] nums, int target) {
        Array.Sort(nums);
        int n = nums.Length;
        if(n < 3) return -1;
        int res = nums[0]+nums[1]+nums[2];
        for(int i = 0; i < n-2; i++){
            int num = nums[i];
            if(i > 0 && num == nums[i-1]) continue;
            int l = i + 1, r = n - 1;
            while(l < r){
                int cur = nums[l] + nums[r] + num;
				if(cur == target){
					return target;
				}
                if(Math.Abs(target-cur) < Math.Abs(target-res)){
                    res = cur;
                }
                if(cur < target){
                    l++;
                }else{
                    r--;
                }
            }
        }
        return res;
    }
}
class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        int n = nums.size();
        if(n < 3) return -1;
        sort(nums.begin(), nums.end());
        int res = nums[0]+nums[1]+nums[2];
        for(int i = 0; i < n-2; i++){
            int num = nums[i];
			if (i > 0 && num == nums[i - 1]) continue;
            int l = i+1, r = n-1;
            while(l < r){
                int cur = nums[l] + nums[r] + num;
				if(cur == target){
					return target;
				}
                if(abs(target-cur)<abs(target-res)){
                    res = cur;
                }
                if(cur < target){
                    l++;
                }else{
                    r--;
                }
            }
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N^2)\)
    数组排序需要 \(O(N \log N)\),外层循环 \(O(N)\) 配合内层双指针 \(O(N)\) 的总耗时为 \(O(N^2)\)。综合时间复杂度为 \(O(N \log N) + O(N^2) = O(N^2)\)
  • 空间复杂度\(O(1)\)
    没有使用额外的非常数级空间,只是使用了左右指针。

3. 神奇字符串

【点击跳转】神奇字符串——力扣

解题思路

神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。

可以看出1和2都至多只能连续出现2次。

我们可以使用一边构造神奇字符串,一边对1计数来获得答案。

构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:

  • 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
  • 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。

实现

function magicalString(n: number): number {
    // 由此可以知道,1和2的连续范围也只在[1,2]
    if(n <= 0) return 0;
    if(n <= 3) return 1;
    // 初始化种子字符串和指针
    let res = 1;
    const s = [1,2,2];
    let i = 2;
    let num = 1;  // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
    while(s.length < n){
        const count = s[i];  // 决定当前数字要加几次
        for(let j = 0; j < count && s.length < n; j++){
            s.push(num);
            if(num === 1){
                res++;
            }
        }
        num = 3 - num;  // 2变1,1变2
        i++;  // 检查下一个
    }
    return res;
};
public class Solution {
    public int MagicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        List<int> s = [1,2,2];
        int res = 1;
        int i = 2;
        int num = 1;
        while(s.Count < n){
            int count = s[i++];
            for(int j = 0; j < count && s.Count < n; j++){
                s.Add(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
}
class Solution {
public:
    int magicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        vector<int> s = {1,2,2};
        int res = 1, i = 2, num = 1;
        while(s.size() < n){
            int count = s[i++];
            for(int j = 0; j < count && s.size() < n; j++){
                s.push_back(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
    每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。
  • 空间复杂度\(O(N)\)
    需要一个长度为n的数组存储神奇字符串。

4. 神奇字符串

【点击跳转】神奇字符串——力扣

解题思路

神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。

可以看出1和2都至多只能连续出现2次。

我们可以使用一边构造神奇字符串,一边对1计数来获得答案。

构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:

  • 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
  • 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。

实现

function magicalString(n: number): number {
    // 由此可以知道,1和2的连续范围也只在[1,2]
    if(n <= 0) return 0;
    if(n <= 3) return 1;
    // 初始化种子字符串和指针
    let res = 1;
    const s = [1,2,2];
    let i = 2;
    let num = 1;  // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
    while(s.length < n){
        const count = s[i];  // 决定当前数字要加几次
        for(let j = 0; j < count && s.length < n; j++){
            s.push(num);
            if(num === 1){
                res++;
            }
        }
        num = 3 - num;  // 2变1,1变2
        i++;  // 检查下一个
    }
    return res;
};
public class Solution {
    public int MagicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        List<int> s = [1,2,2];
        int res = 1;
        int i = 2;
        int num = 1;
        while(s.Count < n){
            int count = s[i++];
            for(int j = 0; j < count && s.Count < n; j++){
                s.Add(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
}
class Solution {
public:
    int magicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        vector<int> s = {1,2,2};
        int res = 1, i = 2, num = 1;
        while(s.size() < n){
            int count = s[i++];
            for(int j = 0; j < count && s.size() < n; j++){
                s.push_back(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
    每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。
  • 空间复杂度\(O(N)\)
    需要一个长度为n的数组存储神奇字符串。

5. 神奇字符串

【点击跳转】神奇字符串——力扣

解题思路

神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。

可以看出1和2都至多只能连续出现2次。

我们可以使用一边构造神奇字符串,一边对1计数来获得答案。

构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:

  • 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
  • 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。

实现

function magicalString(n: number): number {
    // 由此可以知道,1和2的连续范围也只在[1,2]
    if(n <= 0) return 0;
    if(n <= 3) return 1;
    // 初始化种子字符串和指针
    let res = 1;
    const s = [1,2,2];
    let i = 2;
    let num = 1;  // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
    while(s.length < n){
        const count = s[i];  // 决定当前数字要加几次
        for(let j = 0; j < count && s.length < n; j++){
            s.push(num);
            if(num === 1){
                res++;
            }
        }
        num = 3 - num;  // 2变1,1变2
        i++;  // 检查下一个
    }
    return res;
};
public class Solution {
    public int MagicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        List<int> s = [1,2,2];
        int res = 1;
        int i = 2;
        int num = 1;
        while(s.Count < n){
            int count = s[i++];
            for(int j = 0; j < count && s.Count < n; j++){
                s.Add(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
}
class Solution {
public:
    int magicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        vector<int> s = {1,2,2};
        int res = 1, i = 2, num = 1;
        while(s.size() < n){
            int count = s[i++];
            for(int j = 0; j < count && s.size() < n; j++){
                s.push_back(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
    每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。
  • 空间复杂度\(O(N)\)
    需要一个长度为n的数组存储神奇字符串。

6. 神奇字符串

【点击跳转】神奇字符串——力扣

解题思路

神奇字符串是一个无限序列,题目只要求它的前 n 个字符。
因此我们只需要模拟生成过程直到长度达到 n。

可以看出1和2都至多只能连续出现2次。

我们可以使用一边构造神奇字符串,一边对1计数来获得答案。

构造的方法可以先构造出默认的最初状态s='122',然后每次插入的数字num为1和2的轮换——即插入1后,下一次插入2:

  • 假如读取到的 s[i] 为 2,说明当前数字 num 需要连续插入 2 次;
  • 假如读取到的 s[i] 为 1,说明当前数字 num 需要连续插入 1 次。

实现

function magicalString(n: number): number {
    // 由此可以知道,1和2的连续范围也只在[1,2]
    if(n <= 0) return 0;
    if(n <= 3) return 1;
    // 初始化种子字符串和指针
    let res = 1;
    const s = [1,2,2];
    let i = 2;
    let num = 1;  // 下一个要填的数字,交替出现,填了1就变2,填了2就变1。
    while(s.length < n){
        const count = s[i];  // 决定当前数字要加几次
        for(let j = 0; j < count && s.length < n; j++){
            s.push(num);
            if(num === 1){
                res++;
            }
        }
        num = 3 - num;  // 2变1,1变2
        i++;  // 检查下一个
    }
    return res;
};
public class Solution {
    public int MagicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        List<int> s = [1,2,2];
        int res = 1;
        int i = 2;
        int num = 1;
        while(s.Count < n){
            int count = s[i++];
            for(int j = 0; j < count && s.Count < n; j++){
                s.Add(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
}
class Solution {
public:
    int magicalString(int n) {
        if(n <= 0) return 0;
        if(n <= 3) return 1;
        vector<int> s = {1,2,2};
        int res = 1, i = 2, num = 1;
        while(s.size() < n){
            int count = s[i++];
            for(int j = 0; j < count && s.size() < n; j++){
                s.push_back(num);
                if(num == 1){
                    res++;
                }
            }
            num = 3 - num;
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度\(O(N)\)
    每个字符最多被生成一次,因此生成前 n 个字符需要 O(n) 时间。
  • 空间复杂度\(O(N)\)
    需要一个长度为n的数组存储神奇字符串。

注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

posted @ 2026-08-07 22:57  SEHOD  阅读(3)  评论(0)    收藏  举报