您好,登錄后才能下訂單哦!
多模字符串匹配算法在這里指的是在一個字符串中尋找多個模式字符字串的問題。一般來說,給出一個長字符串和很多短模式字符串,如何最快最省的求出哪些模式字符串出現在長字符串中是我們所要思考的。該算法廣泛應用于關鍵字過濾、入侵檢測、病毒檢測、分詞等等問題中。多模問題一般有Trie樹,AC算法,WM算法等等。
背景
在做實際工作中,最簡單也最常用的一種自然語言處理方法就是關鍵詞匹配,例如我們要對n條文本進行過濾,那本身是一個過濾詞表的,通常進行過濾的代碼如下
for (String document : documents) { for (String filterWord : filterWords) { if (document.contains(filterWord)) { //process ... } } }
如果文本的數量是n,過濾詞的數量是k,那么復雜度為O(nk);如果關鍵詞的數量較多,那么支行效率是非常低的。
計算機科學中,Aho–Corasick算法是由AlfredV.Aho和MargaretJ.Corasick發明的字符串搜索算法,用于在輸入的一串字符串中匹配有限組“字典”中的子串。它與普通字符串匹配的不同點在于同時與所有字典串進行匹配。算法均攤情況下具有近似于線性的時間復雜度,約為字符串的長度加所有匹配的數量。然而由于需要找到所有匹配數,如果每個子串互相匹配(如字典為a,aa,aaa,aaaa,輸入的字符串為aaaa),算法的時間復雜度會近似于匹配的二次函數。
原理
在一般的情況下,針對一個文本進行關鍵詞匹配,在匹配的過程中要與每個關鍵詞一一進行計算。也就是說,每與一個關鍵詞進行匹配,都要重新從文檔的開始到結束進行掃描。AC自動機的思想是,在開始時先通過詞表,對以下三種情況進行緩存:
按照字符轉移成功進行跳轉(success表)
按照字符轉移失敗進行跳轉(fail表)
匹配成功輸出表(output表)
因此在匹配的過程中,無需從新從文檔的開始進行匹配,而是通過緩存直接進行跳轉,從而實現近似于線性的時間復雜度。
構建
構建的過程分三個步驟,分別對success表,fail表,output表進行構建。其中output表在構建sucess和fail表進行都進行了補充。fail表是一對一的,output表是一對多的。
按照字符轉移成功進行跳轉(success表)
sucess表實際就是一棵trie樹,構建的方式和trie樹是一樣的,這里就不贅述。
按照字符轉移失敗進行跳轉(fail表)
設這個節點上的字母為C,沿著他父親的失敗指針走,直到走到一個節點,他的兒子中也有字母為C的節點。然后把當前節點的失敗指針指向那個字母也為C的兒子。如果一直走到了root都沒找到,那就把失敗指針指向root。使用廣度優先搜索BFS,層次遍歷節點來處理,每一個節點的失敗路徑。
匹配成功輸出表(output表)
匹配
舉例說明,按順序先后添加關鍵詞he,she,,his,hers。在匹配ushers過程中。先構建三個表,如下圖,實線是sucess表,虛線是fail表,結點后的單詞是ourput表。
代碼
import java.util.*; /** */ public class ACTrie { private Boolean failureStatesConstructed = false; //是否建立了failure表 private Node root; //根結點 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的節點的failure設為根節點 //特殊處理:第二層要特殊處理,將這層中的節點的失敗路徑直接指向父節點(也就是根節點)。 for (Node depthOneState : this.root.children()) { depthOneState.setFailure(this.root); queue.add(depthOneState); } this.failureStatesConstructed = true; // 第二步,為深度 > 1 的節點建立failure表,這是一個bfs 廣度優先遍歷 /** * 構造失敗指針的過程概括起來就一句話:設這個節點上的字母為C,沿著他父親的失敗指針走,直到走到一個節點,他的兒子中也有字母為C的節點。 * 然后把當前節點的失敗指針指向那個字母也為C的兒子。如果一直走到了root都沒找到,那就把失敗指針指向root。 * 使用廣度優先搜索BFS,層次遍歷節點來處理,每一個節點的失敗路徑。 */ 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; //失敗中轉 private Boolean isRoot = false; //是否為根結點 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跳轉 * @param character * @return */ public Node find(Character character) { return map.get(character); } /** * 跳轉到下一個狀態 * @param transition 接受字符 * @return 跳轉結果 */ private Node nextState(Character transition) { Node state = this.find(transition); // 先按success跳轉 if (state != null) { return state; } //如果跳轉到根結點還是失敗,則返回根結點 if (this.isRoot) { return this; } // 跳轉失敗的話,按failure跳轉 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; /** * 構造一個模式串匹配結果 * @param start 起點 * @param end 重點 * @param keyword 模式串 */ public Emit(final int start, final int end, final String keyword) { this.start = start; this.end = end; this.keyword = keyword; } /** * 獲取對應的模式串 * @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()); } } }
總結
以上就是本文關于多模字符串匹配算法原理及Java實現代碼的全部內容,希望對大家有所幫助。感興趣的朋友可以繼續參閱本站:
Java 蒙特卡洛算法求圓周率近似值實例詳解
java算法實現紅黑樹完整代碼示例
java實現的各種排序算法代碼示例
如有不足之處,歡迎留言指出。感謝朋友們對本站的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。