【戀上數據結構】串匹配算法(蠻力匹配、KMP【重點】、Boyer-Moore、Karp-Rabin、Sunday)

數據結構與算法筆記戀上數據結構筆記目錄

串(前綴、後綴)

本課程研究的是開發中非常熟悉的字符串,是由若干個字符組成的有限序列
在這裏插入圖片描述
字符串 thank前綴(prefix)、真前綴(proper prefix)、後綴(suffix)、真後綴(proper suffix)
在這裏插入圖片描述

串匹配算法

  • 查找一個模式串(pattern)在文本串(text)中的位置:
String text = "Hello World";
String pattern = "or";
text.indexOf(pattern); // 7
text.indexOf(pattern); // -1

幾個經典的串匹配算法:

  • 蠻力(Brute Force)
  • KMP
  • Boyer-Moore
  • Karp-Rabin / Rabin-Karp
  • Sunday

下面用 tlen 代表文本串 text 的長度,plen 代表模式串 pattern 的長度;

蠻力(Brute Force)

  • 以字符爲單位,從左到右移動模式串,直到匹配成功 ;
    在這裏插入圖片描述
    蠻力算法有 2 種常見實現思路:

蠻力1 – 執行過程 + 實現

在這裏插入圖片描述
在這裏插入圖片描述

/**
 * 蠻力匹配
 */
public static int indexOf(String text, String pattern) {
	if (text == null || pattern == null) return -1;
	char[] textChars = text.toCharArray();
	int tlen = textChars.length;
	char[] patternChars = pattern.toCharArray();
	int plen = patternChars.length;
	if (tlen == 0 || plen == 0 || tlen < plen) return -1;
	
	int pi = 0, ti = 0;
	while (pi < plen && ti < tlen) {
		if (textChars[ti] == patternChars[pi]) {
			ti++;
			pi++;
		} else {
			ti = ti - pi + 1;
			// ti -= pi - 1;
			pi = 0;
		}
	} 
	return pi == plen ? ti - pi : -1;
}

蠻力1 – 優化

在這裏插入圖片描述

/**
 * 蠻力匹配 - 改進
 */
public static int indexOf(String text, String pattern) {
	if (text == null || pattern == null) return -1;
	char[] textChars = text.toCharArray();
	int tlen = textChars.length;
	char[] patternChars = pattern.toCharArray();
	int plen = patternChars.length;
	if (tlen == 0 || plen == 0 || tlen < plen) return -1;
	
	int pi = 0, ti = 0;
	while (pi < plen && ti - pi <= tlen - plen) { // ti - pi <= tlen - plen 是關鍵
		if (textChars[ti] == patternChars[pi]) {
			ti++;
			pi++;
		} else {
			ti = ti - pi + 1;
			// ti -= pi - 1;
			pi = 0;
		}
	}
	return pi == plen ? ti - pi : -1;
}

蠻力2 – 執行過程 + 實現

在這裏插入圖片描述
在這裏插入圖片描述

/**
 * 蠻力匹配2
 */
public static int indexOf(String text, String pattern) {
	if (text == null || pattern == null) return -1;
	char[] textChars = text.toCharArray();
	int tlen = textChars.length;
	char[] patternChars = pattern.toCharArray();
	int plen = patternChars.length;
	if (tlen == 0 || plen == 0 || tlen < plen) return -1;
	
	// 如果模式串的頭在 tlen - plen 後面, 必然會匹配失敗
	int tiMax = tlen - plen;
	for (int ti = 0; ti <= tiMax; ti++) {
		int pi = 0;
		for (; pi < plen; pi++) {
			if (textChars[ti + pi] != patternChars[pi]) break;
		}
		if (pi == plen) return ti;
	}
	return -1;
}

蠻力 – 性能分析

在這裏插入圖片描述
最好情況

  • 只需一輪比較就完全匹配成功,比較 m 次( m 是模式串的長度)
  • 時間複雜度爲 O(m)
    在這裏插入圖片描述

最壞情況(字符集越大,出現概率越低):

  • 執行了 n – m + 1 輪比較( n 是文本串的長度)
  • 每輪都比較至模式串的末字符後失敗( m – 1 次成功,1 次失敗)
  • 時間複雜度爲 O(m ∗ (n − m + 1)),由於一般 m 遠小於 n,所以爲 O(mn)
    在這裏插入圖片描述

KMP

KMP 是 Knuth–Morris–Pratt 的簡稱(取名自3位發明人的名字),於1977年發佈
在這裏插入圖片描述

蠻力 vs KMP

首先大概瞭解一下兩者的差距:

蠻力算法:會經歷很多次沒有必要的比較。
在這裏插入圖片描述
KMP算法:充分利用了此前比較過的內容,可以很聰明地跳過一些不必要的比較位置。
在這裏插入圖片描述

