后缀数组原理以及实现

后缀数组可以解决后缀排序,字符串查找以及最长重复子串等问题。

给定一个字符串求出其中最大的重复子串,例如s="it was the best time it was" 返回 “it was”;

求出两个字符串的最大公共前缀

从两个字符串起始位置开始遍历比较,直到两个子串某个位置字符不一致,时间复杂度O(N)

        //两个字符串最大公共前缀
	private static int lcp(String s, String t){
		int min = Math.min(s.length(), t.length());
		for(int i=0; i<min; i++){
			if(s.charAt(i)!=t.charAt(i))
				return i;
		}
		return min;
	}

暴力方法求解最大重复子串

利用双层循环,遍历所有子串对的所有情况,对于每一对子串都使用lcp来求解最大公共前缀,时间复杂度为O(N3)

        public String forcelrs(String s){
		String lrs = "";
		for(int i=0; i<s.length(); i++){
			for(int j=i+1; j<s.length(); j++){
				int l = lcp(s.substring(i), s.substring(j));
				if(l>lrs.length()){
					lrs = s.substring(0, l);
				}
			}
		}
		return lrs;
	}

利用后缀数组求解最大重复子串

输入字符串s,获取s的所有后缀子串,将s所有后缀子串排序(排序之后最大重复子串必定相邻),然后依次比较相邻后缀子串的最大公共前缀,可以得到最终的最大重复子串。

在此定义一种数据结构后缀数组来处理字符串,将字符串的后缀子串进行排序。

一个长度为N的字符串,后缀子串有N种,可以利用String substring方法来求解。

后缀数组API

SuffixArray(String s)  有一个字符串构造后缀数组   获取该字符串所有后缀 排序

int length()  返回字符串后缀数组大小

String select(int i)  返回后缀数组中索引为i的子字符串

int index(int i)  返回后缀数组中索引为i的子字符串在原始字符串中的起始位置

int lcp(int i)  返回后缀数组中i以及i-1字符串的最大公共前缀

例如输入s="it was the best time it was"

构造的后缀数组为:

 best time it was
 it was
 the best time it was
 time it was
 was
 was the best time it was
as
as the best time it was
best time it was
e best time it was
e it was
est time it was
he best time it was
ime it was
it was
it was the best time it was
me it was
s
s the best time it was
st time it was
t time it was
t was
t was the best time it was
the best time it was
time it was
was
was the best time it was
it was

将该后缀数组遍历一遍,依次比较相邻两个子字符串的最大公共前缀,即可得字符串的最大重复子串

                String str = "it was the best time it was";
		//求字符串的最大重复子串
		SuffixArray sa = new SuffixArray(str);
		String lrs = "";
		for(int i=1; i<sa.N; i++){
			if(sa.lcp(i) > lrs.length())
				lrs = sa.select(i).substring(0, sa.lcp(i));
		}
		System.out.println(lrs);

由以上可知,该算法的时间复杂度为O(N2)

除此以外,后缀数组能够解决字符串的查找问题,在一段文本中需要查找某一特定字符串,可以先将文本处理得到后缀数组,此时所有的子字符串是有序的,可以使用二分查找来寻找待检索的字符串。

         //返回某个后缀字符串key在后缀数组中的索引值
	//由于后缀数组已经有序  可以使用二分查找
	public int rank(String key){
		int low = 0; 
		int high = N-1;
		while(low<=high){
			int mid = (low + high) / 2;
			int cmp = key.compareTo(suffixs[mid]);
			if(cmp==0) return mid;
			else if(cmp<0) high = mid-1;
			else low = mid + 1;
		}
		return -1;
	}

