文章

字符串专题:Mermaid 图解双指针、滑动窗口与 4 道 LeetCode 核心题

用 Mermaid 图解字符串中的双指针、滑动窗口、回文判断和 4 道 LeetCode 经典题,系统掌握字符串专题。

字符串专题:Mermaid 图解双指针、滑动窗口与 4 道 LeetCode 核心题

字符串题表面上看像“细节题”,实际上是算法面试里的绝对高频。

它们真正考的通常不是字符串 API,而是:

  • 双指针怎么移动
  • 窗口什么时候扩、什么时候缩
  • 子串和子序列有什么区别
  • 如何把字符问题转成计数、下标和区间问题

这篇文章继续用 Mermaid 图解的方式,把字符串题中最常见的双指针、滑动窗口、回文和计数模型讲清楚,再用 4 道 LeetCode 题把高频套路串起来。

学习目标:

  1. 理解字符串题的常见建模方式。
  2. 掌握双指针和滑动窗口的核心逻辑。
  3. 理解子串、子序列、回文问题的区别。
  4. 用 4 道 LeetCode 题覆盖字符串高频模型。
  5. 用一张知识卡片形成字符串题的判断框架。

一、字符串题的本质:在字符序列上做区间和状态管理

字符串本质上是字符数组,因此很多题的核心不是“字符串”,而是:

  • 区间 [l, r]
  • 计数表
  • 匹配关系
  • 顺序约束
flowchart TD
    A[字符串问题] --> B[区间处理]
    A --> C[字符计数]
    A --> D[顺序匹配]
    A --> E[模式识别]

所以字符串题很常见的第一反应应该是:

  • 双指针
  • 滑动窗口
  • 哈希计数
  • 动态规划

二、双指针:维护两个边界

双指针最常见的形式有两类:

  • 左右夹逼
  • 同向滑动
flowchart LR
    A[l] --> B[中间区间]
    B --> C[r]

左右夹逼

适合:

  • 回文判断
  • 有序数组两端逼近

同向双指针

适合:

  • 去重
  • 原地压缩
  • 滑动窗口

三、滑动窗口:可变长度区间的统一模型

滑动窗口本质是:

维护一个满足某种条件的连续子串区间。

flowchart TD
    A[右指针右移 扩大窗口] --> B[更新窗口状态]
    B --> C{窗口是否满足条件}
    C -->|否| D[继续扩张]
    C -->|是| E[尝试左指针右移 缩小窗口]
    E --> F[更新答案]

这类题的关键不是模板,而是两件事:

  • 窗口状态用什么维护
  • 什么时候应该收缩左边界

四、子串、子序列、回文,容易混但其实不同

mindmap
  root((字符串分类))
    子串
      连续
      常见窗口
    子序列
      不要求连续
      常见 DP
    回文
      正反对称
      常见双指针或中心扩展

子串

必须连续。

子序列

不要求连续,只要求相对顺序不变。

回文

关注正反是否一致。

你先分清这三类,很多题型判断会快很多。


五、4 道 LeetCode 题目打通字符串专题

1)LeetCode 125. 验证回文串

题型定位:左右双指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
    bool isPalindrome(string s) {
        int l = 0, r = static_cast<int>(s.size()) - 1;
        while (l < r) {
            while (l < r && !isalnum(static_cast<unsigned char>(s[l]))) ++l;
            while (l < r && !isalnum(static_cast<unsigned char>(s[r]))) --r;
            if (tolower(static_cast<unsigned char>(s[l])) !=
                tolower(static_cast<unsigned char>(s[r]))) {
                return false;
            }
            ++l;
            --r;
        }
        return true;
    }
};
flowchart LR
    A[l 向右找有效字符] --> B[比较两端字符]
    C[r 向左找有效字符] --> B
    B --> D{是否相等}
    D -->|是| E[继续向中间逼近]
    D -->|否| F[返回 false]

这题练的是:

  • 左右双指针
  • 非字母数字过滤

2)LeetCode 3. 无重复字符的最长子串