KMP – next表的使用

KMP 會預先根據模式串的內容生成一張 next 表(一般是個數組):
在這裏插入圖片描述
例如,下圖串匹配時, pi = 7 時 失配

  • 根據失配的索引 7 查表,查到的元素索引爲 3
    next[pi] == 3
  • 將模式串索引爲 3 的元素移動到失配的位置
    pi = next[pi]; // pi = 3
  • 向右移動的距離爲 p - next[pi]

在這裏插入圖片描述

再比如:pi = 3 時失配, next[3] = 0,將 0 位置的元素移到失配處:pi = next[pi]
在這裏插入圖片描述

KMP – 核心原理(構造next表)

在這裏插入圖片描述
d、e 失配時,如果希望 pattern 能夠一次性向右移動一大段距離,然後直接比較 d、c 字符

  • 前提條件是 A 必須等於 B

所以 KMP 必須在失配字符 e 左邊的子串中找出符合條件的 A、B,從而得知向右移動的距離

向右移動的距離:e左邊子串的長度 – A的長度,等價於:e的索引 – c的索引
c的索引 == next[e的索引],所以向右移動的距離:e的索引 – next[e的索引]

總結:

  • 如果在 pi 位置失配,向右移動的距離是 pi – next[pi],所以 next[pi] 越小,移動距離越大
  • next[pi] 是 pi 左邊子串的真前綴後綴的最大公共子串長度

真前綴後綴的最大公共子串長度

如何求 真前綴後綴的最大公共子串長度
在這裏插入圖片描述

構造 next 表

根據最大公共子串長度得到 next 表:
在這裏插入圖片描述

-1的精妙之處

爲什麼要將首字符設置爲 - 1?
在這裏插入圖片描述

KMP – 主算法代碼實現

在這裏插入圖片描述

KMP – 爲什麼是“最大“公共子串長度?

假設文本串AAAAABCDEF模式串AAAAB
在這裏插入圖片描述
在這裏插入圖片描述

KMP – next表的構造思路及實現

在這裏插入圖片描述
已知 next[i] == n

① 如果 pattern.charAt(i) == pattern.charAt(n)

  • 那麼 next[i + 1] == n + 1

② 如果 pattern.charAt(i) != pattern.charAt(n)

  • 已知 next[n] == k
  • 如果 pattern.charAt(i) == pattern.charAt(k)
    那麼 next[i + 1] == k + 1
  • 如果 pattern.charAt(i) != pattern.charAt(k)
    將 k 代入 n ,重複執行 ②

構造 next 表 代碼實現:

private static int[] next(String pattern) {
	char[] chars = pattern.toCharArray();
	int[] next = new int [chars.length];
	
	next[0] = -1;
	int i = 0;
	int n = -1;
	int iMax = chars.length - 1;
	while (i < iMax) {
		if (n < 0 || chars[i] == chars[n]) {
			next[++i] = ++n;
		} else {
			n = next[n];
		}
	}
	return next;
}

KMP – next表的不足之處

假設文本串是 AAABAAAAB,模式串是 AAAAB
在這裏插入圖片描述
在這裏插入圖片描述

KMP – next表的優化思路及實現

在這裏插入圖片描述

  • 如果 pattern[i] != d,就讓模式串滑動到 next[i](也就是n)位置跟 d 進行比較;
  • 如果 pattern[n] != d,就讓模式串滑動到 next[n](也就是k)位置跟 d 進行比較;
  • 如果 pattern[i] == pattern[n],那麼當 i 位置失配時,
    模式串最終必然會滑到 k 位置跟 d 進行比較,
    所以 next[i] 直接存儲 next[n](也就是k)即可;

next 表 的優化代碼實現:

private static int[] next(String pattern) {
	char[] chars = pattern.toCharArray();
	int[] next = new int [chars.length];
	
	next[0] = -1;
	int i = 0;
	int n = -1;
	int iMax = chars.length - 1;
	while (i < iMax) {
		if (n < 0 || chars[i] == chars[n]) { 
			// 優化
			++i;
			++n;
			if (chars[i] == chars[n]) {
				next[i] = next[n];
			} else {
				next[i] = n;
			}
		} else {
			n = next[n];
		}
	}
	return next;
}

KMP – next表的優化效果

在這裏插入圖片描述

KMP – 性能分析

KMP 主邏輯:

  • 最好時間複雜度:O(m)
  • 最壞時間複雜度:O(n),不超過O(2n)

next 表的構造過程跟 KMP 主體邏輯類似:

  • 時間複雜度:O(m)

KMP 整體:

  • 最好時間複雜度:O(m)
  • 最壞時間複雜度:O(n + m)
  • 空間複雜度: O(m)

