多模字符串匹配算法原理及Java實(shí)現(xiàn)代碼
多模字符串匹配算法在這里指的是在一個字符串中尋找多個模式字符字串的問題。一般來說,給出一個長字符串和很多短模式字符串,如何最快最省的求出哪些模式字符串出現(xiàn)在長字符串中是我們所要思考的。該算法廣泛應(yīng)用于關(guān)鍵字過濾、入侵檢測、病毒檢測、分詞等等問題中。多模問題一般有Trie樹,AC算法,WM算法等等。
背景
在做實(shí)際工作中,最簡單也最常用的一種自然語言處理方法就是關(guān)鍵詞匹配,例如我們要對n條文本進(jìn)行過濾,那本身是一個過濾詞表的,通常進(jìn)行過濾的代碼如下
for (String document : documents) {
for (String filterWord : filterWords) {
if (document.contains(filterWord)) {
//process ...
}
}
}
如果文本的數(shù)量是n,過濾詞的數(shù)量是k,那么復(fù)雜度為O(nk);如果關(guān)鍵詞的數(shù)量較多,那么支行效率是非常低的。
計(jì)算機(jī)科學(xué)中,Aho–Corasick算法是由AlfredV.Aho和MargaretJ.Corasick發(fā)明的字符串搜索算法,用于在輸入的一串字符串中匹配有限組“字典”中的子串。它與普通字符串匹配的不同點(diǎn)在于同時(shí)與所有字典串進(jìn)行匹配。算法均攤情況下具有近似于線性的時(shí)間復(fù)雜度,約為字符串的長度加所有匹配的數(shù)量。然而由于需要找到所有匹配數(shù),如果每個子串互相匹配(如字典為a,aa,aaa,aaaa,輸入的字符串為aaaa),算法的時(shí)間復(fù)雜度會近似于匹配的二次函數(shù)。
原理
在一般的情況下,針對一個文本進(jìn)行關(guān)鍵詞匹配,在匹配的過程中要與每個關(guān)鍵詞一一進(jìn)行計(jì)算。也就是說,每與一個關(guān)鍵詞進(jìn)行匹配,都要重新從文檔的開始到結(jié)束進(jìn)行掃描。AC自動機(jī)的思想是,在開始時(shí)先通過詞表,對以下三種情況進(jìn)行緩存:
按照字符轉(zhuǎn)移成功進(jìn)行跳轉(zhuǎn)(success表)
按照字符轉(zhuǎn)移失敗進(jìn)行跳轉(zhuǎn)(fail表)
匹配成功輸出表(output表)
因此在匹配的過程中,無需從新從文檔的開始進(jìn)行匹配,而是通過緩存直接進(jìn)行跳轉(zhuǎn),從而實(shí)現(xiàn)近似于線性的時(shí)間復(fù)雜度。
構(gòu)建
構(gòu)建的過程分三個步驟,分別對success表,fail表,output表進(jìn)行構(gòu)建。其中output表在構(gòu)建sucess和fail表進(jìn)行都進(jìn)行了補(bǔ)充。fail表是一對一的,output表是一對多的。
按照字符轉(zhuǎn)移成功進(jìn)行跳轉(zhuǎn)(success表)
sucess表實(shí)際就是一棵trie樹,構(gòu)建的方式和trie樹是一樣的,這里就不贅述。
按照字符轉(zhuǎn)移失敗進(jìn)行跳轉(zhuǎn)(fail表)
設(shè)這個節(jié)點(diǎn)上的字母為C,沿著他父親的失敗指針走,直到走到一個節(jié)點(diǎn),他的兒子中也有字母為C的節(jié)點(diǎn)。然后把當(dāng)前節(jié)點(diǎn)的失敗指針指向那個字母也為C的兒子。如果一直走到了root都沒找到,那就把失敗指針指向root。使用廣度優(yōu)先搜索BFS,層次遍歷節(jié)點(diǎn)來處理,每一個節(jié)點(diǎn)的失敗路徑。
匹配成功輸出表(output表)
匹配
舉例說明,按順序先后添加關(guān)鍵詞he,she,,his,hers。在匹配ushers過程中。先構(gòu)建三個表,如下圖,實(shí)線是sucess表,虛線是fail表,結(jié)點(diǎn)后的單詞是ourput表。

代碼
import java.util.*;
/**
*/
public class ACTrie {
private Boolean failureStatesConstructed = false;
//是否建立了failure表
private Node root;
//根結(jié)點(diǎn)
public ACTrie() {
this.root = new Node(true);
}
/**
* 添加一個模式串
* @param keyword
*/
public void addKeyword(String keyword) {
if (keyword == null || keyword.length() == 0) {
return;
}
Node currentState = this.root;
for (Character character : keyword.toCharArray()) {
currentState = currentState.insert(character);
}
currentState.addEmit(keyword);
}
/**
* 模式匹配
*
* @param text 待匹配的文本
* @return 匹配到的模式串
*/
public Collection<Emit> parseText(String text) {
checkForConstructedFailureStates();
Node currentState = this.root;
List<Emit> collectedEmits = new ArrayList<>();
for (int position = 0; position < text.length(); position++) {
Character character = text.charAt(position);
currentState = currentState.nextState(character);
Collection<String> emits = currentState.emit();
if (emits == null || emits.isEmpty()) {
continue;
}
for (String emit : emits) {
collectedEmits.add(new Emit(position - emit.length() + 1, position, emit));
}
}
return collectedEmits;
}
/**
* 檢查是否建立了failure表
*/
private void checkForConstructedFailureStates() {
if (!this.failureStatesConstructed) {
constructFailureStates();
}
}
/**
* 建立failure表
*/
private void constructFailureStates() {
Queue<Node> queue = new LinkedList<>();
// 第一步,將深度為1的節(jié)點(diǎn)的failure設(shè)為根節(jié)點(diǎn)
//特殊處理:第二層要特殊處理,將這層中的節(jié)點(diǎn)的失敗路徑直接指向父節(jié)點(diǎn)(也就是根節(jié)點(diǎn))。
for (Node depthOneState : this.root.children()) {
depthOneState.setFailure(this.root);
queue.add(depthOneState);
}
this.failureStatesConstructed = true;
// 第二步,為深度 > 1 的節(jié)點(diǎn)建立failure表,這是一個bfs 廣度優(yōu)先遍歷
/**
* 構(gòu)造失敗指針的過程概括起來就一句話:設(shè)這個節(jié)點(diǎn)上的字母為C,沿著他父親的失敗指針走,直到走到一個節(jié)點(diǎn),他的兒子中也有字母為C的節(jié)點(diǎn)。
* 然后把當(dāng)前節(jié)點(diǎn)的失敗指針指向那個字母也為C的兒子。如果一直走到了root都沒找到,那就把失敗指針指向root。
* 使用廣度優(yōu)先搜索BFS,層次遍歷節(jié)點(diǎn)來處理,每一個節(jié)點(diǎn)的失敗路徑。
*/
while (!queue.isEmpty()) {
Node parentNode = queue.poll();
for (Character transition : parentNode.getTransitions()) {
Node childNode = parentNode.find(transition);
queue.add(childNode);
Node failNode = parentNode.getFailure().nextState(transition);
childNode.setFailure(failNode);
childNode.addEmit(failNode.emit());
}
}
}
private static class Node{
private Map<Character, Node> map;
private List<String> emits;
//輸出
private Node failure;
//失敗中轉(zhuǎn)
private Boolean isRoot = false;
//是否為根結(jié)點(diǎn)
public Node(){
map = new HashMap<>();
emits = new ArrayList<>();
}
public Node(Boolean isRoot) {
this();
this.isRoot = isRoot;
}
public Node insert(Character character) {
Node node = this.map.get(character);
if (node == null) {
node = new Node();
map.put(character, node);
}
return node;
}
public void addEmit(String keyword) {
emits.add(keyword);
}
public void addEmit(Collection<String> keywords) {
emits.addAll(keywords);
}
/**
* success跳轉(zhuǎn)
* @param character
* @return
*/
public Node find(Character character) {
return map.get(character);
}
/**
* 跳轉(zhuǎn)到下一個狀態(tài)
* @param transition 接受字符
* @return 跳轉(zhuǎn)結(jié)果
*/
private Node nextState(Character transition) {
Node state = this.find(transition);
// 先按success跳轉(zhuǎn)
if (state != null) {
return state;
}
//如果跳轉(zhuǎn)到根結(jié)點(diǎn)還是失敗,則返回根結(jié)點(diǎn)
if (this.isRoot) {
return this;
}
// 跳轉(zhuǎn)失敗的話,按failure跳轉(zhuǎn)
return this.failure.nextState(transition);
}
public Collection<Node> children() {
return this.map.values();
}
public void setFailure(Node node) {
failure = node;
}
public Node getFailure() {
return failure;
}
public Set<Character> getTransitions() {
return map.keySet();
}
public Collection<String> emit() {
return this.emits == null ? Collections.<String>emptyList() : this.emits;
}
}
private static class Emit{
private final String keyword;
//匹配到的模式串
private final int start;
private final int end;
/**
* 構(gòu)造一個模式串匹配結(jié)果
* @param start 起點(diǎn)
* @param end 重點(diǎn)
* @param keyword 模式串
*/
public Emit(final int start, final int end, final String keyword) {
this.start = start;
this.end = end;
this.keyword = keyword;
}
/**
* 獲取對應(yīng)的模式串
* @return 模式串
*/
public String getKeyword() {
return this.keyword;
}
@Override
public String toString() {
return super.toString() + "=" + this.keyword;
}
}
public static void main(String[] args) {
ACTrie trie = new ACTrie();
trie.addKeyword("hers");
trie.addKeyword("his");
trie.addKeyword("she");
trie.addKeyword("he");
Collection<Emit> emits = trie.parseText("ushers");
for (Emit emit : emits) {
System.out.println(emit.start + " " + emit.end + "\t" + emit.getKeyword());
}
}
}
總結(jié)
以上就是本文關(guān)于多模字符串匹配算法原理及Java實(shí)現(xiàn)代碼的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:
如有不足之處,歡迎留言指出。感謝朋友們對本站的支持。
相關(guān)文章
Springmvc項(xiàng)目web.xml中servlet-mapping路徑映射配置注意說明
這篇文章主要介紹了Springmvc項(xiàng)目web.xml中servlet-mapping路徑映射配置注意說明,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
spring-cloud-stream結(jié)合kafka使用詳解
這篇文章主要介紹了spring-cloud-stream結(jié)合kafka使用詳解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
MyBatis學(xué)習(xí)教程(五)-實(shí)現(xiàn)關(guān)聯(lián)表查詢方法詳解
本文給大家介紹mybatis關(guān)聯(lián)查詢,包括一對一關(guān)聯(lián)查詢,一對多關(guān)聯(lián)查詢,代碼簡單易懂,感興趣的朋友一起學(xué)習(xí)吧2016-05-05
java hasNextInt判斷是否為數(shù)字的方法
今天小編就為大家分享一篇java hasNextInt判斷是否為數(shù)字的方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07
EVCache緩存在Spring Boot中的實(shí)戰(zhàn)示例
這篇文章主要介紹了EVCache緩存在Spring Boot中的實(shí)戰(zhàn)示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12
Java并發(fā)volatile可見性的驗(yàn)證實(shí)現(xiàn)
這篇文章主要介紹了Java并發(fā)volatile可見性的驗(yàn)證實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
java導(dǎo)出大批量(百萬以上)數(shù)據(jù)的excel文件
這篇文章主要為大家詳細(xì) 介紹了java導(dǎo)出大批量即百萬以上數(shù)據(jù)的excel文件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04

