資料結構與演算法(十一)——串
iwehdio的部落格園:https://www.cnblogs.com/iwehdio/
1、串
-
串(字串):來自字母表的字元所構成的有限序列。
-
一般來說,串長n遠大於字母表中的字元數量。
-
串相等:串長度相等,且對應位置上的字元相同。
-
子串:substr(i,k),從s[i]開始的連續k個字元。
-
字首:prefix(k)=substr(0,k),即最靠前的k個字元。
-
字尾:suffix(k)=substr(n-k,k),即最靠後的k個字元。
-
空串長度為0,是任何串的子串、字首、字尾。
-
串的功能介面:
2、串匹配
-
一般將所要搜尋的串稱為模式P,搜尋的物件稱為文字T。
-
模式匹配:
- 從文字中匹配模式串。
- 是否出現?
- 首次在哪裡出現?(主要問題)
- 共有幾次出現?
- 各出現在哪裡?
-
如何評測串匹配演算法的效能?
- 成功:在T中,隨機取出長度為m的子串作為P;分析平均複雜度。
- 失敗:採用隨機的P;統計平均複雜度。
-
蠻力匹配:
-
自左向右,以字元為單位,依次移動模式串。直到在某個位置,發現匹配。
-
版本1:
int match ( char* P, char* T ) { //串匹配演算法(Brute-force-1) size_t n = strlen ( T ), i = 0; //文字串長度、當前接受比對字元的位置 size_t m = strlen ( P ), j = 0; //模式串長度、當前接受比對字元的位置 while ( j < m && i < n ) //自左向右逐個比對字元 { if ( T[i] == P[j] ) //若匹配 { i ++; j ++; } //則轉到下一對字元 else //否則 { i -= j - 1; j = 0; } //文字串回退、模式串復位 } return i - j; }
-
-
版本2:
int match ( char* P, char* T ) { //串匹配演算法(Brute-force-2) size_t n = strlen ( T ), i = 0; //文字串長度、與模式串首字元的對齊位置 size_t m = strlen ( P ), j; //模式串長度、當前接受比對字元的位置 for ( i = 0; i < n - m + 1; i++ ) { //文字串從第i個字元起,與 for ( j = 0; j < m; j++ ) //模式串中對應的字元逐個比對 { if ( T[i + j] != P[j] ) break; //若失配,模式串整體右移一個字元,再做一輪比對 } if ( j >= m ) break; //找到匹配子串 } return i; }
-
唯一的區別就是版本1中i指向文字T中進行比較的位置,版本2中i指向模式T開始比較的位置。
-
最壞情況下可達到O(m*n)。
-
低效的原因在於,之前比對過的字元,將在模式T失陪進位後再次比較。
-
KMP演算法:
-
所要處理的,就是這種一部分字首匹配,但是在某個位置不匹配。對於下圖有
T[i]!=P[j]
,而其字首是匹配的。 -
因為已經匹配的部分
T[i-j,i)
和P[0,j)
是完全相同的。所以,當X!=Y導致失配時,需要將P移動一段距離L,使得T[i-j+L,i)
與P[0,j-L)
匹配。這種情況只與模式P有關,而且對於P中每個字元都只有一種移動的情況。 -
構造查詢表
next[0,m)
,在任一位置P[j]失敗後,將j替換為next[j]。即相當於P移動了j-next[j]
。 -
實現:
int match ( char* P, char* T ) { //KMP演算法 int* next = buildNext ( P ); //構造next表 int n = ( int ) strlen ( T ), i = 0; //文字串指標 int m = ( int ) strlen ( P ), j = 0; //模式串指標 while ( j < m && i < n ) //自左向右逐個比對字元 { if ( 0 > j || T[i] == P[j] ) //若匹配,或P已移出最左側(兩個判斷的次序不可交換) { i ++; j ++; } //則轉到下一字元 else //否則 j = next[j]; //模式串右移(注意:文字串不用回退) } delete [] next; //釋放next表 return i - j; }
-
next表的作用是,藉助必要條件(自匹配),排除不必要的對齊位置比較,實現快速右移。
-
自匹配:當模式P與文字T,在P[j]處失配時,令t=next[j],進行快速右移。這樣做的條件是,在P[j]的字首P[0,j)中,t是其真字首和真字尾匹配的最大長度,即存在最大的t使得
P[0,t)=P[j-t,j)
。 -
next[0]=-1,是為了在首字元比對失敗的情況下後移一位,對應於後移條件中的 0>j。-1的位置相當於是一個通配哨兵,可以與任何字元匹配。
-
構造next表:
- 遞迴的,對於next表中的第j+1項,如果
P[j]
與P[next[j]]
匹配(因為next[j]已計算出來,所以字首部分必然匹配),則next[j+1]=1+next[j]。 - 如果
P[j]
與P[next[j]]
不匹配,則需要嘗試P[j]
與P[next[next[j]]]
是否匹配,並可能繼續遞迴。
- 遞迴的,對於next表中的第j+1項,如果
-
實現:
int* buildNext ( char* P ) { //構造模式串P的next表 size_t m = strlen ( P ), j = 0; //“主”串指標 int* N = new int[m]; //next表 int t = N[0] = -1; //模式串指標 while ( j < m - 1 ) if ( 0 > t || P[j] == P[t] ) { //匹配 j ++; t ++; N[j] = t; } else //失配 t = N[t]; return N; }
-
時間複雜度O(n),n為T的規模。
-
再改進:
-
對於已經失敗的比對,用同樣的字元再次進行相同的必定失敗的比對,損失了效率。
-
同樣的字元比對必然失敗,因此可以直接指向next[t]。
-
實現:
int* buildNext ( char* P ) { //構造模式串P的next表 size_t m = strlen ( P ), j = 0; //“主”串指標 int* N = new int[m]; //next表 int t = N[0] = -1; //模式串指標 while ( j < m - 1 ) if ( 0 > t || P[j] == P[t] ) { //匹配 j ++; t ++; N[j] = P[j]!=P[t]? t:N[t]; } else //失配 t = N[t]; return N; }
-
-
-
BM演算法:
- 不對稱性:判斷串相等和不等的代價是不同的。即使是對單個字元而言,匹配失敗的概率也遠高於成功的概率。
- 在串匹配中,先對靠後的字元匹配可以獲得更多的教訓。這是因為如果靠後的字元匹配失敗,可以排除較多的對齊位置。
- 因此,在匹配時,應從模式P從後往前匹配。
-
壞字元(bc):
- 模式P從後往前比對時,第一個失配的字元。模式P中為Y,而文字T中為X。X就是壞字元。
- 這意味著模式P需要右移直到其中的X與文字T中的X對齊。然後再從最右端開始比較。位移量取決於失配位置和X在P中的秩,可製表bc待查。
- 如果模式P中的X在匹配位置的右側,則模式P只向右移動一個位置。
- 如果模式P中不包含X,則直接完全越過這個對齊位置。
- 如果模式P中存在多個X,則先選擇字首中秩最大的。
-
構造bc表:記錄所有字元最後一次出現的位置。
-
實現:
int* buildBC ( char* P ) { //構造Bad Charactor Shift表:O(m + 256) int* bc = new int[256]; //BC表,與字元表等長 for ( size_t j = 0; j < 256; j ++ ) bc[j] = -1; //初始化:首先假設所有字元均未在P中出現 for ( size_t m = strlen ( P ), j = 0; j < m; j ++ ) //自左向右掃描模式串P bc[ P[j] ] = j; //將字元P[j]的BC項更新為j(單調遞增)——畫家演算法 return bc; }
-
時間複雜度最好O(n/m),最壞O(n*m)。
-
好字尾(gs):
- 在模式P的字尾G完成與文字T中的子串S的匹配時,可以將完成匹配的部分看作經驗。G就是好字尾。
- 如果模式P還有一部分需要與子串S匹配,則P中與之匹配的部分必然與G完全相同。
- 可以通過製表gs,當好字尾繼續匹配失敗時,移動模式P使得相同的字元與子串S對其。
-
位移量根據bc表和gs表選擇較大者。
-
構造gs表:
-
MS[]子串:P[0,j]的所有後綴中,MS[j]是與P的某一字尾匹配的最長者。
-
ss[]表:ss[j]是MS[j]的長度。其中包含了gs表的所有資訊。
-
從ss[]表到gs[]表:
- 如果s[j]=j+1,則對任一字元P[i](i<m-j-1),m-j-1必是gs[i]的一個候選。
- 如果s[j]<=j,則對字元P[m-ss[j]-1],m-j-1必是gs[m-ss[j]-1]的一個候選。
- 注意這裡第一種情況是對在好字尾之前的任意的失配字元,而第二種情況只是對好字尾之前的哪一個失配字元。
-
-
時間複雜度最好O(n/m),最壞O(n+m)。
-
KR演算法:
- 將字串轉化為整數,這樣一次比較只需要O(1)的實現。
- 對於一個字母表規模為d的字串,都可以對應於一個d進位制的自然數。
- 但是這樣所需的儲存空間太大,可以通過雜湊的方法壓縮。
- 先比較雜湊碼,雜湊衝突時,再進行串中字元的逐一匹配。
- 子串的雜湊過程,可以根據相鄰子串間的相似性,通過移位快速得到。
iwehdio的部落格園:https://www.cnblogs.com/iwehdio/