首发于微信公众号《前端成长记》,写于 2019.11.05javascript
本文记录刷题过程当中的整个思考过程,以供参考。主要内容涵盖:前端
题目地址java
给定一个只包括 '(',')','{','}','[',']'
的字符串,判断字符串是否有效。git
有效字符串需知足:github
注意空字符串可被认为是有效字符串。面试
示例:算法
输入: "()"
输出: true
输入: "()[]{}"
输出: true
输入: "(]"
输出: false
输入: "([)]"
输出: false
输入: "{[]}"
输出: true
复制代码
这道题从题面来看,仍然须要对字符串作遍历处理,找到相互匹配的括号,剔除后继续作处理便可。因此这道题个人解题想法是:数组
有几点须要注意下,能够减小一些计算量:微信
Ⅰ.记录栈函数
代码:
/** * @param {string} s * @return {boolean} */
var isValid = function(s) {
if (s === '') return true;
if (s.length % 2) return false;
// hash 表作好索引
const hash = {
'(': ')',
'[': ']',
'{': '}'
}
let arr = []
for (let i = 0; i < s.length; i++) {
if (!hash[s.charAt(i)]) { // 推入的是右括号
if (!arr.length || hash[arr[arr.length - 1]] !== s.charAt(i)) {
return false
} else {
arr.pop()
}
} else {
if (arr.length >= s / 2) { // 长度超过一半
return false
}
arr.push(s.charAt(i))
}
}
return !arr.length
};
复制代码
结果:
O(n)
发现一个很暴力的解法,虽然效率不高,可是思路清奇。咱们来看看实现:
Ⅰ.暴力正则
代码:
/** * @param {string} s * @return {boolean} */
var isValid = function(s) {
if (s === '') return true;
if (s.length % 2) return false;
while(s.length) {
const s_ = s
s = s.replace('()','').replace('[]','').replace('{}','')
if (s === s_) return false;
}
return true;
};
复制代码
结果:
O(n)
就这题而言,我仍是更倾向于增长一个辅助栈来作记录。由于一旦去掉只包含括号的限制,那么正则将没法解答。
将两个有序链表合并为一个新的有序链表并返回。新链表是经过拼接给定的两个链表的全部节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
复制代码
这道题从题面上就说明了这是一道链表相关问题,要进行链表合并,无非是修改链表指针指向,或者是链表拼接。因此,这道题我有两种思路的解法:
两种方式的区别很明显,修改指针的方式须要存储和不断修改指针指向,拼接的方式直接作链表拼接。
固然这里也有一些特殊值须要考虑进来。
Ⅰ.修改指针
代码:
/** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */
var mergeTwoLists = function(l1, l2) {
if (l1 === null) return l2
if (l2 === null) return l1
// 结果链表
let l = new ListNode(0)
// 不断更新的当前结点指针,对象赋值为传址,因此下面改指针指向便可
let cursor = l
// 会有一个先遍历完,变成 null
while(l1 !== null && l2 !== null) {
if (l1.val <= l2.val) { // 哪一个小,指针就指向哪
cursor.next = l1
l1 = l1.next
} else {
cursor.next = l2
l2 = l2.next
}
// 能够理解为 l.next.next.next ...
cursor = cursor.next
}
// 有一个为空则能够直接拼接
cursor.next = l1 === null ? l2 : l1
return l.next
};
复制代码
结果:
O(m + n)
,分别表明两个链表长度Ⅱ.链表拼接
代码:
/** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */
var mergeTwoLists = function(l1, l2) {
if (l1 === null) return l2
if (l2 === null) return l1
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2)
return l1 // 这个是合并后的了
} else {
l2.next = mergeTwoLists(l1, l2.next)
return l2 // 这个是合并后的了
}
};
复制代码
结果:
O(m + n)
,分别表明两个链表长度思路基本上都是这两种,未发现方向不一样的解法。
无非是有些解法额外开辟了新的链表来记录,或者一些细节上的差别。
这里的链表拼接解法,有没有发现跟 上一期 14题中的分治思路是同样的?对,实际上这个也是分治思路的一个应用。
给定一个排序数组,你须要在原地删除重复出现的元素,使得每一个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 而且原数组 nums 的前两个元素被修改成 1, 2。
你不须要考虑数组中超出新长度后面的元素。
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 而且原数组 nums 的前五个元素被修改成 0, 1, 2, 3, 4。
你不须要考虑数组中超出新长度后面的元素。
复制代码
说明:
为何返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你能够想象内部操做以下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的全部元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
复制代码
若是是单纯的数组去重,那有不少种方法能够作。因此题目也加了限制条件,总结一下比较重要的几点:
O(1)
这意味着不容许使用新的数组来解题,也就是对原数组进行操做。最后一点注意点能够看出,数组项的拷贝复制是一个方向,第二点能够看出数组删除是一个方向。删除元素的话就不会超过,因此不须要考虑二者结合。因此这题我分两个方向来解:
Ⅰ.拷贝数组元素
代码:
/** * @param {number[]} nums * @return {number} */
var removeDuplicates = function(nums) {
if (nums.length === 0) return 0;
var len = 1
for(let i = 1; i < nums.length; i++) {
if(nums[i] !== nums[i - 1]) { // 后一项不等于前一项
nums[len++] = nums[i] // 拷贝数组元素
}
}
return len
};
复制代码
结果:
O(n)
Ⅱ.删除数组元素
代码:
/** * @param {number[]} nums * @return {number} */
var removeDuplicates = function(nums) {
if (nums.length === 0) return 0;
for(let i = 1; i < nums.length;) {
if(nums[i] === nums[i - 1]) { // 后一项等于前一项
nums.splice(i, 1)
} else {
i++
}
}
return nums.length
};
复制代码
结果:
O(n)
这里看见一种很巧妙的解法,双指针法。至关于一个用于计数,一个用于扫描。
Ⅰ.双指针法
代码:
/** * @param {number[]} nums * @return {number} */
var removeDuplicates = function(nums) {
if (nums.length === 0) return 0;
let i = 0;
for(let j = 1; j < nums.length; j++) {
if (nums[j] !== nums[i]) {
nums[++i] = nums[j]
}
}
return i + 1 // 下标 +1 为数组长度
};
复制代码
结果:
O(n)
就三种解法而言,删除数组元素会频繁修改数组,不建议使用。双指针法和拷贝数组元素代码逻辑类似,可是思路上是大相径庭的。
给定一个数组 nums
和一个值 val
,你须要原地移除全部数值等于 val
的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1)
额外空间的条件下完成。
元素的顺序能够改变。你不须要考虑数组中超出新长度后面的元素。
示例:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 而且 nums 中的前两个元素均为 2。
你不须要考虑数组中超出新长度后面的元素。
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 而且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不须要考虑数组中超出新长度后面的元素。
复制代码
说明:
为何返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你能够想象内部操做以下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的全部元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
复制代码
这题跟上一题很是类似,因此咱们能够沿用上题的方向来解这道题:
Ⅰ.删除数组元素
代码:
/** * @param {number[]} nums * @param {number} val * @return {number} */
var removeElement = function(nums, val) {
if (nums.length === 0) return 0;
for(let i = 0; i < nums.length;) {
if (nums[i] === val) {
nums.splice(i, 1)
} else {
i++
}
}
};
复制代码
结果:
O(n)
Ⅱ.双指针法
代码:
/** * @param {number[]} nums * @param {number} val * @return {number} */
var removeElement = function(nums, val) {
if (nums.length === 0) return 0;
let i = 0
for(let j = 0; j < nums.length; j++) {
if (nums[j] !== val) {
nums[i++] = nums[j]
}
}
return i
};
复制代码
结果:
O(n)
看到两个略有差别的方法:
const of
替换一次遍历,只是写法区别,没有本质提高Ⅰ.单指针法
代码:
/** * @param {number[]} nums * @param {number} val * @return {number} */
var removeElement = function(nums, val) {
if (nums.length === 0) return 0;
let i = 0;
for(const num of nums) {
if(num !== val) {
nums[i++] = num;
}
}
return i;
};
复制代码
结果:
O(n)
Ⅱ.交换移除
代码:
/** * @param {number[]} nums * @param {number} val * @return {number} */
var removeElement = function(nums, val) {
if (nums.length === 0) return 0;
let i = nums.length;
for(let j = 0; j < i;) {
if (nums[j] === val) {
nums[j] = nums[--i]
} else {
j++
}
}
return i;
};
复制代码
结果:
O(n)
这里开拓下思路:若是要移除的是多项,那么仍是使用指针法作处理合适;若是是移除单项,那么使用交互移除法其实遍历次数最少。
实现 strStr()
函数。
给定一个 haystack
字符串和一个 needle
字符串,在 haystack
字符串中找出 needle
字符串出现的第一个位置 (从0开始)。若是不存在,则返回 -1
。
示例:
输入: haystack = "hello", needle = "ll"
输出: 2
输入: haystack = "aaaaa", needle = "bba"
输出: -1
复制代码
说明:
当 needle
是空字符串时,咱们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle
是空字符串时咱们应当返回 0
。这与 C
语言的 strstr()
以及 Java
的 indexOf()
定义相符。
这道题很明显是一道字符串搜索的题目,估计是在考察算法,可是受限知识面,因此我就先以现有方式实现做答,再来学习算法了。
IndexOf
这个是原生方法,考察这个就没有意义了,因此不作详细论述IndexOf
Ⅰ.遍历匹配
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
for(let i = 0; i < haystack.length; i++) {
if (i + needle.length > haystack.length) {
return -1
} else {
const str = haystack.substr(i, needle.length)
if (str === needle) {
return i
}
}
}
return -1
};
复制代码
结果:
O(n)
首先查阅《算法导论》,看到字符串匹配有如下四种:
而后再看题解,大概还找到如下三种算法:
Ⅰ.朴素字符串匹配算法
算法说明:
经过一个循环找到全部有效便宜,该循环对 n-m+1
个可能的 s
值进行检测,看可否知足条件 P[1..m] = T[s+1...s+m]
。其中 n
是字符串长度, 'm' 是匹配字符串长度。
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
let i = 0;
let j = 0;
while(j < needle.length && i < haystack.length) {
if(haystack[i] === needle[j]) { // 同位相等,继续判断下一位
i++;
j++;
} else {
i = i - j + 1; // i 偏移
j = 0; // j 重置
if (i + needle.length > haystack.length) { // 我增长的优化点,减小一些运算
return -1
}
}
}
if (j >= needle.length) { // 子串比完了,此时 j 应该等于 needle.length
return i - needle.length;
} else {
return -1
}
};
复制代码
结果:
O(m*n)
Ⅱ.Rabin-Karp 算法
算法说明:
进行哈希运算,将字符串转成对应的哈希值进行比对,相似16进制。这里题目是字符串,我就用 ASCII
值来表示每一个字符的哈希值,那么就能够计算出模式串的哈希值,再进行滚动比较。
每次滚动只须要作固定的 -*+
三个操做,便可得出滚动串的哈希值了。
好比计算 bbc
,哈希值为 hash = (b.charCodeAt() * 128 ^ 2 + b.charCodeAt() * 128 + c.charCodeAt())
,若是要计算后新值 bca
则为 (hash - b.charCodeAt() * 128 ^ 2) * 128 + c.charCodeAt()
。
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
let searchHash = 0 // 搜索字符串的hash值
let startHash = 0 // 字符串起始的hash值
for(let i = 0; i < needle.length; i++) {
searchHash += needle.charCodeAt(i) * Math.pow(2, needle.length - i - 1)
startHash += haystack.charCodeAt(i) * Math.pow(2, needle.length - i - 1)
}
if (startHash === searchHash) return 0
for(let j = 1; j < haystack.length - needle.length + 1; j++) {
startHash = (startHash - haystack.charCodeAt(j - 1) * Math.pow(2, needle.length - 1)) * 2 + haystack.charCodeAt(j + needle.length - 1)
if (startHash === searchHash) {
return j
}
}
return -1
};
复制代码
结果:
O(m*n)
注意:这里可能会存在溢出的状况,因此不是全部状况都适用。
Ⅲ.利用有限自动机进行字符串匹配
算法说明:
经过对文本字符串 T
进行扫描,找出模式 P
的全部出现位置。它们只对每一个文本字符检查一次,而且检查每一个文本字符时所用的时间为常数。一句话归纳:字符输入引发状态机状态变动,经过状态转换图获得预期结果。
这里主要的核心点是判断每次输入,找到最长的后缀匹配,若是最长时的长度等于查找字符串长度,那就必定包含该查找字符串。
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
// 查找最大匹配后缀长度
function findSuffix (Pq) {
let suffixLen = 0
let k = 0
while(k < Pq.length && k < needle.length) {
let i = 0;
for(i = 0; i <= k; i++) {
// 找needle中的多少项为当前状态对应字符串的匹配项
if (Pq.charAt(Pq.length - 1 - k + i) !== needle.charAt(i)) {
break;
}
}
// 全部项都匹配,即找到了后缀
if (i - 1 == k) {
suffixLen = k+1;
}
k++
}
return suffixLen
}
// 获取全部输入的字符集,好比 'abbc' 和 'cd' 合集为 ['a','b','c','d']
const setArr = Array.from(new Set(haystack + needle)) // 用户输入的可选项
// 创建状态机
const hash = {}
for(let q = 0; q < haystack.length; q++) {
for(let k = 0; k < setArr.length; k++) {
const char = haystack.substring(0, q) + setArr[k] // 下个状态的字符
const nextState = findSuffix(char)
// 求例如 0.a 0.b 0.c 的值
if (!hash[q]) {
hash[q] = {}
}
hash[q][char] = nextState
}
}
// 根据状态机求解
let matchStr = ''
for(let n = 0; n < haystack.length; n++) {
const map = hash[n]
matchStr += haystack[n]
const nextState = map[matchStr]
if (nextState === needle.length) {
return n - nextState + 1
}
}
return -1
};
复制代码
结果:
O(n)
Ⅳ.KMP 算法
算法说明:
能够理解为在状态机的基础上,使用了一个前缀函数来进行状态判断。本质上也是前缀后缀的思想。
代码:
// @lc code=start
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
// 生成匹配串各个位置下下的最长公共先后缀长度哈希表
function getHash () {
let i = 0 // arr[i] 表示 i 前面的字符串的最长公共先后缀长度
let j = 1
let hash = {
0: 0
}
while (j < needle.length) {
if (needle.charAt(i) === needle.charAt(j)) { // 相等直接 i j 都后移
hash[j++] = ++i
} else if (i === 0) { // i 为起点且二者不相等,那么必定为0
hash[j] = 0
j++
} else {
// 这里解释一下: 由于i前面的字符串与j前面的字符串拥有相同的最长公共先后缀,也就是说i前面字符串的最长公共后缀与j前面字符串的最长公共前缀相同,因此i只需回到i前面字符串最长公共前缀的后一位开始比较
i = hash[i - 1]
}
}
return hash
}
const hash = getHash()
let i = 0 // 母串中的位置
let j = 0 // 子串中的位置
while(i < haystack.length && j < needle.length) {
if (haystack.charAt(i) === needle.charAt(j)) { // 两个匹配,同时后移
i++
j++
} else if (j === 0) { // 两个不匹配,而且j在起点,则母串后移
i++
} else {
j = hash[j - 1]
}
}
if (j === needle.length) { // 循环完了,说明匹配到了
return i - j
} else {
return -1
}
};
复制代码
结果:
O(n)
Ⅴ.BM 算法
算法说明:
基于后缀匹配,匹配从后开始,但移动仍是从前开始,只是定义了两个规则:坏字符规则和好后缀规则。
通俗来说就是先验证是否为坏字符,而后判断是否在搜索词中进行对应的偏移进行下一步验证。若是匹配的话就从后往前校验,若是仍然匹配,就为好后缀。核心思想是每次位移都在坏字符和好后缀规则中取较大值,因为两个规则都只与匹配项相关,因此能够提早生成规则表。
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
function makeBadChar (needle) {
let hash = {}
for(let i = 0; i < 256; i++) { // ascii 字符长度
hash[String.fromCharCode(i)] = -1 // 初始化为-1
}
for(let i = 0; i < needle.length; i++) {
hash[needle.charAt(i)] = i // 最后出现该字符的位置
}
return hash
}
function makeGoodSuffix (needle) {
let hashSuffix = {}
let hashPrefix = {}
for(let i = 0; i < needle.length; i++) {
hashSuffix[i] = -1
hashPrefix[i] = false
}
for(let i = 0; i < needle.length - 1; i++) { // needle[0, i]
let j = i
k = 0 // 公共后缀子串长度,尾部取k个出来进行比较
while(j >= 0 && needle.charAt(j) === needle.charAt(needle.length - 1 - k)) { // needle[0,needle.length - 1]
--j
++k
hashSuffix[k] = j + 1 // 起始下标
}
if (j === -1) { // 说明所有匹配,意味着此时公共后缀子串也是模式的前缀子串
hashPrefix[k] = true
}
}
return { hashSuffix, hashPrefix }
}
function moveGoodSuffix (j, needle) {
let k = needle.length - 1 - j
let suffixes = makeGoodSuffix(needle).hashSuffix
let prefixes = makeGoodSuffix(needle).hashPrefix
if (suffixes[k] !== -1) { // 找到了跟好后缀同样的子串,获取下标
return j - suffixes[k] + 1
}
for(let r = j + 2; r < needle.length; ++r) {
if (prefixes[needle.length - r]) { // needle.length 是好后缀子串长度
return r // 对齐前缀到好后缀
}
}
return needle.length // 所有匹配,直接移动字符串长度
}
let badchar = makeBadChar(needle)
let i = 0;
while(i < haystack.length - needle.length + 1) {
let j
for(j = needle.length - 1; j >= 0; --j) {
if (haystack.charAt(i + j) != needle[j]) {
break; // 坏字符,下标为j
}
}
if (j < 0) { // 匹配成功
return i // 第一个匹配字符的位置
}
let moveLen1 = j - badchar[haystack.charAt(i + j)]
let moveLen2 = 0
if (j < needle.length -1) { // 若是有好后缀
moveLen2 = moveGoodSuffix(j, needle)
}
i = i + Math.max(moveLen1, moveLen2)
}
return -1
};
复制代码
结果:
O(n)
Ⅵ.Horspool 算法
算法说明:
将主串中匹配窗口的最后一个字符跟模式串中的最后一个字符比较。若是相等,继续从后向前对主串和模式串进行比较,直到彻底相等或者在某个字符处不匹配为止。若是不匹配,则根据主串匹配窗口中的最后一个字符在模式串中的下一个出现位置将窗口向右移动。
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
let hash = {}
for(let i = 0; i < 256; i++) {
hash[i] = needle.length // 默认初始化为最大偏移量,也就是匹配串长度
}
for(let i = 0; i < needle.length - 1; i++) {
hash[needle.charCodeAt(i)] = needle.length - 1 - i // 每一个字符距离右侧的距离
}
let pos = 0
while(pos < (haystack.length - needle.length + 1)) {
let j = needle.length - 1 // 从右往左
while(j >= 0 && haystack.charAt(pos + j) === needle.charAt(j)) {
j--
}
if (j < 0) { // 所有匹配
return pos
} else { // 不匹配
pos += hash[haystack.charCodeAt(pos + needle.length - 1)]
}
}
return -1
};
复制代码
结果:
O(n)
Ⅶ.Sunday 算法
算法说明:
它的思想跟 BM 算法
类似,可是它是从前日后匹配,匹配失败时关注主串内参与匹配的后一位字符。若是该字符不存在匹配字符中,则多偏移一位;若是存在,则偏移匹配串长度减该字符最右出现的位置。
代码:
/** * @param {string} haystack * @param {string} needle * @return {number} */
var strStr = function(haystack, needle) {
if (needle === '') return 0
if (needle.length > haystack.length) return -1
if (needle.length === haystack.length && needle !== haystack) return -1
let hash = {}
for(let i = 0; i < needle.length; i++) {
hash[needle.charAt(i)] = needle.length - i // 偏移表
}
for(let i = 0; i < haystack.length;) {
let j
for(j = 0; j < needle.length; j++) {
if (haystack.charAt(i + j) !== needle.charAt(j)) {
break
}
}
if(j === needle.length) { // 彻底匹配
return i
}
if (i + needle.length >= haystack.length) { // 未找到
return -1
} else {
i += hash[haystack.charAt(i + needle.length)] || needle.length + 1
}
}
return -1
};
复制代码
结果:
O(n)
就理解的难易度来说,我建议先看 Sunday 算法
和 Horspool 算法
,不过 RMP 算法
的匹配思路打开了眼界,利用后缀前缀来处理问题。这里把常见的字符串算法都作了一次尝试,总体下来收获颇丰。
(完)
本文为原创文章,可能会更新知识点及修正错误,所以转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验 若是能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork (转载请注明出处:chenjiahao.xyz)