本文主要针对 leetcode 中部分字符串相关的简单、中等算法题进行总结分析,提供几个易于理解,便于记忆的解题范式,希望可以帮到大家。
其中涉及到的字符串方法,总结在前面,如有不熟悉,建议先看一遍这篇文章:JavaScript 字符串方法总结
查找: indexOf(字符,索引起始)、search(字符)、charAt(索引), includes()
截取:slice[start, end)、substring[start, end)、substr(start, length)
其他:split()、trim()、concat()、reverse()
Set:has()、add()、delete()
回文字符串:正着读和倒着读完全一样
js
function isPalindrome(str){
// 方法一
let newStr = str.split('').reverse().join('')
if(newStr === str) return true
return false
// 方法二
for(let i = 0; i < str.length/2; i++){
if(str[i] !== str[length - 1 - i]) return false
}
return true
}
1 无重复字符的最长子串
题目: 给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度
思路: 无重复考虑 Set,遍历字符串,依次添加到 Set 中,当添加失败时说明出现重复字符。要求连续子串,考虑滑动窗口式的快慢指针遍历,解法如下:
1.初始化一个Set
记录字符是否重复
2.初始化快慢指针,慢指针遍历字符,快指针循环右移,并将字符 add 到Set
中,直至出现重复字符
3.此时Set.size
即该次遍历下最长不重复子串,更新res
4.自第二次遍历起,Set中不断删掉前面遍历过的字符,直到与快指针所在元素不重复后,重新开始 add
js
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
const mySet = new Set();
let res = 0;
let fast = 0
for(let i = 0; i < s.length; i++){
if(i !== 0){
mySet.delete(s.charAt(i-1))
}
while(fast < s.length && !mySet.has(s[fast])){
mySet.add(s[fast])
fast++
}
res = Math.max(res, mySet.size)
}
return res
};
2 字符串相加
题目: 给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
思路: 逆序遍历两个字符串,对位字符转为整形后相加
1.考虑进位carry
2.逆序遍历两个字符串,循环遍历条件为len1 >= 0 || len2 >= 0 || carry != 0
3.当某字符串先遍历完成,缺位补0进行相加const x = len1 >= 0 ? num1.charAt(len1--) - 0 : 0
4.对位字符转数字后求和,取余添加到res
,取整作为进位carry
js
/**
* @param {string} num1
* @param {string} num2
* @return {string}
*/
var addStrings = function(num1, num2) {
let len1=num1.length - 1
let len2=num2.length - 1
let carry = 0
const res = []
while(len1 >=0 || len2 >=0 || carry !== 0){
const x = len1 >= 0 ? num1.charAt(len1) - 0 : 0
const y = len2 >= 0 ? num2.charAt(len2) - 0 : 0
const sum = x + y + carry
const ele = sum % 10
carry = Math.floor(sum / 10)
res.unshift(ele)
len1--
len2--
}
return res.join('')
};
3 比较版本号
题目: 给你两个版本号 version1 和 version2 ,请你比较它们。版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由多位数字组成,可能包含前导零 。 返回规则如下:
- 如果 version1 > version2 返回 1,
- 如果 version1 < version2 返回 -1,
- 除此之外返回 0。
思路: 通过"."将字符串分割为数组,依次将字符转成数值,进行比较大小
1.版本号分割成数组
2.遍历两个数组,将字符转数值然后比较
3.两个数组长度不一致情况,用0补位
js
/**
* @param {string} version1
* @param {string} version2
* @return {number}
*/
var compareVersion = function(version1, version2) {
const arr1 = version1.split('.')
const arr2 = version2.split('.')
let len1 = arr1.length
let len2 = arr2.length
let max = Math.max(len1, len2)
for(let i = 0; i < max; i++){
const num1 = i >= len1 ? 0 : Number(arr1[i])
const num2 = i >= len2 ? 0 : Number(arr2[i])
if(num1 >num2){
return 1
}else if(num1 < num2){
return -1
}
}
return 0
};
4 有效的括号
题目: 给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
思路: 遇到左括号存起来,遇到右括号即进行匹配
1.新建一个Map
,设置左右括号为键值对;新建一个栈,存储左括号
2.遍历字符串,遇到左括号,存到栈中
3.遇到右括号,查询此时栈顶元素是否匹配
js
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
const myMap = new Map([[')','('],[']','['],['}','{']])
const arr = ['(','[','{']
const stack = []
for(let i = 0; i < s.length; i++){
if(arr.includes(s[i])){
stack.push(s[i])
}else{
if(stack.pop() !== myMap.get(s[i])){
return false
}
}
}
return !stack.length
};
5 最长回文子串
题目: 给你一个字符串 s,找到 s 中最长的回文子串。
思路: 从中间向两边循环确认字符是否相等
1.遍历字符串,每个字符都可能是回文子串的中心
2.通过左右指针不断分离确定最长回文子串
3.考虑回文子串两种长度情况:奇数、偶数
js
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
let res = ''
for(let i = 0; i < s.length; i++){
let r = i
let l = i+1
while(r >= 0 && l < s.length && s[r] === s[l]){
r--
l++
}
res = res.length > l-r-1 ? res : s.slice(r+1, l)
let start = end = i
while(start >= 0 && end < s.length && s[start] === s[end]){
start--
end++
}
res = res.length > end-start-1 ? res : s.slice(start+1, end)
}
return res
};
6 括号生成
题目: 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
思路: 从 n-1
推导出 n
的组合情况,只需要遍历 n-1
的所有组合,在所有组合的每个位置填入一对括号()
并去重即可。
1.新建Set
,存储n=1
时的有效括号组合
2.从n=2
开始遍历,考虑Set中每个元素增加1对括号的情况
3.更新Set
js
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function(n) {
let res = new Set(["()"])
for(let i = 2; i <= n; i++){
const mySet = new Set()
res.forEach(item => {
for(let j = 0; j < item.length; j++){
mySet.add(item.slice(0,j) + '()' + item.slice(j))
}
})
res = mySet
}
return [...res]
};
7 字符串相乘
题目: 给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。
思路: 遍历两个字符串,将对位字符转为数值后相乘保存到数组中,最后拼接为字符串
1.新建一个全为0数组res
,长度等于两个字符串长度之和
2.嵌套逆序遍历两个字符串,num1[i]
和num2[j]
的乘积是res[i+j]
(取整累加)和res[i+j+1]
(取余赋值)
3.将res
前置0清空
js
/**
* @param {string} num1
* @param {string} num2
* @return {string}
*/
var multiply = function(num1, num2) {
let len1 = num1.length
let len2 = num2.length
const res = new Array(len1+len2).fill(0)
for(let i = len1-1; i >= 0; i--){
for(let j = len2-1; j >= 0; j--){
const result = num1[i] * num2[j] + res[i+j+1]
res[i+j] += Math.floor(result / 10)
res[i+j+1] = result % 10
}
}
let p = 0
while(res.length > 1 && res[p] === 0){
res.shift()
}
return res.join('')
};
8 最多删除一个字符得到回文
题目: 给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串
思路: 当字符串不回文时,删除左边或者右边
1.封装回文子串判断方法,传参(str, l ,r)
2.初始化两个指针,一个指向字符串头部,另一个指向尾部
3.循环判断两个指针所指的字符是否相等
- 相等,左指针右移,右指针左移
- 不相等,分别判断左指针右移或者右指针左移情况下,是否回文
4.如果两个指针所指的字符串不等那么不对称发生了,意味着这是一个可以"删掉试试看"的操作点
js
/**
* @param {string} s
* @return {boolean}
*/
var validPalindrome = function(s) {
let start = 0
let end = s.length - 1
while(start < end && s[start] === s[end]){
start++
end--
}
if(start < end){
if(isPadlindrome(s, start+1,end) || isPadlindrome(s, start, end-1)) return true
return false
}
return true
};
const isPadlindrome = (str, l, r) => {
while(l < r){
if(str[l] !== str[r]) return false
l++
r--
}
return true
}
9 字符串匹配问题
题目:
设计一个支持以下两种操作的数据结构:
arduino
void addWord(word)
bool search(word)
search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 .
或 a-z 。.
可以表示任何一个字母。
思路:
1.这道题要求字符串既可以被添加、又可以被搜索,这就意味着字符串在添加时一定要被存在某处。键值对存储,我们用 Map(或对象字面量来模拟 Map)。
2.注意,这里为了降低查找时的复杂度,我们可以考虑以字符串的长度为 key,相同长度的字符串存在一个数组中,这样可以提高我们后续定位的效率。
3.难点在于 search 这个 API,它既可以搜索文字,又可以搜索正则表达式。因此我们在搜索前需要额外判断一下,传入的到底是普通字符串,还是正则表达式。若是普通字符串,则直接去 Map 中查找是否有这个 key;若是正则表达式,则创建一个正则表达式对象,判断 Map 中相同长度的字符串里,是否存在一个能够与这个正则相匹配。
js
// 构造函数
const WordDictionary = function (){
this.words = {}
}
// 添加字符串方法
WordDictionary.prototype.addWord = function (word) {
const len = word.length
if(this.words[len]){
this.words[len].push(word)
}else{
this.words[len] = [word]
}
// 搜索字符串方法
WordDictionary.prototype.searchWord = function(word){
const len = word.length
// 没有word长度的数组说明不存在该字符串
if(!this.words[len]) return false
// 不包含'.'说明是普通字符串
if(!word.includes('.')) return this.words[len].includes(word)
// 创建正则表达式对象
const reg = new RegExp(word)
// 只要数组中有一个匹配正则表达式的字符串,就返回true
return this.words[len].some(item => {
return reg.test(item)
})
}
10 颠倒字符串中的单词
题目: 给你一个字符串 s ,颠倒字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格
思考: 考虑快慢指针,核心逻辑是:遍历字符串,遇到一个不为空格的元素放下慢指针,快指针一直遍历到空格,此时 slice(慢,快)即可得到单词,依次 unshift 进数组,最后 res.join(' ')即可
js
/**
* @param {string} s
* @return {string}
*/
var reverseWords = function(s) {
let start = 0
let end = 0
const res = []
const len = s.length
while(start < len && end < len){
if(s[start] !== ' '){
end = start
while(s[end] !== ' ' && end < len){
end++
}
res.unshift(s.slice(start, end))
start = end
}
start++
}
return res.join(' ')
};