全部实现代码如下:

    public class SuffixArray {
	
	public static void main(String[] args) {
		String str = "it was the best time it was";
		//求字符串的最大重复子串
		SuffixArray sa = new SuffixArray(str);
		String lrs = "";
		for(int i=1; i<sa.N; i++){
			if(sa.lcp(i) > lrs.length())
				lrs = sa.select(i).substring(0, sa.lcp(i));
		}
		System.out.println(lrs);
		//System.out.println(sa.forcelrs(str));
	}
	
	private final String[] suffixs;
	private final int N;
	
	public SuffixArray(String s){
		N = s.length();
		suffixs = new String[N];
		for(int i=0; i<N; i++){
			suffixs[i] = s.substring(i);
		}
		System.out.println(Arrays.toString(suffixs));
		Arrays.sort(suffixs);
		System.out.println(Arrays.toString(suffixs));
	}

	//返回某个后缀字符串key在后缀数组中的索引值
	//由于后缀数组已经有序  可以使用二分查找
	public int rank(String key){
		int low = 0; 
		int high = N-1;
		while(low<=high){
			int mid = (low + high) / 2;
			int cmp = key.compareTo(suffixs[mid]);
			if(cmp==0) return mid;
			else if(cmp<0) high = mid-1;
			else low = mid + 1;
		}
		return -1;
	}
	
	public int length(){
		return N;
	}
	
	//后缀索引中i位置字符串
	public String select(int i){
		return suffixs[i];
	}
	
	//后缀索引中i位置字符串在原始字符串中的索引
	public int index(int i){
		return N - suffixs[i].length();
	}
	
	//一个字符串后缀数组索引位置i与前一个位置子字符串的最长重复子串
	public int lcp(int i){
		return lcp(suffixs[i], suffixs[i-1]);
	}
	
	//两个字符串最大公共前缀
	private static int lcp(String s, String t){
		int min = Math.min(s.length(), t.length());
		for(int i=0; i<min; i++){
			if(s.charAt(i)!=t.charAt(i))
				return i;
		}
		return min;
	}
}

上述后缀数组实现需要大量的空间O(N)以及如果所有字符串都相等,则字符串的排序效率过低以及最长重复子串会变为子字符串长度平方级别,因此实现效率不高。

优化后缀数组实现

在空间上,不需要存储每一个后缀字符串。通过创建一个索引数组index来表示字符串在后缀数组中的顺序信息。

核心变量

       //字符串字符存储
	private final char[] text;
	
	//index[j]=i  表示text.substring(j)是第i大子串
	private final int[] index;  
	
	//字符串长度
	private final int n;

存储字符串的每一个字符,保存字符串长度以及保存字符串后缀子串的排序位置;

其中index[j]=i表示text.substring(j)后缀子串在整个后缀数组中排序索引为i,即第i大的子串;

由此可以不需要保存字符串的每一个后缀子串。

后缀数组的构造

给定字符串,将字符串转化为字符数组,然后利用三向快速排序,将字符串所有后缀索引排序

        public SuffixArrayX(String text){
		n = text.length();
		text = text + '\0';
		this.text = text.toCharArray();
		this.index = new int[n];
		for(int i=0; i<n; i++){
			index[i] = i;
		}
		//System.out.println(this.text);
		//System.out.println(Arrays.toString(this.index));
		//三向快速排序
		sort(0, n-1, 0);
		//System.out.println(Arrays.toString(this.index));
		//for(int i=0; i<n; i++){
			//System.out.println(select(i));
		//}
	}

三向快速排序的实现

如果数组元素个数小于一定阈值,则使用插入排序来处理。

其余使用三向快速排序,避免元素大量相同时,快速排序效率过低。

        //三向快速排序
	private void sort(int lo, int hi, int d) {
		//如果数组较小使用插入排序
		if(hi<=lo+CUTOFF){
			insertion(lo, hi, d);
			return;
		}
		int lt = lo;
		int gt = hi;
		char v = text[index[lo]+d];
		int i = lo + 1;
		while(i<gt){
			char t = text[index[i]+d];
			//交换后i指向lt值,此时可以向前移动一个位置
			if(t<v) exch(lt++, i++);
			//交换之后i位置值为gt值,其大小需要与t进行比较,因此i不需要移动
			else if(t>v) exch(i, gt--);
			else i++;
		}
		// a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]. 
		sort(lo, lt-1, d);
		if(v>0) sort(lt, gt, d+1);
		sort(gt+1, hi, d);
	}

	// sort from a[lo] to a[hi], starting at the dth character
	private void insertion(int lo, int hi, int d) {
		for(int i=lo; i<=hi; i++){
			for(int j=i; j>lo&&less(index[j], index[j-1], d); j--){
				exch(j, j-1);
			}
		}	
	}

	private void exch(int j, int i) {
		int tmp = index[i];
		index[i] = index[j];
		index[j] = tmp;
	}

	//is text[i+d..n) < text[j+d..n) ?
	private boolean less(int i, int j, int d) {
		if(i==j) return false;
		i = i + d;
		j = j + d;
		while(i<n &&j<n){
			if(text[i]<text[j]) return true;
			if(text[i]>text[j]) return false;
			i++;
			j++;
		}
		return i > j;
	}

