最长重复子字符串
”hello world hello goodday"这里的最长重复子串为"hello",如何找到这个最长重复子串呢?
首先来看一个简单的问题:
给定两个字符串,找到它们的最长公共前缀。
acctgttaac和accgttaa的最长公共前缀为acc。暴力解法:将字符串每个起始位置为i的子字符串与另一个起始位置为j的子字符串相比较,记录最长子字符串。
private static int lcp(String s,String t){
int N=Math.min(s.length(),t.length());
for (int i=0;i<N;i++){
if(s.charAt(i)!=t.charAt(i))return i;
}
return N;
}
后缀排序
利用排序算法高效地找出了字符串中的最长重复子字符串:
用Java substring()方法创建一个由字符串s的所有后缀字符串(由字符串的所有位置开始得到的后缀字符串)数组,将该数组排序。原字符串的每个子串都是某个后缀字符串的前缀。排序后最长重复字符串会出现在数组的相邻位置。只需变量一遍,即可在相邻元素中找到最长公共前缀。
给定字符串
如果计算长度为n的字符串S的后缀数组。最朴素的方法直接将所有后缀进行排序,将n个长度为
的字符串进行排序的复杂度为
。
算法基本思想是倍增。先计算每个位置开始的长度为2的子串顺序,再利用这个结果计算长度为4的子串顺序,不断倍增,直到长度大于等于n就得到后缀数组。用S[i,k]表示从位置i开始长度为k的字符串子串。其中,当从位置i开始,剩余字符不足k个时,表示从位置i开始到字符串末尾的子串.
便于理解,将下标放入:
以此类推得到:
可使用Java中的API对后缀数组进行排序。
定位字符串
当需要在大量文本中寻找某个特定的子字符串时,就是在进行一次子字符串查找。搜索时预计计算得到一张索引表,理想情况下最好有一张反向符号索引表将每个字符串和所有含有它的文本关联起来。但这样符号表会很大,内存要求较高。我们可以将这些后缀作为键,创建一个有序数组并使用二分查找来搜索数组,比较被查找的键和所有的后缀。
结构实现
import java.util.*;
public class SuffixArray {
private final String[] suffixes;//后缀数组
private final int N;//字符串和数组的长度
public SuffixArray(String s) {
N = s.length();
suffixes = new String[N];
for (int i = 0; i < N; i++) {
suffixes[i] = s.substring(i);
}
Arrays.sort(suffixes);
}
/**
* @return 数组长度|字符串长度
**/
public int length() {
return N;
}
/**
* @param i 后缀数组索引
* @return 后缀字符串
*/
public String select(int i) {
return suffixes[i];
}
/***
*
* @param i
* @return selcet(i)的索引
*/
public int index(int i) {
return N - suffixes[i].length();
}
/***
*
* @param key
* @return小于键key的后缀数量
*/
public int rank(String key) {
//二分查找
int left = 0, right = N - 1;
while (left <= right) {
int mid = (left + right) >> 1;
int cmp = key.compareTo(suffixes[mid]);
if (cmp < 0) {
right = mid - 1;
} else if (cmp > 0) {
left = mid + 1;
} else {
return mid;
}
}
return left;
}
/***
*
* @param s
* @param t
* @return两个字符串的最长公共前缀
*/
public int lcp(String s, String t) {
int N = Math.min(s.length(), t.length());
for (int i = 0; i < N; i++) {
if (s.charAt(i) != t.charAt(i)) return i;
}
return N;
}
/***
*
* @param i
* @return 后缀数组相邻元素suffixes[i]与suffixes[i-1]的最长公共前缀
*/
public int lcp(int i) {
return lcp(suffixes[i], suffixes[i - 1]);
}
}
你也可以不使用这样的API,直接按照思想理解自己书写模板。
实现效果
public class Main {
public static void main(String[] args) {
String text = "abracadabra";
int N = text.length();
SuffixArray sa = new SuffixArray(text);//创建后缀数组
String lrs = "";//最长重复子串
for (int i = 1; i < N; i++) {
int len = sa.lcp(i);
if (len > lrs.length()) {
lrs = sa.select(i).substring(0, len);
}
}
System.out.println("最长重复子串为" + lrs);
}
}
基于后缀数组的字符串匹配算法
判断字符串s1是否是s的子串。
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws IOException {
Scanner br = new Scanner(new BufferedInputStream(System.in));
while (br.hasNext()) {
String s = br.nextLine(), s1 = br.nextLine();
boolean f = false;
SuffixArray sa = new SuffixArray(s);
//在后缀数组中比s1小的
int index = sa.rank(s1);
int len = s1.length();
//从index开始比较
for (int i = index; i < sa.length(); i++) {
if (sa.select(i).length() < s1.length()) {
continue;
}
if (sa.select(i).substring(0, len).equals(s1)) {
f = true;
break;
}
}
if (f) {
System.out.println(s1 + " 是 " + s + " 的子串 ");
} else {
System.out.println(s1 + " 不是 " + s + " 的子串 ");
}
}
}
}
实现效果: