前端刷题路-Day51:无重复字符的最长子串(题号3)

457 阅读3分钟

这是我参与更文挑战的第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-cn.com/problems/lo…

解释

这题啊,这题是重拳出击。

有这样的一种感觉,Leetcode上前面的中等题好像很简单的样子,不知道为啥。

一看到这题就想到了两种解法:

  1. 暴力解法

    暴力嘛,很简单,存就完事了。

    先搞一个对象或者Map,开始循环。之后将i作为key存进这个对象,每到一个新元素时,判断这个元素是否对象中的每个数组存在,如果不存在,将当前元素插入到数组中,如果存在,跳过就完事了。

    不过循环的时候可以进行适当的剪枝,可以通过数组的长度来判断当前的数组是否已经断掉了,也就是已经无法插入元素了,对于这种数组也可以直接跳过,不用判断存在不存在,因为题目要求的是子串,得是连续的。

    这样一顿操作之后我们拿到了一个对象,之后循环对象里的每个元素,取出数组的长度和max对比就完事了。

  2. 动态规划(存疑)

    这里存疑的不是解法对不对,而是这种方法是不是动态规划。

    方法很简单,首先新建个数组,之后每次循环的时候判断数组中是否有这个元素,如果没有直接插入,和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。什么时候停止呢?有两个条件:

  1. 右指针跑到头了,这种情况不用多说,自然是要停止迭代的。
  2. 当前数组中已经有了右指针所在位置的元素了,重复是不行的,所以停止迭代。

这里可以将数组开头的元素当作左指针,因为每次开始循环都要去掉数组中的第一个元素,所以时刻保持左指针在当前循环的位置上。

这样左指针和右指针的区间就是一个窗口了,通过改变左右指针的位置来滑动这个窗口,成为滑动窗口。

👇看看代码(笔者自己写的,和官方有一点点差别)。

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:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