3、无重复字符的最长子串(中等)
题目描述
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串
的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是子串
的长度,“pwke” 是一个子序列,不是子串。
提示:
- 0 <= s.length <= 5 * 104
- s 由英文字母、数字、符号和空格组成
题目思路
对于这道题,要求是找到最长连续的、不包含重复字符的字符子串。
经过观察思考我们可以得知,可以通过滑动窗口的方法来寻找这样的答案。
这里,子串的条件是不能包含重复的字符,因此首先在遍历时,我们可以定义一个哈希表来保存对应字符所处位置,方便查看当前字符是否在子串中。
同时,定义left
与right
指针,分别作为滑动窗口的左右两端,因此子串长度就是right - left
。
在right
从左到右遍历时,对于s[right]
:
- 如果该字符未曾出现、或出现的位置在当前子串左边,那么该字符可以作为
- 否则,就对
left
指针进行更新——即更新到对应已重复字符的下一个位置,将重复排除掉; - 最后,每轮循环都需要记录或刷新当前字符的位置;
算法代码
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
letter_index_map = {}
left, right = 0, 0
max_length = 0
while right < len(s):
curr_letter_index = letter_index_map.get(s[right])
# 说明当前字符要么从未出现、要么在当前子序列之前出现
if curr_letter_index is None or curr_letter_index < left:
max_length = max(max_length, right - left + 1)
else:
left = curr_letter_index + 1
# 记录/刷新当前字符所在位置
letter_index_map[s[right]] = right
right += 1
return max_length
438、找到字符串中所有字母异位词(中等)
题目描述
给定两个字符串 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 仅包含小写字母
题目思路
所谓的异位词,在哈希问题中已有说明: 49、字母异位词分组
在该问题中,既可以直接对单词进行排序来判断。
在这道题中,这里如果直接去排序的话,需要排序的次数就太多了,例如如果s长度是10,p长度是3,那么至少要排序7+1次,更不用说s和p的长度可能会非常大。
因此,另一种方法则是字母计数,只需要使用ASCII码作为索引构造哈希表,然后统计字母出现次数,最后对比两个哈希表是否相同,就可以知道两个单词是否是字母异位词了。
在这道题中,本质上就是以p的长度划定一个滑动窗口,每向右移动一位时:
- 窗口最左侧字符去掉,因此该字符的计数-1;
- 窗口最右侧字符新增,因此该字符的计数+1;
- 如果此时的
s_counter
和p_counte
r是否相同,就将对应索引加入结果中;
需要注意的是,在滑动前,我们自然是要有已经构造好的窗口,因此定义两个字母哈希表s_counter
和p_counter
:
s_counter
,记录滑动过程中,s字符子串字符出现次数;p_counter
,不变量,记录p的字符出现次数,相当于模板;
同时,因为所谓遍历,本质上就是开始滑动窗口,因此构造完s_counter
和p_counter
后,需要首先做一下判断,看下第一个滑动区间是否满足条件。
算法代码
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
s_len, p_len = len(s), len(p)
if s_len < p_len:
return []
s_counter = [0] * 26
p_counter = [0] * 26
total_anagrams_idx = []
for i in range(p_len):
s_counter[ord(s[i]) - ord('a')] += 1
p_counter[ord(p[i]) - ord('a')] += 1
# 第一个滑动区间单独判断
if s_counter == p_counter:
total_anagrams_idx.append(0)
for i in range(s_len - p_len):
# 滑动一位,去掉左侧字符,新增右侧字符
s_counter[ord(s[i]) - ord('a')] -= 1
s_counter[ord(s[i + p_len]) - ord('a')] += 1
if s_counter == p_counter:
total_anagrams_idx.append(i + 1)
return total_anagrams_idx
补充说明
所谓滑动窗口法,又称为“寸取法”,一般用来解决查找满足依一定条件的连续区间的特殊性质(长度等) 等一类问题。
由于区间是连续的,因此当整个区间发生变化时,可以通过对旧有的计算结果对搜索空间的剪枝(一般就是滑动窗口最左侧滑出的部分),从而减少了重复计算、降低了时间复杂度、避免了暴力的搜索。
往往类似于“请找到满足xx条件的最x区间/子串/子数组”这一类问题都可以使用滑动窗口法来进行解决。