1. 程式人生 > >循序漸進,深入理解KMP演算法

循序漸進,深入理解KMP演算法

KMP演算法是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同時發現的。其中第一位就是《計算機程式設計藝術》的作者!

KMP演算法要解決的問題就是在字串(也叫主串)中的模式(pattern)定位問題。說簡單點就是我們平時常說的關鍵字搜尋。模式串就是關鍵字(接下來稱它為P),如果它在一個主串(接下來稱為T)中出現,就返回它的具體位置,否則返回-1(常用手段)。

常規的暴力匹配演算法時間複雜度:O(m*n),而KMP演算法目的是儘快解決字串匹配問題,時間複雜度為O(m+n)。

1、暴力匹配演算法(腦補一下就好,不用看的)

首先,對於這個問題有一個很單純的想法:從左到右一個個匹配,如果這個過程中有某個字元不匹配,就跳回去,將模式串向右移動一位。這有什麼難的?

我們可以這樣初始化:

之後我們只需要比較i指標指向的字元和j指標指向的字元是否一致。如果一致就都向後移動,如果不一致,如下圖:

A和E不相等,那就把i指標移回第1位(假設下標從0開始),j移動到模式串的第0位,然後又重新開始這個步驟:

程式碼:

public class Demo{
	public static int bf(String ts,String ps) {
		char[] t = ts.toCharArray();
		char[] p = ps.toCharArray();
		int i = 0; // 主串的位置
		int j = 0; // 模式串的位置
		while (i < t.length && j < p.length) {
		    if (t[i] == p[j]) { // 當兩個字元相同,就比較下一個
		    		i++;
		    		j++;
		    }else {
		    		i = i - j + 1; // 一旦不匹配,i後退
		    		j = 0; // j歸0
		    }
	    }
		if (j == p.length) {
			return i - j;
	     }else {
	        return -1;
	     } 
	    }
}

2、KMP演算法(重點)

演算法的改進之處在於:每當一趟匹配過程中出現字元比較不相等時,指向主串的指標 i 不倒退,而是利用已經得到的“部分匹配”的結果,將模式串向右滑動儘可能遠的一段距離後,繼續比較。

例如:主串S:"acacbacbabca",模式串P:"acbab"

只進行了三趟比較,就匹配上了,是不是很厲害。我們可以注意到整個過程 i 是不往回走的,所以時間複雜度為O(n+m)。

使用這個演算法,需要解決兩個問題:

  1. 比較不成功時,模式串T向右滑動多少距離
  2. 滑動之後,模式串T的工作指標 j 的值

為了解決這兩個問題,我們需要引入一個數組PMT

我先解釋一下字串的字首和字尾。如果字串A和B,存在A=BS,其中S是任意的非空字串,那就稱B為A的字首。例如,”Harry”的字首包括{”H”, ”Ha”, ”Har”, ”Harr”},我們把所有字首組成的集合,稱為字串的字首集合。同樣可以定義字尾A=SB, 其中S是任意的非空字串,那就稱B為A的字尾,例如,”Potter”的字尾包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然後把所有後綴組成的集合,稱為字串的字尾集合。要注意的是,字串本身並不是自己的字尾。

有了這個定義,就可以說明PMT中的值的意義了。PMT中的值是字串的字首集合與字尾集合的交集中最長元素的長度。例如,對於”aba”,它的字首集合為{”a”, ”ab”},字尾 集合為{”ba”, ”a”}。兩個集合的交集為{”a”},那麼長度最長的元素就是字串”a”了,長 度為1,所以對於”aba”而言,它在PMT表中對應的值就是1。再比如,對於字串”ababa”,它的字首集合為{”a”, ”ab”, ”aba”, ”abab”},它的字尾集合為{”baba”, ”aba”, ”ba”, ”a”}, 兩個集合的交集為{”a”, ”aba”},其中最長的元素為”aba”,長度為3。

例如index=4時,考察字串”ababa”的最大相等k字首和k字尾,最大值為3:

字首串

字尾串

a

a

ab

ba

aba

aba

abab

baba

但是一般在程式設計中不用PMT,而是用next陣列計算。

為了程式設計的方便, 我們不直接使用PMT陣列,而是將PMT陣列向後偏移一位。我們把新得到的這個陣列稱為next陣列。下面給出根據next陣列進行字串匹配加速的字串匹配程式。其中要注意的一個技巧是,在把PMT進行向右偏移時,第0位的值,我們將其設成了-1,這只是為了程式設計的方便,並沒有其他的意義。

next[j]表示: 前j-1位,字串的字首集合與字尾集合的交集中最長元素的長度。

 

我們來通過例子,觀察next陣列是如何解決,之前的兩個問題的:

  1. 比較不成功時,模式串T向右滑動多少距離
  2. 滑動之後,模式串T的工作指標 j 的值

當發生不相等的情況時( S[i] != T[j] ):此時next[6]=4,這個時候讓 S[6]要和T[4]比較(j = next[j]):

 KMP程式碼:

	//返回第一個匹配上的起始位置
	public  static int KMP(String text, String pattern) {
		int i = 0,j = 0;
		while(i<text.length() && j<pattern.length()) {
			if(j==-1 || text.charAt(i)==pattern.charAt(j)) {//匹配上了就繼續往下匹配
				++i;
				++j;
			}else
				j = next[j];//發生不相等的情況
		}
		
		if(j>=pattern.length())return i-pattern.length();//return i-j;
		return -1;
	}

 

那麼next陣列如何計算呢?我們可以將它看成是兩個相同串的匹配,初始位置差1位

next[j]表示: 前j-1位,字串的字首集合與字尾集合的交集中最長元素的長度。

next[2]算的是第0,1位,字首集合與字尾集合的交集中最長元素的長度。

next陣列計算程式碼,和KMP程式碼很像:

	public  static void getNext(String p, int []next)
	{
		next[0] = -1;
		int i = 0, j = -1;
		while (i < p.length())
		{
			if (j == -1 || p.charAt(i) == p.charAt(j))
			{
				++i;
				++j;
				next[i] = j;
			}	
			else
				j = next[j];
		}
	}

彙總兩部分的程式碼:

public class KMP {
	public  static void getNext(String p, int []next)
	{
		next[0] = -1;
		int i = 0, j = -1;
		while (i < p.length())
		{
			if (j == -1 || p.charAt(i) == p.charAt(j))
			{
				++i;
				++j;
				next[i] = j;
			}	
			else
				j = next[j];
		}
	}
	//返回第一個匹配上的起始位置
	public  static int KMP(String text, String pattern , int next[]) {
		int i = 0,j = 0;
		while(i<text.length() && j<pattern.length()) {
			if(j==-1 || text.charAt(i)==pattern.charAt(j)) {
				++i;
				++j;
			}else
				j = next[j];
		}
		
		if(j>=pattern.length())return i-pattern.length();//return i-j;
		return -1;
	}
	
	
	public static void print(int []a) {
		for(int i = 0 ; i < a.length ; i++) {
			System.out.print(a[i]+" ");
		}
	}
	public static void main(String[] args) {
		String s = "abcabcabdabba";
		String t = "abcabd";
		int a[] = new int[s.length()+1];
		getNext(s ,  a);
		int index = KMP(s,t,a);//返回第一個匹配的位置
		System.out.print(index);
	}
}

參考資料: