← 返回计算机基础总览

🎯 双指针与滑动窗口面试题

高频数组与字符串算法技巧

📚 双指针与滑动窗口核心知识

1. 什么是双指针?什么是滑动窗口?它们有什么关系和区别?简单

双指针(Two Pointers)

在同一个数组/字符串上使用两个索引进行遍历,常见形式:

  • 对撞指针:一个从左,一个从右,向中间移动(如有序数组两数之和、回文判断)。
  • 快慢指针:一个走一步,一个走两步(如链表成环检测、找到中点)。

滑动窗口(Sliding Window)

窗口由左右两个指针[l, r]定义,表示当前考虑的子数组/子串,右指针扩张窗口,左指针收缩窗口。

关系与区别

  • 滑动窗口本质上是一种特殊的双指针(左右指针单向前进)。
  • 双指针更广义,还包括对撞、快慢指针等模式。
  • 滑动窗口通常用于连续子区间相关问题(和、长度、包含字符集等)。

典型应用

  • 数组:两数之和(对撞)、最大/最小子数组长度(窗口)。
  • 字符串:无重复字符最长子串、覆盖子串。
2. 有序数组的两数之和如何用对撞指针实现?(LeetCode 167)简单

题目要点

数组有序,找到两个数的下标,使得和为 target。

对撞指针解法

public int[] twoSumSorted(int[] numbers, int target) {
    int left = 0, right = numbers.length - 1;
    while (left < right) {
        int sum = numbers[left] + numbers[right];
        if (sum == target) {
            return new int[]{left + 1, right + 1};  // 题目下标从1开始
        } else if (sum < target) {
            left++;
        } else {
            right--;
        }
    }
    return new int[]{-1, -1};
}

// 时间复杂度:O(n)
// 空间复杂度:O(1)

思路解析

  • 因为数组有序,当和过大时,右指针左移可以减小和。
  • 当和过小时,左指针右移可以增大和。
3. 如何判断一个字符串是否为回文串?(只考虑字母数字,忽略大小写)简单

对撞指针解法(LeetCode 125)

