LeetCode 字符串类题目解析与 Java 实现指南(深度优化版)

一、字符串处理核心知识点

1. Java 字符串特性

(1)不可变性(Immutable)
  • 本质String 类基于 final char[] value 实现,一经创建不可修改,任何修改操作都会生成新对象。
  • 应用场景
    • 频繁修改字符串时,使用 StringBuilder(非线程安全)或 StringBuffer(线程安全),其内部维护可变字符数组,避免频繁创建对象。

      // 反例:循环中用 + 拼接字符串(时间复杂度 O(n²))
      String s = "";
      for (int i = 0; i < n; i++) {
      s += i; // 每次拼接生成新字符串
      }

      // 正例:使用 StringBuilder(时间复杂度 O(n))
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < n; i++) {
      sb.append(i);
      }

(2)常用 API 详解
方法 描述 时间复杂度
charAt(int index) 返回指定索引处的字符,索引范围 [0, length()-1],越界抛出异常 O(1)
substring(int begin) substring(int begin, int end) 截取子串,左闭右开区间 [begin, end),不包含 end 索引字符 O(k)
toCharArray() 将字符串转换为字符数组 O(n)
indexOf(String str) lastIndexOf(String str) 查找子串首次 / 末次出现的位置,未找到返回 -1 O(n×m)
matches(String regex) 验证字符串是否匹配正则表达式 O(n)
split(String regex) 根据正则表达式分割字符串,返回字符串数组 O(n)

2. 高频解题技巧

技巧 核心思想 典型例题(难度) 优化点
双指针 快慢指针或左右指针,用于遍历、交换、验证回文等场景 125. 验证回文串(简单) 344. 反转字符串(简单) 原地操作,空间复杂度 O (1)
滑动窗口 通过维护窗口 [left, right] 动态调整区间,解决子串 / 子数组问题 3. 无重复字符的最长子串(中等) 76. 最小覆盖子串(困难) 用哈希表 / 数组记录字符频率,优化窗口收缩逻辑
哈希映射(HashMap) 统计字符频率、快速查找字符位置,适用于异位词、字符匹配问题 387. 字符串中的第一个唯一字符(简单) 49. 字母异位词分组(中等) int[128] 替代 HashMap,提升访问速度(尤其针对 ASCII 字符)
动态规划(DP) 定义状态转移方程,解决最长公共子串、最长回文子串等问题 5. 最长回文子串(中等) 1143. 最长公共子序列(中等) 状态压缩(如二维数组压缩为一维),降低空间复杂度
栈结构 处理括号匹配、表达式求值、路径简化等具有后进先出特性的问题 20. 有效的括号(简单) 71. 简化路径(中等) Deque 替代 Stack,优先使用 ArrayDeque(性能更优)

二、经典题型解析(附变形题与优化思路)

1. 反转字符串(No. 344)

问题描述 :原地反转字符数组,要求空间复杂度 O (1)。
双指针解法

复制代码
public void reverseString(char[] s) {
    int left = 0, right = s.length - 1;
    while (left < right) {
        // 交换左右指针字符
        char temp = s[left];
        s[left++] = s[right];
        s[right--] = temp;
    }
}

变形题 :反转字符串中的单词(No. 151)
解法思路

  1. 去除首尾空格,按多个空格分割单词;

  2. 反转单词列表,用空格拼接结果。

    public String reverseWords(String s) {
    // 1. 去除首尾空格并按空格分割(+ 表示匹配一个或多个空格)
    String[] words = s.trim().split(" +");
    // 2. 反转单词列表
    Collections.reverse(Arrays.asList(words));
    // 3. 用空格拼接结果
    return String.join(" ", words);
    }

优化点 :避免使用 split 产生的空字符串,改用双指针手动分割单词。

2. 最长无重复子串(No. 3)

滑动窗口 + 哈希表解法

复制代码
public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> map = new HashMap<>(); // 记录字符最后出现的位置
    int maxLen = 0, left = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (map.containsKey(c)) {
            // 窗口左边界至少移动到重复字符的下一个位置
            left = Math.max(left, map.get(c) + 1);
        }
        map.put(c, right); // 更新字符位置
        maxLen = Math.max(maxLen, right - left + 1); // 计算当前窗口长度
    }
    return maxLen;
}

进阶优化 :使用 int[128] 数组替代 HashMap(适用于 ASCII 字符集)

