每天資訊Java資料結構與演算法——字串匹配相關演算法

菜單

Java資料結構與演算法——字串匹配相關演算法

Java裡用的是indexOf函式,其底層就是字串匹配演算法。其中字串匹配演算法主要包括:

Java資料結構與演算法——字串匹配相關演算法

1。 BF(暴力匹配)演算法

1。1概念和原理

Brute Force叫作暴力匹配演算法,也叫樸素匹配演算法。其主要實現原理就是 在主串中,檢查起始位置分別是 0、1、2…n-m 且長度為 m 的 n-m+1 個子串,看有沒有跟模式串匹配的。

Java資料結構與演算法——字串匹配相關演算法

1。2 實現程式碼

public class BFAlth {

public static boolean isMatch(String str,String sub){

for (int i=0;i<=(str。length()-sub。length());i++){

// 實際實現是每個字串做字元比較 ,需要迴圈子串

if(str。substring(i,i+sub。length())。equals(sub)){

return true;

}

}

return false;

}

public static void main(String[] args) {

System。out。println(isMatch(“hello”,“he”));

System。out。println(isMatch(“hello”,“el”));

System。out。println(isMatch(“hello”,“lo”));

}

}

時間複雜度:每次都比對 m 個字元,要比對 n-m+1 次,所以,這種演算法的最壞情況時間複雜度是 O(n*m) ,其中m為匹配串長度 ,n為主串長度。(注意equals方法也是遍歷字元陣列,所以此處的 時間複雜度是O(n*m))

2。 RK演算法

2。1概念和原理

RK 演算法的全稱叫 Rabin-Karp 演算法,是由它的兩位發明者 Rabin 和 Karp 的名字來命名的。

實現思路:透過雜湊演算法對

主串中的 n-m+1 個子串分別求雜湊值

,然後逐個與

模式串的雜湊值

比較大小。

可以自己根據進位制設計一個演算法,比如數123=1*10^2+2*10^1+3*10^0,字母也可以按照此方式來設計

小寫字母26,可以採用26進位制,大小寫字母52進位制,大小寫字母+數字 62進位制,比如都以小寫字母為例子

abc字串,a的ASCII碼是97,b 98 c99 可以將abc轉換為 97*26^2+98*26^1+99*26^0=65572+2548+99=68219

為了減少數字的大小,去掉偏移量97,改為0*26^2+1*26^1+2*26^0=28,一種簡單的計算hash的方法。

2。2 程式碼實現

public class RKAlth {

/**

* 獲取字串的hash值

* @param s

* @return

*/

public static int strToHash(String s){

int hash=0;

int length = s。length();

for (int i = 0; i < length; i++) {

hash*=26;

hash+=s。charAt(i)-97;

}

return hash;

}

public static boolean isMatch(String str,String sub){

int hash = strToHash(sub);

for(int i=0;i<=str。length()-sub。length();i++){

if (hash==strToHash(str。substring(i,i+sub。length()))){

return true;

}

}

return false;

}

}

適用於匹配串型別不多的情況,比如:字母、數字或字母加數字的組合 62 (大小寫字母+數字)

3。BM演算法

3。1 概念和原理

BM演算法是在BF演算法基礎之上改進,RK演算法需要設計hash值的演算法,BM(Boyer-Moore)演算法是一種非常高效的字串匹配演算法,滑動演算法。

演算法原理

BM 演算法包含兩部分,分別是壞字元規則(bad character rule)和好字尾規則(good suffix shift)。

壞字元規則

BM 演算法的匹配順序比較特別,它是按照

模式串下標從大到小的順序

,倒著匹配的。從模式串的末尾往前倒著匹配,當發現某個字元沒法匹配的時候,把這個沒有匹配的字元叫作壞字元(主串中的字元)。

Java資料結構與演算法——字串匹配相關演算法

字元 c 與模式串中的任何字元都不可能匹配,可以將模式串直接往後滑動三位,將模式串滑動到 c 後面的位置,再從模式串的末尾字元開始比較。

Java資料結構與演算法——字串匹配相關演算法

壞字元 a 在模式串中是存在的,模式串中下標是 0 的位置也是字元 a。這種情況下,我們可以將模式串往後滑動兩位,讓兩個 a 上下對齊,然後再從模式串的末尾字元開始,重新匹配

Java資料結構與演算法——字串匹配相關演算法

當發生不匹配的時候,

把壞字元對應的模式串中的字元下標記作 si

。如果壞字元在模式串中存在,

把這個壞字元在模式串中的下標記作 xi

。如果不存在,

把 xi 記作 -1

。那模式串往後移動的位數就等於

si-xi

。(下標,都是字元在模式串的下標)

