代码参考:https://www.shiyanlou.com/courses/492
https://algorithm.yuanbin.me/
处理字符串操作相关问题时,常见的做法是从字符串尾部开始编辑,从后往前逆向操作。这么做的原因是因为字符串的尾部往往有足够空间,可以直接修改而不用担心覆盖字符串前面的数据。
Code 1:https://leetcode.com/problems/implement-strstr/
问题陈述:
对于给定的源字符串和目标字符串,应在源字符串中输出目标字符串的第一个索引(从0)。
如果目标不存在于源中,则返回1。
例子:
If source = “source” and target = “target”, return -1.
If source = “abcdabcdefg” and target = “bcd”, return 1.
挑战:
O(N2)是可以接受的。你能实现O(n)算法吗?(提示:KMP)
澄清
我需要在真实的面试实现KMP算法?
没有必要。当你在面试中遇到这个问题时,面试官可能只是想测试你的基本执行能力。但首先要确保你与面试官的确认。
题解:
对于字符串查找问题,可使用双重 for 循环解决,效率更高的则为 KMP 算法。双重 for 循环的使用较有讲究,因为这里需要考虑目标字符串比源字符串短的可能。对目标字符串的循环肯定是必要的,所以可以优化的地方就在于如何访问源字符串了。简单直观的解法是利用源字符串的长度作为 for 循环的截止索引,这种方法需要处理源字符串中剩余长度不足以匹配目标字符串的情况,而更为高效的方案则为仅遍历源字符串中有可能和目标字符串匹配的部分索引。
代码:
#!/usr/bin/python # -*- coding: utf-8 -*- class Solution: def strStr(self, source, target): if source is None or target is None: return -1 for i in range(len(source) - len(target) + 1): for j in range(len(target)): if source[i + j] != target[j]: break else: # no break return i return -1 info = Solution() print info.strStr('abcdabcdefg','bcd')
执行结果:输出1.
源码分析:
1、边界检查:haystack(source)和needle(target)有可能是空串。
2、边界检查之下标溢出:注意变量i的循环判断条件,如果用的是i < source.length()则在后面的source.charAt(i + j)时有可能溢出。
3、代码风格:
- 运算符==两边应加空格
- 变量名不要起s1“s2这类,要有意义,如target“source
- int i, j;`声明前有一行空格,是好的代码风格
4、是否在for的条件中声明i,j,这个视情况而定,如果需要在循环外再使用时,则须在外部初始化,否则没有这个必要。
需要注意的是有些题目要求并不是返回索引,而是返回字符串,此时还需要调用相应语言的substring方法。Python3 中用range替换了xrange,Python2 中使用xrange效率略高一些。 另外需要注意的是 Python 代码中的else接的是for 而不是if, 其含义为no break, 属于比较 Pythonic 的用法。
复杂度分析:
双重 for 循环,时间复杂度最坏情况下为 O((n-m)*m)O((n−m)∗m).
code 2:比较两个字符串是否互为变位词 http://www.lintcode.com/en/problem/two-strings-are-anagrams/
Write a method anagram(s,t) to decide if two strings are anagrams or not. Example Given s="abcd", t="dcab", return true. Challenge O(n) time, O(1) extra space
题解 1 hashmap 统计字频:
判断两个字符串是否互为变位词,若区分大小写,考虑空白字符时,直接来理解可以认为两个字符串的拥有各不同字符的数量相同。对于比较字符数量的问题常用的方法为遍历两个字符串,统计其中各字符出现的频次,若不等则返回false
. 有很多简单字符串类面试题都是此题的变形题。
代码:
#!/usr/bin/python # -*- coding: utf-8 -*- import collections class Solution: """ @param s: The first string @param b: The second string @return true or false """ def anagram(self, s, t): return collections.Counter(s) == collections.Counter(t) info = Solution() print info.anagram('abcd','dcab')
执行结果,输出True。
其中collections模块自Python 2.4版本开始被引入,包含了dict、set、list、tuple以外的一些特殊的容器类型,分别是:
- OrderedDict类:排序字典,是字典的子类。引入自2.7。
- namedtuple()函数:命名元组,是一个工厂函数。引入自2.6。
- Counter类:为hashable对象计数,是字典的子类。引入自2.7。
- deque:双向队列。引入自2.4。
- defaultdict:使用工厂函数创建字典,使不用考虑缺失的字典键。引入自2.5。
文档参见:http://docs.python.org/2/library/collections.html。
Counter类的目的是用来跟踪值出现的次数。它是一个无序的容器类型,以字典的键值对形式存储,其中元素作为key,其计数作为value。计数值可以是任意的Interger(包括0和负数)。Counter类和其他语言的bags或multisets很相似。具体使用:
import collections print collections.Counter('abcd') ##输出为Counter({'a': 1, 'c': 1, 'b': 1, 'd': 1})
源码分析
- 两个字符串长度不等时必不可能为变位词(需要注意题目条件灵活处理)。
- 初始化含有256个字符的计数器数组。
- 对字符串 s 自增,字符串 t 递减,再次遍历判断letterCount数组的值,小于0时返回false.
在字符串长度较长(大于所有可能的字符数)时,还可对第二个for循环做进一步优化,即t.size() > 256时,使用256替代t.size(), 使用i替代t[i].
复杂度分析
两次遍历字符串,时间复杂度最坏情况下为 O(n), 使用了额外的数组,空间复杂度 O(1).
题解2 – 排序字符串
另一直接的解法是对字符串先排序,若排序后的字符串内容相同,则其互为变位词。题解1中使用 hashmap 的方法对于比较两个字符串是否互为变位词十分有效,但是在比较多个字符串时,使用 hashmap 的方法复杂度则较高。
代码:
class Solution: """ @param s: The first string @param b: The second string @return true or false """ def anagram(self, s, t): return sorted(s) == sorted(t) info = Solution() print info.anagram('abcd','dcba')
输出结果为True。
源码分析
对字符串 s 和 t 分别排序,而后比较是否含相同内容。对字符串排序时可以采用先统计字频再组装成排序后的字符串,效率更高一点。
复杂度分析
C++的 STL 中 sort 的时间复杂度介于 O(n)和 O(n^2)之间,判断s == t时间复杂度最坏为 O(n).
code 3:比较字符串 http://www.lintcode.com/en/problem/compare-strings/
Compare two strings A and B, determine whether A contains all of the characters in B. The characters in string A and B are all Upper Case letters. Example For A = "ABCD", B = "ABC", return true. For A = "ABCD" B = "AABC", return false.
题解:
code 2的变形题。题目意思是问B中的所有字符是否都在A中,而不是单个字符。比如B=”AABC”包含两个「A」,而A=”ABCD”只包含一个「A」,故返回false. 做题时注意题意,必要时可向面试官确认。
既然不是类似 strstr 那样的匹配,直接使用两重循环就不太合适了。题目中另外给的条件则是A和B都是全大写单词,理解题意后容易想到的方案就是先遍历 A 和 B 统计各字符出现的频次,然后比较频次大小即可。嗯,祭出万能的哈希表。
Python 的dict
就是hash, 所以python 在处理需要用到hash的地方非常方便。
代码:
import collections class Solution: def compare_strings(self, A, B): # return a dict with default value set to 0 letters = collections.defaultdict(int) for a in A: letters[a] += 1 for b in B: if b not in letters: return False elif letters[b] <= 0: return False else: letters[b] -= 1 return True info = Solution() print info.compare_strings('abcde','dcba')
执行结果输出True。
源码解析:
- 异常处理,B 的长度大于 A 时必定返回false, 包含了空串的特殊情况。
- 使用额外的辅助空间,统计各字符的频次。
复杂度分析:
遍历一次 A 字符串,遍历一次 B 字符串,时间复杂度最坏 O(2n), 空间复杂度为 O(26).
code 4:字谜游戏 https://leetcode.com/problems/anagrams/
Given an array of strings, return all groups of strings that are anagrams. Example Given ["lint", "intl", "inlt", "code"], return ["lint", "inlt", "intl"]. Given ["ab", "ba", "cd", "dc", "e"], return ["ab", "ba", "cd", "dc"]. Note All inputs will be in lower-case
题解1 – 双重for循环(TLE)
题 Two Strings Are Anagrams(code 2) 的升级版,容易想到的方法为使用双重for循环两两判断字符串数组是否互为变位字符串。但显然此法的时间复杂度较高。还需要 O(n)的数组来记录字符串是否被加入到最终结果中。
代码:
class Solution: # @param strs: A list of strings # @return: A list of strings # @return: A list of strings def anagrams(self, strs): if len(strs) < 2 : return strs result=[] visited=[False]*len(strs) for index1,s1 in enumerate(strs): hasAnagrams = False for index2,s2 in enumerate(strs): if index2 > index1 and not visited[index2] and self.isAnagrams(s1,s2): result.append(s2) visited[index2]=True hasAnagrams = True if not visited[index1] and hasAnagrams: result.append(s1) return result def isAnagrams(self, str1, str2): if sorted (str1) == sorted(str2): return True return False info = Solution() print info.anagrams(["lint", "intl", "inlt", "code"])
输出:[“lint”, “intl”, “inlt”]。
源码分析
- strs 长度小于等于1时直接返回。
- 使用与 strs 等长的布尔数组表示其中的字符串是否被添加到最终的返回结果中。
- 双重循环遍历字符串数组,注意去重即可。
- 私有方法isAnagrams用于判断两个字符串是否互为变位词。
复杂度分析
私有方法isAnagrams最坏的时间复杂度为 O(2L), 其中 L 为字符串长度。双重for循环时间复杂度近似为 1/2 O(n^2), n 为给定字符串数组数目。总的时间复杂度近似为 O(n^2 L). 使用了Vector String “visited”,空间复杂度可认为是 O(n).
题解2 – 排序 + hashmap
在题 Two Strings Are Anagrams 中曾介绍过使用排序和 hashmap 两种方法判断变位词。这里我们将这两种方法同时引入!只不过此时的 hashmap 的 key 为字符串,value 为该字符串在 vector 中出现的次数。两次遍历字符串数组,第一次遍历求得排序后的字符串数量,第二次遍历将排序后相同的字符串取出放入最终结果中。
leetcode 上此题的 signature 已经更新,需要将 anagrams 按组输出,稍微麻烦一点点。
代码:
class Solution: # @param strs: A list of strings # @return: A list of strings # @return: A list of strings def anagrams(self, strs): strDict={} result=[] for string in strs: if "".join(sorted(string)) not in strDict.keys(): strDict["".join(sorted(string))] = 1 else: strDict["".join(sorted(string))] += 1 for string in strs: if strDict["".join(sorted(string))] >1: result.append(string) return result info = Solution() print info.anagrams(["ab", "ba", "cd", "dc", "e"])
输出:[‘ab’, ‘ba’, ‘cd’, ‘dc’]。
复杂度分析:
遍历一次字符串数组,复杂度为 O(n), 对单个字符串排序复杂度近似为 O(LlogL). 两次遍历字符串数组,故总的时间复杂度近似为 O(nLlogL). 使用了哈希表,空间复杂度为 O(K)O(K), 其中 K 为排序后不同的字符串个数。
code 5:Longest Common Substring
Given two strings, find the longest common substring. Return the length of it. Example Given A="ABCD", B="CBCE", return 2. Note The characters in substring should occur continuously in original string. This is different with subsequence.
题解:
求最长公共子串,注意「子串」和「子序列」的区别!简单考虑可以使用两根指针索引分别指向两个字符串的当前遍历位置,若遇到相等的字符时则同时向后移动一位。