原题说明
Given a string S
, consider all duplicated substrings: (contiguous) substrings of S that occur 2 or more times. (The occurrences may overlap.)
Return any duplicated substring that has the longest possible length. (If S
does not have a duplicated substring, the answer is “”
.)
Example 1:
Input: “banana”
Output: “ana”
Example 2:
Input: “abcd”
Output: “”
Note:
2 <= S.length <= 10^5
S
consists of lowercase English letters.
解题思路
题目要求给出一个字符串中,重复出现至少2
次的最长子串。
首先,我们考虑如果采用暴力破解,若字符串长度是n
,那么从n
到1
依次从大到小计算,第一次遇到重复出现两次的子串,就是我们的答案。
但是,暴力算法显然不是最优解,也不能AC。
显然,最长子串的长度最大至多是n-1
,最小是0
。也就提醒我们可以按照这个空间进行binary search
。
然后,在每一次二分搜索中,我们判断在当前长度下有没有相同的子串重复出现至少2次。如果存在,那向上搜索;如果不存在,那向下搜索,直到找到答案。这里,用哈希表记录下每一次出现过的子串。但是,如果单纯用子串作为key
,我们会发现memory exceed
。显然,我们需要对字符串的存储做个优化。但如何优化一开始我也没想出来。提示中告诉我们,To check whether an answer of length K exists, we can use Rabin-Karp 's algorithm.
。什么是Rahin-karp
算法呢?根据wiki
https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm 给我们的解释,它就是一种优化快速查询文本中出现给定子串的的方法。
简单来说,对于一个字符串,用一个哈希函数求出对应的哈希值。当两个哈希值不同时,这两个字符串一定不同,而当两个哈希值相同时,这两个字符串有大概率相同。因为查找子串的过程,可以高效利用之前求出的哈希值,不用太多的重复计算(wiki
中称为rolling hash function
),因此能快速计算哈希值并且高效储存。
具体到哈希值的计算,我们举例说明:
首先需要确定一个base
,也就是进制,这里我们选择26
。然后我们选择一个modulus
,比如31
。按题目中给出的banana
,我们计算长度是3
的各个子串的哈希值。
ban
: (1 * 26 * 26 + 0 * 26 + 13) % 31 = 7ana
: (0 * 26 * 26 + 13 * 26 + 0) % 31 = 28nan
: (13 * 26 * 26 + 0 * 26 + 13) % 31 = 28ana
: (0 * 26 * 26 + 13 * 26 + 0) % 31 = 28
我们可以观察到,ana
与nan
拥有相同的哈希值,但是它们确实是不同的字符串。因为这种哈希值的计算过程中,有重复计算的部分,所以我们可以rolling
这部分的内容,优化时间。同时,哈希值的空间大小也是明显优于那些超长字符串的。
需要说明的是,这个方法会有false positive
的可能,因为hash function
有collision
的产生。但当modulus
很大时,概率是极低的。
示例代码 (cpp)
1 | class Solution { |
示例代码 (java)
1 | class Solution { |
示例代码 (python)
1 | class Solution(object): |
复杂度分析
时间复杂度: O(NlogN),N
是字符串的长度。
空间复杂度: O(N),N
是字符串的长度。
归纳总结
Rahin-karp
的算法,不知道的人在做题时不会想到。同样,也不大可能在面试的时候自己想出来。非常正常。但我们了解后,还是比较简单的。同时,用哈希的方法转化的思想值得我们注意。
我们在Youtube上更新了视频讲解,欢迎关注!