Java資料結構與演算法——字串匹配相關演算法

第一次移動 c 在模式串中不存在,xi=-1,si=2,移動位數n=si-xi=3

第二次移動a 在模式串中存在,xi=0,si=2,移動位數n=si-xi=2

好字尾規則

Java資料結構與演算法——字串匹配相關演算法

把已經匹配的,拿它在模式串中查詢,如果找到了另一個跟{u}相匹配的子串{u},那我們就將模式串滑動到子串{u}與主串中{u}對齊的位置

Java資料結構與演算法——字串匹配相關演算法

如果在模式串中找不到另一個等於{u}的子串,我們就直接將模式串,滑動到主串中{u}的後面,因為之前的任何一次往後滑動,都沒有匹配主串中{u}的情況。

Java資料結構與演算法——字串匹配相關演算法

過度滑動情況:

Java資料結構與演算法——字串匹配相關演算法

當模式串滑動到字首與主串中{u}的字尾有部分重合的時候,並且重合的部分相等的時候,就有可能會存在完全匹配的情況。針對這種情況,不僅要看好字尾在模式串中,是否有另一個匹配的子串,還要考察好字尾的字尾子串(c),是否存在跟模式串的字首子串(c)匹配的。

3。2 程式碼實現

壞字元實現程式碼

匹配最佳化

可以用一個長度為256的陣列,來記錄每個字元在模式串中的位置,陣列小標就直接對應ASCII碼的值,陣列的值為字元在模式串中的位置,預設為-1。(ASCII能表示256個字元),如果模式串中有重複的字元,存最後一次出現的字元的位置。

Java資料結構與演算法——字串匹配相關演算法

package com。david。stringmatch;

public class BMAlth {

/**

* 初始化模式串的字典陣列

* @param b

* @param dc

*/

private static void generateDc(char[] b,int[] dc){

//初始化字典陣列,每一位存-1

for (int i = 0; i < dc。length; i++) {

dc[i]=-1;

}

for (int i = 0; i < b。length; i++) {

int asc=(int)b[i];//字元的ASCII碼

dc[asc]=i;//欄位陣列對應的位置存字元在模式串的下標

}

}

/**

* 字串匹配

* @param b 主字串的char陣列

* @param s 模式串的char陣列

* @return 不存在返回-1,存在返回對應的下標

*/

public static int isMatch(char[] b,char[] s){

int m=b。length;//主串的長度

int n=s。length;//模式串的長度

int[] dc=new int[256];//模式串字典陣列

//初始化欄位陣列

generateDc(s,dc);

//i表示主串與模式串對齊的第一個字元

int i=0;

while(i<=m-n){

//j 壞字元在模式串中的下標

int j;

for(j=n-1;j>=0;j——){ //從模式串的最後一個字元開始

//主串從正序開始,會加上子串的長度

// a b c d e f

// d e f

//第一次匹配就應該從c開始,所以才有j+i

if (b[j+i]!=s[j]) break;//不匹配

}

//如果j<0,說明模式串中所有字元都匹配成功了

if (j<0){

return i;//匹配成功,返回主串與模式串第一個匹配的字元的位置

}

//滑動位置計算

// 這裡等同於將模式串往後滑動j-bc[(int)a[i+j]]位

// j:si bc[(int)a[i+j]]:xi 找到主串中匹配的哪個字元,再去字典表找對應字元在

//模式串中的位置從而獲取 xi

i = i + (j - dc[(int)b[i+j]]);

}

return -1;

}

public static void main(String[] args) {

System。out。println(isMatch(“abcdefghijklmn”。toCharArray(),“abc”。toCharArray()));//0

System。out。println(isMatch(“abcdefghijklmn”。toCharArray(),“lmn”。toCharArray()));//11

System。out。println(isMatch(“abcdefghijklmn”。toCharArray(),“efg”。toCharArray()));//4

System。out。println(isMatch(“abcdefghijklmn”。toCharArray(),“xxx”。toCharArray()));//-1

}

}

3。3 小結

BM演算法的時間複雜度是O(N/M) n:主串長度 m:模式串長度 應用 BM演算法比較高效,在實際開發中,特別是一些文字編輯器中,用於實現查詢字串功能。

4。 Trie樹

4。1 概念和原理

4。1。1 概念

Trie 樹,也叫“字典樹”。它是一個樹形結構。它是一種專門處理字串匹配的資料結構,用來解決在一組字串集合中快速查詢某個字串的問題。Trie 樹的本質,就是利用字串之間的公共字首,將重複的前綴合並在一起 。

Java資料結構與演算法——字串匹配相關演算法

