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等高级算法难题。

相关推荐
西岭千秋雪_2 分钟前
@Lazy原理与实战
java·服务器·spring boot·spring
liang_jy8 分钟前
Java this
java·面试
CodeCraft Studio9 分钟前
国产化Excel处理组件Spire.XLS教程:用 Java 获取所有 Excel 工作表名称(图文详解)
java·excel·数据处理·spire
子豪-中国机器人11 分钟前
C++ 信息学奥赛总复习题
java·jvm·算法
Java中文社群19 分钟前
Dify实战案例:MySQL查询助手!嘎嘎好用
java·人工智能·后端
程序猿阿伟22 分钟前
《深度探秘:Java构建Spark MLlib与TensorFlow Serving混合推理流水线》
java·spark-ml·tensorflow
TDengine (老段)31 分钟前
TDengine 开发指南—— UDF函数
java·大数据·数据库·物联网·数据分析·tdengine·涛思数据
键盘林34 分钟前
分布式系统简述
java·开发语言
可儿·四系桜35 分钟前
如何在 Java 中优雅地使用 Redisson 实现分布式锁
java·开发语言·分布式
Stanford_11061 小时前
关于大数据的基础知识(二)——国内大数据产业链分布结构
大数据·开发语言·物联网·微信小程序·微信公众平台·twitter·微信开放平台