经典算法学习——串的模式匹配

在刷题时有一道很经典的题型:从输入的字符串中,找出给定的子字符串的位置。列如:在字符串 “goose is not good” 中找出 “good” 的位置。

暴力循环——朴素的模式匹配算法

不考虑任何优化和性能,我们直接挨个遍历主串和子串(双重循环),依次检查每个字符是否相等,只要输入正确,就一定能检查。

        //查找子串在主串中出现的位置
        static int Index(string S, string T)
        {
            int i = 0;//i用于定位主串的下标
            int j = 0;//j用于定位子串的下标
            while (i < S.Length && j < T.Length)
            {
                if (S[i] == T[j])
                {
                    i++;
                    j++;
                }
                else
                {
                    //主串下标后腿至上次匹配的首位的下一位
                    //如果确定子串的字符是完全互不相等,i可以不用回退
                    i = i - j + 1;
                    j = 0;//子串下标也退回到首位,准备重新匹配
                }
            }
            if (j >= T.Length)
                return i - T.Length;
            else
                return -1;
        }
输出结果:
13

分析一下:如果“good”在首位,那就是一开始就能匹配成功,比如“good idea”中查找“good”,时间复杂度为O(1)。但是如上述例子中所诉good在最后,那就需要从头遍历到尾是最坏的情况则主串需要循环(n-m+1)次(n为主串的长度,m为子串的长度),此时算法的时间复杂度则O((n-m+1)*m)。

KMP模式匹配算法

在上面查找good的案例中我们其实做了一些无用功,当程序查找到goos时,发现该组不能匹配,我们将主串下标移到了第二位 “o” 其实这之后的三次匹配肯定都是无效的,我们可以直接从s的下一位开始匹配,去掉无用的匹配过程。如果主串S=“abcdefgabcdex”(这里可以很长,举例子就写简短一点),匹配子串T="abcdex",如果用匹配算法的话,当匹配到“f”之后,又从第二开始匹配,这时匹配“bcdef”都属于无效匹配,其实可以直接从f这个元素开始匹配。

注意这是KMP算法的关键:

  • 我们已经确定了T串中的字符均不相等,避免回溯。我们已经知道了T中的第二位和主串的第二位相等了,那样就意味着T串中的首位肯定和主串中的第二位不想等。以此类推,下一轮匹配时可以直接从"f"开始了
  • T子串也出现字符相等的情况例如将上述的主串设置为S=“abcabxabcabedefg”需要匹配的子串设置为S=“abcabe”,当匹配到 “x” 前面已经有 “abcab” 已经匹配上了,这个时候主串直接从第四位开始匹配,子串从第三位开始匹配。
  • 难点:怎么确定当局部匹配失败后,下次从哪一位上开始从新匹配呢?KMP算法的难点就在于求最长匹配公共前缀和后缀的长度。把子串的匹配位置变化定义为一个next数组,在求解next数组之前,需要理解一个最长前后缀字符的概念:
    字符串“ABC” :前缀字符串"A",“AB”,“ABC”,后缀字符子串:“C”、“BC”、“ABC”,在计算next数组判断前后缀字符子串的来判断next的值
j012345
子串abcabe
next[j]-100012

分析如下:
(1)当j=0时,设置为默认状态 next[j]=-1;
(2)当j=1时, 此时j由0到j-1的串前后缀没有重复字符next[1]=0;
(3)当j=2时,此时j由0到j-1的串前后缀没有重复字符,next[2]=0;
(4)当j=3时,此时j由0到j-1的串前后缀没有重复字符,next[3]=0;
(5)当j=4时,此时j由0到j-1的串时 “abca” ,前缀字符 “a” 和后缀字符 “a” 相等,因此k值为1,因此next[4]=1;
(6)当j=5时,此时j由1到j-1的串时 “abcab” ,前缀字符 “ab” 和后缀字符 “ab” 相等,因此k值为2,因此next[5]=2;

又例如求T="aaaaab"的next变化数组

j012345
子串aaaaab
next[j]-101234

分析如下:
(1)当j=0时,next[0]为默认的-1;
(2)当j=1时,此时j由1到j-1的串前后缀没有重复字符next[1]=0;
(3)当j=2时,此时j由1到j-1的串 “aa”, 前后缀字符子串均为**“a”,因此next[2]=1;
(4)当j=3时,此时j由1到j-1的串
“aaa”**,前后缀字符子串均为 “a”, “aa”, 因此next[3]=2

next[j],从第1位开始,实际上就是j由0到j-1的最长公共子串的长度。
KMP 算法的核心,先求出 next 数组:

        static int[] get_next(string T)
        {
            int[] next = new int[T.Length];

            int i = 0;
            int j = -1;
            next[0] = -1;
            while (i < T.Length-1)
            {
                if (j == -1 || T[i] == T[j])
                {

                    j++;
                    i++;
                    next[i] = j;

                }
                else
                {
                    j = next[j];
                      
                }
            }

            return next;
        }
输入:“aaaaab”
输出: -1  0  1  2  3  4

在确定了next数组后,我们就可以将朴素的模式匹配进行优化:

        static int Index_KMP(string S, string T)
        {
            int i = 0;//i用于定位主串的下标
            int j = 0;//j用于定位子串的下标
            var next = get_next(T);
            while (i < S.Length && j < T.Length)
            {
                if (j==0||S[i] == T[j])
                {
                    i++;
                    j++;
                }
                else
                {
                   //相比暴力的解法,KMP的主串不会回退
                    j = next[j];//子串下标也退回到合适的位置,准备重新匹配
                }
            }
            if (j >= T.Length)
                return i - T.Length;
            else
                return -1;
        }

算法分析: 计算next数组时候只涉及了简单的循环因此时间复杂度为O(m),由于在KMP算法中i值不回溯,因此在主串中查找匹配的时候时间复杂度为O(n),因此整体的时间复杂度为O(m+n)相对暴力的模式匹配的时间复杂度O((n-m+1)*m)来说相对好些。不过KMP算法在主串和子串之间存在许多“部分匹配”的情况下才能体现它的优势,如果子串的字符是互不相等的情况,朴素的模式匹配和KMP模式匹配的差异并不明显。

Next数组还有优化空间,当子串类似“aaaab”存在多个连续相同的字符是,next数组回溯还是存在无用功。只需要将核心逻辑添加一个判断即可:

                    if (T[i] != T[j])
                        next[i] = j;
                    else
                        next[i] = next[j];
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值