public boolean isPalindrome(String s) {
    int left = 0, right = s.length() - 1;
    while (left < right) {
        char c1 = s.charAt(left);
        char c2 = s.charAt(right);
        if (!Character.isLetterOrDigit(c1)) {
            left++;
            continue;
        }
        if (!Character.isLetterOrDigit(c2)) {
            right--;
            continue;
        }
        if (Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

// 时间复杂度:O(n)
// 空间复杂度:O(1)
4. 无重复字符的最长子串如何用滑动窗口解决?(LeetCode 3)中等

滑动窗口模板

public int lengthOfLongestSubstring(String s) {
    Map window = new HashMap<>();
    int left = 0, right = 0;
    int maxLen = 0;

    while (right < s.length()) {
        char c = s.charAt(right);
        right++;
        window.put(c, window.getOrDefault(c, 0) + 1);

        // 当窗口中某个字符出现次数 > 1 时,收缩左边界
        while (window.get(c) > 1) {
            char d = s.charAt(left);
            left++;
            window.put(d, window.get(d) - 1);
        }
        maxLen = Math.max(maxLen, right - left);
    }
    return maxLen;
}

// 时间复杂度:O(n)
// 空间复杂度:O(k),k 为字符集大小

关键点

  • 右指针扩张窗口,维护窗口内字符频次。
  • 当存在重复字符时,用 while 收缩左指针直到窗口合法。
  • 每次窗口合法时更新答案。
5. 最小覆盖子串问题如何用滑动窗口解决?(LeetCode 76)困难

模板级滑动窗口

public String minWindow(String s, String t) {
    if (s.length() < t.length()) return "";

    Map need = new HashMap<>();
    for (char c : t.toCharArray()) {
        need.put(c, need.getOrDefault(c, 0) + 1);
    }

    Map window = new HashMap<>();
    int left = 0, right = 0;
    int valid = 0;  // 窗口中满足 need 条件的字符种类数

    int start = 0, len = Integer.MAX_VALUE;

    while (right < s.length()) {
        char c = s.charAt(right);
        right++;

        if (need.containsKey(c)) {
            window.put(c, window.getOrDefault(c, 0) + 1);
            if (window.get(c).equals(need.get(c))) {
                valid++;
            }
        }

        while (valid == need.size()) {
            if (right - left < len) {
                start = left;
                len = right - left;
            }
            char d = s.charAt(left);
            left++;
            if (need.containsKey(d)) {
                if (window.get(d).equals(need.get(d))) {
                    valid--;
                }
                window.put(d, window.get(d) - 1);
            }
        }
    }

    return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}

// 时间复杂度:O(n)
// 空间复杂度:O(k)

思路归纳

  • need 统计目标串 t 中每个字符的需求次数。
  • window 维护当前窗口中字符次数,valid 记录满足需求的字符种类。
  • 当 valid == need.size() 时说明窗口已满足条件,开始收缩左边界,缩小窗口。
6. 长度最小的子数组(和 ≥ target)如何用滑动窗口解决?(LeetCode 209)中等

解法

public int minSubArrayLen(int target, int[] nums) {
    int left = 0, sum = 0;
    int minLen = Integer.MAX_VALUE;

    for (int right = 0; right < nums.length; right++) {
        sum += nums[right];
        while (sum >= target) {
            minLen = Math.min(minLen, right - left + 1);
            sum -= nums[left++];
        }
    }

    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}

// 时间复杂度:O(n)
// 空间复杂度:O(1)
7. 字符串的排列:s2 中是否包含 s1 的某个排列?(LeetCode 567)中等

固定窗口大小的滑动窗口

public boolean checkInclusion(String s1, String s2) {
    if (s1.length() > s2.length()) return false;
    int[] need = new int[26];
    int[] window = new int[26];

    for (char c : s1.toCharArray()) {
        need[c - 'a']++;
    }

    for (int i = 0; i < s2.length(); i++) {
        window[s2.charAt(i) - 'a']++;
        if (i >= s1.length()) {
            window[s2.charAt(i - s1.length()) - 'a']--;
        }
        if (Arrays.equals(need, window)) {
            return true;
        }
    }
    return false;
}

// 时间复杂度:O(26 * n) ≈ O(n)
// 空间复杂度:O(1)
8. 替换最多 K 个字符得到最长重复字符子串(LeetCode 424)如何用窗口解决?中等

思路

窗口长度为 windowLen,窗口内出现次数最多的字符为 maxCount,若 windowLen - maxCount > K,说明需要收缩窗口。

public int characterReplacement(String s, int k) {
    int[] count = new int[26];
    int left = 0, right = 0, maxCount = 0, maxLen = 0;

    while (right < s.length()) {
        int idx = s.charAt(right) - 'A';
        count[idx]++;
        maxCount = Math.max(maxCount, count[idx]);
        right++;

        while (right - left - maxCount > k) {
            count[s.charAt(left) - 'A']--;
            left++;
        }
        maxLen = Math.max(maxLen, right - left);
    }
    return maxLen;
}

// 时间复杂度:O(n)
// 空间复杂度:O(1)
9. 有序数组原地删除多余重复元素,每个元素最多保留两个(LeetCode 80)。中等

快慢指针

public int removeDuplicates(int[] nums) {
    if (nums.length <= 2) return nums.length;
    int slow = 2;
    for (int fast = 2; fast < nums.length; fast++) {
        if (nums[fast] != nums[slow - 2]) {
            nums[slow++] = nums[fast];
        }
    }
    return slow;
}

// 时间复杂度:O(n)
// 空间复杂度:O(1)

思路

允许每个元素出现两次,只需保证 nums[slow-2] != nums[fast] 时才将 fast 元素写入 slow。

10. 盛最多水的容器问题如何用对撞指针解决?(LeetCode 11)中等

解法

public int maxArea(int[] height) {
    int left = 0, right = height.length - 1;
    int max = 0;
    while (left < right) {
        int h = Math.min(height[left], height[right]);
        int area = h * (right - left);
        max = Math.max(max, area);
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    return max;
}

// 时间复杂度:O(n)
// 空间复杂度:O(1)

为什么正确?

移动短板指针是有依据的:如果移动长板,只会减小宽度且高度不可能变大到超过原来的短板高度,因此面积不会增大。

11. 三数之和问题如何用排序 + 双指针解决?(LeetCode 15)困难

解法

public List> threeSum(int[] nums) {
    List> res = new ArrayList<>();
    Arrays.sort(nums);

    for (int i = 0; i < nums.length - 2; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;  // 去重
        if (nums[i] > 0) break; // 剪枝

        int left = i + 1, right = nums.length - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum == 0) {
                res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                left++;
                right--;
                while (left < right && nums[left] == nums[left - 1]) left++;
                while (left < right && nums[right] == nums[right + 1]) right--;
            } else if (sum < 0) {
                left++;
            } else {
                right--;
            }
        }
    }
    return res;
}

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)(不计结果集)
12. 四数之和如何在三数之和的基础上扩展?(LeetCode 18)困难

核心思路

  • 先排序。
  • 用两层循环固定前两个数 i、j,剩下的用双指针找两数之和。
  • 注意大量剪枝和去重。

时间复杂度

外层两层循环 O(n²),内层双指针 O(n),总时间 O(n³)。

13. 双指针与滑动窗口在字符串处理中的常见题型有哪些?中等

常见模式

  • 最长/最短子串:无重复字符最长子串、最小覆盖子串、限定字符的最长子串。
  • 包含/排除某些字符:包含 t 中所有字符、只包含 0/1 的最长子串。
  • 子数组和问题:和 ≥ / ≤ target 的最短/最长子数组。
  • 排列/异位词:是否包含某个字符串的排列、所有异位词的起始下标。

解题套路

  • 先写出「暴力解」枚举所有子串,再思考如何用左右指针维护一个「动态区间」。
  • 用一个或多个计数结构(数组/HashMap)维护窗口状态。
  • 右指针只前进一次,左指针按需前进,整体 O(n)。
14. 在接口限流设计中,如何用滑动窗口统计某时间段内的请求次数?中等

滑动时间窗口限流

  • 维护一个按时间排序的队列,队列中存最近一段时间(如1分钟)内的请求时间戳。
  • 新请求到来时:
    • 弹出队首所有早于当前时间 - 窗口大小的时间戳。
    • 看队列长度是否超过限制阈值。
class RateLimiter {
    private Deque window = new ArrayDeque<>();
    private long interval;
    private int maxRequests;

    public RateLimiter(long intervalMillis, int maxRequests) {
        this.interval = intervalMillis;
        this.maxRequests = maxRequests;
    }

    public synchronized boolean allow() {
        long now = System.currentTimeMillis();
        while (!window.isEmpty() && now - window.peekFirst() > interval) {
            window.pollFirst();
        }
        if (window.size() < maxRequests) {
            window.offerLast(now);
            return true;
        }
        return false;
    }
}
15. 双指针/滑动窗口题目很多,你在刷题过程中是如何系统化总结这类题的?困难

可在面试中展示的学习方法

  • 按题型归类:两数之和/三数之和类、子串/子数组长度类、子集/排列类等。
  • 整理模板:
    • 对撞指针模板(左右夹逼)。
    • 快慢指针模板(链表相关)。
    • 滑动窗口模板(窗口内计数 + 收缩条件)。
  • 记录「不变量」:每道题窗口内需要维护的约束是什么?如「无重复」「和 >= target」「包含所有字符」。
  • 错误本子:把容易出错的边界情况和 bug 记录下来(比如 while 条件、left/right 更新顺序)。