这是我参与更文挑战的第15天,活动详情查看: 更文挑战
无重复字符的最长子串(题号3)
题目
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
链接
解释
这题啊,这题是重拳出击。
有这样的一种感觉,Leetcode上前面的中等题好像很简单的样子,不知道为啥。
一看到这题就想到了两种解法:
-
暴力解法
暴力嘛,很简单,存就完事了。
先搞一个对象或者
Map
,开始循环。之后将i
作为key
存进这个对象,每到一个新元素时,判断这个元素是否对象中的每个数组存在,如果不存在,将当前元素插入到数组中,如果存在,跳过就完事了。不过循环的时候可以进行适当的剪枝,可以通过数组的长度来判断当前的数组是否已经断掉了,也就是已经无法插入元素了,对于这种数组也可以直接跳过,不用判断存在不存在,因为题目要求的是子串,得是连续的。
这样一顿操作之后我们拿到了一个对象,之后循环对象里的每个元素,取出数组的长度和
max
对比就完事了。 -
动态规划(存疑)
这里存疑的不是解法对不对,而是这种方法是不是动态规划。
方法很简单,首先新建个数组,之后每次循环的时候判断数组中是否有这个元素,如果没有直接插入,和
max
进行对比,如果有就去掉开头的元素,再进行判断,如果还有,就继续去掉开头的元素,进行一个遍历操作,直到元素不存在,然后插入新元素。这就完事了。
为什么说是DP呢?因为每次新的位置的最长字符串,都是和前一位的最长字符串有关的,所以数组中的值一直都是当前位置的最长字串,有点像动态规划,但有可能只是记忆化,这一点有待商榷,清楚的同学可以在评论区指出。
自己的答案(暴力->对象)
逻辑很简单,注意剪枝即可。
var lengthOfLongestSubstring = function(s) {
var map = new Map()
max = 0
for (let i = 0; i < s.length; i++) {
for (let j = 0; j < i; j++) {
var res = map.get(j)
if (res && res.length > i - 1 - j && !res.includes(s[i])) {
map.set(j, [...res, s[i]])
}
}
map.set(i, [s[i]])
}
for (const item of map.values()) {
max = Math.max(item.length, max)
}
return max
};
逻辑在解释部分都说过,这里只是实现而已。
美中不足的就是没有AC,在走到986个用例的时候GG了,已经倒数第二个了,可惜还是没有通过。
自己的答案(动态规划->数组)
var lengthOfLongestSubstring = function(s) {
var arr = []
max = 0
for (const chat of s) {
while (arr.includes(chat)) {
arr.shift()
}
arr.push(chat)
max = Math.max(arr.length, max)
}
return max
};
代码量少了不少,逻辑也很简单。
这种解法感觉就是钻了空子,因为题目要求必须时子串,言外之意就是必须时连续的,所以只要持续去掉开头的元素就完事了,没有任何困难可言。
更好的方法(滑动窗口->存疑)
其实滑动窗口的解法感觉不应该放在更好的方法中,因为这其实并没有之前的动态规划的解法效率高,动态规划的时间和空间都达到了90%以上,但这种解法只能在60%~70%左右徘徊。
滑动窗口的主要逻辑和动态规划差不多,但还是有一点区别的。
官方的解释比较复杂,这里简单翻译下。
首先,没到一个新的位置,都要去掉数组前的前一位元素,保持数组的开头必是当前元素。
之后搞个右指针,从0开始迭代。
在迭代过程中忘数组中插入元素,然后右指针递增1。什么时候停止呢?有两个条件:
- 右指针跑到头了,这种情况不用多说,自然是要停止迭代的。
- 当前数组中已经有了右指针所在位置的元素了,重复是不行的,所以停止迭代。
这里可以将数组开头的元素当作左指针,因为每次开始循环都要去掉数组中的第一个元素,所以时刻保持左指针在当前循环的位置上。
这样左指针和右指针的区间就是一个窗口了,通过改变左右指针的位置来滑动这个窗口,成为滑动窗口。
👇看看代码(笔者自己写的,和官方有一点点差别)。
var lengthOfLongestSubstring = function(s) {
var set = new Set()
max = 0
right = 0
len = s.length
for (let i = 0; i < len; i++) {
if (i !== 0) {
set.delete(s.charAt(i - 1))
}
while (right < len && !set.has(s.charAt(right))) {
set.add(s.charAt(right))
right++
}
max = Math.max(max, right - i)
}
return max
};
区别在哪里?在于右指针的初始位置,官方答案的初始位置是在-1
,但笔者试了,从0
开始也是可以的,所以笔者就不理解了,为什么要从-1
开始呢?有知道的同学欢迎在评论区留言或者私信笔者,感激不尽。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