找到字符串中所有字母异位词

标签: 哈希表 字符串 滑动窗口

难度: Medium

给定两个字符串 s 和 p,找到 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.length, p.length <= 3 * 104
  • s 和 p 仅包含小写字母

Submission

运行时间: 61 ms

内存: 16.5 MB

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        need = {}
        for c in p:
            need[c] = need.get(c, 0) + 1
        window = {}
        left, right = 0, 0
        valid = 0
        res = []
        while right < len(s):
            c = s[right]
            right += 1
            if c in need:
                window[c] = window.get(c, 0) + 1
                if window[c] == need[c]: valid += 1
            while valid == len(need):
                if right - left == len(p):
                    res.append(left)
                d = s[left]
                left += 1
                if d in need:
                    if window[d] == need[d]: valid -= 1
                    window[d] -= 1
        return res

Explain

该题解采用了滑动窗口的方法。首先,使用一个哈希表 need 记录字符串 p 中每个字符的出现次数。然后,使用另一个哈希表 window 来记录当前窗口中各字符的出现次数。接着,不断扩展右边界 right,直到窗口中包含了 p 中所有字符。此时,开始收缩左边界 left,直到窗口中不再包含 p 的所有字符。在收缩的过程中,如果窗口的长度恰好等于 p 的长度,则说明找到了一个异位词的起始索引,将其添加到结果列表中。重复这个过程,直到遍历完字符串 s。

时间复杂度: O(n)

空间复杂度: O(p)

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        need = {}
        for c in p:  # 统计 p 中每个字符出现的次数
            need[c] = need.get(c, 0) + 1
        window = {}
        left, right = 0, 0  # 定义滑动窗口的左右边界
        valid = 0  # 用于记录窗口中满足 need 条件的字符个数
        res = []  # 存储结果
        while right < len(s):
            c = s[right]  # 将即将移入窗口的字符
            right += 1  # 右移窗口
            if c in need:
                window[c] = window.get(c, 0) + 1  # 更新窗口中的字符计数
                if window[c] == need[c]:  # 如果字符 c 的数量满足了 need 中的要求
                    valid += 1
            while valid == len(need):  # 当窗口满足所有字符需求时
                if right - left == len(p):  # 如果窗口大小等于 p 的长度
                    res.append(left)  # 添加起始索引到结果列表
                d = s[left]  # 将要移出窗口的字符
                left += 1  # 左移窗口
                if d in need:
                    if window[d] == need[d]:  # 如果字符 d 的数量满足了 need 中的要求
                        valid -= 1  # 减少满足条件的字符个数
                    window[d] -= 1  # 更新窗口中的字符计数
        return res

Explore

滑动窗口技术在这个问题中非常高效,因为它可以在O(n)的时间复杂度内完成任务,其中n是字符串s的长度。使用滑动窗口,可以动态地调整窗口的大小并实时检查窗口内的字符串是否为异位词,而不需要对每个子串进行重新排序或重新建立数据结构。相比之下,排序比较需要将每个子串排序后比较,时间复杂度至少为O(n*m*log(m)),其中m是字符串p的长度;使用Trie树则需要构建和维护一个复杂的数据结构,对于简单的异位词检查而言过于复杂且效率不高。因此,滑动窗口因其简洁和高效而成为首选方法。

在哈希表`need`中,如果字符串`p`中的某个字符出现多次,我们将该字符作为键,其出现次数作为值存入哈希表。这是通过遍历字符串`p`,对每个字符进行计数并更新哈希表实现的。例如,如果`p`中字符`a`出现了两次,则在`need`中存储为`{'a': 2}`。这样,我们就可以确保在检查字符串`s`的子串时,能够准确匹配`p`中每个字符的确切出现次数。

在滑动窗口中,`valid`变量用来计数窗口内满足`need`字符数量要求的不同字符的个数。当`valid`的值等于`need`的长度时,意味着窗口中的每个字符都恰好与`p`中的字符数量相匹配,且所有`p`中的字符都被考虑到了。因此,这确保了窗口中的字符串不仅包含了`p`中的所有字符,而且每个字符的数量也与`p`完全相同,从而构成一个有效的异位词。

该算法在处理非常大的字符串`s`时通常效率很高,时间复杂度为O(n),其中n是字符串`s`的长度。算法效率的关键在于滑动窗口的操作,它避免了重复的工作,只需遍历一次字符串`s`即可。然而,如果字符串`s`极其大,或者字符集很广(例如Unicode字符),则哈希表的操作可能会成为性能瓶颈,因为维护和更新哈希表的成本会随着字符集大小增加而增加。此外,如果`p`字符串长度远小于`s`,而`s`包含大量不在`p`中的字符,那么滑动窗口中的很多操作可能是处理这些无关字符,这可能会导致一些效率损失。