完整代码实现:

    public class SuffixArrayX {
	
	public static void main(String[] args) {
		String str = "it was the best time it was";
		//求字符串的最大重复子串
		SuffixArrayX sax = new SuffixArrayX(str);
		String lrs = "";
		for(int i=1; i<sax.n; i++){
			System.out.println(sax.lcp(i) + "  " + lrs.length());
			if(sax.lcp(i) > lrs.length()){	
				lrs = sax.select(i).substring(0, sax.lcp(i));
				System.out.println("lrs = " + lrs);
			}
			//System.out.println("lrs = " + lrs);
		}
		System.out.println("lrs = " + lrs);
		System.out.println(lrs);
	}
	
	private static final int CUTOFF = 5;
	
	//字符串字符存储
	private final char[] text;
	
	//index[j]=i  表示text.substring(j)是第i大子串
	private final int[] index;  
	
	//字符串长度
	private final int n;
	
	public SuffixArrayX(String text){
		n = text.length();
		text = text + '\0';
		this.text = text.toCharArray();
		this.index = new int[n];
		for(int i=0; i<n; i++){
			index[i] = i;
		}
		//System.out.println(this.text);
		//System.out.println(Arrays.toString(this.index));
		//三向快速排序
		sort(0, n-1, 0);
		//System.out.println(Arrays.toString(this.index));
		//for(int i=0; i<n; i++){
			//System.out.println(select(i));
		//}
	}

	//三向快速排序
	private void sort(int lo, int hi, int d) {
		//如果数组较小使用插入排序
		if(hi<=lo+CUTOFF){
			insertion(lo, hi, d);
			return;
		}
		int lt = lo;
		int gt = hi;
		char v = text[index[lo]+d];
		int i = lo + 1;
		while(i<gt){
			char t = text[index[i]+d];
			//交换后i指向lt值,此时可以向前移动一个位置
			if(t<v) exch(lt++, i++);
			//交换之后i位置值为gt值,其大小需要与t进行比较,因此i不需要移动
			else if(t>v) exch(i, gt--);
			else i++;
		}
		// a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]. 
		sort(lo, lt-1, d);
		if(v>0) sort(lt, gt, d+1);
		sort(gt+1, hi, d);
	}

	// sort from a[lo] to a[hi], starting at the dth character
	private void insertion(int lo, int hi, int d) {
		for(int i=lo; i<=hi; i++){
			for(int j=i; j>lo&&less(index[j], index[j-1], d); j--){
				exch(j, j-1);
			}
		}	
	}

	private void exch(int j, int i) {
		int tmp = index[i];
		index[i] = index[j];
		index[j] = tmp;
	}

	//is text[i+d..n) < text[j+d..n) ?
	private boolean less(int i, int j, int d) {
		if(i==j) return false;
		i = i + d;
		j = j + d;
		while(i<n &&j<n){
			if(text[i]<text[j]) return true;
			if(text[i]>text[j]) return false;
			i++;
			j++;
		}
		return i > j;
	}
	
	public int length(){
		return n;
	}
	
	//第i大子字符串在原始字符串的起始位置
	public int index(int i){
		return index[i];
	}
	
	public int lcp(int i){
		return lcp(index[i], index[i-1]);
	}
	
	//返回text从i,j位置开始的最大公共前缀
	private int lcp(int i, int j){
		int length = 0;
		while(i<n && j<n){
			//System.out.println(text[i] + " " + text[j]);
			//System.out.println(i + " " + j + " " + select(i) + "                 " + select(j) + "----" + text[i] + " " + text[j]);
			if(text[i]!=text[j]) return length;
			i++;
			j++;
			length++;
		}
		return length;
	}
	
	public String select(int i){
		return new String(text, index[i], n-index[i]);
	}
	
	public int rank(String query) {
        int lo = 0, hi = n - 1;
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            int cmp = compare(query, index[mid]);
            if      (cmp < 0) hi = mid - 1;
            else if (cmp > 0) lo = mid + 1;
            else return mid;
        }
        return lo;
    } 

    // is query < text[i..n) ?
    private int compare(String query, int i) {
        int m = query.length();
        int j = 0;
        while (i < n && j < m) {
            if (query.charAt(j) != text[i]) return query.charAt(j) - text[i];
            i++;
            j++;
        }
        if (i < n) return -1;
        if (j < m) return +1;
        return 0;
    }

}

https://algs4.cs.princeton.edu/code/edu/princeton/cs/algs4/SuffixArray.java.html

https://algs4.cs.princeton.edu/code/edu/princeton/cs/algs4/SuffixArrayX.java.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值