leetcode-3-无重复字符的最长子串

无重复字符的最长子串

1. 题目描述

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

示例 1:

ini 复制代码
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

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

示例 3:

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

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

2. 解决方案

暴力解法

  1. 思路
  • 枚举字符串 s 的所有子串,对于每个子串,检查其是否包含重复字符。
  • 从长度为 1 的子串开始,逐渐增加子串长度,记录下不包含重复字符的最长子串的长度。
  1. 代码实现
ts 复制代码
function lengthOfLongestSubstringBruteForce(s: string): number {
    let maxLength = 0;
    const n = s.length;
    for (let i = 0; i < n; i++) {
        for (let j = i; j < n; j++) {
            const subStr = s.slice(i, j + 1);
            const charSet = new Set();
            let isDuplicate = false;
            for (const char of subStr) {
                if (charSet.has(char)) {
                    isDuplicate = true;
                    break;
                }
                charSet.add(char);
            }
            if (!isDuplicate) {
                maxLength = Math.max(maxLength, subStr.length);
            }
        }
    }
    return maxLength;
}
  1. 分析
  • 时间复杂度 :(O(n^3))。外层循环遍历子串的起始位置 i,时间复杂度为 (O(n));内层循环遍历子串的结束位置 j,时间复杂度为 (O(n));对于每个子串,检查是否有重复字符需要遍历子串中的每个字符,时间复杂度为 (O(n))。所以总的时间复杂度为 (O(n \times n \times n))。
  • 空间复杂度 :(O(min(n, m))),其中 n 是字符串 s 的长度,m 是字符集的大小。在最坏情况下,子串中包含所有不同字符,此时空间复杂度为 (O(n));如果字符集大小有限,如 ASCII 字符集大小为 128,则空间复杂度为 (O(m))。
  1. 缺点:时间复杂度非常高,在处理较长字符串时,运行效率极低,会导致超时。

滑动窗口解法

  1. 思路
  • 使用滑动窗口来维护一个不包含重复字符的子串。
  • 用一个集合(Set)来记录当前窗口内的字符。
  • 初始化窗口的左右边界 leftright 都为 0。
  • 移动右边界 right,将新字符加入集合。如果新字符已在集合中,说明出现重复,移动左边界 left,并从集合中移除相应字符,直到窗口内不再有重复字符。
  • 每次移动后,更新最长无重复字符子串的长度。
  1. 代码实现
ts 复制代码
function lengthOfLongestSubstring(s: string): number {
    const charSet = new Set();
    let left = 0;
    let maxLength = 0;
    for (let right = 0; right < s.length; right++) {
        while (charSet.has(s[right])) {
            charSet.delete(s[left]);
            left++;
        }
        charSet.add(s[right]);
        maxLength = Math.max(maxLength, right - left + 1);
    }
    return maxLength;
}
  1. 分析
  • 时间复杂度 :(O(n))。虽然有一个嵌套的 while 循环,但 leftright 指针最多各移动 n 次,所以总的时间复杂度为 (O(n))。
  • 空间复杂度 :(O(min(n, m))),与暴力解法相同,取决于字符串长度 n 和字符集大小 m
  1. 优点:时间复杂度大大降低,相比于暴力解法,在处理长字符串时效率有显著提升。

优化的滑动窗口解法(使用数组替代集合)

  1. 思路
  • 如果字符串只包含 ASCII 字符(共 128 个),可以使用一个长度为 128 的数组来替代 Set 记录字符出现的位置。
  • 数组的索引对应字符的 ASCII 码值,数组的值记录该字符上次出现的位置。
  • 移动右边界时,检查当前字符上次出现的位置是否在当前窗口内,如果在,则更新左边界。
  1. 代码实现
