题目描述

给出一个字符串 S,考虑其所有重复子串(S 的连续子串,出现两次或多次,可能会有重叠)。
返回任何具有最长可能长度的重复子串。(如果 S 不含重复子串,那么答案为 ""。)

最简单的想法肯定是从length(S)-1开始,判断所有该长度的字符串里是否有重复的字符串,没有则递减长度,直到有重复的字符串,或者长度递减至0,这种方法虽然简单,但是算法复杂度太高。

方法1:后缀数组法

后缀数组法:将字符串所有的后缀字符串存放在一个数组里,然后进行排序,遍历数组,寻找相邻两个字符串的最长公共前缀子串即为字符串的最长重复子串。
以banana为例,其后缀字符串的数组为[“banana”, “anana”, “nana”, “ana”, “na”, “a”],进行排序得[“a”, “ana”, “anana”, “banana”, “na”, “nana”];不难看出相邻字符串的最长前缀重复子串为:“a”, “ana”, “”, “”, “na”,所以banana的最长重复子串为“ana”。
这里可能有个疑问,为什么通过相邻后缀字符串的最长前缀重复子串可以找到目标字符串的最长重复子串?直观点来理解,最长重复子串肯定是在两个不同的后缀字符串里,假设在A、B两个后缀字符串里,则A、B前面某一部分字符串必然是相同的,且经过排序两者必然是相邻的。简单进行证明,若最长重复子串(假设长L)在A、B两个后缀字符串里,但是排序后AB不相邻,则AB中间的字符串C必有A[0:L-1]=B[0:L-1]=C[0:L-1],不可能有A[L]=C[L],否则和最长长度为L矛盾,则依然有相邻的A、C包含最长重复子串。
字符长很长的时候,存放后缀字符串的数组会很大,占内存很多。

// 后缀数组法
import java.util.Arrays;
class Solution {
    public String longestDupSubstring(String S) {
        int len = S.length();
        String result = "";
        int maxLen = 0;
        if(len <= 1)
            return "";
        String[] strs = new String[len];  // 存放S的后缀字符串
        for(int i = 0; i < len; i++){
            strs[i] = S.substring(i, len);
        }
        Arrays.sort(strs);  // 进行排序
        for(int i = 0; i < len-1; i++){  // 两个相邻字符串的最长公共前缀
            int tmp = lenTwoStr(strs[i], strs[i+1]);
            if(tmp > maxLen){
                maxLen = tmp;
                result = strs[i].substring(0,maxLen);
            }
        }
        return result;
    }
    // 两个后缀子串的前缀最长公共子串
    public int lenTwoStr(String str1, String str2){
        if(str1.length() == 0 || str2.length() == 0)
            return 0;
        int i = 0;
        while(i < str1.length() && i < str2.length() && str1.charAt(i) == str2.charAt(i))
            i ++;
        return i;
    }
}

方法2:字符串编码法

一句话来描述,就是将一段字符串转化为数字。这里只考虑小写字母,将字符转换为0-25的数字,即26进制,存到数组nums假设当前查找长度为L的最长重复子串,起始位置为start; 则 n u m C o d e ( s t a r t ) = ∑ i = s t a r t s t a r t + L − 1 2 6 s t a r t + L − 1 − i ∗ n u m s [ i ] numCode(start)=\sum_{i=start}^{start+L-1} 26^{start+L-1-i}*nums[i] numCode(start)=i=startstart+L126start+L1inums[i] 判断是否存在Set集合中,存在则找到长度为L的重复子串,若不存在,将numCode放入到Set集合,并计算下一个子串的编码。计算下一个编码时,相比于上一个子串只是去掉最左边的字符,在右侧加入一个新的字符 n u m C o d e ( s t a r t + 1 ) = ( n u m C o d e [ s t a r t ] − n u m s [ s t a r t ] ∗ 2 6 L − 1 ) ∗ 26 + n u m s [ s t a r t + L ] numCode(start+1)=(numCode[start]-nums[start]*26^{L-1})*26+nums[start+L] numCode(start+1)=(numCode[start]nums[start]26L1)26+nums[start+L]对L从length(S)-1开始递减重复上述过程直至找到重复字符串或者L降至0。需要注意的是,在求编码的过程了为了防止溢出,需要选一个足够大的数字取模运算。
字符串很长的时候,简单递减长度时间复杂度也很高。

