垃圾詞匹配演算法 對比與概述

2022-07-20 18:51:11 字數 4958 閱讀 9525

前面工作接到乙個專案需求,需要對交流訊息做垃圾詞(敏感詞)做處理,特地去了解了一波敏感詞匹配演算法,這裡對演算法調研做一下文件記錄,方便後續需求

這種方式是最簡單的,就是迴圈把每個敏感詞在目標文字中從頭到尾搜尋一遍,如果有文字高亮或替換的話,那就找到乙個就處理乙個

演算法簡單,對於開發人員來說,簡單的演算法會使**實現上簡單,開發難度最小

效率太低,因為迴圈每個敏感詞,所以當敏感詞很多、目標文字很長時,其效率可以說是該演算法的致命問題

字典樹又稱字首樹,trie樹,是一種樹形結構,是一種雜湊樹的變種,一種有序樹,用於儲存關聯陣列,其中的鍵通常是字串,且鍵是由節點在樹中的位置決定的。典型應用是用於統計,排序和儲存大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計、搜尋提示

構建敏感詞字首樹,三個指標,分別為指標1,指標2,指標3

是一種樹形結構,利用字串的公共字首來減少查詢時間,最大限度地減少無謂的字串比較,提高查詢效率

一旦匹配失敗,又要從根開始

利用kmp演算法防止匹配失敗時字串回溯(最大最小字首),改進了之後其實就像ac自動機了

ac自動機首先將模式組記錄為trie字典樹的形式,以節點表示不同狀態,邊上標以字母表中的字元,表示狀態的轉移。根節點狀態記為0狀態,表示起始狀態。當乙個狀態處有乙個模式串終結則標記一下

ac演算法主要是解決多字串匹配問題,比如字串ushers,作為長字串, 多模式串:he/ she/ hers/ his ,作為匹配串。要解決的問題就是:在長字串ushers中,是否包含多模式串(也就是ushers中,是否存在he,是否存在she…等等)

所以也常常被用於處理敏感詞匹配問題

匹配的過程是:從0狀態起點開始,以字元流輸入,進行適當的狀態轉移,如果可以抵達某一標記終結的狀態,則成功匹配模式,串值為從0到終結點的路徑

按照傳統的說法,狀態機有三個主要函式支撐:goto(狀態正常轉移),fail(狀態失配轉移),output(傳回匹配結果),而我認為與其規定是具體的函式,倒不如說是三個功能的模組,有不同於函式的實現形式

goto是自動機基本的狀態轉移過程,很好想,就是在建立trie樹時讓每個狀態維護一組指標(廣義的),使得在每一狀態對於輸入都可以正確轉移,沒有對應的則報錯(現在回答剛才的問題,什麼是失配?失配就是乙個狀態接受了無法轉移的字元,記fail)。除了字典樹中的樹枝以外,還有乙個轉移就是在開始節點,對於不能流進自動機的字元,不報錯而是再一次轉到開始節點,很好理解,對於待匹配串λthis,λ為不含t,h的任意串,真正的模式匹配是在去除了它以後開始的

正常的狀態流轉已經建立好了,現在看失配時我們的狀態流何去何從。舉乙個栗子,如果輸入thip這個串,狀態的流轉應該如下

那3狀態處報錯後應該怎麼處理呢?最好想的方法當然是錯開一位,再從頭開始匹配(這種方法就像一位老人家曾經說過,太年輕太簡單,有時還很幼稚),ac二位的辦法是——利用圖中的關係計算出一套跳轉關係——在x點處失配的串不打回開頭來過,而是跳到y點——繼續匹配當前字元。這套規則叫做失配函式,也就是fail功能模組。要點在於當前字元不向前回溯,想想著很適合字元流的關鍵字匹配對不對

接著說一下3狀態的失配跳轉在6狀態,先不用管怎麼得到的,先想想這個過程:3得到p字元,失配,憑goto無法轉移狀態,使用失配時通用的fail,狀態跳至6,接受p——還是這個字元,成功匹配到終結狀態7。單趟遍歷目標串完成

正式開始之前請認真思考這個情況:已知2狀態的失配跳轉為5,怎麼求3狀態的失配跳轉?從圖中很容易看出,2通過i流向3,而5恰有對i的goto,自然地,3失配時可以跳轉至6

