题干
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度
示例一:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例二:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例三:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
题解
解题思路
- 通过一个数组维护无重复字符列表
- 循环目标字符串,当字符存在于数组时,记录数组当前长度与历史长度的较大值,并根据匹配位切割数组
'dvcdf'
匹配到第四位时的数组变换[d, v, c]
=>[v, c, d]
- 最后取数组与历史记录值的较大值
function lengthOfLongestSubstring(s) {
let arr = []
let len = 0
s.split('').forEach(s => {
const i = arr.indexOf(s)
if(i > -1) {
arr = arr.slice(i + 1, arr.length)
}
arr.push(s)
len = Math.max(arr.length, len)
})
return len
}
该方法通过了 Leetcode 的检测,但是执行耗时过长,排名在 75%
优化方案
- 原先用于存储无重复字符列表的数组空间可以节省,因为字符列表是连续的,所以可以通过一个下标标记实现
- 匹配的时间复杂度和原来是一样的,数组切割的部分可以移除,节省这部分性能
function lengthOfLongestSubstring(s) {
let len = 0
let q = 0
const arr = s.split('')
for (let i = 0; i < arr.length; i++) {
for (let j = q; j < i; j++) {
if(arr[j] === arr[i]) {
q = j + 1
break
}
}
len = Math.max(i - q + 1, len)
}
return len
}
该方法执行耗时和内存都有提升
后续
后续我又想了两种基于缓存的方案,能将时间复杂度降低到 O(2N)
基于对象的缓存方案
通过一个对象缓存匹配值列表的下标
function lengthOfLongestSubstring(s) {
let len = 0
let q = 0
let matchIndex = 0
const cache = {}
const arr = s.split('')
for (let i = 0; i < arr.length; i++) {
matchIndex = cache[arr[i]]
if (matchIndex !== undefined) {
for (let j = q; j < matchIndex + 1; j++) {
delete cache[arr[j]]
}
q = matchIndex + 1
}
cache[arr[i]] = i
len = Math.max(i - q + 1, len)
}
return len
}
基于数组的缓存方案
利用字符可以通过 charCodeAt
方法转换为数字,然后通过数组下标的方式缓存
function lengthOfLongestSubstring(s) {
let len = 0
let q = 0
let matchIndex = 0
let item = null
const cache = []
const arr = s.split('')
for (let i = 0; i < arr.length; i++) {
item = arr[i].charCodeAt()
matchIndex = cache[item]
if (matchIndex !== undefined) {
for (let j = q; j < matchIndex + 1; j++) {
cache[arr[j].charCodeAt()] = undefined
}
q = matchIndex + 1
}
cache[item] = i
len = Math.max(i - q + 1, len)
}
return len
}
总结:这两种方案使用了空间来换时间,比较依赖缓存的实现,js
的数组与对象访问的时间复杂度我没过多了解,对标 Java
的数组与 HashMap
,此场景不存在 Hash 冲突,其访问的时间复杂度为 O(1)