LeetCode热题100——438.找到字符串中的所有字母异位词

53 阅读5分钟

题目描述

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

 

示例:

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

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • s 和 p 仅包含小写字母

解法

滑动窗口

var findAnagrams = function(s, p) {
    const sLen = s.length, pLen = p.length;

    if (sLen < pLen) {
        return [];
    }

    const ans = [];
    const count = Array(26).fill(0);
    for (let i = 0; i < pLen; ++i) {
        ++count[s[i].charCodeAt() - 'a'.charCodeAt()];
        --count[p[i].charCodeAt() - 'a'.charCodeAt()];
    }

    let differ = 0;
    for (let j = 0; j < 26; ++j) {
        if (count[j] !== 0) {
            ++differ;
        }
    }

    if (differ === 0) {
        ans.push(0);
    }

    for (let i = 0; i < sLen - pLen; ++i) {
        if (count[s[i].charCodeAt() - 'a'.charCodeAt()] === 1) {  // 窗口中字母 s[i] 的数量与字符串 p 中的数量从不同变得相同
            --differ;
        } else if (count[s[i].charCodeAt() - 'a'.charCodeAt()] === 0) {  // 窗口中字母 s[i] 的数量与字符串 p 中的数量从相同变得不同
            ++differ;
        }
        --count[s[i].charCodeAt() - 'a'.charCodeAt()];

        if (count[s[i + pLen].charCodeAt() - 'a'.charCodeAt()] === -1) {  // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从不同变得相同
            --differ;
        } else if (count[s[i + pLen].charCodeAt() - 'a'.charCodeAt()] === 0) {  // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从相同变得不同
            ++differ;
        }
        ++count[s[i + pLen].charCodeAt() - 'a'.charCodeAt()];

        if (differ === 0) {
            ans.push(i + 1);
        }
    }

    return ans;
};

整体思路:通过差值数组滑动窗口来实现

count大小为 26 的数组 用来记录当前窗口目标 p 之间每个字符的数量差异。

利用ASCII码差值来实现:

小写字母 'a' 到 'z' 在 ASCII 表中是连续编码的: 'a' → ASCII 值 97 ;'b' → 98 ... 'z' → 122

小写字母 - 'a' 的结果:

'a' - 'a' = 0

'b' - 'a' = 1

...

'z' - 'a' = 25

这样就将 26 个字母映射到 0~25 的整数,完美对应数组索引。

具体实现是 首先记录s的前pLen个字符,p字符串中的字符,s中的出现的字符+1,p中的字符-1。那么count的值

  • 1 s字符串当前窗口有该字符,p字符串没有该字符
  • -1 s字符串当前窗口没有该字符,p字符串有该字符
  • 0 s当前窗口和p字符串都有该字符

diff整数,记录 count 数组中非零元素的个数(即不平衡的字符种类数)。

通过diff来记录s和p之间的差异,先循环遍历count,只要不为当前count[i]不为0,说明s和p在该字符上有差异,diff+1

然后通过一个for循环,不断的在s字符串上向右移动从i到i+pLen这样一个窗口

在移动中:

  1. 首先去除s[i]的存在致使的diff变化,看count[s[i].charCodeAt() - 'a'.charCodeAt()]
  • 若为1,说明它之前会造成++diff,那么现在就要--diff
  • 若为0,那么删除s[i]后对应的字符数量不再一致,就要--diff

然后count中对应的s[i]字符数量-1

  1. 然后在添加s[i+pLen]的加入致使的diff变化,仍然是看count[s[i+pLen]].charCodeAt() - 'a'.charCodeAt()]的值
  • 若为-1,添加s[i+pLen]后,这个字母的数量一致,--diff
  • 若为1,diff不变
  • 若为0,添加s[i+pLen]后,这个字母的数量不再一致,++diff

然后count中对应的s[i+pLen]字符数量+1

最后进行判断当前diff是否为0,若为0,证明当前的s[i+1] 到 s[i+pLen]是p的字母异位词,将开始序号i+1添加到ans中,再开启下一次循环

最终返回ans

时间,空间复杂度

好的,下面是对您提供的代码的时间复杂度和空间复杂度的纯文本分析。


代码复杂度分析

时间复杂度分析:O(N)

用 N 表示字符串 s 的长度,用 M 表示目标字符串 p 的长度。

  1. 初始化 count 数组: 代码首先执行一个循环,遍历 pLen 次(即 M 次),在循环内部,它进行两次 O(1) 的数组访问和增减操作。因此,初始化阶段的复杂度为 O(M)。

  2. 初始化 differ 变量: 接下来,代码遍历固定的 26 个字符位置来统计初始差异。这是一个固定的操作次数,因此复杂度为 O(26)常数次,即 O(1)。

  3. 滑动窗口主循环: 主逻辑在一个 for 循环中完成,该循环从 i=0 运行到 sLen−pLen−1。这相当于循环了 N−M 次。

    • 循环内部操作: 在每次循环中,代码处理一个字符的移除(窗口左侧)和一个字符的加入(窗口右侧)。所有对 count 数组的访问、对 differ 的增减判断,以及对 if/else 条件的判断,都是固定的几次操作,其复杂度为 O(1)。

将所有步骤的复杂度相加:O(M)+O(1)+O(N−M)×O(1)。当 N 远大于 M 时,总的时间复杂度被简化为O(N),即线性时间复杂度。

空间复杂度分析:O(1)

  1. count 数组: 代码的核心数据结构是一个大小为 26 的数组 (count),用于存储 26 个小写字母的频率差异。这个大小是固定的,不随输入字符串 s 和 p 的长度 N 和 M 变化而变化。
  2. 其他变量: 变量如 sLenpLendiffer 都是占用固定空间的基本数据类型。

因此该算法的空间复杂度O(1)(常数空间)。