源本科技 | 码上会

JavaScript String

2026/01/13
9
0

学习目标

  • 理解 JavaScript 字符串的不可变性及其对算法实现的影响

  • 掌握常用字符串方法的实际应用

  • 能够使用 JavaScript 高效解决简单、中等、困难级别的字符串算法题

  • 熟悉经典字符串算法的基本思想与应用场景


JS 字符串基础特性

在 JavaScript 中,字符串是原始类型且不可变。这意味着:

  • 字符串一旦创建,其内容无法被修改

  • 所有“修改”操作(如替换、截取)都会返回一个新字符串

  • 可通过索引访问字符(str[i]),但不能赋值(str[0] = 'x' 无效)

let s = "Dev";
console.log(s[0]);     // "D"
console.log(s.length); // 3

// 尝试修改(无效)
s[0] = 'X';
console.log(s); // 仍然是 "Dev"

提示:由于不可变性,频繁拼接字符串(如循环中 str += char)可能导致性能问题,建议使用数组 + join() 优化。


常用字符串方法

方法

功能

应用场景

length

获取字符串长度

边界判断、循环控制

charAt(index)

获取指定位置字符

字符比较、回文检测

slice(start, end)

截取子串(支持负索引)

子串提取、滑动窗口

substring(start, end)

截取子串(不支持负索引)

同上,但行为略有不同

toUpperCase() / toLowerCase()

大小写转换

忽略大小写的比较(如回文、异位词)

trim()

去除首尾空白

输入预处理

replace(search, new)

替换子串(首次匹配)

模式清理、占位符替换

split(separator)

按分隔符转为数组

单词分割、路径解析

includes(sub)

判断是否包含子串

子序列检查、关键词匹配

concat(str1, str2...)

拼接字符串

构造结果(但性能不如模板字符串)

注意:slice()substring() 在处理负数时行为不同:

  • slice(-2) 表示从倒数第 2 位开始

  • substring(-2) 会被视为 substring(0)


字符串简单问题

1. 反转字符串

function reverseString(s) {
    return s.split('').reverse().join('');
}
// 或使用双指针(适用于字符数组)

2. 回文检测

function isPalindrome(s) {
    const clean = s.toLowerCase().replace(/[^a-z0-9]/g, '');
    return clean === clean.split('').reverse().join('');
}

3. 判断子序列

function isSubsequence(s, t) {
    let i = 0;
    for (let char of t) {
        if (char === s[i]) i++;
    }
    return i === s.length;
}

4. 异位词判断

function isAnagram(s1, s2) {
    if (s1.length !== s2.length) return false;
    const sorted1 = s1.split('').sort().join('');
    const sorted2 = s2.split('').sort().join('');
    return sorted1 === sorted2;
}

字符串中等问题

1. 实现 atoi(字符串转整数)

需处理空格、正负号、溢出:

function myAtoi(s) {
    const num = parseInt(s.trim());
    if (isNaN(num)) return 0;
    const INT_MAX = 2**31 - 1;
    const INT_MIN = -(2**31);
    return Math.min(Math.max(num, INT_MIN), INT_MAX);
}

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

滑动窗口

function lengthOfLongestSubstring(s) {
    const set = new Set();
    let left = 0, maxLen = 0;
    for (let right = 0; right < s.length; right++) {
        while (set.has(s[right])) {
            set.delete(s[left++]);
        }
        set.add(s[right]);
        maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

3. 最长回文子序列(动态规划)

注意:子序列 ≠ 子串(不要求连续)


字符串困难问题

1. KMP 字符串匹配算法

用于高效查找模式串在文本中的位置,时间复杂度 O(n + m):

function computeLPS(pattern) {
    const lps = new Array(pattern.length).fill(0);
    let len = 0, i = 1;
    while (i < pattern.length) {
        if (pattern[i] === pattern[len]) {
            lps[i++] = ++len;
        } else {
            if (len > 0) len = lps[len - 1];
            else lps[i++] = 0;
        }
    }
    return lps;
}

function KMPSearch(text, pattern) {
    const lps = computeLPS(pattern);
    let i = 0, j = 0;
    while (i < text.length) {
        if (text[i] === pattern[j]) {
            i++; j++;
            if (j === pattern.length) return i - j; // 找到匹配
        } else {
            if (j > 0) j = lps[j - 1];
            else i++;
        }
    }
    return -1; // 未找到
}

2. Rabin-Karp 算法

基于哈希的字符串匹配,适用于多模式匹配或大文本搜索。

3. 最长有效括号

使用栈或动态规划解决括号匹配问题。


最佳实践

  • 避免频繁字符串拼接:使用 Array.join() 代替 +=

    // 不推荐
    let result = '';
    for (let c of str) result += c;
    
    // 推荐
    let arr = [];
    for (let c of str) arr.push(c);
    let result = arr.join('');
  • 正则表达式慎用:虽然简洁,但在高频调用中可能成为性能瓶颈

  • 利用内置方法:如 includes()startsWith() 比手动循环更高效且可读性高


重点总结

  • JavaScript 字符串是不可变的原始类型,所有操作返回新字符串

  • 熟练掌握 split, join, slice, replace, includes 等方法是解决 DSA 问题的基础

  • 简单问题常通过双指针、哈希表、排序解决

  • 中等问题多涉及滑动窗口、动态规划、状态机

  • 困难问题需掌握KMP、Rabin-Karp、高级 DP 等经典算法

  • 注意边界条件(空串、单字符、全相同字符等)和性能优化


思考题

  1. 为什么 s.split('').reverse().join('') 能反转字符串?如果字符串包含 Unicode 表情符号(如 "👨‍💻"),这种方法是否仍然安全?为什么?

  2. 在“无重复字符的最长子串”问题中,为什么使用 Set 而不是 Array.includes() 来检查重复?时间复杂度有何差异?

  3. KMP 算法中的 LPS(最长前缀后缀)数组是如何帮助跳过不必要的比较的?请用 "ABABCABABA" 作为模式串手动计算其 LPS 数组。