字符串类型的题目一般与哈希,栈,双指针,DP紧密相关。理解了这几个知识与结构,字符串类型的题目一般也可以迎刃而解。另外,针对字符串特有的KMP算法以及Manacher算法的变形题也是常见题,需要掌握。
哈希
[leetcode]318. Maximum Product of Word Lengths
难点是怎么判断两个字符串是否不含有相同的字符。可以用一个int32位(小写字母只有26个),后26位用来表示对应的字符是否存在。最后两个int相与,如果结果为0,说明两个对应的字符串没有相同的字符。
class Solution:
def maxProduct(self, words: List[str]) -> int:
res = 0
d = collections.defaultdict(int)
N = len(words)
for i in range(N):
w = words[i]
for c in w:
d[w] |= 1 << (ord(c) - ord('a'))
for j in range(i):
if not d[words[j]] & d[words[i]]:
# if not set(list(words[j])) & set(list(words[i])): #Time Limit Exceeded
res = max(res, len(words[j]) * len(words[i]))
return res
[leetcode]336. Palindrome Pairs
同样的,难点在于如何判断两个数是否能组成回文串。由于这里要求两个字符串的索引,可以使用hashmap存储字符串:索引形式,每次判断的时候只需要判断与该字符配对的回文串是否在hashmap中即可。
class Solution:
def palindromePairs(self, words: List[str]) -> List[List[int]]:
dic = {w : i for i, w in enumerate(words)}
def isPalindrome(word):
return word == word[::-1]
res = set()
for idx, word in enumerate(words):
if word and isPalindrome(word) and "" in dic:
nidx = dic[""]
res.add((idx, nidx))
res.add((nidx, idx))
rword = word[::-1]
if word and not isPalindrome(word) and rword in dic:
nidx = dic[rword]
res.add((idx, nidx))
res.add((nidx, idx))
for x in range(1, len(word)):
left, right = word[:x], word[x:]
rleft, rright = left[::-1], right[::-1]
if isPalindrome(left) and rright in dic:
res.add((dic[rright], idx))
if isPalindrome(right) and rleft in dic:
res.add((idx, dic[rleft]))
return list(res)
使用额外空间降低时间复杂度是常见的套路了。一般在python中可以使用dict或者set结构,如果上题只需要求出对应字符串,用set就可以了。
双指针
[leetcode]3.Longest Substring Without Repeating Characters
动态规划和双指针的思想。首先定义两个指针变量,用来控制输入串的子串的头和尾,设置一个dict存储读入的字符位置。头部指针不动,尾部指针向后移动,如果尾指针指向的字符没有出现在dict中,则加入,否则找到dict的值。由于遍历时顺序的遍历,头指针直接指到与尾指针重复的字符的后一个,即dict的值+1,这一步操作非常重要。重复3的步骤即可直到末尾,记得注意更新max长度,需要加入就更新,因为跳出循环不一定是重新重复步骤也可以是到字符串末尾。
class Solution:
def lengthOfLongestSubstring(self, s):
start = maxLength = 0
usedChar = {}
for i in range(len(s)):
if s[i] in usedChar and start <= usedChar[s[i]]: #起始指针在重复字符之前才会更新
start = usedChar[s[i]] + 1
maxLength = max(maxLength, i - start + 1)
usedChar[s[i]] = i
return maxLength
[leetcode]76. Minimum Window Substring
滑动窗口的思想。一个right指针遍历s,每次遇到t中的字符,在map中减少一个,同时用一个count做统计,当t中所有字符被遍历的时候,做一次统计,并且将left指针移动,直到count != t.length() ,相当于一个窗口在s字符串上配合map表动态滑动。
class Solution(object):
def minWindow(self, s, t):
need, missing = collections.Counter(t), len(t)
i = I = J = 0
for j, c in enumerate(s, 1):
missing -= need[c] > 0
need[c] -= 1
if not missing:
while need[s[i]] < 0:
need[s[i]] += 1
i += 1
if not J or j - i <= J - I:
I, J = i, j
return s[I:J]
[leetcode]424. Longest Repeating Character Replacement
滑动窗口的思想。两个指针start和end,end向后移动,每一次都找出start到end这个窗口内出现次数最多的字符,那么这个窗口内就应该把其余的字符改成这个字符。需要更改的数目是end - start + 1 - maxCount。如果说数目大于k,那么需要start向后移动。
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
res = start = end = max_cur = 0
count = collections.defaultdict(int)
while end < len(s):
# end_ind = ord(s[end])-ord('A')
count[s[end]] += 1
max_cur = max(max_cur,count[s[end]])
if end-start+1-max_cur > k:
count[s[start]]-=1
start += 1
res = max(res,end-start+1)
end +=1
return res
[leetcode]438.Find All Anagrams in a String
滑动窗口的思想。end对每个路过的字符-1,begin对每个字符+1,这样begin和end中间的字符信息就记录在字典中了,字典中的值表示当前子串还需要几个对应的字符(负数表示不需要)和p匹配。同时用count记录当前串是否完成匹配,count主要是记录字典的统计信息的,这样就不用去遍历字典检查信息了。
import collections
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
start, end = 0, 0
missing,need = len(p),collections.Counter(p)
res = []
while end < len(s):
if need[s[end]] > 0:
missing -= 1
need[s[end]] -= 1
end += 1
#匹配成功
if missing == 0:
res.append(start)
#字串长度和p相等,begin向前移动
if end - start == len(p):
# start向前移动
if need[s[start]] >= 0:
missing += 1
need[s[start]] += 1
start += 1
return res
[leetcode]567. Permutation in String
几乎是上一题的简化题。我们需要考虑s2中是否包含s1中同样个数的字符,并且这些字符是连在一起。因此,我们可以使用一个滑动窗口,在s2上滑动。在这个滑动窗口中的字符及其个数是否刚好等于s1中的字符及其个数,此外滑动窗口保证了这些字符是连在一起的。
import collections
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
start, end = 0, 0
missing,need = len(s1),collections.Counter(s1)
while end < len(s2):
if need[s2[end]] > 0:
missing -= 1
need[s2[end]] -= 1
end += 1
#匹配成功
if missing == 0:
return True
#字串长度和p相等,begin向前移动
if end - start == len(s1):
# start向前移动
if need[s2[start]] >= 0:
missing += 1
need[s2[start]] += 1
start += 1
return False
字符串加双指针组成滑动窗口是常规的解题手段,配合哈希更佳。
栈
[leetcode]402. Remove K Digits
使用一个栈作为辅助,遍历数字字符串,当当前的字符比栈最后的字符小的时候,说明要把栈的最后的这个字符删除掉。为什么呢?你想,把栈最后的字符删除掉,然后用现在的字符进行替换,是不是数字比以前的那种情况更小了?所以同样的道理,做一个while循环,在每一个数字处理的时候,都要做一个循环,使得栈里面最后的数字比当前数字大的都弹出去。最后,如果K还没用完,那要删除哪里的字符呢?毋庸置疑肯定是最后的字符,因为前面的字符都是小字符。
class Solution:
def removeKdigits(self, num: str, k: int) -> str:
if len(num) == k:
return '0'
stack = []
for n in num:
while stack and k and int(stack[-1]) > int(n):
stack.pop()
k -= 1
stack.append(n)
while k:
stack.pop()
k -= 1
return str(int("".join(stack)))
[leetcode]316. Remove Duplicate Letters
需要借助一个栈来实现字符串构造的操作。具体操作如下:
从输入字符串中逐个读取字符c,并把c的字符统计减一。
如果当前字符c已经在栈里面出现,那么跳过。
如果当前字符c在栈里面,那么:
- 如果当前字符c小于栈顶,并且栈顶元素有剩余(后面还能再添加进来),则出栈栈顶,标记栈顶不在栈中。重复该操作直到栈顶元素不满足条件或者栈为空。
- 入栈字符c,并且标记c已经在栈中。
class Solution:
def removeDuplicateLetters(self, s: str) -> str:
count = collections.Counter(s)
stack = []
visited = collections.defaultdict(bool)
for c in s:
count[c] -= 1
if visited[c]:
continue
while stack and count[stack[-1]] and c<stack[-1] :
visited[stack[-1]] = False
stack.pop()
visited[c] = True
stack.append(c)
return "".join(stack)
这是一道hard题。解题的精髓在于设置visited哈希表,与常规的记录字符出现的个数不同,visited记录一个char是否出现过。
分治
[leetcode]395. Longest Substring with At Least K Repeating Characters
分治的思想。如果字符串s的长度少于k,那么一定不存在满足题意的子字符串,返回0;如果一个字符在s中出现的次数少于k次,那么所有的包含这个字符的子字符串都不能满足题意。所以,应该去不包含这个字符的子字符串继续寻找。这就是分而治之的思路,返回不同子串的长度最大值。如果s中的每个字符出现的次数都大于k次,那么s就是我们要求的字符串。
class Solution:
def longestSubstring(self, s: str, k: int) -> int:
if len(s) < k:
return 0
for c in set(s):
if s.count(c) < k:
return max([self.longestSubstring(t, k) for t in s.split(c)])
return len(s)