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:
 7

// Input:
let animals = ""
animals.indexOf(pattern: "")

// Output:
 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和源字符串的长度。这样算法才能真正的帮助你提高效率。

你可能感兴趣的:(Swift算法-摩尔字符算法Boyer-Moore String Search)