dfa是一種計算模型,資料來源是乙個有限個集合,通過當前狀態和事件來確定下乙個狀態,即 狀態+事件=下一狀態,由此逐步構建乙個有向圖,其中的節點就是狀態,所以在dfa演算法中只有查詢和判斷,沒有複雜的計算,從而提高演算法效率

構造資料結構

將敏感詞轉換成樹結構,舉例有著王八蛋和王八羔子兩個敏感詞,這兩個詞的二叉樹構造為

把每個敏感詞字串拆散成字元,再儲存到hashmap(其他語言可用字典實現hashmap)中,如下

,

"羔": },}

}}

判斷邏輯

將每個詞的第乙個字元作為key,vlue則是另乙個hashmap,value對應的hashmap的key為第二個字元,如果還有第三個字元,則儲存到以第二個字元為key的value中,當然這個value還是乙個hashmap,以此類推下去,直到最後乙個字元,當然最後乙個字元對應的value也是hashmap,只不過這個hashmap只需要儲存乙個結束標誌就行了

上面最後就是儲存了乙個 的hashmap,來標識這個value對應的key是敏感詞的最後乙個字元

先從文字的第乙個字開始檢查,比如 你個王八羔子 ,第乙個字 你 ,在樹的第一層找不到這個節點,那麼繼續找第二個字,到了 王 的時候,第一層節點找到了,那麼接著下一層節點中查詢 八 ,同時判斷這個節點是不是結尾節點,若是結尾節點,則匹配成功了,反之繼續匹配

效率高掃瞄,指掃瞄文章,看看是否有需要和髒字表開始進行對比的情況

檢索,指已經發現可能存在情況了,在將文字和髒字表進行對比的過程

起始符,指髒字表中條目中的第乙個字元

如果我們只要掃瞄,不需要檢索就可以完成任務,那一定是最快的,不過目前沒有找到這樣的演算法又或者,如果我們掃瞄一遍,而檢索全中,那也很不錯,很不幸,還是沒見過很明顯,掃瞄不應該多於1遍,否則肯定效率不可能高。那麼檢索就是演算法的關鍵了!拆開來,提高檢索質量有下列幾個方式:

一次掃瞄

只要發現「起始符」就觸發檢索

檢索的時候,需要遍歷的字元數是 1+2+3+...+n,這裡的n是被命中的髒詞的長度,或者最接近的長度

每次檢索,需要重複計算hashcode,不要忘了,計算hashcode,也是需要掃瞄字串的,也就是又要遍歷1+2+3+..+n個字元

分析xdmp演算法會發現一些問題,如下:

難道每次遇到「起始符」了,就一定要觸發檢索嗎?哎呀媽呀,這個也要檢索(因為髒字表裡面可能有mb)

難道每次觸發檢索,都非得要檢索長度為1的,長度為2的,長度為3的……直到檢索成功,或者出現非髒字表字元的時候嗎

難道每次檢索,我們都需要把特定長度的待檢文字擷取出來嗎

難道每次檢索,都需要從頭開始計算雜湊值嗎?不能利用同一次觸發檢索後,上一次檢索的雜湊值,來減少本次計算的不必要運算量嗎

xdmp演算法的最大問題就是遇到起始符就開始匹配,計算雜湊值,ttmp演算法就逆向匹配,在觸發對關鍵字的檢索時,從後面往前面檢索。比如說:

髒字表:wxyz、yz待檢文字: wxyza當我們遇到了結束符z的時候,我們會回過頭來檢視剛才到底都遇到了什麼文字。由於我們之前的掃瞄已經得到了兩個「起始符」的相關資訊,因此我們只要按發現起始符的逆序找 yz和wxyz。於是,最終我們命中的關鍵字是yz,而不是最先遇到的wxyz

找到的一定是最短的匹配

在分析正常文字的時候,效率可能相對會更高

重點說一下為什麼效率可能會相對更高,這裡有兩個原因

要做最大匹配,意味著要付出更高昂的額外計算成本

由於是逆順序檢索,如果我們選擇遇到起始符就預先計算雜湊值,就很有可能做了一些不必要的運算,即使是在命中的情況下。考慮:髒字表:abc、c待建文字: abc則在遇到a的時候,就會開始計算雜湊值,直到c字元。但是可以看到,對ab進行雜湊值計算,很有可能就是不必要的關於這個缺點,其實只是「眼看著還有白費的運算無法消除」而已,實際上相對可能還是更快的

