438_无重复字符的最长子串(算法思路,优化和复杂度分析)

117 阅读3分钟

题目描述

给定两个字符串 sp,找到 s 中所有 p异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

思路

  1. 初始化检查
    • 如果字符串 s 的长度小于字符串 p,则无法存在异位词,直接返回空结果。
  2. 字符频率映射
    • 使用两个长度为 26 的数组 map1map2 分别记录字符串 p 和当前滑动窗口内 s 的字符频率。
    • charToIndex 函数将字符转换为对应的索引('a' 对应 0'b' 对应 1,依此类推)。
  3. 初始窗口处理
    • 遍历字符串 p 的长度,将 p 的字符频率存入 map1,同时将 s 中前 p.length 个字符的频率存入 map2
  4. 比较初始窗口
    • 定义 isSame 函数比较 map1map2 是否完全一致。
    • 如果初始窗口的字符频率与 p 相同,记录起始索引 0
  5. 滑动窗口遍历
    • 从s的第p.length个字符开始,逐步向右滑动窗口:
      • 添加新字符:将新进入窗口的字符频率在 map2 中增加。
      • 移除最左字符:将滑出窗口的字符频率在 map2 中减少。
      • 比较频率:调用 isSame 函数,如果当前窗口的字符频率与 p 相同,记录当前窗口的起始索引 i - p.length + 1

注意点

为什么是i - p.length + 1
  • 窗口大小:与字符串 p 的长度相同,即 p.length
  • 窗口位置:窗口在字符串 s 中从左到右滑动,每次移动一个字符。
  • 变量 i:表示当前窗口的结束位置(右边界)的索引。

当窗口滑动到位置 i 时,窗口的范围是 [i - p.length + 1, i](包含两端)。因此,窗口的起始位置的索引是 i - p.length + 1

复杂度分析

  • 时间复杂度O(n),其中 n 是字符串 s 的长度。
    • 初始化频率映射:遍历 p 的长度 m,时间复杂度为 O(m)
    • 滑动窗口遍历:遍历 s 的剩余部分,每一步进行常数时间的操作(添加、移除字符频率和比较),总体为 O(n - m)
    • 比较频率isSame 函数每次比较固定长度的数组(26 个元素),时间复杂度为 O(1)
    • 综合O(m) + O(n - m) = O(n)
  • 空间复杂度O(1)
    • 使用了两个固定大小的数组 map1map2,无论输入字符串的大小如何,空间占用保持不变。

code

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s, p) {
    const res = []
    if (s.length < p.length) return res
    //字母映射
    const map1 = Array(26).fill(0), map2 = Array(26).fill(0)
    //计算字符在映射中的索引
    const charToIndex = (c) => c.charCodeAt() - 'a'.charCodeAt()
    //给初始的字符建立映射,后续只需要对s字符串进行遍历修改映射,p是不用变的
    for (let i = 0; i < p.length; i++) {
        map1[charToIndex(p[i])]++
        map2[charToIndex(s[i])]++
    }
    //比较是否相同
    const isSame = () => {
        for (let i = 0; i < 26; i++) {
            if (map1[i] !== map2[i]) return false
        }
        return true
    }
    //如果相同,说明0位置的是异位词
    if (isSame()) res.push(0)

    //遍历s,重新建立映射(删除左边的,添加正在遍历的)
    for (let i = p.length; i < s.length; i++) {
        map2[charToIndex(s[i])]++
        // 移除最左字符,实际操作的是s[0],s[1],s[2]....位置的这个字母
        map2[charToIndex(s[i - p.length])]--
        if (isSame()) {
            res.push(i - p.length + 1)
        }
    }
    return res
};