本文是作者刷算法题之余,将刷题的经验分享出来,欢迎和我交流探讨。
(Easy) —— 验证回文串 II
给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。
示例 1:
输入: s = "aba"
输出: true
示例 2:
输入: s = "abca"
输出: true
解释: 可以删除 "c" 字符 或者 "b" 字符
示例 3:
输入: s = "abc"
输出: false
分析
首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。
这道题目的关键词是:
- 字符串
- 回文
- 删除
判断字符串是否回文有固定的套路。难点在于要删除某个字符,删除字符这个操作可以使用 指针 跳过来模拟。
我们先使用 对撞双指针 从两端不断地比较两端字符是否相等,目的就是要找到那两个不相等的字符。一旦出现不相等的,分别尝试从 左指针、右指针 跳过一个字符,之后套用验证回文串的通用方法验证是否是回文串。
指针跳过某个字符判断是否回文
/**
* @param {string} s
* @return {boolean}
*/
var validPalindrome = function (s) {
// 初始化指针
let j = 0
let k = s.length - 1
while (j < k && s[j] === s[k]) {
j++
k--
}
// 跳过j
if (isPalindrome(s, j + 1, k)) {
return true
}
// 跳过k
if (isPalindrome(s, j, k - 1)) {
return true
}
// 判断是否回文串的固定套路
function isPalindrome(str, start, end) {
while (start < end) {
if (str[start] !== str[end]) {
return false
}
start++
end--
}
return true
}
return false
}
- 时间复杂度:O(n),n为字符串长度,最多把字符完全遍历一次
- 空间复杂度:O(1),变量数跟遍历无关
总结
- 判断回文串有固定的 套路,一定记住
- 删除某个字符的操作,想到用 指针 模拟
(Easy) —— 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入: s = "()"
输出: true
示例 2:
输入: s = "()[]{}"
输出: true
示例 3:
输入: s = "(]"
输出: false
分析
首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。
这道题目的关键词是:
- 字符串
- 对称性
根据题意,假设第一个字符是 (, 那么最后一个字符也要是 ),有点类似于先入后出?可以联想到 栈 的数据结构。
我们将左右括号的对应关系维护到一个字典对象中,一旦匹配到左括号 (,就将对应的右括号 ) push 到 栈 中,随着遍历,越来越多左括号对应的右括号被 push 到了 栈 中。这时候,先被 push 的字符在栈底,后被 push 的字符在栈顶,如果要出栈的话,后被 push 的字符先出栈,和左右符号的匹配规则相符(最后出现的左括号将会第一个匹配到他的右括号)。
根据出栈先后验证对称性
const leftToRight = {
'(': ')',
'{': '}',
'[': ']',
}
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function (s) {
// 极端情况,空字符串,一定不符合要求
if (!s.length) {
return false
}
const stack = [] // 初始化栈
const len = s.length
for (let i = 0; i < len; i++) {
const cur = s[i]
// 如果匹配到左括号,将对应的右括号入栈
if (cur === '(' || cur === '{' || cur === '[') {
stack.push(leftToRight[cur])
}
// 如果匹配到的是右括号,如果栈为空 || 栈顶不是当前右括号,即为匹配失败
else {
if (!stack.length || stack.pop() !== cur) {
return false
}
}
}
// 如果字符串是有效的,最终的栈一定是空的
if (stack.length) {
return false
} else {
return true
}
}
总结
- 字符串的 对称性 问题,联想到
栈的出入栈
(Medium) —— 添加与搜索单词 - 数据结构设计
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
WordDictionary()初始化词典对象void addWord(word)将word添加到数据结构中,之后可以对它进行匹配bool search(word)如果数据结构中存在字符串与word匹配,则返回true;否则,返回false。word中可能包含一些'.',每个.都可以表示任何一个字母。
示例:
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
WordDictionary()初始化词典对象void addWord(word)将word添加到数据结构中,之后可以对它进行匹配bool search(word)如果数据结构中存在字符串与word匹配,则返回true;否则,返回false。word中可能包含一些'.',每个.都可以表示任何一个字母。
示例:
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True
分析
首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。
这道题目的关键词是:
- 字符串
- 设计数据结构
- 字符串查询
设计数据结构 这类题,没有太好的方法,只能多做题,拓宽思路,记住各种的设计方式。
对于查询,效率最高的当然是对象(时间复杂度是 O(1)),为了降低查找的复杂度,我们可以考虑以字符串的长度为 key,相同长度的字符串存在于一个数组中。
现在数据结构定下了,addWord 方法也不是问题。现在来解决 search 支持查找 字符串 和 正则 的问题。根据题意分析:
- 如果不包含
.,根据字符串精确查询 - 如果包含
.,套用正则查询
对象 + 数组 + 正则 设计字符串数据结构
/*
* @lc app=leetcode.cn id=211 lang=javascript
*
* [211] 添加与搜索单词 - 数据结构设计
*/
// @lc code=start
var WordDictionary = function () {
this.words = new Map()
}
/**
* @param {string} word
* @return {void}
*/
WordDictionary.prototype.addWord = function (word) {
const len = word.length
if (this.words.has(len)) {
this.words.get(len).push(word)
} else {
this.words.set(len, [word])
}
return true
}
/**
* @param {string} word
* @return {boolean}
*/
WordDictionary.prototype.search = function (word) {
const len = word.length
if (!this.words.has(len)) {
return false
}
// 普通字符串
if (!word.includes('.')) {
return this.words.get(len).includes(word)
}
// 正则
const reg = new RegExp('^' + word + '$')
return this.words.get(len).some((item) => {
return reg.test(item)
})
}
/**
* Your WordDictionary object will be instantiated and called as such:
* var obj = new WordDictionary()
* obj.addWord(word)
* var param_2 = obj.search(word)
*/
// @lc code=end
总结
- 字符串的存储结构,可能是对象+数组
- 字符串的题目很大概率会涉及正则表达式,正则表达式的基本用法要清楚
(Medium) —— 字符串转换整数 (atoi)
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。
函数 myAtoi(string s) 的算法如下:
- 读入字符串并丢弃无用的前导空格
- 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
- 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
- 将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。如果没有读入数字,则整数为
0。必要时更改符号(从步骤 2 开始)。 - 如果整数数超过 32 位有符号整数范围
[−2^31, 2^31 − 1],需要截断这个整数,使其保持在这个范围内。具体来说,小于−2^31的整数应该被固定为−2^31,大于2^31 − 1的整数应该被固定为2^31 − 1。 - 返回整数作为最终结果。
注意:
- 本题中的空白字符只包括空格字符
' '。 - 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。
示例 1:
输入: s = "42"
输出: 42
解释: 加粗的字符串为已经读入的字符,插入符号是当前读取的字符。
第 1 步:"42"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"42"(读入 "42")
^
解析得到整数 42 。
由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。
示例 2:
输入: s = " -42"
输出: -42
解释:
第 1 步:" -42"(读入前导空格,但忽视掉)
^
第 2 步:" -42"(读入 '-' 字符,所以结果应该是负数)
^
第 3 步:" -42"(读入 "42")
^
解析得到整数 -42 。
由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。
示例 3:
输入: s = "4193 with words"
输出: 4193
解释:
第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)
^
解析得到整数 4193 。
由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193 。
分析
首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。
这道题目的关键词是:
- 字符串
- 边界计算
- 字符的匹配:这道题是空格、符号、非数字字符的匹配
边界计算较为简单,先来实现这个。
// 边界限制
const max = Math.pow(2, 31) - 1 // 计算最大值
const min = -Math.pow(2, 31) // 计算最小值
if (value > max) {
value = max
} else if (value < min) {
value = min
} else {
// do nothing
}
再来看字符的匹配,优先想到正则表达式,看下这道题能不能套用正则表达式。
- 读入字符串并丢弃无用的前导空格 =>
\s - 正负号 =>
[\+-]? - 非数字字符或到达输入的结尾 =>
.*
OK,正则表达式完全符合题目要求,再来看剩下的要求怎么满足。
- 数字转换为整数 =>
+。如果没有读入数字,则整数为0=>isNaN()判断 - 边界要求 => 已实现
现在,尝试写一下代码实现
字符串转整数,包含边界要求
/**
* @param {string} s
* @return {number}
*/
var myAtoi = function (s) {
// 初始化正则规则,注意要用捕获组包裹你想要的部分
// const reg = new RegExp(/\s*([\+-]?[0-9]*).*/)
const reg = /\s*([\+-]?[0-9]*).*/
// 得到捕获组
const groups = s.match(reg)
/*
groups的内容如下:
example input: "4193 with words"
匹配成功:
[
'4193 with words',
'4193',
index: 0,
input: '4193 with words',
groups: undefined
]
匹配失败:
[ 'words', '', index: 0, input: 'words', groups: undefined ]
*/
const max = Math.pow(2, 31) - 1 // 计算最大值
const min = -Math.pow(2, 31) // 计算最小值
let res = 0 // 转化后的整数值
// 假设匹配成功
if (groups[1]) {
// 转化为整数
res = +groups[1]
// 兼容转化后的结果是NaN的情况,此时赋值为0
if (isNaN(res)) {
res = 0
}
}
// 判断是否超出边界范围,如果超过,进行转换
if (res > max) {
return max
} else if (res < min) {
return min
}
// 不超过,正常返回
return res
}
总结
- 字符串的题目很大概率会涉及正则表达式,正则表达式的基本用法要清楚。这道题主要使用了正则中匹配组的写法 + 获取匹配组的方法