ts 复制代码
function lengthOfLongestSubstringOptimized(s: string): number {
    const charIndex = new Array(128).fill(-1);
    let left = 0;
    let maxLength = 0;
    for (let right = 0; right < s.length; right++) {
        left = Math.max(left, charIndex[s.charCodeAt(right)] + 1);
        charIndex[s.charCodeAt(right)] = right;
        maxLength = Math.max(maxLength, right - left + 1);
    }
    return maxLength;
}
  1. 分析
  • 时间复杂度 :(O(n))。与普通滑动窗口解法相同,leftright 指针最多各移动 n 次。
  • 空间复杂度 :(O(m)),这里 m 是固定的 128,因为使用了一个固定大小的数组。相比使用 Set,在空间上更优。

最优解及原因

  1. 最优解:优化的滑动窗口解法(使用数组替代集合)是最优解。
  2. 原因 :它在时间复杂度上与普通滑动窗口解法相同,都是 (O(n)),但在空间复杂度上,对于只包含 ASCII 字符的字符串,它使用固定大小的数组,空间复杂度为 (O(m))(m = 128),优于普通滑动窗口解法的 (O(min(n, m)))。同时,数组的访问和更新操作比 Set 的操作在某些情况下效率更高。

3. 拓展和题目变形

拓展

  • 找到所有不含有重复字符的最长子串。

思路

  • 在滑动窗口的基础上,每次更新最长子串长度时,将符合长度的子串记录下来。

代码实现

ts 复制代码
function findAllLongestSubstrings(s: string): string[] {
    const charIndex = new Array(128).fill(-1);
    let left = 0;
    let maxLength = 0;
    const result: string[] = [];
    for (let right = 0; right < s.length; right++) {
        left = Math.max(left, charIndex[s.charCodeAt(right)] + 1);
        charIndex[s.charCodeAt(right)] = right;
        if (right - left + 1 === maxLength) {
            result.push(s.slice(left, right + 1));
        } else if (right - left + 1 > maxLength) {
            maxLength = right - left + 1;
            result.length = 0;
            result.push(s.slice(left, right + 1));
        }
    }
    return result;
}

题目变形

  • 给定一个字符串 s 和一个整数 k,找到最长的包含不超过 k 个不同字符的子串。

思路

  • 使用滑动窗口,用一个对象记录窗口内不同字符的出现次数,当窗口内不同字符数超过 k 时,移动左边界,更新最长子串长度。

代码实现

ts 复制代码
function lengthOfLongestSubstringKDistinct(s: string, k: number): number {
    const charCount: { [key: string]: number } = {};
    let left = 0;
    let maxLength = 0;
    for (let right = 0; right < s.length; right++) {
        charCount[s[right]] = (charCount[s[right]] || 0) + 1;
        while (Object.keys(charCount).length > k) {
            charCount[s[left]]--;
            if (charCount[s[left]] === 0) {
                delete charCount[s[left]];
            }
            left++;
        }
        maxLength = Math.max(maxLength, right - left + 1);
    }
    return maxLength;
}
相关推荐
卡尔曼的BD SLAMer8 小时前
计算机视觉与深度学习 | 传统图像处理技术的未来发展前景分析
图像处理·算法·计算机视觉
星期天要睡觉8 小时前
(纯新手教学)计算机视觉(opencv)实战十一——轮廓近似(cv2.approxPolyDP)
opencv·算法·计算机视觉
软糖工程0018 小时前
python中的分代垃圾回收机制的原理【python进阶二、2】
python·算法
玉木子9 小时前
机器学习(四)KNN算法-分类
算法·机器学习·分类
白仑色9 小时前
java中常见的几种排序算法
java·算法·排序算法
码了三年又三年9 小时前
【排序算法】冒泡 选排 插排 快排 归并
算法·排序算法
qq_4335545410 小时前
C++ Bellman-Ford算法
开发语言·c++·算法
何妨重温wdys10 小时前
跳跃游戏(二):DFS 求解最少跳跃次数与最优路径
算法·深度优先
j_xxx404_11 小时前
数据结构:单链表的应用(力扣算法题)第二章
c语言·数据结构·算法·leetcode