【LeetCode】无重复字符的最长子串-3

11/9/2021

# 题目

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

  • 示例:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
      请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters (opens new window)

# 感受

第一次实际解算法题目,遇到很多 我不理解 的地方
虽然到目前为止,还没正式的完全独立写出来(惭愧)
当不妨碍我精进向前
本次也仅仅只是记录分析官方的代码

# 滑动窗口

参考官方的解题思路:

设定「开始位置(左指针)」和「结束位置(右指针)」组合成一个「窗口」。
滑动「左指针」「右指针」,枚举窗口内符合要求的子串

javascript 中 set 数据结构符合判重的要求

两重循环遍历字符串
外层循环 移动的是 左指针
内存循环 移动的是 右指针

var lengthOfLongestSubstring = function(s) {
    // 哈希集合,记录每个字符是否出现过
    const occ = new Set();
    const n = s.length;
    // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
    let rk = -1, ans = 0;
    for (let i = 0; i < n; ++i) {
        if (i != 0) {
            // 左指针向右移动一格,移除一个字符
            occ.delete(s.charAt(i - 1));
        }
        while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
            // 不断地移动右指针
            occ.add(s.charAt(rk + 1));
            ++rk;
        }
        // 第 i 到 rk 个字符是一个极长的无重复字符子串
        ans = Math.max(ans, rk - i + 1);
    }
    return ans;
};
log标注版
let lengthOfLongestSubstring = function (s) {
  // 哈希集合,记录每个字符是否出现过
  const occ = new Set();
  const n = s.length;
  console.log("长度:", n, "内容:", s);
  // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
  let rk = -1,
    ans = 0;
  for (let i = 0; i < n; ++i) {
    console.group(`${i}轮 左指针字符(开始字符):${s.charAt(i)}`);
    if (i != 0) {
      // 左指针向右移动一格,移除一个字符
      console.log(`左指针移动一格,移除一个字符 ${s.charAt(i - 1)}`);
      occ.delete(s.charAt(i - 1));
    }
    // 右指针+1 并且 set内不能有该 添加一次 循环到条件不通过
    while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
      // 不断地移动右指针
      occ.add(s.charAt(rk + 1));
      ++rk;
      console.log(
        `右指针位置${rk + 1} < 长度${n} 且 元素内不包含字符:${s.charAt(
          rk
        )}(右指针位置字符) ,右指针位置+1,子串内容:`,
        occ
      );
    }
    // 第 i 到 rk 个字符是一个极长的无重复字符子串
    ans = Math.max(ans, rk - i + 1);
    console.log(
      `碰到重复字符:${s.charAt(rk + 1)},while循环结束。当前最长子串:`,
      occ
    );
    console.groupEnd(`${i}`);
  }
  return ans;
};

lengthOfLongestSubstring("abcabcbb");

log内容
// start
长度: 8 内容: abcabcbb
第0轮 左指针字符(开始字符):a
    右指针位置1 < 长度8元素内不包含字符:a(右指针位置字符) ,右指针位置+1,子串内容: Set(1) {'a'}
    右指针位置2 < 长度8元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(2) {'a', 'b'}
    右指针位置3 < 长度8元素内不包含字符:c(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'a', 'b', 'c'}
    碰到重复字符:a,while循环结束。当前最长子串: Set(3) {'a', 'b', 'c'}1轮 左指针字符(开始字符):b
    左指针移动一格,移除一个字符 a
    右指针位置4 < 长度8元素内不包含字符:a(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'b', 'c', 'a'}
    碰到重复字符:b,while循环结束。当前最长子串: Set(3) {'b', 'c', 'a'}2轮 左指针字符(开始字符):c
    左指针移动一格,移除一个字符 b
    右指针位置5 < 长度8元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'c', 'a', 'b'}
    碰到重复字符:c,while循环结束。当前最长子串: Set(3) {'c', 'a', 'b'}3轮 左指针字符(开始字符):a
    左指针移动一格,移除一个字符 c
    右指针位置6 < 长度8元素内不包含字符:c(右指针位置字符) ,右指针位置+1,子串内容: Set(3) {'a', 'b', 'c'}
    碰到重复字符:b,while循环结束。当前最长子串: Set(3) {'a', 'b', 'c'}4轮 左指针字符(开始字符):b
    左指针移动一格,移除一个字符 a
    碰到重复字符:b,while循环结束。当前最长子串: Set(2) {'b', 'c'}5轮 左指针字符(开始字符):c
    左指针移动一格,移除一个字符 b
    右指针位置7 < 长度8元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(2) {'c', 'b'}
    碰到重复字符:b,while循环结束。当前最长子串: Set(2) {'c', 'b'}6轮 左指针字符(开始字符):b
    左指针移动一格,移除一个字符 c
    碰到重复字符:b,while循环结束。当前最长子串: Set(1) {'b'}7轮 左指针字符(开始字符):b
    左指针移动一格,移除一个字符 b
    右指针位置8 < 长度8元素内不包含字符:b(右指针位置字符) ,右指针位置+1,子串内容: Set(1) {'b'}
    碰到重复字符:,while循环结束。当前最长子串: Set(1) {'b'}
// end

# 思考

在题目的评论区有大神表示,该代码会有无意义的循环,使用跳跃式收缩窗口能解决这个问题

  • 结论

缓存下每个字符及其位置: {a:1,b:2,c:3,d:4,e:5,f:6,a:7}
当出现重复字符时,直接将「左指针」移动到重复字符的后一个位置
可理解为跳跃式收缩窗口,可减少循环

  • 例一

a b c d e f c g h ,重复字符是 c
当窗口为 [a b c d e f c] 时,检测到重复字符 c
这时可 跳跃收缩窗口 [d e f c],因为不会有比窗口 [a ... f c] 更长的子串, [a b c] 窗口的遍历是多余操作

  • 例二

a b c d e f a g h ,重复字符 a
当窗口为 [a b c d e f a] 时,检测到重复字符 a
这时是正常的收缩窗口,没有发生跳跃





Last Updated: 11/16/2021, 5:51:06 PM