题型定位:滑动窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_set<char> window;
        int ans = 0;
        for (int l = 0, r = 0; r < static_cast<int>(s.size()); ++r) {
            while (window.count(s[r])) {
                window.erase(s[l++]);
            }
            window.insert(s[r]);
            ans = max(ans, r - l + 1);
        }
        return ans;
    }
};
flowchart TD
    A[右指针加入新字符] --> B{窗口内有重复吗}
    B -->|有| C[左指针右移并删除字符]
    C --> B
    B -->|无| D[更新最长长度]

这题训练的是:

  • 窗口状态维护
  • 什么时候扩、什么时候缩

3)LeetCode 438. 找到字符串中所有字母异位词

题型定位:固定长度滑动窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> res;
        if (s.size() < p.size()) return res;
        vector<int> need(26, 0), window(26, 0);
        for (char c : p) ++need[c - 'a'];
        int k = static_cast<int>(p.size());
        for (int i = 0; i < static_cast<int>(s.size()); ++i) {
            ++window[s[i] - 'a'];
            if (i >= k) --window[s[i - k] - 'a'];
            if (window == need) res.push_back(i - k + 1);
        }
        return res;
    }
};
flowchart TD
    A[窗口长度固定为 k] --> B[新字符进入]
    B --> C[旧字符移出]
    C --> D[比较窗口计数和目标计数]
    D --> E[相等则记录起点]

这题练的是:

  • 固定窗口
  • 计数数组建模

4)LeetCode 5. 最长回文子串

题型定位:中心扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
    string longestPalindrome(string s) {
        if (s.size() < 2) return s;
        int start = 0, max_len = 1;
        for (int i = 0; i < static_cast<int>(s.size()); ++i) {
            int len1 = expand(s, i, i);
            int len2 = expand(s, i, i + 1);
            int len = max(len1, len2);
            if (len > max_len) {
                max_len = len;
                start = i - (len - 1) / 2;
            }
        }
        return s.substr(start, max_len);
    }

private:
    int expand(const string& s, int l, int r) {
        while (l >= 0 && r < static_cast<int>(s.size()) && s[l] == s[r]) {
            --l;
            ++r;
        }
        return r - l - 1;
    }
};
flowchart TD
    A[选择一个中心] --> B[向两边扩展]
    B --> C{两侧字符相同吗}
    C -->|是| D[继续扩展]
    C -->|否| E[停止并计算长度]

这题训练的是:

  • 回文的中心性质
  • 奇数长度和偶数长度中心

六、字符串题怎么快速判断模型

flowchart TD
    A[看到字符串题] --> B{要求连续子串吗}
    B -->|是| C[优先考虑滑动窗口 / 双指针]
    B -->|否| D{要求顺序匹配但不连续吗}
    D -->|是| E[考虑子序列 DP / 双指针]
    D -->|否| F{是否是回文问题}
    F -->|是| G[双指针 / 中心扩展 / DP]
    F -->|否| H[再看哈希 计数 模式匹配]

七、字符串常见错误

1)子串和子序列混淆

子串连续,子序列不连续。

2)窗口收缩条件写错

滑动窗口题,最容易错的就是“什么时候开始缩左边”。

3)字符大小写和非法字符没处理

尤其是回文串类题。

4)窗口状态更新顺序错

先加还是先删,会直接影响答案。

flowchart TD
    A[窗口扩张] --> B[更新计数]
    B --> C[判断是否满足条件]
    C --> D[必要时收缩]

八、字符串知识卡片

mindmap
  root((字符串))
    双指针
      左右夹逼
      同向移动
    滑动窗口
      可变长度
      固定长度
    回文
      双指针
      中心扩展
    分类
      子串
      子序列
      计数匹配

复习版要点:

  • 字符串题常被转成区间、计数和顺序问题
  • 连续子串优先想到滑动窗口
  • 回文问题优先想到双指针或中心扩展
  • 固定窗口常用计数数组
  • 先分清子串、子序列、回文三类题

九、最后总结

如果只记一句话,请记这个:

字符串题的关键,不是字符本身,而是“区间、计数和顺序”怎么维护。

做题时先判断:

  • 是连续子串还是非连续子序列
  • 是窗口问题还是回文问题
  • 窗口状态该用什么维护

把这篇里的 4 道题做透,字符串高频题型就会非常稳。

本文由作者按照 CC BY 4.0 进行授权