zoukankan      html  css  js  c++  java
  • 字符串匹配算法:KMP与BM

    字符串匹配算法

    朴素思想(暴力)

    任何一种问题,我们都习惯先写出暴力做法,然后再去想如何优化。对于字符串匹配也是如此,话不多说,直接上代码,暴力遍历比较。

    for (int i = 0; i < n; i ++ )
    {
        bool flag = true;
        for (int j = 0; j < n-m+1; j ++ )
        {
            if (text[i + j ] != pattern[j])
            {
                flag=false;
                break;
            }
        }
        if(flag)
            return i;
    }
    

    虽然说暴力做法一直是效率低下的代言词,但对于平常使用来说,这种做法就已经够用了。因为在实际开发中,大部分情况下的主串和模式串的长度都不会太长。并且它思路清晰简单,出问题也好修复。实际上一般 string 的查找函数就是这种做法。

    但是也不能就会这一招就阿尼陀佛,万事大吉了。总有需要优化的时候,计算机之所以在现代社会有着举足轻重的地位,就在于人在不断思考,并优化它的执行方式。缺少了人的思考,它只是一坨笨的不能再笨的铁而已。

    KMP

    朴素做法虽然简单,但主串与模式串的指针都要来回移动。我们去书上找东西一般都是一眼扫过去,有没有什么办法可以让主串的指针不往回走,降低比较的趟数?

    在上面这个例子中,很明显更快的做法是在第一次匹配失败之后,跳过第二次匹配,进行第三次匹配。也就是说,应该向后移动两格而不是一格。KMP的思想就在于利用已知信息,不把"搜索位置"移回已经比较过的位置,继续向后移。

    我们可以利用模式串自身的特点,利用已经匹配过的结果来减少枚举过程,跳过不可能成功的比较,加速匹配过程。画个抽象点的图:

    在主串指针不往后退的前提下, 我们的模式串指针最多回退到哪里就可以继续进行匹配呢?

    如上图,回退之后的字符串肯定是要能与主串匹配成功的。也就是说它与之前的模式串有着交集。从图中可以看到,P串的后缀与P'的前缀相同。那个这个问题:在已有N个匹配成功的结果下,第N+1个字符匹配失败时,模式串应该退回到第?个匹配成功的结果下?也就转换成了:模式串的各个字串中,能使前后缀相等的最大长度是多少?

    我们用next数组来存储回退到的下标

    虽然说1那里应该等于0,但其实这时候已经退无可退了。所以应该退出循环,让主串指针向前走一位,开始新一轮比较。

    利用这个next数组,比如我们比较到下标3的时候发现不匹配,没关系,我们退一步。退一步之后就到了下标1(回退看的next应该是已经匹配成功的那个,也就是看下标2存的是几),如果这时候还不匹配,那我们就会退回到-1,也就是说,这轮匹配失败了,主串可以往前走了。

    看看代码就清楚了

    int strStr(string haystack, string needle)
    {
    	if (!needle.size()) return 0;
    	if (!haystack.size()) return -1;
    	vector<int> next(needle.size());
    	next[0] = -1;
    	//求next数组
    	for (int i = 1, j = -1; i < needle.size(); i++)
    	{
    		while (j >= 0 && needle[i]!=needle[j+1])
    		{
    			//不匹配就退一步看看
    			j = next[j];
    		}
    		if (needle[i] == needle[j + 1])
    		{
    			//匹配成功就继续往后走
    			//看看还能不能匹配成功
    			j++;
    		}
    		next[i] = j;
    	}
    	//开始匹配
    	for (int i = 0, j = -1; i < haystack.size(); i++)
    	{
    		while (j != -1 && haystack[i] != needle[j+1])
    		{
    			//不匹配咱就回退
    			j = next[j];
    		}
    		if (haystack[i] == needle[j+1])
    		{
    			j++;
    		}
    		if (j == needle.size() - 1)
    		{
    			return i-j;
    		}
    	}
    	return -1;
    }
    

    BM

    虽然KMP比较出名,但其实只是因为它比较难懂而已。在效率上有很多的算法都比它要好。现在介绍的BM算法,其效率就要比KMP好上3到4倍。

    BM算法包含两部分:坏字符规则和好后缀规则

    坏字符规则

    BM算法与我们平常接触的字符串比较方法不同,它是按模式串从大到小的顺序,倒着比的。这样做也是有好处的,起码直观上是这样感觉的。就像做算数选择题,出卷老师为了让你花的时间久一点,故意把正确答案放到C跟D上。所以聪明点的做法应该是先算C跟D。这跟这个比较方法有点类似。

    考虑下面这张图:

    别忘了我们是从模式串最大的开始往后匹配,所以这里先比较了C和D。这个时候,有意思的来了,这个 D 在模式串中就没有出现过,是一个坏字符,有它在的字串可能不匹配。

    惹不起还躲不起吗?溜之大吉,模式串直接移动五位,重新匹配。一下移动这么多,是不是特别的爽?

    但你千万不要以为只有没在模式串中出现过的才叫坏字符,实际上这个只是从后往前第一个不匹配的字符。一旦发生不匹配,坏字符规则的做法是模式串指针继续往回走,找到第一个与其匹配的字符停止,然后再继续新一轮的匹配。

    也就是说,移动的距离等于:当前模式串的下标(Si) - 往回走找到的第一个与当前不匹配字符匹配的下标(Xi)。如果没有找到,则减数为-1。

    这么一想,好像用坏字符规则就万事大吉了。但因为实际代码中,我们不会每次不匹配都会往前找,那样太耗费时间,取而代之的是使用散列表纪录不同字符在模式串中“最后出现的位置”,并不是 Si 的位置往前查找的第一个位置,所以可能会出现 Xi 大于 Si 的情况。比如上图的例子,主串"ABABDABABCAB",模式串"ABABC"当从左开始数的一个B不匹配时,找到的A的下标是最后一次在模式串中出现的下标(也就是最后一个a的位置,比A大)。这时候,模式串非但不往前滑动,还回退了。为了解决这个问题,我们需要好后缀来帮忙。(实际上,两个规则都可以独立使用,如果坏字符你是往回遍历而不是保存在散列表里面的话)

    好后缀规则

    当遇到上图的情况时,我们依然可以用坏字符规则来移动,但这次让我们来看看好后缀是如何工作的?

    我们把在主串中已经匹配成功的字符串用 u标记,现在要做的是找到模式串中与其匹配的u*如果找到了,那模式串就滑动到使得u*与u对齐的位置。如果不匹配,那么溜之大吉,直接移动一个模式串长度的位置。虽然一次移动那么多是很爽,但这样做有可能错过可以匹配的情况。

    实际上这里应该跟KMP一样,如果模式串中前缀与好后缀的后缀相同,那么就移动相应位置使其匹配。

    双剑合璧

    两个规则你都知道了,并且他们都可以独立使用,那么到底该选哪一个呢?

    可以参考的建议

    • 如果处理的是字符集很大的字符串匹配问题,那么坏字符规则对内存的消耗会比较多。因为两个规则是独立的,所以可以考虑仅使用好后缀
    • 如果同时使用两个规则,滑动的大小应该是两个规则中最大的那一个,这样可以避免负数的产生

    代码实现

    如前面所说,坏字符如果每次都在模式串中遍历的话,会对性能造成影响。那么有没有什么高效的办法代替遍历呢?

    我们可以将模式串中的每个字符及其下标都存到散列表中,这样就能快速的找到坏字符在模式串的对应下标了。

    先打好坏字符规则

    private:
        static const int SIZE = 256;
        
        //求bc
        vector<int> getBC(string pattern)
        {
            vector<int> bc(SIZE,-1);
            for (int i = 0; i < pattern.size(); i++)
            {
                int ascii = (int)pattern[i];
                bc[ascii] = i;
            }
            return bc;
        }
    
        int strStr(string haystack, string needle)
        {
            if (needle.size() == 0) return 0;
    
            vector<int> bc(SIZE);
            bc = getBC(needle);
            
            int i = 0;
            while (i <= haystack.size() - needle.size())
            {
                int j;
                //别忘了,BM是从后往前匹配哦
                for (j = needle.size() - 1; j >= 0; j--)
                {
                    //不等于咱就要去找滑动位置了
                    if (haystack[i + j] != needle[j])
                        break;
                }
                if (j < 0)
                {
                    //这是匹配成功!我们是从后往前的!!!
                    return i;
                }
                //找到模式串中最近的坏字符
                i = i + j - bc[(int)haystack[i + j]];
            }
            return -1;
        }
    

    好后缀稍微麻烦一点,与KMP类似,我们用一个int数组suffix来保存。其下标表示后缀子串的长度。让我们来明确一下要干的事:在好后缀的后缀子串中,查找最长的、能跟模式串前缀子串匹配的后缀子串。所以存储的应该是在模式串中跟好后缀u相匹配的子串u*的起始下标值。

    等等!我们好像漏了什么?在模式串中查找跟好后缀匹配的另一个子串,并且在好后缀的后缀子串中,查找最长能跟模式串前缀子串匹配的后缀子串。

    suffix数组只能完成前半段话,那前缀子串的问题该如何解决呢?我们只能是再开一个bool数组prefix 来做这件事了。

    求这两个数组的方法也十分讨巧,纪录公共后缀子串u*的起始下标为j,如果j==0,说明是前缀字串(因为已经走到尽头了),prefix为true。代码实现是下面这样:

        //好后缀
        void genGS(string pattern)
        {
            int len = pattern.size();
            for (int i = 0; i < len; i++)
            {
                suffix.push_back(-1);
                prefix.push_back(false);
            }
            for (int i = 0; i < len - 1; i++)
            {
                int j = i;
                //公共后缀u*的长度
                int k = 0;
                while (j >= 0 && pattern[j] == pattern[len - 1 - k])
                {
                    j--;
                    k++;
                    //保存的是在pattern中的起始下标
                    suffix[k] = j + 1;
                }
                if (j == -1)
                    prefix[k] = true;
            }
        }
    

    现在让我们来把两个规则一起用在字符串匹配上

    private:
        static const int SIZE = 256;
        vector<int> bc;
        vector<int> suffix;
        vector<bool> prefix;
    
        //坏字符
        void getBC(string pattern)
        {
            for (int i = 0; i < SIZE; i++)
            {
                bc.push_back(-1);
            }
            for (int i = 0; i < pattern.size(); i++)
            {
                int ascii = (int)pattern[i];
                bc[ascii] = i;
            }
        }
        //好后缀
        void genGS(string pattern)
        {
            int len = pattern.size();
            for (int i = 0; i < len; i++)
            {
                suffix.push_back(-1);
                prefix.push_back(false);
            }
            for (int i = 0; i < len - 1; i++)
            {
                int j = i;
                //公共后缀u*的长度
                int k = 0;
                while (j >= 0 && pattern[j] == pattern[len - 1 - k])
                {
                    j--;
                    k++;
                    //保存的是在pattern中的起始下标
                    suffix[k] = j + 1;
                }
                if (j == -1)
                    prefix[k] = true;
            }
        }
    
        int moveByGS(int j, int len)
        {
            //好后缀长度
            int k = len - 1 - j;
            if (suffix[k] != -1)
                return j - suffix[k] + 1;
            for (int i = j + 2; i < len; i++)
            {
                if (prefix[len = i])
                    return i;
            }
            return len;
        }
    
        int strStr(string haystack, string needle)
        {
            if (needle.size() == 0) return 0;
    
            getBC(needle);
            genGS(needle);
            int i = 0;
            while (i <= haystack.size() - needle.size())
            {
                int j;
                //别忘了,BM是从后往前匹配哦
                for (j = needle.size() - 1; j >= 0; j--)
                {
                    //不等于咱就要去找滑动位置了
                    if (haystack[i + j] != needle[j])
                        break;
                }
                if (j < 0)
                {
                    //这是匹配成功!我们是从后往前的!!!
                    return i;
                }
                //找到模式串中最近的坏字符
                int x =  j - bc[(int)haystack[i + j]];
                
                int y = 0;
                //如果有好后缀
                if (j < needle.size() - 1)
                {
                    y = moveByGS(j,needle.size());
                }
                i = i + max(x, y);
            }
            return -1;
        }
    

  • 相关阅读:
    UnQLite简介
    .net中webconfig自定义配置
    webservice有关application/xop+xml的异常
    .NET4缓存过期策略摘录
    关于sea.js的笔记
    npm笔记和bower
    使用npm安装一些包失败了的看过来(npm国内镜像介绍)
    easyui的datagird动态设置当前页数
    Oracle中Merge into用法总结
    Oracle 12.1.0.2 对JSON的支持
  • 原文地址:https://www.cnblogs.com/AD-milk/p/13040713.html
Copyright © 2011-2022 走看看