3. 无重复字符的最长子串(中等)
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc"
,所以其
长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b"
,所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke"
,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke"
是一个子序列,不是子串。
方法:滑动窗口
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if not s:
return 0
n = len(s)
lookup = set()
left = 0
cur_len, max_len = 0, 0
for i in range(n):
cur_len += 1
while s[i] in lookup:
lookup.remove(s[left])
cur_len -= 1
left += 1
if cur_len > max_len:
max_len = cur_len
lookup.add(s[i])
return max_len
76. 最小覆盖子串(困难)
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
提示:
如果 S 中不存这样的子串,则返回空字符串 ""
。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
方法: 滑动窗口的思想:
用i,j表示滑动窗口的左边界和右边界,通过改变i,j来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度j-i+1,这些长度中的最小值就是要求的结果。
步骤一
不断增加j使滑动窗口增大,直到窗口包含了T的所有元素
步骤二
不断增加i使滑动窗口缩小,因为是要求最小字串,所以将不必要的元素排除在外,使长度减小,直到碰到一个必须包含的元素,这个时候不能再扔了,再扔就不满足条件了,记录此时滑动窗口的长度,并保存最小值
步骤三
让i再增加一个位置,这个时候滑动窗口肯定不满足条件了,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到j超出了字符串S范围。
面临的问题:
如何判断滑动窗口包含了T的所有元素?
我们用一个字典need来表示当前滑动窗口中需要的各元素的数量,一开始滑动窗口为空,用T中各元素来初始化这个need,当滑动窗口扩展或者收缩的时候,去维护这个need字典,例如当滑动窗口包含某个元素,我们就让need中这个元素的数量减1,代表所需元素减少了1个;当滑动窗口移除某个元素,就让need中这个元素的数量加1。
记住一点:need始终记录着当前滑动窗口下,我们还需要的元素数量,我们在改变i,j时,需同步维护need。
值得注意的是,只要某个元素包含在滑动窗口中,我们就会在need中存储这个元素的数量,如果某个元素存储的是负数代表这个元素是多余的。比如当need等于{'A':-2,'C':1}时,表示当前滑动窗口中,我们有2个A是多余的,同时还需要1个C。这么做的目的就是为了步骤二中,排除不必要的元素,数量为负的就是不必要的元素,而数量为0表示刚刚好。
回到问题中来,那么如何判断滑动窗口包含了T的所有元素?结论就是当need中所有元素的数量都小于等于0时,表示当前滑动窗口不再需要任何元素。
优化
如果每次判断滑动窗口是否包含了T的所有元素,都去遍历need看是否所有元素数量都小于等于0,这个会耗费O(k)的时间复杂度,k代表字典长度,最坏情况下,k可能等于len(S)。
其实这个是可以避免的,我们可以维护一个额外的变量needCnt来记录所需元素的总数量,当我们碰到一个所需元素c,不仅need[c]的数量减少1,同时needCnt也要减少1,这样我们通过needCnt就可以知道是否满足条件,而无需遍历字典了。
前面也提到过,need记录了遍历到的所有元素,而只有need[c]>0大于0时,代表c就是所需元素
图示
以S="DOABECODEBANC",T="ABC"为例
初始状态:
步骤一:不断增加j使滑动窗口增大,直到窗口包含了T的所有元素,need中所有元素的数量都小于等于0,同时needCnt也是0
步骤二:不断增加i使滑动窗口缩小,直到碰到一个必须包含的元素A,此时记录长度更新结果
步骤三:让i再增加一个位置,开始寻找下一个满足条件的滑动窗口
class Solution:
def minWindow(self, s: str, t: str) -> str:
from collections import defaultdict
lookup = defaultdict(int)
for val in t:
# lookup[val] = lookup.get(val, 0) + 1
lookup[val] += 1
n, m = len(s), len(t)
left = 0
min_len, res = n+1, ""
for right in range(n):
if lookup[s[right]] > 0:
m -= 1
lookup[s[right]] -= 1
while m == 0:
if min_len > right - left + 1:
min_len = right - left + 1
res = s[left:right+1]
if lookup[s[left]] == 0:
m += 1
lookup[s[left]] += 1
left += 1
return res
567. 字符串的排列(中等)
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
注意:
输入的字符串只包含小写字母
两个字符串的长度都在 [1, 10,000] 之间
方法:滑动窗口+排序
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
n1, n2 = len(s1), len(s2)
s1 = sorted(s1)
for i in range(n2 - n1 + 1):
temp = s2[i: i+n1]
if sorted(temp) == s1:
return True
return False
30. 串联所有单词的子串(困难)
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例 1:
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:
s = "wordgoodgoodgoodbestword",
words = ["word","good","best","word"]
输出:[]
方法
1
:滑动窗口
+hash
因为单词长度固定的,我们可以计算出截取字符串的单词个数是否和
words
里相等,所以我们可以借用哈希表。
一个是哈希表是
words
,一个哈希表是截取的字符串,比较两个哈希是否相等。
因为遍历和比较都是线性的,所以时间复杂度:
O(n^2)
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
from collections import Counter
if not s or not words:
return []
one_word = len(words[0])
all_len = len(words)*one_word
n = len(s)
words = Counter(words)
res = []
for i in range(0, n-all_len+1):
temp = s[i:i+all_len]
c_temp = []
for j in range(0, all_len, one_word):
c_temp.append(temp[j:j+one_word])
if Counter(c_temp) == words:
res.append(i)
return res
方法
2
:双指针
每次左右指针的移动,最主要是去除多余的元素,去除多余元素分为两种:
- 右指针向右移动时,元素个数超过了words中存在的元素个数,此时左指针向右移动直到去除了相应的一个元素(元素个数不再超过words为止)
b | a | r | t | o | o | t | o | o | b | a | r | b | a | r |
left |
|
|
|
|
|
|
|
| right |
|
|
|
|
|
此时包含了两个“too”,左指针一直向右移动直到去除了相应的“too”元素为止。
b | a | r | t | o | o | t | o | o | b | a | r | b | a | r |
|
|
|
|
|
| left |
|
| right |
|
|
|
|
|
- 右指针向右移动时,发现了不存在words中的元素,此时右指针一直向右移动直到找到words存在的元素为止。
t | h | e | t | h | e | b | a | r | t | o | o |
left |
|
|
|
|
| right |
|
|
|
|
|
右指针的右边遇到了words中不存在的字符串“the”,此时右指针不断地向右移动直到遇到了words中存在的“bar”为止
t | h | e | t | h | e | b | a | r | t | o | o |
|
|
|
|
|
| left right |
|
|
|
|
|
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
from collections import Counter
if not s or not words:
return []
one_word = len(words[0])
word_num = len(words)
n = len(s)
if n < one_word:
return []
words = Counter(words)
res = []
for i in range(0, one_word):
cur_count = 0
left, right = i, i
cur_Counter = Counter()
while right + one_word <= n:
temp = s[right:right+one_word]
right += one_word
if temp not in words:
left = right
cur_Counter.clear()
cur_count = 0
else:
cur_Counter[temp] += 1
cur_count += 1
while cur_Counter[temp] > words[temp]:
left_temp = s[left:left+one_word]
left += one_word
cur_Counter[left_temp] -= 1
cur_count -= 1
if cur_count == word_num:
res.append(left)
return res
632. 最小区间(困难)
你有 k
个升序排列的整数列表。找到一个最小区间,使得 k
个列表中的每个列表至少有一个数包含在其中。
我们定义如果 b-a < d-c
或者在 b-a == d-c
时 a < c
,则区间 [a,b] 比 [c,d] 小。
示例:
输入:[[4,10,15,24,26], [0,9,12,20], [5,18,22,30]]
输出:[20,24]
解释:
列表 1:[4, 10, 15, 24, 26],24 在区间 [20,24] 中。
列表 2:[0, 9, 12, 20],20 在区间 [20,24] 中。
列表 3:[5, 18, 22, 30],22 在区间 [20,24] 中。
提示:
给定的列表可能包含重复元素,所以在这里升序表示 >= 。
1 <= k
<= 3500
-105 <= 元素的值
<= 105
对于使用Java的用户,请注意传入类型已修改为List<List<Integer>>。重置代码模板后可以看到这项改动。
方法
1
:堆
给定
k
个列表,需要找到最小区间,使得每个列表都至少有一个数在该区间中。该问题可以转化为,从
k
个列表中各取一个数,使得这
k
个数中的最大值与最小值的差最小
。
假设这
k
个数中的最小值是第
i
个列表中的
x
,对于任意
j!=i
,设第
j
个列表中被选为
k
个数之一的数是
y
,则为了找到最小区间,
y
应该取第
j
个列表中大于等于
x
的最小的数。简单证明如下:假设
z
也是第
j
个列表中的数,且
z>y
,则有
z−x>y−x
,同时包含
x
和
z
的区间一定不会小于同时包含
x
和
y
的区间。因此,其余
k−1
个列表中应该取大于等于
x
的最小的数。
由于
k
个列表都是升序排列的,因此对每个列表维护一个指针,通过指针得到列表中的元素,指针右移之后指向的元素一定大于或等于之前的元素。
使用最小堆维护
k
个指针指向的元素中的最小值,同时维护堆中元素的最大值。初始时,
k
个指针都指向下标
0
,最大元素即为所有列表的下标
0
位置的元素中的最大值。每次从堆中取出最小值,根据最大值和最小值计算当前区间,如果当前区间小于最小区间则用当前区间更新最小区间,然后将对应列表的指针右移,将新元素加入堆中,并更新堆中元素的最大值。
如果一个列表的指针超出该列表的下标范围,则说明该列表中的所有元素都被遍历过,堆中不会再有该列表中的元素,因此退出循环。
class Solution:
def smallestRange(self, nums: List[List[int]]) -> List[int]:
rangeLeft, rangeRight = -10**9, 10**9
maxValue = max(row[0] for row in nums)
priorityQueue = [(vec[0], i, 0) for i, vec in enumerate(nums)]
heapq.heapify(priorityQueue)
while True:
minValue, row, col = heapq.heappop(priorityQueue)
if maxValue - minValue < rangeRight - rangeLeft:
rangeLeft, rangeRight = minValue, maxValue
if col == len(nums[row]) - 1:
break
maxValue = max(maxValue, nums[row][col+1])
heapq.heappush(priorityQueue, (nums[row][col+1], row, col+1))
return [rangeLeft, rangeRight]
方法
2
:
1143. 最长公共子序列(中等)
给定两个字符串 text1
和 text2
,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
- 输入的字符串只含有小写英文字符。
方法:DP
dp[i+1][j+1]:表示以str1[i]结尾的、str2[j]结尾的字符串str1,str2的最长公共子序列。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m):
for j in range(n):
if text1[i]==text2[j]:
dp[i+1][j+1] = dp[i][j] + 1
else:
dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
return dp[m][n]
718. 最长重复子数组(中等)
给两个整数数组 A
和 B
,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
提示:
1 <= len(A), len(B) <= 1000
0 <= A[i], B[i] < 100
方法1:DP
令 dp[i+1][j+1] 表示 A[:i] 和 B[:j] 的最长公共前缀,那么答案即为所有 dp中的最大值。如果 A[i] == B[j],那么 dp[i+1][j+1] = dp[i][j] + 1,否则 dp[i][j] = 0。
时间复杂度: O(N×M)
空间复杂度: O(N×M)
N 表示数组 A 的长度,M 表示数组 B 的长度。
空间复杂度还可以再优化,利用滚动数组可以优化到 O(min(N,M))。
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
m, n = len(A), len(B)
dp = [[0]*(n+1) for _ in range(m+1)]
res = 0
for i in range(m):
for j in range(n):
if A[i] == B[j]:
dp[i+1][j+1] = dp[i][j] + 1
res = max(res, dp[i+1][j+1])
return res
方法2:滑动窗口
1 2 3 2 1
3 2 1 4 7
1 2 3 2 1
3 2 1 4 7