<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
一般的字串匹配演演算法都是匹配一個子串,例如KMP、Trie,那麼如果同時匹配多個子串呢?此時就需要用到AC自動機了。
AC自動機演演算法是一個多模式字串匹配演演算法,在模式匹配領域被廣泛應用,例如違禁詞查詢替換、搜尋關鍵詞查詢等等。
關於Trie樹和KMP演演算法,我們此前已經講解過了:
AC自動機演演算法常被認為是Trie樹+KMP演演算法的結合體,為什麼呢?我們先看看它的構建步驟:
第一步,對所有的關鍵詞構建Trie字首樹。這一步利用Trie的特點構建快速字首查詢結構,trie樹的特點是可以從字串頭部開始匹配,並且相同字首的詞共用前面的節點,因此它可以避免相同字首pattern的重複匹配,但是對於相同的字尾無能為力。
第二步,為Trie樹上的所有節點構建fail失配指標,即匹配失敗後應該跳到哪個節點。所謂節點的失配指標,就是指向當前字串最長真字尾位置的指標節點。這裡需要理解KMP的next陣列的概念,這一步就是利用KMP前字尾匹配的思想實現關鍵詞字首失配時利用相同的字尾資訊快速跳轉到另一個關鍵詞繼續字首匹配。他們的區別是:
例如兩個關鍵詞dabab,ababd,那麼關鍵詞dabab中第二個b位置的失敗指標應該指向關鍵詞ababd中的第二個b。即dabab的字尾與ababd的字首能夠匹配的最長子串為abab。
在這裡,我們給出一個比較簡單的節點的定義。
class AcNode { /** * 經過該節點的模式串的下層節點 */ Map<Character, AcNode> next = new HashMap<>(); /** * 模式串的長度,也是節點的深度 */ int depth; /** * 失配指標,如果沒有匹配到,則跳轉到此狀態。 */ AcNode failure; public boolean hashNext(char nextKey) { return next.containsKey(nextKey); } public AcNode getNext(char nextKey) { return next.get(nextKey); } }
構建Ac自動機的Trie的方法和構建普通Trie的方法幾乎一致。在新增每個模式串成功後,會為最後一個節點的depth賦值為當前模式串的長度。
/** * trie根節點 */ private AcNode root; /** * 加入模式串,構建Trie * * @param word 模式串,非空 */ public void insert(String word) { AcNode cur = root; for (char c : word.toCharArray()) { if (!cur.next.containsKey(c)) { cur.next.put(c, new AcNode()); } cur = cur.next.get(c); } cur.depth = word.length(); }
構建fail失配指標的一種常見的方法如下,實際上是一個BFS層序遍歷的演演算法:
1.Trie的root節點沒有失配指標,或者說失配指標為null,其他節點都有失配指標,或者說不為null。
2.遍歷root節點的所有下一層直接子節點,將它們的失配指標設定為root。因為這些節點代表著所有模式串的第一個字元,基於KMP的next陣列定義,單個字元沒有最長真字尾,此時直接指向root。
3.繼續迴圈向下遍歷每一層的子節點,由於bfs的遍歷,那麼上一層父節點的失配指標肯定都已經確定了。基於next陣列的構建思想,子節點的失配指標可以通過父節點的是失配指標快速推匯出來。設當前遍歷的節點為c,它的父節點為p,父節點的失配指標為pf。
/** * 為所有節點構建失配指標,一個bfs層序遍歷 */ public void buildFailurePointer() { ArrayDeque<AcNode> queue = new ArrayDeque<AcNode>(); //將所有root的直接子節點的failure設定為root,並且加入queue for (AcNode acNode : root.next.values()) { acNode.failure = root; queue.addLast(acNode); } //bfs構建失配指標 while (!queue.isEmpty()) { //父節點出佇列 AcNode parent = queue.pollFirst(); //遍歷父節點的下層子節點,基於父節點求子節點的失配指標 for (Map.Entry<Character, AcNode> characterAcNodeEntry : parent.next.entrySet()) { //獲取父節點的失配指標 AcNode pf = parent.failure; //獲取子節點 AcNode child = characterAcNodeEntry.getValue(); //獲取子節點對應的字元 Character nextKey = characterAcNodeEntry.getKey(); //如果pf節點不為null,並且pf節點的子節點對應的字元中,沒有包含了當前節點的所表示的字元 while (pf != null && !pf.hashNext(nextKey)) { //繼續獲取pf節點的失配指標節點,繼續重複判斷 pf = pf.failure; } //pf為null,表示找到了根節點,並且根節點的子節點也沒有匹配 if (pf == null) { //此時直接將節點的失配指標指向根節點 child.failure = root; } //pf節點的子節點對應的字元中,包含了當前節點的所表示的字元 else { //節點的失配指標可以直接指向pf節點下的相同字元對應的子節點 child.failure = pf.getNext(nextKey); } //最後不要忘了,將當前節點加入佇列 queue.addLast(child); } } }
構建完AC自動機之後,下面我們需要進行文字的匹配,匹配的方式實際上比較簡單。
1.遍歷文字的每個字元,依次匹配,從Trie的根節點作為cur節點開始匹配:
2.將當前字元作為nextKey,如果cur節點不為null且節點的next對映中不包含nextKey,那麼當前cur節點指向自己的failure失配指標。
3.如果cur節點為null,說明當前字元匹配到了root根節點且失敗,那麼cur設定為root繼續從根節點開始進行下一輪匹配。
4.否則表示匹配成功的節點,cur指向匹配節點,獲取該節點繼續判斷:
/** * 匹配文字 * * @param text 文字字串 */ public List<ParseResult> parseText(String text) { List<ParseResult> parseResults = new ArrayList<>(); char[] chars = text.toCharArray(); //從根節點開始匹配 AcNode cur = root; //遍歷字串的每個字元 for (int i = 0; i < chars.length; i++) { //當前字元 char nextKey = chars[i]; //如果cur不為null,並且當前節點的的子節點不包括當前字元,即不匹配 while (cur != null && !cur.hashNext(nextKey)) { //那麼通過失配指標轉移到下一個節點繼續匹配 cur = cur.failure; } //如果節點為null,說明當前字元匹配到了根節點且失敗 //那麼繼續從根節點開始進行下一輪匹配 if (cur == null) { cur = root; } else { //匹配成功的節點 cur = cur.getNext(nextKey); //繼續判斷 AcNode temp = cur; while (temp != null) { //如果當前節點是某個關鍵詞的結尾,那麼取出來 if (temp.depth != 0) { int start = i - temp.depth + 1, end = i; parseResults.add(new ParseResult(start, end, new String(chars, start, temp.depth))); //System.out.println(start + " " + end + " " + new String(chars, start, temp.depth)); } //繼續判斷該節點的失配指標節點 //因為失配指標節點表示的模式串是當前匹配的模式串的在這些關鍵詞中的最長字尾,且由於當前節點的路徑包括了失配指標的全部路徑 //並且失配指標路徑也是一個完整的關鍵詞,需要找出來。 temp = temp.failure; } } } return parseResults; } class ParseResult { int startIndex; int endIndex; String key; public ParseResult(int startIndex, int endIndex, String key) { this.startIndex = startIndex; this.endIndex = endIndex; this.key = key; } @Override public String toString() { return "{" + "startIndex=" + startIndex + ", endIndex=" + endIndex + ", key='" + key + ''' + '}'; } }
基於上面的方法,假如關鍵詞為:he、shes、shers、hes、h、e,那麼最終構建的AC自動機的結構如下(紅色圈表示某個關鍵詞的結束位置)。
假如我們的文字為sheshe,那麼我們來看看AC自動機匹配的過程:
遍歷文字,cur首先指向root。
nextKey=s,cur.next包含s,表示這是一個匹配成功的節點,那麼獲取到該節點s,cur=s,s不是某個關鍵詞的結尾,failure節點也不包含模式串,那麼查詢完畢進行下一輪。
nextKey=h,cur=s,cur.next包含h,表示這是一個匹配成功的節點,那麼獲取到該節點h,cur=h,h節點不是某個關鍵詞的結尾,但是它的failure節點包含模式串h,因此找到了第1個匹配的關鍵詞h,查詢完畢後進行下一輪。
nextKey=e,cur=h,cur.next包含e,表示這是一個匹配成功的節點,那麼獲取到該節點e,cur=e,e節點不是某個關鍵詞的結尾,但是它的failure節點包含模式串he,因此找到了第2個匹配的關鍵詞he。
而fuilure節點e又包含另一個模式串e,因此找到了第3個匹配的關鍵詞e,查詢完畢後進行下一輪。
nextKey=s,cur=e,cur.next包含s,表示這是一個匹配成功的節點,那麼獲取到該節點e,cur=e,s節點是關鍵詞shes的結尾,因此找到了第4個匹配的關鍵詞shes。
繼續判斷s的failure,它的failure節點包含模式串hes,因此找到了第5個匹配的關鍵詞hes,查詢完畢後進行下一輪。
nextKey=h,cur=s,cur.next不包含h,表示這是一個匹配失敗的節點,那麼繼續匹配它的failure節點,經過s-s-s的匹配,最終匹配到子節點h。
該節點h並不是關鍵詞的結尾,但是h的failure,它的failure節點包含模式串h,因此找到了第6個匹配的關鍵詞h,查詢完畢後進行下一輪。
nextKey=e,cur=h,cur.next包含e,表示這是一個匹配成功的節點,那麼獲取到該節點e,cur=e,e節點不是某個關鍵詞的結尾,但是它的failure節點包含模式串he,因此找到了第7個匹配的關鍵詞he。
而fuilure節點e又包含另一個模式串e,因此找到了第8個匹配的關鍵詞e。
到此字串遍歷完畢,查詢完畢!
最終文字sheshe,匹配到關鍵詞如下:
[{startIndex=1, endIndex=1, key='h'}, {startIndex=1, endIndex=2, key='he'},
{startIndex=2, endIndex=2, key='e'}, {startIndex=0, endIndex=3, key='shes'},
{startIndex=1, endIndex=3, key='hes'}, {startIndex=4, endIndex=4, key='h'},
{startIndex=4, endIndex=5, key='he'}, {startIndex=5, endIndex=5, key='e'}]
AC自動機匹配某個文字text,需要遍歷文字的每個字元,每次遍歷過程中,都可能涉及到迴圈向上查詢失配指標的情況,但是這裡的迴圈次數不會超過Trie樹的深度,在最後匹配成功時,同樣涉及到向上查詢失配指標的情況,這裡的迴圈次數不會超過Trie樹的深度。
設匹配的文字長度m,模式串平均長度n,那麼AC自動機演演算法的匹配的時間複雜度為O(m*n)。可以發現,匹配的時間複雜度和關鍵詞的數量無關,這就是AC自動機的強大之處。如果考慮模式串平均長度不會很長,那麼時間複雜度近似O(m)。
到此這篇關於Java資料結構之AC自動機演演算法的實現的文章就介紹到這了,更多相關Java AC自動機演演算法內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45