字符串-JS
注意,四、替换数字是Node.js环境,笔试经常是这种环境,而不是leetcode,需要注意。
一、基础知识
JavaScript 字符串基础知识梳理
1. 字符串的定义与创建
- 字面量创建:
let str1 = 'Hello'; let str2 = "World"; let str3 = `模板字符串`; // ES6 支持插值:`Hello ${name}`
- 构造函数创建(不推荐):
let str = new String("Hello"); // 返回对象,非原始值
2. 字符串的特性
- 不可变性:字符串一旦创建,无法直接修改其中的字符。
let s = "abc"; s[0] = "d"; // 无效,s 仍为 "abc"
- 原始类型:但可以调用方法(JS 引擎自动装箱为临时对象)。
3. 常用方法
3.1 查询与访问
length
:获取长度。"abc".length; // 3
charAt(index)
/[index]
:获取指定位置字符。"abc".charAt(1); // "b" "abc"[1]; // "b"(更简洁)
indexOf(substr)
/includes(substr)
:查找子串。"abc".indexOf("b"); // 1 "abc".includes("bc"); // true
3.2 截取与分割
slice(start, end)
:截取子串(支持负数索引)。"abcdef".slice(1, 3); // "bc" "abcdef".slice(-2); // "ef"
substring(start, end)
:类似slice
,但不支持负数。split(separator)
:按分隔符拆分为数组。"a,b,c".split(","); // ["a", "b", "c"]
3.3 修改(返回新字符串)
toLowerCase()
/toUpperCase()
:大小写转换。"AbC".toLowerCase(); // "abc"
trim()
:去除两端空格。" abc ".trim(); // "abc"
replace(old, new)
:替换子串(仅替换第一个匹配)。"abcabc".replace("a", "x"); // "xbcabc"
repeat(count)
:重复字符串。"a".repeat(3); // "aaa"
3.4 正则相关
match(regexp)
:匹配正则表达式。"abc123".match(/\d+/); // ["123"]
search(regexp)
:返回匹配的索引。"abc123".search(/\d/); // 3
4. 字符串与数组的转换
- 字符串 → 数组:
"abc".split(""); // ["a", "b", "c"] Array.from("abc"); // ["a", "b", "c"](支持 Unicode 代理对)
- 数组 → 字符串:
["a", "b", "c"].join(""); // "abc"
5. Unicode 与特殊字符
- Unicode 表示:
"\u0041"; // "A" "😊".length; // 2(代理对占两个码元)
- 码点操作:
"A".codePointAt(0); // 65 String.fromCodePoint(65); // "A"
6. 模板字符串(ES6)
- 插值表达式:
let name = "Alice"; `Hello, ${name}!`; // "Hello, Alice!"
- 多行字符串:
`Line 1Line 2`;
7. 性能注意事项
- 避免频繁拼接:使用
+=
在循环中拼接字符串性能差,推荐用数组push
+join
。// 低效 let result = ""; for (let i = 0; i < 1000; i++) result += i;// 高效 let arr = []; for (let i = 0; i < 1000; i++) arr.push(i); let result = arr.join("");
8. 实用技巧
- 反转字符串:
"abc".split("").reverse().join(""); // "cba"
- 检查前缀/后缀:
"file.txt".endsWith(".txt"); // true "https://".startsWith("http"); // true
9. 常见面试题
- 实现字符串反转(需区分 Unicode 安全方式)。
- 判断回文字符串:
function isPalindrome(s) {return s === s.split("").reverse().join(""); }
- 统计字符出现次数:
"abcabc".split("a").length - 1; // 2
总结
- 核心特性:不可变、原始类型、Unicode 支持。
- 重点方法:
slice
、split
、replace
、trim
。 - ES6+:模板字符串、
includes
、repeat
。 - 性能:避免循环内拼接,优先使用数组操作。
二、344.反转字符串
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组s
的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"] 输出:["o","l","l","e","h"]示例 2:
输入:s = ["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"]提示:
1 <= s.length <= 105
s[i]
都是 ASCII 码表中的可打印字符
方法1.封装swap函数
并传递数组引用
直接在原地对字符串进行交换,需要注意,下面这种交换函数是错的。
var swap = (a, b) => {let temp; temp = a;a = b;b = temp;return a, b;
};
- 错误原因:
- JavaScript 的函数参数是 按值传递(对于原始类型如
number
/string
),函数内的a
和b
只是局部变量的副本,修改它们不会影响外部变量。 return a, b;
实际上是返回b
(逗号操作符返回最后一个值),且调用方未接收返回值。
- JavaScript 的函数参数是 按值传递(对于原始类型如
正确代码:
/*** @param {character[]} s* @return {void} Do not return anything, modify s in-place instead.*/
var swap = (arr, i, j) => {[arr[i], arr[j]] = [arr[j], arr[i]]; // 修改原数组
};var reverseString = function(s) {for(let i = 0; i < Math.floor(s.length / 2); i++) { // 只需遍历一半swap(s, i, s.length - 1 - i);}return s;
};
方法 2:直接操作数组(推荐)双指针
var reverseString = function(s) {let left = 0, right = s.length - 1;while (left < right) {// 直接交换数组元素[s[left], s[right]] = [s[right], s[left]]; // ✅ 解构赋值left++;right--;}return s;
};
- 优点:无需额外函数,直接通过索引修改原数组。
方法3:使用JS库函数
本题直接return s.reverse()即可。但显然不是让用这个。
const arr = ["h", "e", "l", "l", "o"];
arr.reverse(); // 原地反转
console.log(arr); // ["o", "l", "l", "e", "h"]
4.拓展
- 扩展思考:如果是字符串(而非字符数组),需先转为数组再操作:
- 字符串转数组 s=s.spilt('');
- 数组转为字符串s= s.join('');
三、541.反转字符2
541. 反转字符串 II
给定一个字符串
s
和一个整数k
,从字符串开头算起,每计数至2k
个字符,就反转这2k
字符中的前k
个字符。
- 如果剩余字符少于
k
个,则将剩余字符全部反转。- 如果剩余字符小于
2k
但大于或等于k
个,则反转前k
个字符,其余字符保持原样。示例 1:
输入:s = "abcdefg", k = 2 输出:"bacdfeg"示例 2:
输入:s = "abcd", k = 2 输出:"bacd"提示:
1 <= s.length <= 104
s
仅由小写英文组成1 <= k <= 104
思路
这道题目的关键点就是2k个数据是一组,然后对前k个数据进行操作。
题目要求 每
2k
个字符为一组,反转其中的前k
个字符。因此:
-
i
的初始值为 0(从字符串开头开始)。- 每次循环后,
i
跳转到下一个2k
的起始位置,即i += 2 * k
。
- 例如
k=2
时,2k=4
,所以每次处理4
个字符中的前2
个:
- 第一组:
i=0
(处理s[0..3]
的前2
个)- 第二组:
i=4
(处理s[4..7]
的前2
个)- 依此类推。
如何保证剩余字符正确处理?
关键在于
right
的边界控制:let right = Math.min(i + k - 1, arr.length - 1);
-
i + k - 1
:当前组前k
个字符的最后一个索引。-
arr.length - 1
:防止越界(剩余字符不足k
时直接取到末尾)。
方法1:双指针法
/*** @param {string} s* @param {number} k* @return {string}*/var reverseStr = function(s, k) {const arr = s.split('');for (let i = 0; i < arr.length; i += 2 * k) {let left = i;let right = Math.min(i + k - 1, arr.length - 1); // 防止越界// 反转前k个字符while (left < right) {[arr[left], arr[right]] = [arr[right], arr[left]];left++;right--;}}return arr.join('');
};
方法2:直接操作字符串(非原地,但更简洁)
var reverseStr = function(s, k) {let result = '';for (let i = 0; i < s.length; i += 2 * k) {// 反转前k个字符const segment = s.slice(i, i + k).split('').reverse().join('');// 添加剩余字符(不反转)const remaining = s.slice(i + k, i + 2 * k);result += segment + remaining;}return result;
};
无论选择哪种解法关键点还是要想到拆分为2k步进行操作。
四、替换数字
给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。
例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。
对于输入字符串 "a5b",函数应该将其转换为 "anumberb"
输入:一个字符串 s,s 仅包含小写字母和数字字符。
输出:打印一个新的字符串,其中每个数字字符都被替换为了number
样例输入:a1b2c3
样例输出:anumberbnumbercnumber
数据范围:1 <= s.length < 10000。
解决思路
- 遍历字符串:检查每个字符是否为数字。
- 替换数字:
- 如果字符是数字(
0-9
),替换为"number"
。 - 否则,保留原字符。
- 如果字符是数字(
- 拼接结果:将处理后的字符拼接成新字符串。
方法 1:正则表达式(最简洁)
利用 String.prototype.replace()
结合正则表达式全局匹配数字:
function replaceDigits(s) {return s.replace(/\d/g, 'number');
}
解释:
/\d/g
:正则表达式匹配所有数字(\d
等价于[0-9]
),g
表示全局匹配。'number'
:替换所有匹配项。
方法 2:遍历字符串(显式逻辑)
如果不允许用正则表达式,可以手动遍历:
function replaceDigits(s) {let result = '';for (let char of s) {if (char >= '0' && char <= '9') {result += 'number';} else {result += char;}}return result;
}
优化点:
- 使用
for...of
直接遍历字符,避免索引操作。 - 通过比较字符的 ASCII 值(
'0'
到'9'
)判断是否为数字。
方法 3:数组映射(函数式风格)
将字符串转为数组后处理:
function replaceDigits(s) {return s.split('').map(char => char >= '0' && char <= '9' ? 'number' : char).join('');
}
适用场景:适合链式操作,但性能略低于直接遍历。
Node.js 环境(常见于笔试系统)
const readline = require("readline");const rl = readline.createInterface({input: process.stdin,output: process.stdout
})function replaceDigits(s) {let result = '';for (let char of s) {if (char >= '0' && char <= '9') {result += 'number';} else {result += char;}}return result;
}// 监听输入
rl.on('line', (input) => {console.log(replaceDigits(input)); // 输出结果rl.close(); // 关闭输入流
});
说明:
readline
是 Node.js 内置模块,用于逐行读取输入。rl.on('line')
监听用户输入的一行数据。- 替换逻辑与之前相同(正则表达式或遍历均可)。
- 适用场景:大多数在线编程笔试系统(如牛客、ACM 模式)。
五、151.反转字符串中的单词
给你一个字符串
s
,请你反转字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。
s
中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串
s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。示例 1:
输入:s = "the sky is blue" 输出:"blue is sky the"示例 2:
输入:s = " hello world " 输出:"world hello" 解释:反转后的字符串中不能存在前导空格和尾随空格。示例 3:
输入:s = "a good example" 输出:"example good a" 解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。提示:
1 <= s.length <= 104
s
包含英文大小写字母、数字和空格' '
s
中 至少存在一个 单词
进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用
O(1)
额外空间复杂度的 原地 解法。
方法1:使用栈
解题思路:
- 按空格分割字符串:
s.split(' ')
更简单,无需逐个字符处理。 - 过滤空字符串:
if (word !== '')
避免多余的空格。 - 栈的正确使用:先压入所有单词,再依次弹出实现反转。
- 结果拼接:用数组
reversed
存储结果,最后join(' ')
合并。
/*** @param {string} s* @return {string}*/
// 用栈进行处理 把每个单词作为一个字符串存在数组里 以空格为区分
var reverseWords = function(s) {let arr = s.split(' ');let stack = [];// 将非空单词压入栈for (let word of arr) {if (word !== '') {stack.push(word);}}// 从栈中弹出单词(实现反转)let reversed = [];while (stack.length > 0) {reversed.push(stack.pop());}return reversed.join(' ');
};
方法2:优化版本(更简洁)
如果不需要显式用栈,可以进一步简化:
var reverseWords = function(s) {return s.split(' ').filter(word => word !== '') // 过滤空字符串.reverse() // 反转单词顺序.join(' '); // 合并为字符串
};
为什么可以直接用 split(' ')
分割单词?
在 JavaScript 中,split(' ')
是处理以空格分隔的单词的最直接方法。它的工作原理和注意事项如下:
1. split(' ')
的核心机制
- 功能:按空格字符
' '
将字符串拆分成数组。 - 示例:
"the sky is blue".split(' '); // 结果: ["the", "sky", "is", "blue"]
- 连续空格的处理:
"a b".split(' '); // 结果: ["a", "", "", "b"](空字符串需过滤)
2. 为什么能正确分割单词?
- 英语文本的规则:单词之间通常用单个空格分隔。
- JavaScript 的自动拆分:
split(' ')
会严格按空格拆分,无论连续多少个空格。- 后续通过
filter
或遍历可清理空字符串。
3.split(' ')
和 split('')
的区别
在 JavaScript 中,split()
方法的参数决定了字符串如何被分割。以下是两者的关键区别:
1. split(' ')
—— 按空格分割
- 功能:将字符串按 空格字符
' '
拆分成数组,分割点为每个空格。 - 示例:
"hello world".split(' '); // 结果: ["hello", "world"]
- 连续空格的处理:
"a b".split(' '); // 结果: ["a", "", "", "b"](空字符串来自连续空格)
- 适用场景:拆分句子中的单词(需配合
filter
清理空字符串)。
2. split('')
—— 按字符分割
- 功能:将字符串拆分为 单个字符的数组,包括空格。
- 示例:
"hello".split(''); // 结果: ["h", "e", "l", "l", "o"]
"a b".split(''); // 结果: ["a", " ", "b"](空格也被保留为数组元素)
- 适用场景:需要操作每个字符(如反转字符串、统计字符频率)。
对比总结
方法 | 分割依据 | 结果示例(输入 "a b" ) | 典型用途 |
---|---|---|---|
split(' ') | 按空格 | ["a", "b"] | 拆分单词 |
split('') | 按每个字符 | ["a", " ", "b"] | 操作字符(如反转、统计) |
常见问题
Q1:如果想忽略连续空格,如何优化 split(' ')
?
用正则表达式合并连续空格:
"a b".split(/\s+/);
// 结果: ["a", "b"](自动忽略多余空格)
Q2:split('')
能正确处理 Unicode 字符(如表情符号)吗?
不能。像 "😊"
这样的字符占 2 个码元,直接 split('')
会拆成乱码:
"😊".split('');
// 结果: ["\ud83d", "\ude0a"](错误拆分)
正确方法:
Array.from("😊");
// 结果: ["😊"](支持 Unicode)
代码示例
场景 1:反转句子中的单词
function reverseWords(s) {return s.split(' ') // 按空格拆分.filter(word => word !== '') // 去空值.reverse() // 反转单词顺序.join(' '); // 合并为字符串
}console.log(reverseWords("hello world")); // "world hello"
场景 2:反转字符串中的字符
function reverseString(s) {return s.split('') // 拆分为字符数组.reverse() // 反转字符顺序.join(''); // 合并为字符串
}console.log(reverseString("abc")); // "cba"
总结
-
split(' ')
:用于拆分单词(需处理连续空格)。 -
split('')
:用于操作单个字符(注意 Unicode 问题)。 - 关键区别:空格是否作为分割符或保留为数组元素。