js中字符操作方法
在JavaScript中,字符串(String)是一种基本的数据类型,也是一种不可变的字符序列。字符串有许多内置的方法,可以进行各种常见的操作。以下是在LeetCode刷题中可能会遇到的字符串相关操作:
字符串查询
charAt(index)
: 返回指定位置的字符。charCodeAt(index)
: 返回在指定的位置的字符的 Unicode 编码。indexOf(substring)
: 返回子字符串在字符串中首次出现的位置。lastIndexOf(substring)
: 返回子字符串在字符串中最后出现的位置。includes(substring)
: 判断字符串是否包含指定的子字符串。
字符串截取
substring(startIndex, endIndex)
: 返回字符串中介于两个指定下标之间的字符。slice(startIndex, endIndex)
: 提取字符串的某个部分,并返回一个新的字符串。substr(startIndex, length)
: 返回从指定位置开始的指定长度的子字符串。
字符串修改
replace(searchFor, replaceWith)
: 在字符串中用一些字符替换另一些字符。toUpperCase()
: 将字符串转换为大写。toLowerCase()
: 将字符串转换为小写。trim()
: 从字符串的两端删除空白字符。
字符串分割
split(separator, limit)
: 通过分隔符将字符串分割成子字符串数组。
字符串连接
concat(str1, str2, ...)
: 连接两个或多个字符串,并返回新的字符串。- 使用
+
运算符也可以连接字符串。
其他操作
repeat(count)
: 返回指定次数重复的字符串。startsWith(substring)
: 判断字符串是否以指定的子字符串开头。endsWith(substring)
: 判断字符串是否以指定的子字符串结尾。padStart(targetLength, padString)
: 在字符串的开头填充指定的字符,直到达到指定的长度。padEnd(targetLength, padString)
: 在字符串的末尾填充指定的字符,直到达到指定的长度。
使用正则表达式
JavaScript中的字符串与正则表达式(RegExp)紧密集成,可以进行复杂的模式匹配和搜索操作,如:
match(regexp)
: 返回一个数组,包含匹配的结果。search(regexp)
: 返回第一个与正则表达式匹配的子串的起始位置。replace(regexp, replaceWith)
: 使用正则表达式进行搜索和替换。
在刷题时,这些字符串操作方法可以帮助你轻松处理字符串问题,如字符串的查找、替换、分割、连接等常见场景。熟悉这些方法对于解题至关重要。
字符串方法使用
58. 最后一个单词的长度
给你一个字符串 s
,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。
单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。
思路:首先想到按空格分隔字符串,调用split方法,分隔方式是空格' ';
其次,考虑尾部可能有多余的空格,因此在分割字符串前需要用trim方法去掉首尾空格。
最后返回字符数组的最后一项的长度。可以用数组的pop方法返回字符串,用.length方法获取字符串的长度
/**
* @param {string} s
* @return {number}
*/
var lengthOfLastWord = function (s) {
let str = s.trim().split(' ');//去首尾空格,并用空格分开为数组
return str.pop().length;//返回数组最后一项字符串的长度
};
14. 最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""
。
思路:以第一个字符串为标准,尝试取前缀进行后面的字符比较。在比较过程中如果发现有字符小于这个前缀可提前结束比较。通过slice复制前缀字符串,通过index显示记录前缀遍历的下标。如果比较过程中有字符与前缀不等,返回之前存储的pre前缀信息。一趟for循环遍历后,如果都相等,将subStr更新为pre。
/**
* @param {string[]} strs
* @return {string}
*/
var longestCommonPrefix = function (strs) {
let pre = '';
let subStr = '';
let index = 1;
while (index <= strs[0].length) {
let subStr = strs[0].slice(0, index);
for (let i = 1; i < strs.length; i++) {
if (strs[i].length < index) {//某个字符串的长度小于了前缀的长度,返回前缀
return pre;
}
if (strs[i].slice(0, index) != subStr) {
return pre;
}
}
pre = subStr;
index++;
}
return pre;
};
151. 反转字符串中的单词
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
思路:最简单的方式就是调用js的字符串方法处理。现用正则匹配所有多余的空格,多余的只保留一个空格。将整个字符串按照空格转成字符串数组。对数组进行翻转,翻转后的数组拼接为字符串。
/**
* @param {string} s
* @return {string}
*/
var reverseWords = function (s) {
s = s.trim().replace(/\s+/g, ' ');//去除多余空格
s = s.split(' ').reverse().join(' ');//转成数组,用数组的reverse翻转整个字符串数组,在拼成字符串返回
return s;
};
在正则表达式中,
/\s+/g
这个表达式可以分解成以下几部分:
/
: 正则表达式的开始和结束标志,表示正则表达式的开始和结束。\s
: 表示匹配任意空白字符,包括空格、制表符、换行符等。+
: 表示匹配前面的元素一次或多次,即匹配一个或多个空白字符。g
: 表示全局匹配模式,即在整个字符串中查找所有匹配的内容,而不是在找到第一个匹配后就停止。
空间复杂度分析:s.trim()
:trim()
方法会创建一个新的字符串,去除原始字符串首尾的空格,因此空间复杂度为O(n);s.replace(/\s+/g, ' ')
:replace()
方法会创建一个新的字符串,用于替换多余的空格为单个空格,因此空间复杂度为O(n);s.split(' ')
:split()
方法会创建一个新的数组,将字符串按空格分割成单词数组,空间复杂度为O(n)。总的空间复杂度为O(n)
进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1)
额外空间复杂度的 原地 解法。
整体思路:考虑原地求解,现将数组用双指针原地翻转;然后,考虑去除多余空格;然后翻转每个单词。
由于字符串s本身可能有多个空格,操作字符串后字符串长度可能小于原始长度。如何返回操作后的字符串呢?
利用双指针,慢指针存储翻转后取出多余空格的字符,快指针向右遍历,右指针结束的时候,此时慢指针指的位置就是最终的字符串最后一项。
此时,直接将字符串的
length
属性设置为新的长度,这样就相当于在原数组的基础上截断了字符串。这种方法并不会创建新的字符串,因此空间复杂度为常数。
在JavaScript中,字符串是不可变的,所以你不能直接在原数组上修改字符串。但是,如果你有一个字符数组,你可以进行原地翻转字符串。这样其实空间复杂度还是o(n),所以在js中,其实没有更好的方案,以下是参考
/**
* @param {string} s
* @return {string}
*/
var reverseWords = function (s) {
// 字符串转数组
s = Array.from(s);
reverse(s, 0, s.length - 1); //翻转整个字符串
removeSpace(s); //原地删除多余空格
let start = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === " ") {
reverse(s, start, i - 1);
start = i + 1;
}
}
reverse(s, start, s.length - 1);
return s.join("");
};
// 辅助函数:反转字符串中指定范围的字符
function reverse(s, start, end) {
while (start < end) {
let temp = s[start];
s[start] = s[end];
s[end] = temp;
start++;
end--;
}
return s;
}
function removeSpace(s) {
let slow = 0,
quick = 0;
while (quick < s.length) {
if (s[quick] != " ") {
s[slow++] = s[quick];
} else if (slow != 0 && quick < s.length - 1 && s[quick + 1] != " ") {
//s[quick]=' '但是后面不等于空格
s[slow++] = s[quick];
}
quick++;
}
s.length = slow;
}
滑动窗口
438. 找到字符串中所有字母异位词
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
思路:字符串比数组麻烦多了。这个题利用p.length长度的滑动窗口,对窗口内的字符串与p字符串进行比较,如果相等就将滑动窗口左边的下标记录在结果数组中。但是这个题对时间复杂度有限制,会导致有些用例超时,所以在比较两个字符串是否相等的时候最好不要用sort排序。其实可以用滑动窗口的动态维护一个存放字符频率的数组numsCur。利用字符出现的频率和p数组字符频率进行比较,如果有一个不相等就为false。并且numsCur每次只会删除和新增一次。这里题目都是小写字母,用字母的编码-小写字符a的编码就能将a-z映射到0-25个数组下标了。
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
var findAnagrams = function (s, p) {
if (s.length < p.length) return [];
let res = [];
let numsP = Array(26).fill(0);//统计每个字符出现的频率
let numsCur = Array(26).fill(0);
let left = 0; right = p.length - 1;
for (let i = 0; i < p.length; i++) {//初始化P,cur数组
let indexP = p[i].charCodeAt(0) - 'a'.charCodeAt(0);
numsP[indexP]++;
let indexCur = s[i].charCodeAt(0) - 'a'.charCodeAt(0);
numsCur[indexCur]++;
}
//判断第一个p.length子串是否相等
if (isSame(numsCur, numsP)) {
res.push(left);
}
while (right < s.length - 1) {
//减少left对应字符的出现频率,增加right的字符出现频率
numsCur[s[left].charCodeAt(0) - 'a'.charCodeAt(0)]--;
left++;
right++;
numsCur[s[right].charCodeAt(0) - 'a'.charCodeAt(0)]++;
if (isSame(numsCur, numsP)) {
res.push(left);
}
}
return res;
};
// 检查两个字符数组是否为异位词
function isSame(num1, num2) {
for (let i = 0; i < 26; i++) {
if (num1[i] !== num2[i]) {
return false;
}
}
return true;
}
76. 最小覆盖子串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
思路:字符串的一道困难题。思路不难,边界条件比较多。从思考到写debugg一个小时完成。思路就是利用左右指针构建一个滑动窗口,left的边界值是s.length-t.length。通过一个mapT存储t中元素出现的频率,同样维护一个mapS,记录在t中有key的元素以及频率。在left向右和right向右过程中,动态更新mapS。判断mapT和mapS是否可以相等。
如果mapT和mapS大小相等,所有mapS已经有了mapT中所有元素,此时再看频率,如果mapS有元素频率小于mapT中相应元素的频率,返回fasle。
如何更新left,如果滑动窗口里已经找到了相等的串,移动左指针,尝试看是否有更小的串。
可以看提示给的信息,更容易理解:
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function (s, t) {
if (s.length < t.length) return "";
let mapT = new Map();
let mapS = new Map();
let minStr = "";
let left = 0,
right = t.length - 1; //设置左指针的起始位置
//初始化mapT
for (let i = 0; i < t.length; i++) {
mapT.set(t[i], mapT.has(t[i]) ? mapT.get(t[i]) + 1 : 1); //设置字符出现的频率
}
//初始化mapS,只记录与mapT相同的key
for (let i = left; i <= right; i++) {
if (mapT.has(s[i])) {
//如果T有这个字符
mapS.set(s[i], mapS.has(s[i]) ? mapS.get(s[i]) + 1 : 1);
}
}
while (right < s.length && left <= s.length - t.length) {
if (compareTwoMap(mapT, mapS)) {
//判断窗口map是否相等
const curStr = s.slice(left, right + 1);
if (minStr.length) {
if (minStr.length > curStr.length) {
minStr = curStr;
}
} else {
minStr = curStr;
}
//如果相等,尝试移动左指针,更新map缩小返回,看是否相等
if (mapS.has(s[left])) {
const count = mapS.get(s[left]) - 1;
if (count == 0) {
mapS.delete(s[left]);
} else {
mapS.set(s[left], count);
}
}
left++;
} else {
//不相等,右指针扩大范围,更新map
right++;
if (mapT.has(s[right])) {
//如果T有这个字符
mapS.set(s[right], mapS.has(s[right]) ? mapS.get(s[right]) + 1 : 1);
}
}
}
return minStr;
};
function compareTwoMap(mapT, mapS) {
//比较两个map是否相等
if (mapT.size != mapS.size) {
return false;
} else {
//size相等,说明字符都出现了,看频率是否相等
//比较相同key的val是否相等
for (let key of mapT.keys()) {
if (mapT.get(key) > mapS.get(key)) {
//s数量如果小的话
return false;
}
}
}
return true;
}