3. 无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 "abc"
,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 "b"
,所以其长度为 1。
同样使用双指针,lo、hi分别表示一个可行解,lo~hi表示一个子串,此外我们需要:
- 判断子串内无重复字符,然后求解子串长度;
- 更新窗口
滑动窗口,lo~hi;lo用来缩小范围,hi用于扩大范围:先固定lo左边界,扩大右边界,寻找可行解,找到之后固定hi,计算窗口内容,然后缩小lo左边界,继续寻找。
无重复字符的判断:使用一个hash表,用于记录窗口内的字符;或者是词频数组。
算法步骤:
1、“窗口”的右边界一直向右边滑动,直到发现“窗口”内的元素出现了重复,或者右边界超过了字符串的末尾,在右边界扩张的过程中,“滑动窗口”的长度在逐渐增大,此时记录下最大值;
2、只要出现了“重复”,“窗口”的右边界停止,此时左边界向右边移动,直到“滑动窗口”内没有重复的元素,跳转到第 1 步。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
size = len(s)
# 特判
if size < 2:
return size
left, right = 0, 0
# 滑动窗口的逻辑是尝试向右移动一位,因此,初始值是 -1
# 设置成 128 与测试用例有关
counter = [0 for _ in range(128)]
# 因为自己一定是不重复的子串,在字符串非空的情况下,至少结果为 1
res = 0
while right < size:
# counter[s.charAt(right)] == 0 表示在 [left, right] 这个区间里没有出现
if counter[ord(s[right])] == 0:
# 表示没有重复元素,right 可以加 1
counter[ord(s[right])] += 1
res = max(res, right - left + 1)
right += 1
else:
# 如果下一个字符已经越界了,或者右边第 1 个字母是频率数组是曾经出现过的
# 把左边从频数数组中挪掉,频数减 1
counter[ord(s[left])] -= 1
left += 1
return res
# 集合判重
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
ans = 0
hash_set = set()
left, right = 0, 0
while right < len(s):
while s[right] in hash_set:
hash_set.remove(s[left])
left += 1
hash_set.add(s[right])
ans = max(ans, right - left + 1)
right += 1
return ans
同时,第二步中,我们可以进一步优化,将left的移动一步到位,直接移动到重复元素的下一位,如”abca“,移动到b上。因此,我们需要保存字母及其下标的对应关系。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 特判
size = len(s)
if size < 2:
return size
# key 为字符,val 记录了当前读到的字符的索引,在每轮循环的最后更新
d = dict()
left = 0
# 非空的时候,结果至少是 1 ,因此初值可以设置成为 1
res = 1
for right in range(size):
# d[s[right]] >= left,表示重复出现在滑动窗口内
# d[s[right]] < left 表示重复出现在滑动窗口之外,之前肯定计算过了
if s[right] in d and d[s[right]] >= left:
# 下一个不重复的子串至少在之前重复的那个位置之后
# 【特别注意】快在这个地方,左边界直接跳到之前重复的那个位置之后
left = d[s[right]] + 1
# 此时滑动窗口内一定没有重复元素
res = max(res, right - left + 1)
# 无论如何都更新位置
d[s[right]] = right
return res
438. 找到字符串中所有字母异位词
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。
一种思路:我们在s串中找到所有长度等于len§的子串,判断子串的排序结果和p串的排序结果是否相同,如果相同说明找到一个解,记录下标。
另一种思路: 采用滑动窗口。我们先记录p串,保存词频数组。然后使用滑动窗口遍历s串,找到所有符合条件的异位词。对于s串,我们也可以使用一个词频数组,判断两个数组相等来确定是否找到异位词。我们的滑动窗口保持长度为len§。
class Solution:
def findAnagrams(self, s: str, p: str):
result = []
scount = [0] * 26 # 字母计数数组,用于s
pcount = [0] * 26 # 字母计数数组,用于p
# 计算p中每个字母的频率
for c in p:
pcount[ord(c) - ord('a')] += 1
ssize = len(s)
psize = len(p)
# 如果s的长度小于p,直接返回空结果
if ssize < psize:
return result
# 遍历字符串s
for i in range(ssize):
# 更新当前字符在s中的计数
scount[ord(s[i]) - ord('a')] += 1
# 如果窗口大小超过了p的长度,移动窗口
if i >= psize:
scount[ord(s[i - psize]) - ord('a')] -= 1
# 如果两个计数数组相等,则找到了一个字母异位词的起始索引
if scount == pcount:
result.append(i - psize + 1)
return result
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> result;
vector<int> scount(26), pcount(26);
for (char c: p) ++pcount[c-'a'];
int ssize = s.size(), psize = p.size();
if (ssize < psize) return result;
for (int i=0; i<ssize; ++i) {
scount[s[i]-'a']++;
if (i>=psize) scount[s[i-psize]-'a']--;
if (scount == pcount)
result.push_back(i-psize+1);
}
return result;
}
};