1. 程式人生 > >字串搜尋演算法總結

字串搜尋演算法總結

因為在網上搜尋hash演算法的知識,無意中又找到一些字串搜尋演算法。 由於之前已經學習過一些搜尋演算法,覺得應該可以歸為一類。因此就寫一篇文章來記錄下學習的過程。 

問題: 

在一長字串中找出其是否包含某子字串。 

首先當然還是簡單演算法,通過遍歷來檢索所有的可能: 

	public static int naiveSearch(String content, String sub) {
		for(int i = 0; i < (content.length() - sub.length() + 1); i++) {
			boolean found = true;
			for(int j = 0 ; j < sub.length(); j++) {
				if(content.charAt(i + j) != sub.charAt(j)) {
					found = false;
					break;
				}
			}
			
			if(found) return i;
		}
		
		return -1;
	}
時間複雜度為 Θ((n-m+1) m)

Rabin–Karp,即hash檢索法: 

	public static int rabinKarp(String content, String sub) {
		long hcontent = rshash(content.substring(0, sub.length()));
		long hsub = rshash(sub);
		
		for(int i = 0; i < (content.length() - sub.length()); i++) {
			//hcontent = rshash(content.substring(i, sub.length() + i));

			if(hsub == hcontent) {
				if(sub.equals(content.substring(i, i + sub.length()))) {
					return i;
				}
			}
			
			hcontent = newhash(content, hcontent, i + 1, sub.length());
		}
		
		return -1;
	}
	
	private static long rshash(String str)  
	{
	   int a = 63689;
	   long hash = 0;
	   
	   for(int i = 0; i < str.length(); i++)
	   {
	      hash += a * str.charAt(i);
	   } 
	   return hash;
	}
	
	private static long newhash(String str, long previous, int i, int length)  
	{
	   int a = 63689;
	   
	   long minHash = str.charAt(i - 1) * a;
	   
	   long plusHash = str.charAt(i + length - 1) * a;
	   
	   return (previous - minHash + plusHash);
	}

這個演算法的核心思想是,通過hash值,我們可以一次匹配一整條字串,速度上要快很多。 
關鍵: 選擇這樣一種hash演算法,使得從前一個hash值到後一個hash值僅需要常量的步驟。 

我這裡實現的hash演算法可以做到這點,但是有效性並不高,應該還有其他的hash演算法可以更好了減少衝突的發生。 

KMP演算法 
KMP演算法說簡單也不簡單,說複雜也不復雜。只要你理解了它的核心思想,程式碼量其實非常少。可是想要解釋它的思想,卻也不是一件容易的事情。 

考慮再三,還是覺得自己無法勝任這個解釋工作,於是找了一篇自己認為解釋KMP演算法比較透徹的文章,翻譯出來,看看大家有沒有更哈的建議。 

KMP algorithm 


http://en.wikipedia.org/wiki/Knuth-Morris-Pratt_algorithm 

例項 
為了解釋該演算法的細節,我們首先利用一個例項來把演算法的步驟過一遍。在這個過程中的任意時間點,該演算法的狀態都由兩個變數來決定, m和i。 m代表在S中,某個對於W(pattern)匹配的起始位置。 i代表在W中的當前正在進行匹配工作的位置。我們來描述一下當演算法開始時的狀態: 
             1         2  
m: 01234567890123456789012 
S: ABC ABCDAB ABCDABCDABDE 
W: ABCDABD 
i: 0123456 

我們首先從0位開始匹配W和S中平行的字串,如果匹配,則前進到下一位。然而當我們到第四步的時候,我們發現S[3]是空格而W[3]=‘D’,出現了第一個不匹配。在傳統的模式匹配演算法中,接下來我們應該從S[1]的位置重新開始匹配。然而,如果我們仔細觀察,在S的0到3位中(也就是我們剛剛進行過匹配成功的位),除了0位,其它都沒有‘A‘出現過。而假設某字串中有和W匹配的子串,那麼這個子串必須是以’A‘開頭的。因此,我們可以確定,S的0到3位中,都不可能存在這樣的子串,於是我們決定從S的4位中重新找起。也就是說,m=4, i=0. 

             1         2  
m: 01234567890123456789012 
S: ABC ABCDAB ABCDABCDABDE 
W:     ABCDABD 
i:     0123456 


