Swift算法-摩尔字符算法Boyer-Moore String Search

声明:算法和数据结构的文章均是作者从github上翻译过来,为方便大家阅读。如果英语阅读能力强的朋友,可以直接到swift算法俱乐部查看所有原文,以便快速学习。作者同时也在学习中,欢迎交流

摩尔算法的目的:用swift写出一个字符搜索算法,不需要引入Foundation或者使用NSString中的rangeOfString()函数。

换句话说,我们想要在string上实现一个indexOf(pattern: String)方法的拓展,可以用来检索指定字符串里是否存在pattern并得到它的String.Index值,或者当这个pattern无法检索到的时候,返回nil

示例:

// Input:
let s = "Hello, World"
s.indexOf(pattern: "World")

// Output:
<String.Index?> 7

// Input:
let animals = "🐶🐔🐷🐮🐱"
animals.indexOf(pattern: "🐮")

// Output:
<String.Index?> 6

注意: 这里奶牛的索引值是6而不是3。因为在字符串里面,每一个emoji的字符使用更多的存储空间。String.Index的真实数字并不重要,只要它能得到字符在字符串里所在的正确位置即可。

通常情况下,暴力检索运行效率尚可,但是在检索大量数据的时候,这样的过程并不是很高效。因为结果往往是,你并不需要检索字符串里面的所有字符--中间大部分字符都可以直接跳过。

这个跳过继续检索的算法叫做摩尔算法。它存在已久,并且被认为是所有字符检索算法的基准。

以下是在swift中我们实现它的代码:

extension String {
    func index(of pattern: String) -> Index? {
        // 存储pattern的长度值
        let patternLength = pattern.characters.count
        guard patternLength > 0, patternLength <= characters.count else { return nil }

        // 创建跳过表格
        // 当pattern里的某一个字符被检索到,这个表格可以决定我们可以跳过多少长度
        var skipTable = [Character: Int]()
        for (i, c) in pattern.characters.enumerated() {
            skipTable[c] = patternLength - i - 1
        }

        // 这里得到pattern里最后一个字符.
        let p = pattern.index(before: pattern.endIndex)
        let lastChar = pattern[p]

        // 
        // 核对过程是由pattern的右向左,所以我们跳过的长度为pattern的长度
        // 这里扣掉1是因为startIndex已经指向源字符串的首字符位置
        var i = index(startIndex, offsetBy: patternLength - 1)

        
        // 此函数将源字符串和pattern从指定位置开始对比匹配,当对应位置的字符
        // 不一致时候返回nil,反之返回字符串上面经过匹配对比的最前一位的位置
        func backwards() -> Index? {
            var q = p
            var j = i
            while q > pattern.startIndex {
                j = index(before: j)
                q = index(before: q)
                if self[j] != pattern[q] { return nil }
            }
            return j
        }

        // 主循环.在找到完全匹配时候结束,或者全部检索完依然没有匹配时候返回nil
        while i < endIndex {
            let c = self[i]

            // 检测源字符中当前位置的字符与pattern的最后一位字符是否相同
            if c == lastChar {

                // 当发现可能匹配的时候,进行暴力匹配对比
                if let k = backwards() { return k }

                // 如果不匹配,直接前进一个位置.
                i = index(after: i)
            } else {
                // 当前字符状态为不匹配,所以需要向前移动,移动的距离由跳过表格决定        
                //如果当前字符与pattern没有任何匹配,则直接前进一个pattern长度的距离。否则,则根据跳过表格的距离决定。
                i = index(i, offsetBy: skipTable[c] ?? patternLength, limitedBy: endIndex) ?? endIndex
            }
        }
        return nil
    }
}

算法具体运行过程如下:

我们将需要检索的pattern和源字符串放在一起对比。从源字符中与pattern的最后一个字符对应位置的字符开始比较

source string:  Hello, World
search pattern: World
                    ^

此时存在三种可能性:
1.pattern的最后一个字符与源字符串的对应位置的字符相同,可能会有匹配
2.pattern的最后一个字符与源字符串的对应位置的字符不相同,但是源字符串的这个位置的字符,在pattern里面也存在。
3.pattern的最后一个字符与源字符串的对应位置的字符不相同,同时源字符串的这个位置的字符,在pattern里面不存在。

在上面的示例中,源字符串的o与pattern的'd'并不相同。但是o在pattern里面同样存在。所以,我们可以将pattern前进几个位置,直到pattern的o与源字符串的o在同一个位置,如下:

source string:  Hello, World
search pattern:    World
                       ^

现在两个字符串对应的o都在同一个位置,我们先从pattern的最后一个字符开始进行匹配对比。dW不相同,说明当前仍然不匹配。但是W依然是pattern里面含有的字符,继续前进到相关位置:

source string:  Hello, World
search pattern:        World
                           ^

这一次我们发现pattern与源字符串里面对应的字符完全匹配。这里匹配过程由backward函数检测。

在检索过程中,每一次要跳过的距离由跳过表格决定。在示例中,pattern的跳过表格如下:

W: 4
o: 3
r: 2
l: 1
d: 0

字符在pattern里面距离最后一个字符的距离越长,可以跳过的距离也就越长。如果当前决定跳过距离的字符在pattern里出现多次,即跳过表格里面存在同个字符多个距离,则选择最短的距离进行跳过。

注意:如果需要搜索的pattern只包含少数字符,则直接用暴力检索会更加快速。因为我们需要权衡创建跳过表格消耗的时间和直接暴力检索消耗的时间。

Boyer-Moore-Horspool 算法

Boyer-Moore-Horspool算法是摩尔算法的变形。和摩尔算法一样,此算法也是用跳过表格来跳过不需要检索的字符,不同点在于如何检测部分匹配。在之前的版本中,如果我们发现部分匹配(pattern的最后一个字符与当前位置下源字符里的字符匹配),但是并不是完全匹配,我们选择向前只跳一个字符的距离。而在这个版本中,我们会继续使用跳过表格来决定跳过距离。

以下为Boyer-Moore-Horspool的代码:

extension String {
    func index(of pattern: String) -> Index? {
        // 存储pattern的长度值
        let patternLength = pattern.characters.count
        guard patternLength > 0, patternLength <= characters.count else { return nil }

        // 创建跳过表格
        // 当pattern里的某一个字符被检索到,这个表格可以决定我们可以跳过多少长度
        var skipTable = [Character: Int]()
        for (i, c) in pattern.characters.enumerated() {
            skipTable[c] = patternLength - i - 1
        }

        // 这里得到pattern里最后一个字符.
        let p = pattern.index(before: pattern.endIndex)
        let lastChar = pattern[p]

        // 核对过程是由pattern的右向左,所以我们跳过的长度为pattern的长度
        // 这里扣掉1是因为startIndex已经指向源字符串的首字符位置
        var i = index(startIndex, offsetBy: patternLength - 1)


        // 此函数将源字符串和pattern从指定位置开始对比匹配,当对应位置的字符
        // 不一致时候返回nil,反之返回字符串上面经过匹配对比的最前一位的位置
        func backwards() -> Index? {
            var q = p
            var j = i
            while q > pattern.startIndex {
                j = index(before: j)
                q = index(before: q)
                if self[j] != pattern[q] { return nil }
            }
            return j
        }

        // 主循环.在找到完全匹配时候结束,或者全部检索完依然没有匹配时候返回nil
        while i < endIndex {
            let c = self[i]

            // 检测源字符中当前位置的字符与pattern的最后一位字符是否相同
            if c == lastChar {

                // 当发现可能匹配的时候,进行暴力匹配对比
                if let k = backwards() { return k }

                // 确保至少向前进1个字符的距离,因为我们最早开始检索的时候,第一个可能匹配的字符也出现在跳过表格里面
                //而跳过表格提供的跳过距离为0,如果不限制的话检索无法继续
                let jumpOffset = max(skipTable[c] ?? patternLength, 1)
                i = index(i, offsetBy: jumpOffset, limitedBy: endIndex) ?? endIndex
            } else {
                // 当前字符状态为不匹配,所以需要向前移动,移动的距离由跳过表格决定        
                //如果当前字符与pattern没有任何匹配,则直接前进一个pattern长度的距离。否则,则根据跳过表格的距离决定。
                i = index(i, offsetBy: skipTable[c] ?? patternLength, limitedBy: endIndex) ?? endIndex
            }
        }
        return nil
    }
}

总的来说,此版本的摩尔算法较优于一开始的版本。但是同样的,在使用过程中,仍然需要权衡一下pattern和源字符串的长度。这样算法才能真正的帮助你提高效率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 转自: JS正则表达式一条龙讲解,从原理和语法到JS正则、ES6正则扩展,最后再到正则实践思路 温馨提示:文章很长...
    前端渣渣阅读 1,805评论 1 32
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,226评论 0 4
  • 在挖掘分析的过程当中对字符串的处理是极为重要的,且出现也较为频繁,R语言作为当前最为流行的开源数据分析和可视化平台...
    果果哥哥BBQ阅读 5,806评论 0 8
  • 感谢方玉姐送的照片。 秋,如四季的嫁衣 带点黄绿 留点雨迹 秋,是时间的齿轮 把夏季马上拐冬季 先铺黄了地面 再染...
    采田阅读 188评论 0 4