普通的字串匹配演算法,暴力的字串匹配演算法,其實就是個組合的過程,一般來說有兩個量,乙個是i乙個是j,i在目標串上移動,j在模式串上移動,如果target[i]==text[j],那麼i++,j++,如果不等於,不好意思,請 i 回到開始匹配成功的下乙個位置,j=0,重複匹配,可見這樣的演算法的複雜度是o(n*m),相當地慢,這種演算法沒什麼技巧

如果學過資料結構的人都知道,對於字串匹配還有比暴力法好得多的一種演算法,那就是雜湊法,雜湊法通過算字串的雜湊值來匹配模式串,我們只用把模式串雜湊一次然後就可以把維持m長度的指標從目標串的0位置匹配到n-m位置就可以了,這個演算法的複雜度是上界是o(n),看上去很不錯,但是事實上如果模式串很大的話,我們要找到乙個很好的雜湊函式(第乙個保證雜湊值都是唯一的(不可能用鍊錶法,因為可能雜湊值會很大,重複很多效率就下降了),雜湊值最好是能從上乙個推出下乙個的),而且要開闢乙個相當大的空間來儲存雜湊值

kmp演算法就是乙個很好的雜湊的演算法

kmp演算法的根本目的就是想讓i不後退(雜湊法也是這樣想的),這就要求我們把匹配過的資訊進行儲存,kmp演算法用到乙個next陣列,是整個演算法的關鍵

講next陣列之前我們先來明白一下什麼是模式串的字首字尾最長公共長度,先來看乙個表:

從這個表我們可以很清楚看到,所謂的字首和字尾其實就是在第i個位置從前往後數和從pos位置從後往前數的一樣的子串的長度

構建next陣列

模式串與目標串進行匹配,假設現在模式串和目標串已經匹配到j和i,那麼

如果target[i]==text[j],則i++,j++(這個和暴力演算法一致)

如果target[i]!=text[j],則使j通過next陣列跳轉到前乙個j(假設是k位置,的相當於是使j匹配從pos向前移動pos-j個位置,再判斷target[i]是否等於text[k],否則繼續執行k=next[k],直到target[i]==text[k],如果無法找到合適的元素使target[i]==text[k],則另k=0,且i++

可以看到next陣列是用來解決暴力解法的當匹配失敗時就要j=0的缺點,可以讓j跳轉到某個合適的位置,繼續匹配

當然了這個位置也不是隨便亂選的,而這個位置就是剛好是當前元素前面元素的字首字尾最長公共長度的後乙個位置,當然了我們要要從當前位置跳轉,我們關注的是當前位置前面的元素的情況,所以我們把上面那個表所有值往前移動乙個單位,然後把0位置設定成-1就得到了next表了

大家看到這裡可能會很疑惑,究竟為什麼跳轉到前面元素的字首和字尾最長公共的後乙個位置前面乙個位置就是可以的呢?這個的確不太好說明,我們只從例子上說明問題,我們先來舉乙個例子

從面例子中,其實移動到前注意和字尾最長公共位置是合理的,因為我們前字尾是相等的,我們移動後可以保證當前元素前面的元素都是匹配的,比如圖中這個例子,移動以後ac還是和前面配過的元素一樣,最後完成匹配,最後當然了,如果移動後還失配,還是需要相同的移動方法。直到移動到模式串的最前端為止(移動到最前端說明已經找不到任何匹配的元素了,相當於重新匹配模式串了)

DFA 演算法實現關鍵詞匹配

ahocorasick esmre 但是其實包都是基於dfa 實現的 這裡提供原始碼如下 usr bin python2.6 coding utf 8 import time class node object def init self self.children none self.flag f...

垃圾收集演算法

首先標記出所有需要 的物件,在標記完成後統一 所有被標記的物件 將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體 記憶體分配時也就不用考慮記憶體碎片...

垃圾收集演算法

一.標記 清除演算法 分為 標記 和 清除 兩個階段 方案 1.標記所有需要 的物件 2.統一 被標記的物件 缺點 1.效率問題,標記 和 清除 效率都不高 2.空間問題,被 清除 後會產生大量的不連續的記憶體碎片 當大物件找不到足夠的連續空間,就得提前gc 二.複製演算法 方案 將記憶體分為兩塊,...