此時,我們獲得了一個幾乎匹配的“ABCDAB”,然後在W[6](S[10])這個位置上,我們又出現了一個不匹配。於是,我們應該繼續擴大m的值去尋找下一個可能的匹配。那麼m的下一個值應該設定為多少呢? 

整個演算法的核心就在於此,我們可以從不匹配的位置開始,即m=10,然後這並不是一個正確的選擇,我們發先在S的4到10位中,第8位也是‘A’。因此,如果我們從第十位開始繼續找起的話,有可能就錯過了某個匹配。 

S中第八位的‘A’和第九位的‘B’,分別跟W的第0第1位相匹配。KMP演算法對此的處理是,新的m=8(即首位匹配的值),新的i=2(因為前兩位根據統計,已經是匹配好的了)。然後我們從S的m+i開始匹配W的i位。


             1         2  
m: 01234567890123456789012 
S: ABC ABCDAB ABCDABCDABDE 
W:         ABCDABD 
i:         0123456 

跟第一步類似,我們在i=2就出錯了,下一步我們應該跳轉到哪裡呢?當然是m=11,i=0: 

             1         2  
m: 01234567890123456789012 
S: ABC ABCDAB ABCDABCDABDE 
W:            ABCDABD 
i:            0123456 

此時,我們又在m=17的位置上出錯了,根據第二步的解釋,我們這次跳到m=15而i=2: 

             1         2  
m: 01234567890123456789012 
S: ABC ABCDAB ABCDABCDABDE 
W:                ABCDABD 
i:                0123456 

找到該模式,演算法結束,返回m的值15. 

部分匹配表 
如果我們剛才的分析那樣,整個匹配演算法的核心就在於,當某次匹配過程出現不匹配的值時,如何尋找下一個做匹配的位置(這裡的位置包括兩個概念,即起始位置和我們應該從哪個值開始做匹配)。這樣的值當然是越大越好,因為選擇的下一個匹配位置越大,我們跳過的值就越多,整個演算法就越快。 

如果單看我們之前的分析,好像這個確定下一個匹配位置的工作關係到S和W兩張表。其實不是這樣的,我們只需要對W進行一個預處理,就可以做到這點。 

還是來看,當W是“ABCDABD”這樣一個字串時。我們會發現除了起始位置是‘A’以外,4位的值也是‘A’,那麼如果我們在某次匹配時,匹配到了W的第4位,那麼下一次做匹配查詢時,就應當從W第4位對應的那個字元開始: 
m     123456 
S ... ABCDAX... 
W     ABCDABD 
i     0123456 
因為我們已經知道S[5]和W[4]是匹配的了,那麼其實就不需要再匹配一次了。因此匹配的起始位置是m=5,但是應當從i=1那裡開始進行匹配。 

如果是這樣的一個情況呢? 
m     1234567 
S ... ABCDABX.. 
W     ABCDABD 
i     0123456 
同樣的道理,匹配的起始位置依然是m=5,但是應當從i=2開始匹配。 

下面給出求出當從任一位出現不匹配是,應該從哪裡開始從新匹配的演算法: 

	private static void next(char[] input, int[] table) {
		int pos = 2;
		int cnd = 0;
		
		table[0] = -1;
		table[1] = 0;
		
		while(pos < input.length) {
			if(input[pos - 1] == input[cnd]) {
				table[pos] = cnd + 1;
				pos++;
				cnd++;
			} else if(cnd > 0) {
				cnd = table[cnd];
			} else {
				table[pos] = 0;
				pos++;
			}
		}
	}

既然已經得到了這樣一個表,那麼寫出整個KMP演算法也不是什麼難事了: 
	private static void next(char[] input, int[] table) {
		int pos = 2;
		int cnd = 0;
		
		table[0] = -1;
		table[1] = 0;
		
		while(pos < input.length) {
			if(input[pos - 1] == input[cnd]) {
				table[pos] = cnd + 1;
				pos++;
				cnd++;
			} else if(cnd > 0) {
				cnd = table[cnd];
			} else {
				table[pos] = 0;
				pos++;
			}
		}
	}

最後兩個演算法分別是BM演算法和有限自動機演算法。昨天我花了一天的時間研究BM演算法,對演算法的本質有了一定的瞭解,但是對於如何編碼還是有點困惑。 

決定把這兩個演算法先放一下,在字元搜尋演算法上停留的時間有點長。還是等以後再繼續學習。