在《字符串匹配算法研究(一)》一文中主要介紹了多串匹配問題和解決單串匹配問題的KMP算法,接下來我們可以利用其思想解決多串匹配問題。 不過在此之前需要引入單詞查找樹的概念。
單詞查找樹是一棵無限延伸的26叉樹,每個結點的26個通向子結點的邊分別被命名爲a,b,...,z;(之前曾提到過,將原文和模式串假設成僅由26個小寫英文字母組成)。這樣從根結點到任一非根結點p路徑上的字母均可組成一字符串Sp,且任一字符串均可在單詞樹中找到唯一一條從根結點出發,與之對應的路徑。
對於模式串單詞樹的建立我們一般先建立一棵空樹,再將模式串按照單詞查找樹的方式一個個插入單詞樹中。不過光有模式串單詞樹並不能解決多串匹配問題。因此我們還需要引入單詞前綴樹的概念。
單詞前綴樹首先是一棵單詞查找樹,但其每一非根結點都有一個前綴指針。對於樹中結點p的前綴指針prefix定義如下:p->prefix=n|n in tree and Sn=Sp.subString(min{k},Sp.length),其中整數k∈[1,Sp.length)。
前綴指針的建立方法可仿照kmp算法中前綴函數的建立方法:根據父結點的前綴指針找到當前結點的前綴指針。下面給出用單詞前綴樹實現多串匹配算法的Java代碼:
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* 單詞前綴樹類
* 包含用單詞前綴樹解決多串匹配問題的算法
* @author liuxy
*
*/
public class CharPrefixTree {
//該結點的26個子結點
private CharPrefixTree[] children = new CharPrefixTree[26];
//用一List動態存儲子結點以便在遍歷時減少循環次數
private List childrenList = new LinkedList();
//該結點的父指針
private CharPrefixTree father = null;
//該結點的前綴指針
private CharPrefixTree prefix = null;
//標記該結點是否爲模式串的終點,若是則將模式串的引用賦給它
private LinkedList Okey = null;
//父結點到該結點邊上的字符
private char value;
/**
* 得到字符串s中第i個字母在孩子數組中的對應下標
*/
private static int getIndex(String s, int i){
return s.charAt(i) - 'a';
}
/**
* 得到該結點字符對應孩子數組的下標
* @return
*/
private int getIndex(){
return value - 'a';
}
/**
* 給一課單詞樹添加前綴指針
* 算法:採用廣度優先算法遍歷樹中結點並調用setPrefix設置其前綴指針,
* 保證給當前結點設置前綴指針時比它深度小的結點的前綴指針都已設置完畢
*/
private void generatePrefix(){
LinkedList l = new LinkedList();
l.addLast(this);
while(l.size() > 0){
CharPrefixTree p = (CharPrefixTree)l.removeFirst();
Iterator iter = p.childrenList.iterator();
while(iter.hasNext()){
CharPrefixTree child = (CharPrefixTree)iter.next();
child.setPrefix();
l.addLast(child);
}
}
}
/**
* 設置當前結點的前綴指針
* 算法:假設當前結點爲p
* q<-p->father->prefix
* index<-此時q的父親到q邊上字母對應的下標
* While q!=Root and q->children[index]=null Do
* q<-q.prefix
* If q->children[index]!=null Then p->prefix=q->children[index]
*
*/
private void setPrefix(){
CharPrefixTree q = this.father.prefix;
int index = getIndex();
if(q == null)return;
while(q.father != null && q.children[index] == null){
q = q.prefix;
}
if(q.children[index] != null){
this.prefix = q.children[index];
if(this.prefix.Okey != null && this.prefix.Okey.size() > 0){
if(this.Okey == null)this.Okey = new LinkedList();
this.Okey.addAll(this.prefix.Okey);
}
}
}
/**
* 在單詞樹中插入單詞
* @param s:需要插入的單詞
*/
private void insertString(String s){
CharPrefixTree p = this;
for(int i = 0; i < s.length(); ++i){
int index = getIndex(s,i);
if(p.children[index] == null){
p.children[index] = new CharPrefixTree();
p.children[index].father = p;
p.children[index].value = s.charAt(i);
p.children[index].prefix = this;
p.childrenList.add(p.children[index]);
}
p = p.children[index];
}
if(p.Okey == null)p.Okey = new LinkedList();
p.Okey.addLast(s);
System.out.println(p.Okey);
}
/**
* 在單詞樹中插入單詞數組
* @param patterns:需插入的單詞數組
*/
private void insertString(String[] patterns){
for(int i = 0; i < patterns.length; ++i)
this.insertString(patterns[i]);
}
/**
* 多串匹配主算法
* 算法:
* create CharPrefixTree
* q<-root
* For i<-0 to n-1 Do
* index<-text[i]-'a';
* while q!=root and q->children[index] Do
* q<-q->prefix
* If q->children[index]!=null Then q<-q->children[index];
* If q->Okey Then Display Matching Message
* End For
*
* 構造單詞前綴樹總時間複雜度爲O(Sum{Li|Li=patterns[i].length});
* 多串匹配算法時間複雜度爲O(text.length)
* @param patterns:模式串
* @param text:正文
*/
public static void mpMatching(String[] patterns, String text){
CharPrefixTree q = new CharPrefixTree();
q.insertString(patterns);
q.generatePrefix();
for(int i = 0; i < text.length(); ++i){
int index = getIndex(text,i);
while(q.father != null && q.children[index] == null)
q = q.prefix;
if(q.children[index] != null)q = q.children[index];
if(q.Okey != null && q.Okey.size() > 0){
Iterator iter = q.Okey.iterator();
while(iter.hasNext()){
String pattern = (String)iter.next();
System.out.println("pattern:" + pattern + " position:" + (i - pattern.length() + 1));
}
}
}
}
public static void main(String args[]){
String text = "zababaabc";
String[] patterns = {"b","aba","babaa","abc"};
mpMatching(patterns,text);
}
}