在這裏插入圖片描述

KMP完整源碼

public class KMP {
	public static int indexOf(String text, String pattern) {
		// 檢測數據合法性
		if (text == null || pattern == null) return -1;
		char[] textChars = text.toCharArray();
		int tlen = textChars.length;
		char[] patternChars = pattern.toCharArray();
		int plen = patternChars.length;
		if (tlen == 0 || plen == 0 || tlen < plen) return -1;
		
		// next表
		int[] next = next(pattern);
		
		int pi = 0, ti = 0;
		while (pi < plen && ti < tlen) {
			// next表置-1的精妙之處, pi = -1 則 pi = 0, ti++ 相當於模式串後一一位
			if (pi < 0 || textChars[ti] == patternChars[pi]) {
				ti++;
				pi++;
			} else {
				pi = next[pi];
			}
		} 
		return pi == plen ? ti - pi : -1;
	}
	
	/**
	 * next 表構造 - 優化
	 */
	private static int[] next(String pattern) {
		char[] chars = pattern.toCharArray();
		int[] next = new int [chars.length];
		
		next[0] = -1;
		int i = 0;
		int n = -1;
		int iMax = chars.length - 1;
		while (i < iMax) {
			if (n < 0 || chars[i] == chars[n]) {
				++i;
				++n;

				if (chars[i] == chars[n]) {
					next[i] = next[n];
				} else {
					next[i] = n;
				}
			} else {
				n = next[n];
			}
		}
		return next;
	}
	
	/**
	 * next表構造
	 */
	private static int[] next2(String pattern) {
		char[] chars = pattern.toCharArray();
		int[] next = new int [chars.length];
		
		next[0] = -1;
		int i = 0;
		int n = -1;
		int iMax = chars.length - 1;
		while (i < iMax) {
			if (n < 0 || chars[i] == chars[n]) {
				next[++i] = ++n;
			} else {
				n = next[n];
			}
		}
		return next;
	}
	
}

蠻力 vs KMP

蠻力算法爲何低效?

當字符失配時:

  • 蠻力算法
    ti 回溯到左邊位置
    pi 回溯到 0
  • KMP 算法
    ti 不必回溯
    pi 不一定要回溯到0

Boyer-Moore

Boyer-Moore 算法,簡稱 BM 算法,由 Robert S.Boyer 和 J Strother Moore 於 1977 年發明;

  • 最好時間複雜度:O(n / m),最壞時間複雜度:O(n + m)
  • 該算法從模式串的尾部開始匹配(自後向前

BM 算法的移動字符數是通過 2 條規則計算出的最大值:

  • 壞字符規則(Bad Character,簡稱 BC)
  • 好後綴規則(Good Suffix,簡稱 GS)

壞字符規則(Bad Character)

在這裏插入圖片描述

當 Pattern 中的字符 E 和 Text 中的 S 失配時,稱 S壞字符

  • 如果 Pattern 的未匹配子串中不存在壞字符,直接將 Pattern 移動到壞字符的下一位
  • 否則,讓 Pattern 的未匹配子串中最靠右的壞字符與 Text 中的壞字符對齊

好後綴規則(Good Suffix)

在這裏插入圖片描述
MPLE 是一個成功匹配的後綴,ELEPLEMPLE 都是 好後綴

  • 如果 Pattern 中找不到與好後綴對齊的子串,直接將 Pattern 移動到好後綴的下一位
  • 否則,從 Pattern 中找出子串與 Text 中的好後綴對齊

BM算法最好情況與最壞情況

最好情況,時間複雜度:O(n / m);
在這裏插入圖片描述
最壞情況,時間複雜度:O(n + m) ,其中 O(m) 爲構造表的時間;
在這裏插入圖片描述

Karp-Rabin / Rabin-Kary

Rabin-Karp 算法(或 Karp-Rabin 算法),簡稱 RK 算法,是一種 基於hash 的字符串匹配算法

  • 由 Richard M.Karp 和 Michael O.Rabin 於 1987 年發明

大致原理:

  • 將 Pattern 的 hash 值與 Text 中每個子串的 hash 值進行比較
  • 某一子串的 hash 值可以根據上一子串的 hash 值在 O(1) 時間內計算出來

Sunday

Sunday 算法由 Daniel M.Sunday 在 1990 年提出,它的思想跟 BM算法 很相似

  • 從前向後匹配(BM算法是從後往前)
  • 當匹配失敗時,關注的是 Text 中參與匹配的子串的下一位字符 A
    如果 A 沒有在 Pattern 中出現,則直接跳過,即 移動位數 = Pattern長度 + 1
    否則,讓 Pattern 中最靠右的 A 與 Text 中的 A 對齊

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章