多模式串匹配 - AC 自動(dòng)機(jī)

多模式串匹配概念

多模式串匹配哭懈,即多個(gè)模式串在一個(gè)主串中進(jìn)行匹配唤冈。

雖然單模式串也能完成多模式串的匹配,但每個(gè)模式串都需要與主串進(jìn)行匹配银伟,如果遇到主串太長(zhǎng)的情況你虹,效率不高。

而多模式串匹配彤避,只需要掃描一次主串傅物,大大提高效率。

有一種經(jīng)典的多模式串匹配算法 -- AC 自動(dòng)機(jī)琉预,全稱(chēng)是 Aho-Corasick 算法董饰。下面來(lái)介紹其實(shí)現(xiàn)。

AC 自動(dòng)機(jī)

AC 自動(dòng)機(jī)Trie 樹(shù)的基礎(chǔ)上圆米,增加了類(lèi)似 KMPnext 數(shù)組卒暂,即每個(gè)節(jié)點(diǎn)會(huì)有個(gè) fail 指針,指向某個(gè)節(jié)點(diǎn)娄帖。數(shù)據(jù)結(jié)構(gòu)如下:

class ACNode {
    var data: Character
    
    // 字符集 26 個(gè)小寫(xiě)字母
    var children: [ACNode?]
    var isEnd: Bool = false
    var fail: ACNode? = nil
    var len: Int = 0
    
    init(_ data: Character) {
        self.data = data
        children = [ACNode]()
        
        // 初始化 26 個(gè)
        var i = 0
        while i < 26 {
            children.append(nil)
            i += 1
        }
    }
}

準(zhǔn)備工作

  1. 將多個(gè)模式串構(gòu)建成 Trie 樹(shù)
  2. 構(gòu)建 fail 指針

構(gòu)建 Trie 樹(shù)

參考 Trie 樹(shù) 的文章也祠。

構(gòu)建 fail 指針

fail 指針的生成類(lèi)似于 KMP 中的next 數(shù)組計(jì)算方式,基于前一個(gè)值來(lái)計(jì)算近速。

fail 指針的定義

每個(gè)節(jié)點(diǎn)都有 fail 指針诈嘿。假設(shè)節(jié)點(diǎn)為 p,當(dāng)前串 strrootp 的節(jié)點(diǎn)路徑削葱,找到其與所有模式串前綴匹配的最長(zhǎng)后綴子串奖亚,那么 fail 就指向所匹配前綴的最后一個(gè)字符節(jié)點(diǎn)。

后綴子串:不包括開(kāi)頭字符的子串析砸。

這里需要注意的是最長(zhǎng)后綴子串昔字,比如當(dāng)前串為 abc,模式串為 bc首繁、c作郭。雖然其與 bcc 都匹配蛮瞄,但最長(zhǎng)的后綴子串是 bc所坯。

其中root.fail = NULL,若proot的直接子節(jié)點(diǎn)挂捅,則p.fail = root芹助。

fail 指針的生成方法

假設(shè)當(dāng)前節(jié)點(diǎn)為 pp.fail = q闲先,pc 記為 p 的某個(gè)子節(jié)點(diǎn)状土,qc 記為 q 的某個(gè)子節(jié)點(diǎn)。

  1. pc == qc伺糠,則 pc.fail = qc蒙谓。如下圖所示:
image.png
  1. pc != qc,則不斷循環(huán) q = q.fail训桶,直到找到 q 的子節(jié)點(diǎn)與 pc 相等累驮,或者 q == root結(jié)束酣倾。但如果 q == root,則說(shuō)明沒(méi)有后綴與模式串的前綴匹配谤专,此時(shí)則令 pc.fail = root躁锡。再繼續(xù)這兩個(gè)過(guò)程。
image.png

按照樹(shù)的廣度遍歷置侍,逐個(gè)生成節(jié)點(diǎn)的 fail 指針映之。

最終結(jié)果如下圖所示:

image.png

Swift 代碼如下:

// 構(gòu)建失敗指針
func buildFailPointer(_ root: ACNode) {
    var queue = Array<ACNode>()
    queue.append(root)
    
    while !queue.isEmpty {
        // 取出頭部
        let p = queue.removeFirst()
        
        // 遍歷其子節(jié)點(diǎn)
        if !p.children.isEmpty {
            for pc in p.children {
                if let pc = pc {
                    // 如果父節(jié)點(diǎn)是 root,則 fail 指針直接指向 root
                    if p === root {
                        pc.fail = root
                    } else {
                        var q = p.fail
                        while q != nil {
                            // 如果 pc 在 q 的子節(jié)點(diǎn) qc 中存在蜡坊,則直接指向 qc
                            let index = indexOfChar(pc.data)
                            if (index >= 0 && index <= q!.children.count) {
                                if let qc = q!.children[index] {
                                    pc.fail = qc
                                } else {
                                    // 不存在杠输,找 q 的 fail 指針
                                    q = q!.fail
                                }
                            }
                        }
                        
                        // 不存在,則指向 root
                        if q == nil {
                            pc.fail = root
                        }
                    }
                    
                    queue.append(pc)
                }
            }
        }
    }
}

