一个需求引发的算法及优化(KMP算法)

1.需求

搜索3.jpg

先分析下这个需求,这是一个简单的搜索功能,在你输入一段字符后会得到后端返回的搜索结果,很常见.但是问题是需要将你输入的字符串在搜索结果中变色,那就要得到子串在父串中的位置.

其实就是在一个字符串里面匹配另一个字符串,然后如果匹配成功返回在主串中子串的 startIndex.

2.算法分析

说完需求,再来看算法,其实看完这个需求我就想到在 leetCode 上刷过的一道题 链接.
以下图来分析:

1.png

首先遍历父串,得到与子串中第一个字符相等的 index,然后在遍历子串,比较子串与父串 index 位之后字符是否相等.如果不相等,那么继续遍历父串得到下一个 index, 直到找到子串匹配父串或者没找到退出循环.
这是个简单的思路,就直接贴代码:

/*
 impStr()
 * Time Complexity: O(nm), Space Complexity: O(n)
 */
func impStr(haystack: String, needle: String) -> Int {
    let hChars = Array(haystack.characters)
    let nChars = Array(needle.characters)

    guard hChars.count >= nChars.count else {
        return -1
    }
    guard nChars.count != 0 else {
        return 0
    }

    for i in 0...(hChars.count - nChars.count) {
        // 找到父串中与子串第一个字符相等的 index
        if hChars[i] == nChars[0] {
            for j in 0..<nChars.count {
                // 如果子串某一位和父串不相等,退出循环
                if hChars[i+j] != nChars[j] {
                    break
                }
                // 找到子串匹配父串
                if j + 1 == nChars.count {
                    return i
                }
            }
        }
    }
    return -1
}

小需求搞定,easy!
但是...时间复杂度是O(nm)啊...再优化下?

3.算法优化

再回头看看上面的分析,在子串某一位匹配失败后,父串开始下一次循环,然后之前已经对齐的子串错开,那么父串的匹配必然失败,在上面的算法中没有处理这一块信息,如果有一个很长的子串到最后几位才匹配失败的话,那这个算法的效率就非常低下,毕竟是O(nm)的复杂度.
那有什么办法的?

KMP算法

KMP 算法会利用之前已经匹配成功的部分子串来减少父串的循环次数,当子串匹配失败后,不去让父串继续遍历,而且通过移动子串的 index 来重新开始下一次匹配.

KMP 算法小解

2.png

从上图来分析下 KMP 的流程,在第一次子串的最后一位D与父串E不相等,此时父串ABCDAB与子串的ABCDAB是匹配的,此时父串和子串拥有相同的前缀AB,如果父串下次循环的其实位置就是AB时,就是父串的后缀和子串的前缀对齐,那么下一次匹配开始时已经成功匹配了两个字符,然后继续匹配.
不难看出,这个算法充分利用了匹配好的字符串,减少了父串循环的次数,但是问题是需要去计算匹配成功的字符串的是否存在相同的前缀与后缀,怎么计算之后再说,先看子串移动的位数也就是父串循环的 index的起始位置的偏移量的计算.
父串向右偏移的位数 = 匹配成功的字符串长度 - 已匹配成功的字符串的最大公共前缀后缀长度
上图: 父串向右偏移的位数(4) = = 匹配成功的字符串长度(6) - 已匹配成功的字符串的最大公共前缀后缀长度(2)
那就需要另一个算法来计算一个字符串的最大公共前缀后缀长度,贴一下算法:

func getNext(patternStr: String) -> [Int] {
     let count = patternStr.characters.count
     var k = -1
     var next = Array(repeating: -1, count: count)
     for var j in stride(from: 0, to: count - 1, by: 1) {
        while (k != -1 && patternStr[patternStr.index(patternStr.startIndex, offsetBy: j)] != patternStr[patternStr.index(patternStr.startIndex, offsetBy: k)]) {
            k = next[k]
         }
         k += 1
         j += 1
         next[j] = k
      }
      return next
}

这个函数入参就是子串,得到的一个等于子串 length的字符串,每个 index 是除了当前 character 的最大前后缀长度.

字符串匹配

计算完成 next数组之后,接下来就可以用这个数组去在父串中找到子串的出现位置,假设匹配到 i 位置时,父串匹配了子串的第一个character, 接下来就要比较父串的 i+1和子串的1来匹配,直到出现第j 个位置不匹配,那么就将子串中0..<j的字符串去从 next 数组中找到最长公共前后缀长度.接下来就讲父串的 i偏移 next 数组中得到的 index,然后继续匹配.

   /*
     KMP 算法 字符串匹配
     */
    func strStr(_ haystack: String, _ needle: String) -> Int {
        guard haystack.characters.count >= needle.characters.count else {
            return -1
        }
        guard needle.characters.count != 0 else {
            return 0
        }
        guard haystack.contains(needle) else {
            return -1
        }

        var indexI = haystack.startIndex
        var indexJ = needle.startIndex
        var j = 0

        let next = getNext(patternStr: needle)

        while (indexI < haystack.endIndex && indexJ < needle.endIndex) {
            if j == -1 || haystack[indexI] == needle[indexJ] {
                j += 1
                if indexJ == needle.index(before: needle.endIndex) {
                    return haystack.distance(from: indexJ, to: indexI)
                }
                indexI = haystack.index(indexI, offsetBy: 1)
                indexJ = needle.index(indexJ, offsetBy: 1)
            } else {
                let distance = haystack.distance(from: needle.startIndex, to: indexJ)
                j = next[distance]
                if j == -1 {
                    indexJ = needle.startIndex
                } else {
                    indexJ = needle.index(needle.startIndex, offsetBy: j)
                }
            }
        }
        return -1
    }

KMP 算法的复杂度是 O(m+n),比之前的O(mn)好多了,基本上这个需求也就可以完美收工了.

4.闲话

其实对客户端来说,算法重要不重要呢?能不能用到呢?
其实我的答案是重要,当你遇到一个类似我遇到的这种需求,不管你用了怎么耗时的算法完成,从 UI 的表现来说可能都一样,但是会觉得不够好,还可以去优化,我觉得这才是能让自己去不断进步的一种想法.

5.参考

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
http://blog.csdn.net/yutianzuijin/article/details/11954939/
https://github.com/cbangchen/SwiftAlgorithms

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

推荐阅读更多精彩内容