[总结]字符串

字符串类型的题目一般与哈希,栈,双指针,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)
原文地址:https://www.cnblogs.com/hellojamest/p/11677929.html