复制代码
public int lengthOfLongestSubstring(String s) {
    int[] freq = new int[128]; // 初始化全为 0
    int maxLen = 0, left = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        freq[c]++;
        // 窗口内出现重复字符时,收缩左边界
        while (freq[c] > 1) {
            freq[s.charAt(left++)]--;
        }
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

3. 有效括号(No. 20)

栈结构解法

复制代码
public boolean isValid(String s) {
    Deque<Character> stack = new ArrayDeque<>(); // 优先使用双端队列
    Map<Character, Character> map = Map.of(')', '(', ']', '[', '}', '{'); // Java 9+ 语法

    for (char c : s.toCharArray()) {
        if (!map.containsKey(c)) { // 左括号入栈
            stack.push(c);
        } else { // 右括号匹配
            if (stack.isEmpty() || stack.pop() != map.get(c)) {
                return false;
            }
        }
    }
    return stack.isEmpty(); // 最终栈为空则匹配成功
}

易错点 :右括号出现时,需先判断栈是否为空,避免 NullPointerException

三、进阶算法与复杂题型

1. KMP 算法(No. 28 实现 strStr ())

核心思想 :利用前缀函数(lps 数组)记录模式串的最长公共前后缀,避免重复匹配。
算法步骤

  1. 构建前缀表(lps 数组)
    lps[i] 表示模式串前 i 个字符的最长公共前后缀长度(不包含自身)。

  2. 模式匹配:通过前缀表跳过已匹配的部分,减少不必要的回溯。

    public int strStr(String haystack, String needle) {
    int n = haystack.length(), m = needle.length();
    if (m == 0) return 0; // 空字符串特殊处理

    复制代码
     // 1. 构建前缀表(lps 数组)
     int[] lps = new int[m];
     for (int i = 1, len = 0; i < m;) { // i 为当前字符索引,len 为最长公共前后缀长度
         if (needle.charAt(i) == needle.charAt(len)) {
             lps[i++] = ++len; // 匹配成功,长度+1
         } else if (len > 0) {
             len = lps[len - 1]; // 回溯到上一个最长公共前后缀
         } else {
             lps[i++] = 0; // 无公共前后缀
         }
     }
    
     // 2. 模式匹配
     for (int i = 0, j = 0; i < n;) { // i 为主串索引,j 为模式串索引
         if (haystack.charAt(i) == needle.charAt(j)) {
             i++;
             j++;
             if (j == m) return i - m; // 匹配成功,返回起始位置
         } else if (j > 0) {
             j = lps[j - 1]; // 模式串回溯到 lps[j-1] 位置
         } else {
             i++; // 主串后移一位
         }
     }
     return -1; // 未找到匹配

    }

时间复杂度:构建前缀表 O (m),匹配过程 O (n),总体 O (n+m)。

2. 字符串排列(No. 567 验证子串是否为排列)

滑动窗口 + 频率数组解法

复制代码
public boolean checkInclusion(String s1, String s2) {
    int[] count = new int[26]; // 记录 s1 字符频率
    for (char c : s1.toCharArray()) {
        count[c - 'a']++;
    }

    int left = 0, right = 0, match = 0; // match 记录匹配的字符数
    while (right < s2.length()) {
        int rChar = s2.charAt(right) - 'a';
        if (count[rChar] > 0) { // 字符在 s1 中存在
            count[rChar]--;
            match++;
            right++;
        } else { // 字符不在 s1 中,重置窗口
            count[s2.charAt(left) - 'a']++; // 左边界字符放回频率数组
            if (match > 0 && s2.charAt(left) - 'a' == rChar) { // 若移除的是不匹配字符,重置 match
                match = 0;
            }
            left++;
            right = left; // 右边界回到左边界,重新开始匹配
        }

        if (match == s1.length()) { // 窗口内所有字符匹配 s1
            return true;
        }
    }
    return false;
}

优化点 :通过 match 变量记录匹配进度,避免每次比较整个窗口的频率数组。

四、常见易错点与避坑指南

易错点 示例代码(反例) 修正方法
索引越界 char c = s.charAt(s.length()); 先判断 s.length() > 0,或使用 try-catch 捕获异常
字符串比较误区 if (s1 == s2)(比较引用而非内容) 使用 s1.equals(s2),或对可能为 null 的对象用 Objects.equals(s1, s2)
不可变陷阱 for (int i=0; i<n; i++) s += i;(低效拼接) 改用 StringBuilder.append()
字符与数字转换 int num = 'a' - 'A';(正确,得 32) int digit = s.charAt(i) - '0'(需确保字符是数字) 转换前用 Character.isDigit(c) 验证,避免非法字符导致错误
边界条件遗漏 空字符串 ""、全空格 " "、超长整数 "2147483648"(超出 Integer 范围) 测试用例中必须包含空输入、极端值,处理大数时用 long 过渡并判断溢出
正则表达式转义 s.split(".")(无法分割,. 是正则通配符) 转义为 s.split("\\."),或使用 Pattern.quote(".")

五、系统化刷题策略

1. 分阶段训练路线

阶段 目标 推荐题型(题号 / 难度) 学习要点
基础 掌握字符串特性与基础 API 344. 反转字符串(简单) 20. 有效括号(简单) 双指针、栈的基本使用
进阶 熟练运用滑动窗口、哈希表 3. 无重复字符的最长子串(中等) 49. 字母异位词分组(中等) 窗口收缩逻辑、字符频率统计
高阶 动态规划、KMP 等复杂算法 5. 最长回文子串(中等) 28. 实现 strStr ()(中等) 状态转移方程设计、前缀函数理解
挑战 综合应用与优化 76. 最小覆盖子串(困难) 567. 字符串排列(中等) 多技巧结合、性能优化(如用数组替代哈希表)

2. 测试用例设计模板

复制代码
// 以反转字符串为例
public class TestReverseString {
    @Test
    public void testCases() {
        // 1. 空输入
        char[] s1 = {};
        reverseString(s1);
        assertArrayEquals(new char[]{}, s1);

        // 2. 单字符
        char[] s2 = {'a'};
        reverseString(s2);
        assertArrayEquals(new char[]{'a'}, s2);

        // 3. 偶数长度
        char[] s3 = {'h', 'e', 'l', 'l', 'o'};
        reverseString(s3);
        assertArrayEquals(new char[]{'o', 'l', 'l', 'e', 'h'}, s3);

        // 4. 奇数长度
        char[] s4 = {'A', ' ', 'm', 'a', 'n'};
        reverseString(s4);
        assertArrayEquals(new char[]{'n', 'a', 'm', ' ', 'A'}, s4);
    }
}

3. 代码模板总结

(1)双指针模板(反转 / 回文)
复制代码
public void twoPointersTemplate(char[] s) {
    int left = 0, right = s.length - 1;
    while (left < right) {
        // 交换/比较操作
        char temp = s[left];
        s[left++] = s[right];
        s[right--] = temp;
    }
}
(2)滑动窗口模板(子串问题)
复制代码
public int slidingWindowTemplate(String s) {
    int[] freq = new int[128];
    int left = 0, maxLen = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        freq[c]++;
        // 窗口收缩条件(根据题意调整)
        while (freq[c] > 1) {
            freq[s.charAt(left++)]--;
        }
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

通过系统掌握字符串处理的核心技巧,配合合理的Java API使用,能够显著提升解决LeetCode字符串类题目的效率。建议从简单题开始建立信心,逐步挑战中等难度综合题,最后攻克KMP等高级算法难题。

相关推荐
Jackson@ML几秒前
用Python构建学生成绩管理系统的基本方案
开发语言·python
叒卮3 分钟前
小白编程学习之巧解「消失的数字」
数据结构·学习·算法
cpp_learners9 分钟前
QT 使用QPdfWriter和QPainter绘制PDF文件
开发语言·qt·pdf
悠哉清闲10 分钟前
Kotlin 协程 (一)
android·开发语言·kotlin
Bro_cat11 分钟前
在Java项目中集成Deepseek大语言模型实践指南
java·开发语言·人工智能·spring boot·语言模型
i1yo_kiki12 分钟前
idea部署本地仓库和连接放送远程仓库
java·ide·intellij-idea
asom2217 分钟前
Java 09Stream流与File类
java·开发语言
噜噜噜噜鲁先森26 分钟前
MVDR源码(可直接运行)
算法·matlab·信号处理·阵列信号处理·声源定位算法
林开落L27 分钟前
C语言:基础篇之常见概念
c语言·开发语言
孔明click3332 分钟前
Sa-Token v1.43.0 发布 🚀,新增 SSO 单设备注销、消息推送,多 Access-Token 并存能力
java·sa-token·springboot·登录·权限认证