zoukankan      html  css  js  c++  java
  • KMP算法详解

    说道KMP算法,一定要先指出问题所在之处了,问题如下:

    已知一个字符串a,b
    想找到在字符串a中b首次完整出现的位置。

    听到这个问题,首先想到的就是朴素算法,挨个来进行匹配,然后找到对应的答案。

    朴素算法

    朴素算法很简单,就是挨着遍历,但是会有很多的重复计算,算法复杂度为O(mn)
    代码如下:

    # 朴素算法
    def algorithm1(src, target):
        for i in range(len(src)):
            flag = True
            for j in range(len(target)):
                if i + j >= len(src):
                    return -1
    
                if src[i + j] != target[j]:
                    flag = False
                    break
    
            if flag:
                return i
    
        return -1

    考虑如下字符串,重复的计算就很多了,因为直到最后才最终匹配。

    src:abbacabbacabbacd
    target:abbacd

    如果匹配如上两个字符串,就会出现大量的重复计算,所以才有了之后的优化算法

    KMP算法

    KMP算法跟朴素算法是对朴素算法的一个优化,朴素算法很简单,但是会有大量的重复匹配,而KMP算法就是要把重复的匹配去掉。
    KMP算法有个核心的概念,就是next数组,该数组记录最长共有元素的长度。
    以abcab为例,其next = [0,0,0,1,2]
    说起来比较难以理解,就是部分匹配的值。

    算法思想

    部分匹配的实质就是其实也是KMP算法的核心思想,KMP算法的核心思想在我看来就是减少匹配的次数,将重复的匹配次数去掉,那这个信息该如何去掉呢?
    我们使用两个标志位,一个为主字符串的标志位,一个是模式串的标志位。

    代码如下:

    # KMP 算法
    def algorithm2(src, target):
        l = make_next(target)
        target_length = len(target)
        src_length = len(src)
        src_index = 0
        target_index = 0
        while src_index < src_length:
            while target_index < target_length:
                if src_index + target_index >= src_length:
                    return -1
                if src[src_index + target_index] == target[target_index]:
                    target_index += 1
                elif target_index >= 1:
                    src_index += target_index - l[target_index - 1]
                    target_index = l[target_index - 1]
                else:
                    src_index += 1
                    target_index = 0
    
            if target_index == target_length:
                return src_index
    
        return -1

    先不看next数组的计算方法,直观的来看KMP算法的话,我们就能理解其思想了,KMP算法虽然是2层循环叠加,但是本质上,src_index 和 target_index 都是只进行了单次遍历的,所以算法复杂度为O(m + n)
    比朴素算法的O(mn)快了很多。

    了解了算法思想就该了解next数组的计算原理了

    next数组

    看如下代码

    # next 数组的计算
    def make_next(target):
        l = [0] * len(target)
        flag = 0
        for i in range(1, len(target)):
            if target[i] == target[flag]:
                flag += 1
            else:
                flag = 0
            l[i] = flag
    
        return l

    看代码可能不慎明了,其实这是计算相同的前后缀来的。
    以字符串abcab为例,它的next数组为[0,0,0,1,2],
    其前三个字符没有公共的前后缀,所以都是0,到了第四位,模式串有了一个公共的前后缀,就是a然后其字符长度1,所以next[3] = 1
    到了第五位,其公共的前后缀变成了ab,所以next[4] = 2
    详细列如下:

    next值 模式串 前缀 后缀 公共部分
    2 abcab abca,abc,ab,a bcab,cab,ab,a ab
    1 abca abc,ab,a bca,ca,a a
    0 abc ab,a bc,c

    后面的就不用说了,所有的公共前后缀均为无

    而公共的前后缀可以用来计算下次的src_index的偏移距离,如果模式串在匹配到src_index位置时不匹配了,可以根据之前src_index 的下次偏移的位置应该就是src_index += target_index - l[target_index - 1]
    增量为模式串已经匹配的长度与next数组的在该处值的差

    其实仔细想想就可以理解该处的意义,因为l[target_index - 1]其实代表的是之前最后匹配处的公共匹配值,如果没有匹配,那么偏移的距离则正好应该为target_index

    说的比较复杂,实际上就是有2个问题需要弄清楚

    • 为何src_index的偏移需要增加l[target_index - 1]
    • 为何target_index的偏移需要回到l[target_index - 1]

    target_index的偏移

    先说这个,为何要回到l[target_index - 1]的位置,其实就是因为前面所说道的前后缀的概念。
    当前的位置不匹配了,但是前面的内容其实都是匹配的,而匹配的内容,刚好跟前面有重复的前缀,所以,target_index 需要回到之前前缀的后面的位置,也就是l[target_index - 1]的位置。

    src_index的偏移

    因为l[target_index - 1]的偏移位置已经变化了,所以src_index也必须进行变化,才能够让我们匹配的位置对应起来。

    举例来说:
    假设匹配串为abcaabcaabcab,模式串为abcab,第一次匹配,在src_index = 0,target_index = 4时出现了第一次不匹配,但是abcab的l[target_index - 1] = 1,所以src_index挪移的距离就应该为3,target_index也回到了l[target_index - 1]的位置
    这样就保证了匹配总能够保证最少,可以不进行重复匹配

  • 相关阅读:
    Concept with HTTP API && RPC
    倒排索引
    What is the difference between routine , method , procedure , function ? please explain it with example?
    第一章 计算机系统漫游
    PHP 编译安装
    清空/重置队列
    解决window.location.href参数太长 post提交数据
    linux安装jdk1.8
    Hibernate中对象的三种状态即save(),update(),saveOrUpdate()的使用【转】
    eclipse中的任务标记(TODO、FIXME、XXX)
  • 原文地址:https://www.cnblogs.com/qitian1/p/6461588.html
Copyright © 2011-2022 走看看