// 字符串编码法
import java.util.HashSet;
class Solution {
    public String longestDupSubstring(String S) {
    	// 只编码
        int len = S.length();
        int a = 26;  // 26进制
        long module = (long) Math.pow(2, 32);  // 取一个足够大的数,用以取模
        if(len <= 1)
            return "";
        int[] nums = new int[len];
        for(int i = 0; i < len; i++){
            nums[i] = (int) S.charAt(i) - (int) 'a';  // 只考虑小写字母
        }
        for(int i = len-1; i >= 1; i--){  // 从子串长度len-1开始
            HashSet<Long> hashSet = new HashSet<Long>(); 
            long tmp = 0;
            long aL = 1;
            for(int j = 0; j < i; j++){
                tmp = (tmp *a + nums[j]) % module;  // 防止溢出
                //System.out.println(tmp);
                aL = (aL*a) % module;
            }
            hashSet.add(tmp);
            for(int j = 1; j <= len - i; j++){  // 长度为i的窗口
                tmp = (tmp*a - nums[j-1]*aL%module + module) % module;  // 去掉前一位
                tmp = (tmp + nums[j+i-1]) % module;
                if(hashSet.contains(tmp))
                    return S.substring(j, j+i);
                hashSet.add(tmp);
            }
        }
        return "";
    }
}

方法3:二分查找+字符串编码法

这种方法是在方法二的基础上,针对长度简单的从length(S)-1递减时间过长的问题。如果有长度为L的最长重复子串,则必然有L_0<L的重复子串,因此先采用二分法寻找到最长重复子串的长度。寻找过程看程序很容易理解,至于为什么最终low-1即为所寻找的长度,每次找到新的L_0,总设置low=L_0+1,没找到新的L_0时,变化的是high,low不变;整个过程low和high是不断靠近直至相等,相等时找到L,此时也必有low=L+1。

import java.util.HashSet;
class Solution {
    public String longestDupSubstring(String S) {
		int len = S.length();
        int a = 26;  // 26进制
        long module = (long) Math.pow(2, 32);  // 取一个足够大的数,用以取模
        if(len <= 1)
            return "";
        int[] nums = new int[len];
        for(int i = 0; i < len; i++){
            nums[i] = (int) S.charAt(i) - (int) 'a';  // 只考虑小写字母
        }
        int low = 1;
        int high = len;
        while(low != high) {
        	int L = (high-low)/2 + low;
        	if(search(L, a, module, nums) != -1)
        		low = L + 1;
        	else
        		high = L;
        }
        int start = search(low-1, a, module, nums);
        if(start == -1)
        	return "";
        else
        	return S.substring(start, start+low-1);
    }
	// 返回重复字符串的起始位置
	// 参数:L-重复字符串的长度,a-进制,module-取模数,nums-字符串的编码数组
	public int search(int L, int a, long module, int[] nums) {
		int len = nums.length;
		HashSet<Long> hashSet = new HashSet<Long>(); 
        long tmp = 0;
        long aL = 1;
        for(int j = 0; j < L; j++){
            tmp = (tmp *a + nums[j]) % module;  // 防止溢出
            //System.out.println(tmp);
            aL = (aL*a) % module;
        }
        hashSet.add(tmp);
        for(int j = 1; j <= len - L; j++){  // 长度为i的窗口
            tmp = (tmp*a - nums[j-1]*aL%module + module) % module;  // 去掉前一位
            tmp = (tmp + nums[j+L-1]) % module;
            if(hashSet.contains(tmp))
                return j;
            hashSet.add(tmp);
        }
		return -1;
	}
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