LeetCode题3无重复字符的最长字串
0、分析
简单分析一下:要求无重复字符的最长子串的长度。长度值可以用一个变量保存,至于最大长度,只需要在每轮循环中对长度变量值与当前无重复字符的子串长度求最大值即可,那么问题就转变成了如何在循环中找出所有无重复字符的子串。
为便于叙述,下文使用s
表示原字符串,bs
表示子串,ndcs
表示无重复字符的子串,lndcs
表示最长的无重复字符的子串
1、暴力双循环
基于以上分析,可以用双循环暴力去解决,下面是一种实现👇:
function lengthOfLongestSubstring(s) {
const n = s.length
let ans = 0
for (let i = 0; i < n; i += 1) {
let j = i
// 为了找出 s[i] 开头的 lndcs
for (j; j < n; j += 1) {
// 找出一个 bs
const subStr = s.slice(i, j + 1)
if ((new Set(subStr)).size < subStr.length) {
// 若该 bs 存在重复字符,则 s[i] 开头的 lndcs 截止到 s[j] 前一个字符;退出循环
break
}
// 若该 bs 无重复字符,则 s[i] 开头的 ndcs 长度还未达到最大
}
// 本轮得到的最长无重复字符的子串
ans = Math.max(ans, j - i)
if (j === n) {
// 当前 bs 已经到尾了,后面不会有比这更长的 ndcs
break
}
}
return ans
}
本解法好处是粗暴直观,坏处就是最坏时间复杂度为O(n^2)(代码是已经优化过的,尽可能地提前退出循环)。本方案的核心思想是利用双重循环生成所有 bs
然后找出 ndcs
进而求解,虽直观易懂但耗时太久。要求ndcs
,难道就只能先尝试生成所有bs
后再找出 ndcs
吗?
2、滑动窗口算法思想
关于滑动窗口的算法思想,网上有很多讲得很好的文章,这里不再展开。针对本题,使用滑动窗口算法思想的关键在于,子串是原始字符串中的一部分连续字符。
2.1 滑动窗口思想实现
function lengthOfLongestSubstringL(s) {
const n = s.length
const lookup = new Set()
let lp = 0
let rp = 0
let ans = 0
// 左指针主循环
while (lp < n) {
if (lp !== 0) {
// 当左指针向右移动时,滑动窗口起始位置改变,子串起始位也要改变
lookup.delete(s[lp - 1])
}
while (rp < n && !lookup.has(s[rp])) {
// 不停地将右指针向右移动,直到字符串末尾或者遇到重复字符
const rpElem = s[rp]
lookup.add(rpElem)
rp+=1
}
// 右指针移动结束时,即可得到本轮的最长无重复字符子串
ans = Math.max(ans, rp - lp)
lp+=1
if (rp === n) {
break
}
}
return ans
}
仔细观察可以发现,👆上面代码其实就是暴力双循环解法的优化版本,可以将暴力双循环中的i
理解为左指针,j
立即为右指针。暴力双循环解法的问题在于判重时里层循环是先进入再中断,而且每次里层循环都需要调用slice()
方法和生成Set
对象。
2.2 使用Map优化
上述方案还可以优化,当在存储结构中检测到相同字符时,左指针应该直接跳到这个字符的下一位,而没必要缓慢地一步一步地往右挪动。基于以上分析,我们需要一种数据结构同时存储字符及其位置,最适合的就是 Map 了。
function lengthOfLongestSubstring(s) {
const n = s.length
const map = new Map()
let lp = 0
let rp = 0
let ans = 0
// 右指针主循环
while (rp < n) {
const curr = s[rp]
const pos = map.get(curr)
// 由于使用 Map 结构存储,每次更新 lp 时未删除 lp 前面的元素
// 所以只有 Map 中包含当前字符且 lp <= pos 时才代表出现重复字符
if (map.has(curr) && pos >= lp) {
lp = pos + 1
}
// 将当前字符及其位置存入 Map
map.set(curr, rp)
ans = Math.max(ans, rp - lp + 1)
rp+=1
}
return ans
}
3、从子串角度考虑
这里,我们先看一下「子串」这个概念:
In formal language theory and computer science, a substring is a contiguous sequence of characters within a string. For instance, "the best of" is a substring of "It was the best of times". In contrast, "Itwastimes" is a subsequence of "It was the best of times", but not a substring.
不难发现,bs
就是s
的任意一部分,在s
中连续的字符在ndcs
中也必定是连续的。几个例子:
- 若
s
是abcdefg
,那么lndcs
就是abcdefg
- 若
s
是bbbb
,那么lndcs
就是b
- 若
s
是abcabcbb
,那么lndcs
就是abc
前面的算法,都是从索引的角度思考问题;根据「子串」的特性,现在从子串的角度想问题。既然是找到 lndcs
,那么我们就用一个变量来保存 bs
,然后在在一个遍历字符串的过程中动态地调整这个 bs
从而得到每一轮的 lndcs
,根本毋需用到当前遍历的索引。
function lengthOfLongestSubstring(s) {
let noDuplicateCharSubStr = ''
let ans = 0
for (const char of s) {
const charPositionInSubStr = noDuplicateCharSubStr.indexOf(char)
if (charPositionInSubStr !== -1) {
// 若子串包含与当前字符相同的字符,说明本次的 ndcs 已经到最长了
// 则子串的起始位置调整为此字符的下一个位置
noDuplicateCharSubStr = noDuplicateCharSubStr.slice(charPositionInSubStr + 1)
}
// 每次都要将新遍历到的字符继续存入子串
noDuplicateCharSubStr += char
// 更新答案
ans = Math.max(ans, noDuplicateCharSubStr.length)
}
return ans
}