模式串匹配

首先需要明確以下兩點(diǎn):

  1. p 的節(jié)點(diǎn)路徑 A ( root 到 p 的路徑)匹配主串秕衙,則 p.fail 的節(jié)點(diǎn)路徑 B 也是匹配的蠢甲。因?yàn)?A 的后綴子串與B 的前綴是相同的,所以前面肯定匹配灾梦,同理 p.fail.fail 的節(jié)點(diǎn)路徑也是峡钓。

    因此只需要不斷遍歷其 fail 指針節(jié)點(diǎn),判斷是否為結(jié)束符若河,如果是能岩,則該模式串就是匹配主串的。

  2. 在某條分支路徑上 A 做匹配萧福,如果遇上不匹配的情況拉鹃,則切到其 fail 指針指向的另外一條分支 B ,再繼續(xù)匹配鲫忍。因?yàn)槠淝昂缶Y相同膏燕。

具體算法

逐個(gè)遍歷主串的字符,判斷該字符是否存在當(dāng)前節(jié)點(diǎn) p 的子節(jié)點(diǎn)中悟民。

  1. 如果存在坝辫,則 p 指向其子節(jié)點(diǎn),然后循環(huán)遍歷 p 鏈?zhǔn)降?fail 指針指向的節(jié)點(diǎn)是否為模式串的結(jié)尾射亏,若是近忙,該模式串匹配完成。
  2. 如果不存在智润,則循環(huán)遍歷 fail 的鏈?zhǔn)街羔樳M(jìn)行查找及舍,若沒(méi)找到,則節(jié)點(diǎn) p 重新指回 root窟绷。重復(fù)這 2 個(gè)步驟锯玛。

Swift 代碼如下:

// 進(jìn)行匹配
func match(_ text: String, _ root: ACNode) {
    // 逐個(gè)遍歷主串
    var p: ACNode? = root
    var i = 0
    while i < text.count {
        let strIndex = text.index(text.startIndex, offsetBy: i)
        
        let ch = text[strIndex]
        
        // 判斷 p 的子節(jié)點(diǎn)是否匹配 ch,如果不匹配兼蜈,則往 fail 指針找
        let index = indexOfChar(ch)
        
        while p?.children[index] == nil && p !== root {
            p = p?.fail
        }
        
        p = p?.children[index]
        
        // 一直沒(méi)有匹配攘残,重新指回 root
        if p == nil {
            p = root
        }
        
        // 遍歷其 fail 指針拙友,找到結(jié)束的字符,即為匹配
        var tmp = p
        while tmp != nil {
            if tmp!.isEnd {
                print("match startPosition:\(i - tmp!.len + 1)")
            }
            
            tmp = tmp?.fail
        }
        
        i += 1
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肯腕,一起剝皮案震驚了整個(gè)濱河市献宫,隨后出現(xiàn)的幾起案子实撒,更是在濱河造成了極大的恐慌,老刑警劉巖涉瘾,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件知态,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡立叛,警方通過(guò)查閱死者的電腦和手機(jī)负敏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)秘蛇,“玉大人其做,你說(shuō)我怎么就攤上這事×藁梗” “怎么了妖泄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)艘策。 經(jīng)常有香客問(wèn)我蹈胡,道長(zhǎng),這世上最難降的妖魔是什么朋蔫? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任罚渐,我火速辦了婚禮,結(jié)果婚禮上驯妄,老公的妹妹穿的比我還像新娘荷并。我一直安慰自己,他們只是感情好青扔,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布源织。 她就那樣靜靜地躺著,像睡著了一般赎懦。 火紅的嫁衣襯著肌膚如雪雀鹃。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天励两,我揣著相機(jī)與錄音黎茎,去河邊找鬼。 笑死当悔,一個(gè)胖子當(dāng)著我的面吹牛傅瞻,可吹牛的內(nèi)容都是我干的踢代。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嗅骄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼胳挎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起溺森,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤慕爬,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后屏积,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體医窿,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年炊林,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了姥卢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡渣聚,死狀恐怖独榴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奕枝,我是刑警寧澤棺榔,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站倍权,受9級(jí)特大地震影響掷豺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜薄声,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一当船、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧默辨,春花似錦德频、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至表谊,卻和暖如春钞护,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爆办。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工难咕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓余佃,卻偏偏與公主長(zhǎng)得像暮刃,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子爆土,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容