描述
给定两个字符串 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
仅包含小写字母
超时代码
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
li = [s[i:i+len(p)] for i in range(0, len(s)-len(p)+1, 1)]
lis = []
for i in range(len(li)):
if sorted(li[i]) == sorted(p):
lis.append(i)
else:
continue
return lis
leecode题解labuladong
from collections import Counter, defaultdict
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
need = Counter(p) # 统计目标字符串t中每个字符的频率
window = defaultdict(int) # 记录窗口中字符的频率
left, right = 0, 0 # 左右指针初始化
valid = 0 # 记录当前窗口中满足条件的字符数
res = [] # 存储结果的列表
while right < len(s): # 遍历字符串s
c = s[right] # 当前字符
right += 1 # 移动右指针
# 进行窗口内数据的一系列更新
if c in need:
window[c] += 1
if window[c] == need[c]:
valid += 1
# 判断左侧窗口是否需要收缩
while right - left >= len(p):
# 当窗口符合条件时,把起始索引加入res
if valid == len(need):
res.append(left)
d = s[left] # 要移出窗口的字符
left += 1 # 移动左指针
# 进行窗口内数据的一系列更新
if d in need:
if window[d] == need[d]:
valid -= 1
window[d] -= 1
return res
整体分析
1. 初始化阶段
-
统计目标字符串
t
中每个字符的频率: 使用Counter
创建字典need
,记录t
中每个字符的频率。 -
初始化窗口字典: 使用
defaultdict(int)
创建字典window
,用于记录当前窗口中字符的频率。 -
设置左右指针: 初始化左右指针
left
和right
均为0。 -
初始化辅助变量: 变量
valid
用于记录当前窗口中满足条件的字符数。 列表res
用于存储结果,即符合条件的起始索引。
2. 扩展右边界
-
遍历字符串
s
: 使用while
循环遍历字符串s
,右指针right
从左到右移动。 -
更新窗口字符频率: 将
right
指针指向的字符加入窗口,更新window
字典中该字符的频率。 -
检查是否满足条件: 如果该字符在
need
字典中,并且其频率与need
中的频率一致,则更新valid
计数。
3. 收缩左边界
-
判断窗口大小: 当窗口的大小达到字符串
t
的长度时,进入内层while
循环。 -
检查窗口是否符合条件: 如果当前窗口中的字符频率与
t
中字符频率一致,则将left
指针的位置加入结果列表res
。 -
收缩窗口: 移动
left
指针,缩小窗口,并更新窗口中左边字符的频率。 -
更新满足条件的字符数: 如果移除的字符在
need
字典中,并且其频率在窗口中不再满足need
的要求,则更新valid
计数。
4. 返回结果
- 输出结果列表: 当所有字符遍历完成后,返回结果列表
res
,其中存储了所有符合条件的起始索引。
关键点
- 滑动窗口:通过左右指针维护一个大小为
t
长度的窗口。 - 频率统计:使用
Counter
和defaultdict
统计字符频率。 - 条件判断:通过
valid
判断当前窗口是否符合条件。 - 结果存储:符合条件时,将窗口的起始索引存储到结果列表中。
滑动窗口
维护一个窗口,不断滑动,然后更新答案。算法技巧的时间复杂度是 O(N),比字符串暴力算法要高效得多。细节:如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。
python框架
from collections import defaultdict
def sliding_window(s, t):
# 创建need字典,记录目标字符串t中每个字符的频率
need = defaultdict(int)
for c in t:
need[c] += 1
window = defaultdict(int) # 创建window字典,记录当前窗口中每个字符的频率
left, right = 0, 0 # 初始化左、右指针
valid = 0 # 记录当前窗口中满足条件的字符数
while right < len(s):
# c 是将移入窗口的字符
c = s[right]
# 右移窗口
right += 1
# 进行窗口内数据的一系列更新
# 此处应填写窗口右移时的处理逻辑
...
# debug 输出的位置
print(f"window: [{left}, {right})")
# 判断左侧窗口是否要收缩
while window_needs_shrink(): # 此处应填写判断窗口是否需要收缩的条件
# d 是将移出窗口的字符
d = s[left]
# 左移窗口
left += 1
# 进行窗口内数据的一系列更新
# 此处应填写窗口左移时的处理逻辑
...
def window_needs_shrink():
# 这是一个占位函数,用于判断窗口是否需要收缩
# 你需要根据具体逻辑实现该函数
return False
其中两处 ... 表示的更新窗口数据的地方,到时候你直接往里面填就行了。
而且,这两个 ... 处的操作分别是右移和左移窗口更新操作,等会你会发现它们操作是完全对称的。
框架说明
上述Python框架代码主要涉及以下几个重要的知识点和数据结构:
1. defaultdict
from collections import defaultdict
- **定义**:`defaultdict` 是 `collections` 模块中的一个类,它继承自内置的 `dict` 类。与普通字典不同的是,`defaultdict` 在访问不存在的键时,不会抛出 `KeyError`,而是会根据提供的默认工厂函数生成一个默认值。
- **用途**:在需要频繁处理不存在的键的情况下,`defaultdict` 可以简化代码,避免手动检查键是否存在。
need = defaultdict(int)
for c in t:
need[c] += 1
在这里,`need` 是一个 `defaultdict`,默认值为 `int` 类型,即默认值为 0。这样,在统计字符频率时,如果某个字符在 `need` 中不存在,会自动添加并初始化为 0。
2. 滑动窗口
滑动窗口是一种常用于字符串或数组问题的算法技巧,用于在一维数据结构中维护一个动态范围(窗口)并根据特定条件进行调整。
- **用途**:适用于需要在一维数据结构(如字符串或数组)中查找满足特定条件的子数组或子字符串的问题。
- **核心思想**:通过两个指针(左指针 `left` 和右指针 `right`)维护一个窗口,动态调整窗口大小,移动窗口的位置以找到符合条件的子数组或子字符串。
- **示例**:
left, right = 0, 0
while right < len(s):
c = s[right]
right += 1
...
while window_needs_shrink():
d = s[left]
left += 1
...
3. 字符频率统计
使用字典或 `defaultdict` 统计字符串中字符的频率,是处理字符串问题的常用技巧。
- **用途**:在涉及字符串比较、匹配等操作时,字符频率统计是一个基础步骤。
- **示例**:
need = defaultdict(int)
for c in t:
need[c] += 1
在这里,`need` 字典用于统计字符串 `t` 中每个字符的频率。
4. 占位符函数
def window_needs_shrink(): return False
- **用途**:在初步搭建框架时,占位符函数用于表示某个需要实现的具体逻辑。
- **实现**:在实际应用中,这个函数需要根据特定问题的需求进行实现,以判断窗口是否需要收缩。
5. 字符串操作
在字符串中,通过索引访问字符是常见的操作。
- **示例**:
c = s[right] # 获取当前右指针指向的字符
d = s[left] # 获取当前左指针指向的字符
通过这种方式,可以获取窗口左右边界的字符,进行相应的逻辑处理。
总结
上述代码框架结合了 `defaultdict`、滑动窗口、字符频率统计等数据结构和算法技巧,为解决类似字符串匹配或子数组查找的问题提供了基础结构。通过填充具体的逻辑,可以实现特定功能的滑动窗口算法。
速记口诀
整体思路
滑动窗口算法的思路是这样:
1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,**也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
下面画图理解一下,needs 和 window 相当于计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。
初始状态:
{:align=center}
增加 right,直到窗口 [left, right) 包含了 T 中所有字符:
{:align=center}
现在开始增加 left,缩小窗口 [left, right)。
{:align=center}
直到窗口中的字符串不再符合要求,left 不再继续移动。
{:align=center}
之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。