字符串
注意:
- 在js里,字符不能直接和数字比较,需要先转换为ASCII码,
'a'.charCodeAt() === 97 - 大小写转换:
'a'.toUpperCase() === 'A','A'.toLowerCase() === 'a' - 字符串的运算:字符和字符的加法、字符和数字的加法是OK的,其他的减法、乘法都不行js
console.log('b' + 'a' === 'ba', 'b' + 'a') //true 'ba' console.log('b' * 2 === 'bb', 'b' * 2) //false NaN console.log('b' - 'a' == 1, 'b' - 'a') //false NaN console.log(97 - 'a', 'a' - 97) //NaN NaN console.log(97 + 'a', 'a' + 97) //97a a97
KMP手写
描述:实现getNext函数,返回next数组, 实现strStr函数,返回子串在主串中的位置
js
function getNext(s) {
// next[i-1] 存的是字符串str的[0,i]部分的最长相等前后缀长度
// 当kmp用next的时候,如果s[i] != s[j],那么把j回退到next[j-1]
const next = [0]
let j = 0, i = 1 // j是前缀末尾,i是后缀末尾
while (i < s.length) {
while (j > 0 && s[i] !== s[j]) {
j = next[j - 1]
}
if (s[i] === s[j]) {
j++
}
next[i] = j
i++
}
return next
}js
function strStr(ogStr, subStr) {
const next = getNext(subStr)
let i = 0, j = 0
while (i < ogStr.length && j < subStr.length) {
while (j > 0 && ogStr[i] !== subStr[j]) {
j = next[j - 1]
}
if (ogStr[i] === subStr[j]) {
j++
}
i++
}
return j === subStr.length ? i - j : -1
}409. 最长回文串
- 描述:给定一个字符串,求其中的字符能组成的最长回文串长度
- 思路:
- 这题是可以用字符组合成回文串,所以考虑如何组合得到一个回文串
- 回文串 ===
任意个出现偶数次的字符 + (中间一个出现奇数次的字符) - Step1:统计每个字符出现的次数
- Step2:遍历表,出现偶数次的字符,直接累加
- Step3:如果存在出现奇数次的字符,可以把其中的偶数次累加,最后再+1
5. 最长回文子串
描述:给定一个字符串,求其中的最长回文子串
注意:这题是子串问题,要保持原顺序,找到原始串中满足条件的一个子串
思路:
- 暴力枚举所有字符串,再判断是否是回文串,维护最大值,O(n^3)
- 中心扩散法,遍历中心i,再以i为中心向两侧扩散,维护最大值,O(n^2)
- 动态规划法,
dp[i][j]表示[i,j]是否是回文串,存个 boolean 即可,dp[i][j] = dp[i+1][j-1] && s[i] === s[j],维护最大值
- 动态规划法,
实现:
- 中心扩散法:
- 遍历中心i,再以i为中心向两侧扩散,维护最大值
- 注意:中心点可能是单个字符,也可能是两个字符,考虑两种情况,从
[i,i]开始扩散AND从[i,i+1]开始扩散
- 动态规划法:
dp[i][j]表示[i,j]是否是回文串,故这个二维数组只有右上角的值是有效的,初始化只处理dp[i][i]dp[i][j]依赖于dp[i+1][j-1],所以i从大到小遍历,j从i+1到大遍历- 只有当
j>i+1时,才需要判断dp[i][j],否则dp[i][j] = dp[i] === dp[j]
- 中心扩散法:
3. 无重复字符的最长子串
- 描述:给定一个字符串,求其中的无重复字符的最长子串
- 注意:子串问题,保持原顺序,找到原始串中满足条件的一个子串
- 思路:维护一个滑动窗口,遍历入字符,如果字符在窗口内存在,则左边界右移动缩小窗口
- 实现:
- 数组实现:靠
indexOf判断是否存在,靠shift和push维护窗口
jswhile (window.length && window.indexOf(s[j]) != -1) { window.shift() } window.push(s[j]) // window.length 就是窗口长度- set实现:靠
hash判断是否存在,靠add和delete维护窗口,自己移动左边界
jswhile (window.has(s[j])) { window.delete(s[i]) i++ // 自己移动左边界 } window.add(s[j]) // j - i + 1 就是窗口长度 - 数组实现:靠
49. 字母异位词分组
- 描述:输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"],输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
- 思路:
- 这题让我想到了昨天的存在重复元素 III,要把字母异位词分组放到一个桶里,考虑映射到同一个值
- 映射函数
let sortedStr = str.slice().split('').sort((a, b) => a.charCodeAt() - b.charCodeAt()).join('') - 然后就是,遍历strs,把每个str映射到同一个值,然后放到哈希表的同一个key下
- 最后遍历哈希表,组合答案
151. 翻转字符串里的单词
- 描述:"the sky is blue" -> "blue is sky the"
- 注意:JS中的字符串API,
splitjointrim左闭右开slice - 实现:
Method 1: 构建words数组,并反转,然后加上' '
return s.split(' ').filter((val) => val !== '').reverse().join(' ')
Method 2: 快慢双指针实现
- 因为要求反转,所以从后向前遍历,快指针指向单词前的空字符,慢指针指向单词的尾巴字符
- 初始化时,跳过末尾空格,此时快慢指针相等,都指向单词末尾
- 快指针向前遍历,直到遇到空格,此时快慢指针之间就是单词,将单词加入结果数组
- 加入后,跳过空格,再次让快慢指针相等,都指向单词末尾
jsvar reverseWords = function (s) { let i = s.length - 1 let res = '' // 初始时,去除尾部空格,ij相等,都指向单词末尾 while (s[i] == ' ' && i > 0) i-- let j = i while (i >= 0) { // i 向前找单词 while (i >= 0 && s[i] != ' ') i-- res = res.concat(s.slice(i + 1, j + 1)).concat(' ') // slice函数是左闭右开的,记得加个末尾空格,最后再移除 // 跳过空格,使ij相等,都指向单词末尾 while (i >= 0 && s[i] === ' ') i-- j = i } return res.slice(0, res.length - 1) }
415. 字符串相加
- 描述:给定两个用字符串表示的非负整数,求它们的和
- 实现:遍历位数相加,注意进位
- 下面的写法是最优雅代码量最少的,用一个大while控制了所有循环
while (i >= 0 || j >= 0 || carry)
jsvar addStrings = function (num1, num2) { let i = num1. length - 1 let j = num2. length - 1 let carry = 0 let res = [] while (i >= 0 || j >= 0 || carry) { let a = i >= 0 ? Number(num1[i]) : 0 let b = j >= 0 ? Number(num2[j]) : 0 let sum = a + b + carry let digit = sum % 10 carry = sum / 10 | 0 // res.unshift(digit) //在头部插入,因为先算的个位 res.push(digit) i-- j-- } return res.reverse().join('') } - 下面的写法是最优雅代码量最少的,用一个大while控制了所有循环
## 43. 字符串相乘
- 描述:给定两个用字符串表示的非负整数,求它们的乘积
- 思路:[乘法的剖解](https://leetcode.cn/problems/multiply-strings/solutions/188815/gao-pin-mian-shi-xi-lie-zi-fu-chuan-cheng-fa-by-la/)
- 
- resArr的初始大小为`num1. length + num2. length`,因为两个数相乘,结果最多为两个数的长度之和
- 处理i和j时,恰好对应resArr的`i+j`和`i+j+1`,主要是`i+j+1`低位加和
- 最后把resArr转换为字符串,并去除前导0
```js
var multiply = function (num1, num2) {
if (num1[0] == 0 || num2[0] == 0) return '0'
const size = num1. length + num2. length
const resArr = new Array(size).fill(0)
for (let i = num1. length - 1; i >= 0; i--) {
for (let j = num2. length - 1; j >= 0; j--) {
let multi = Number(num1[i]) * Number(num2[j])
if (resArr.length > 1) {
let sum = multi + resArr[i + j + 1] // 加上原来的值
resArr[i + j + 1] = sum % 10 // 新的该位
resArr[i + j] += Math.floor(sum / 10) // 进位
}
}
}
let start = 0
while (start < resArr.length && resArr[start] === 0) start++
return resArr.slice(start).join('')
};28. 找出字符串中第一个匹配项的下标
- 描述:KMP,但是直接用
indexOf就能解决
208. 实现Trie(前缀树)
- 描述:实现一个Trie,支持插入、查找、前缀查找
- 实现:
- Node结构包括
children = {}和isEnd = false - 插入、查找和前缀查找都是相似的逻辑,一层层往下
jsTrie.prototype.insert = function (word) { let cur = this.root for (let char of word) { if (cur.children[char] !== undefined) { cur = cur.children[char] // 向下 } else { const node = new Node() cur.children[char] = node cur = node } } cur.isEnd = true }; - Node结构包括
8. 字符串转换整数 (atoi)
这题题目把算法都讲出来了,先跳过前导空格,然后取正负号(没有则为正号),然后再把字符转为数字,途中一旦遇到非数字就退出。
不需要想太多,平铺直叙地面向过程地写就好,注意溢出的判断。
js
let sign = 1
// 1. Skip space
while(s[i] == ' ') i++
// 2. Get sign
if(s[i] == '+' || s[i]=='-'){
sign = s[i] === '-' ? -1 : 1
}
// 3. Str to num
while(i < len-1 && isDigit(s[i])){
let digit = s[i] - '0' // 这个很巧哇
ans = ans * 10 + digit
}
// 4. limit range
clamp(ans, -2^32, 2^32 -1)76. 最小覆盖子串
子串问题,可以考虑可变大小的滑动窗口,通过双指针实现,先向右扩张找到符合条件的子串,再缩小窗口找到最优窗口。
这题复杂在,设计怎么样的数据结构来判断当前是否符合条件?
- 注意 t 中是会有重复字符的,所以应该是 Map,记录 key 及其数量
- 其他字符不用管,我们只关心当前子串是否出现目标字符
采用两个哈希表,一个是目标,一个是当下
js
const ogMap = new Map() // 目标
const curMap = new Map() // 当下
// 辅助函数
const addone = (map, key) => {
//给 map 的 key 加一或初始化为 1
}
const valid = () => {
// 遍历目标 Map 取出目标字符和数量, 检查当前 Map 中是否满足条件
return bool
}
// 初始化目标哈希表
for (const c of t) {
addone(ogMap, c)
}
// 滑动窗口的同时维护 curMap
let l = 0, r = 0
let windowSize = Infinity, ansL = -1, ansR = -1
while (r < s.length) {
// 我们只关心目标字符是否出现
const char = s[r]
if (ogMap.has(char)) {
addone(curMap, char)
}
// 尝试缩小窗口
while (l <= r && valid()) {
// 当有更小的窗口时就更新答案
const curSize = r - l + 1
if (curSize < windowSize) {
ansL = l
ansR = r
windowSize = curSize
}
// 缩小窗口同时维护 curMap,同样的,我们只关心目标字符
const leftChar = s[l]
if (curMap.has(leftChar)) {
curMap.set(leftChar, curMap.get(leftChar) - 1)
}
l++
}
r++
}
// return ans