其中根節點不包含任何資訊。每個節點表示一個字串中的字元,從根節點到紅色節點的

一條路徑表示一個字串

(紅色節點為葉子節點)。

4。1。2 插入原理

Java資料結構與演算法——字串匹配相關演算法

Java資料結構與演算法——字串匹配相關演算法

如上圖,每次插入,將字串拆分為字元陣列,從根節點依次查詢,如果不存在,就新增一個節點,如果存在就繼續迴圈,直到插入字串的最後一個字元,將最後一個字元標記為是最終字元(isEnding)。

4。1。3 查詢原理

在 Trie 樹中查詢一個字串的時候,比如查詢字串“her”,那我們將要查詢的字串分割成單個的字元 h,e,r,然後從 Trie 樹的根節點開始匹配。如圖所示,綠色的路徑就是在 Trie 樹中匹配的路徑。

Java資料結構與演算法——字串匹配相關演算法

4。1。4 樹結構最佳化

Trie 樹是一個多叉樹,可以透過一個下標與字元一一對映的陣列,來儲存子節點的指標 。如果只考慮小寫字母的話,就26個字母,子節點的陣列長度就26,下標為 0 的位置,儲存指向子節點a 的指標,下標為 1 的位置儲存指向子節點 b 的指標,以此類推,下標為 25 的位置,儲存的是指向的子節點 z 的指標。如果某個字元的子節點不存在,我們就在對應的下標的位置儲存 null。

Java資料結構與演算法——字串匹配相關演算法

4。2 程式碼實現

4。2。1 Trie樹節點

class TrieNode{

char data;

TrieNode[] childs= new TrieNode[26];

boolean isEndingChar;

public TrieNode(char data) {

this。data = data;

}

}

4。2。1 存和查詢

public class Trie {

private TrieNode root = new TrieNode(‘/’); // 儲存無意義字元

public void insert(char[] text){

TrieNode p=root;

for (int i = 0; i < text。length; i++) {

int index=text[i]-97;

if (p。childs[index]==null){

p。childs[index]=new TrieNode(text[i]);

}

//切換到子節點,繼續遍歷新增

p=p。childs[index];

}

//遍歷完成 p節點肯定是最後一個字元,設定一個結束標記

p。isEndingChar=true;

}

public boolean find(char[] s){

TrieNode p = root;

for (int i = 0; i < s。length; i++) {

int index=s[i]-97;

if (p。childs[index]==null){

return false;

}

//匹配上了,就繼續找下一個節點

p=p。childs[index];

}

//全部都匹配了,判斷p有沒有結束字元標記 有可能存在 源字元 是 her 查詢字元是he

//這種不算完全匹配,只能算字首匹配

if (p。isEndingChar){

return true;

}else {

return false;

}

}

public static void main(String[] args) {

Trie t=new Trie();

t。insert(“hello”。toCharArray());

t。insert(“her”。toCharArray());

t。insert(“hi”。toCharArray());

t。insert(“how”。toCharArray());

t。insert(“see”。toCharArray());

t。insert(“so”。toCharArray());

System。out。println(t。find(“how”。toCharArray()));//true

System。out。println(t。find(“he”。toCharArray()));//false

System。out。println(t。find(“her”。toCharArray()));//true

}

}

4。3 小結

時間複雜度

如果要在一組字串中,頻繁地查詢某些字串,用 Trie 樹會非常高效。

構建 Trie 樹的過程,需要掃描所有的字串,時間複雜度是 O(n)

(n 表示所有字串的長度和)。但是一旦構建成功之後,後續的查詢操作會非常高效。每次查詢時,如果要查詢的字串長度是 k,那我們只需要比對大約 k 個節點,就能完成查詢操作。跟原本那組字串的長度和個數沒有任何關係。所以說,

構建好 Trie 樹後,在其中查詢字串的時間複雜度是 O(k),k 表示要查詢的字串的長度。

典型應用

利用 Trie 樹,實現搜尋關鍵詞的提示功能 我們假設關鍵詞庫由使用者的熱門搜尋關鍵片語成。我們將這個詞庫構建成一個 Trie 樹。當用戶輸入其中 某個單詞的時候,把這個詞作為一個字首子串在 Trie 樹中匹配。為了講解方便,我們假設詞庫裡只有 hello、her、hi、how、so、see 這 6 個關鍵詞。當用戶輸入了字母 h 的時候,我們就把以 h 為字首的 hello、her、hi、how 展示在搜尋提示框內。當用戶繼續鍵入字母 e 的時候,我們就把以 he 為字首的 hello、her 展示在搜尋提示框內。這就是搜尋關鍵詞提示的最基本的演算法原理。

Java資料結構與演算法——字串匹配相關演算法