LeetCode 第3题和第76题

17 阅读3分钟

第3题:无重复字符的最长子串

题目描述

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

解法:滑动窗口 + 哈希表

/**
 * 无重复字符的最长子串
 * @param {string} s
 * @return {number}
 */
function lengthOfLongestSubstring(s) {
    if (!s || s.length === 0) return 0;
    
    // 使用 Map 存储字符及其最后出现的索引
    const charIndexMap = new Map();
    let maxLength = 0;
    let left = 0; // 滑动窗口左边界
    
    for (let right = 0; right < s.length; right++) {
        const currentChar = s[right];
        
        // 如果当前字符已经在窗口中出现过
        if (charIndexMap.has(currentChar) && charIndexMap.get(currentChar) >= left) {
            // 移动左边界到重复字符的下一个位置
            left = charIndexMap.get(currentChar) + 1;
        }
        
        // 更新字符最新出现的位置
        charIndexMap.set(currentChar, right);
        
        // 更新最大长度
        maxLength = Math.max(maxLength, right - left + 1);
    }
    
    return maxLength;
}

// 测试用例
console.log(lengthOfLongestSubstring("abcabcbb")); // 输出: 3
console.log(lengthOfLongestSubstring("bbbbb"));    // 输出: 1
console.log(lengthOfLongestSubstring("pwwkew"));   // 输出: 3
console.log(lengthOfLongestSubstring(""));         // 输出: 0
console.log(lengthOfLongestSubstring("abcdef"));   // 输出: 6

算法思路

  1. 滑动窗口:维护一个窗口 [left, right]
  2. 哈希表:记录每个字符最后出现的位置
  3. 遇到重复:移动左边界到重复字符的下一个位置
  4. 更新答案:每次扩展右边界时更新最大长度

第76题:最小覆盖子串

题目描述

给你一个字符串 s 和一个字符串 t,返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""。

解法:滑动窗口

/**
 * 最小覆盖子串
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
function minWindow(s, t) {
    if (!s || !t || s.length < t.length) return "";
    
    // 记录 t 中各字符出现次数
    const need = new Map();
    const window = new Map();
    
    // 统计 t 中各字符频次
    for (let char of t) {
        need.set(char, (need.get(char) || 0) + 1);
    }
    
    let left = 0, right = 0; // 滑动窗口边界
    let valid = 0; // 记录满足 need 条件的字符种类数
    
    // 记录最小覆盖子串的起始索引及长度
    let start = 0;
    let minLen = Infinity;
    
    while (right < s.length) {
        // c 是将移入窗口的字符
        const c = s[right];
        // 扩大窗口
        right++;
        
        // 进行窗口内数据的一系列更新
        if (need.has(c)) {
            window.set(c, (window.get(c) || 0) + 1);
            // 如果窗口中该字符的数量等于需要的数量
            if (window.get(c) === need.get(c)) {
                valid++;
            }
        }
        
        // 判断左侧窗口是否要收缩
        while (valid === need.size) {
            // 更新最小覆盖子串
            if (right - left < minLen) {
                start = left;
                minLen = right - left;
            }
            
            // d 是将移出窗口的字符
            const d = s[left];
            // 缩小窗口
            left++;
            
            // 进行窗口内数据的一系列更新
            if (need.has(d)) {
                // 如果移除前刚好满足条件,移除后就不满足了
                if (window.get(d) === need.get(d)) {
                    valid--;
                }
                window.set(d, window.get(d) - 1);
            }
        }
    }
    
    // 返回最小覆盖子串
    return minLen === Infinity ? "" : s.substring(start, start + minLen);
}

// 测试用例
console.log(minWindow("ADOBECODEBANC", "ABC")); // 输出: "BANC"
console.log(minWindow("a", "a"));              // 输出: "a"
console.log(minWindow("a", "aa"));             // 输出: ""
console.log(minWindow("ab", "b"));             // 输出: "b"
console.log(minWindow("abc", "cba"));          // 输出: "abc"

算法思路

  1. 扩展窗口:不断向右扩展,直到包含所有目标字符
  2. 收缩窗口:一旦满足条件,开始收缩左边界
  3. 更新答案:在满足条件时记录最优解
  4. 维护计数:使用两个 Map 维护字符计数和满足条件的种类数

JavaScript 特殊技巧

  • 使用 Map 而不是普通对象,避免原型链污染
  • 使用 substring() 获取子串
  • 使用 has() 方法检查键是否存在

时间复杂度

  • 第3题:O(n),其中 n 是字符串长度
  • 第76题:O(|s| + |t|),双指针最多各遍历一次

空间复杂度

  • 两题都是 O(k),其中 k 是字符集大小