串的模式匹配和KMP算法

  在对字符串的操作中,我们经常要用到子串的查找功能,我们称子串为模式串,模式串在主串中的查找过程我们成为模式匹配,KMP算法就是一个高效的模式匹配算法。KMP算法是蛮力算法的一种改进,下面我们先来介绍蛮力算法。

  蛮力算法使用两个int型变量当做当前匹配位置的指针,我们假设主串的位置指针为i,模式串的位置指针为j。蛮力算法的策略便是在i和j所指的位置的字符相等时,继续向后匹配,当发生失配时,便将i回溯到本次匹配前位置的后一个位置,而将j设置为0,从而对所有位置完成逐一比对,通过观察i和j是否越界判断整体匹配是否成功,若模式串位置指针j越界,显然此前所有位置都已完全匹配,那么也就可以返回i-j也即完全匹配时主串中匹配子串的下标位置。

int brute(char * mString ,char * subString) {
        int i = 0 ,j = 0;
        int m = strlen(mString),
            n = strlen(subString);
        while ( i < m &&j < n) {
            if (mString[i] == subString[j]) {
                i++;
                j++;
            }
            else {
                i -= j - 1;
                j = 0;
            }
        }if (j >= n)
            return i - j;
     if(i>=m)
            return -1;
}

 

  通过观察,我们不难发现,如果主串中有大量与模式串相似的字符,从而每次比对都要比较到模式串的最后一个字符才发生失配,从而在每个比较位置都与模式串进行模式串长度n次的比较,而一共有主串长度m次的比较位置。从而时间复杂度达到了O(m*n)。这种情况在串中字符的种类较少时尤其容易发生,如主串为“00000000000001”,模式串为“00001”。

  显然,在每次发生失配时,i指针都要回溯到原来位置的下一个位置,而j指针则是复位至0,一切又从下一个位置从新开始。然而这种做法其实浪费了大量之前比较时所获得的有用信息。在发生失配时,可以分为两种情况:主串失配字符位置i之前的若干字符如果和模式串中的某个真前缀相同(为了避免丢失信息,必须选择最长的一种情况),那么显然只需将模式串与主串中对应字符对齐,而在当前位置与模式串的这个真前缀后的那个字符进行比较即可;而如果不能找到这样的情况,显然主串失配位置前的字符都无法派上用场,从而直接将模式串的开头与当前位置对齐并进行比较即可(注意,如果这种情况中开头位置依然失配,只需让i自增,从下一个位置开始和整个模式串的匹配即可)。这两种情况中的i指针始终没有回溯。因此在最坏情况下的时间复杂度也不会超过O(m)。

int KMP(char *mString ,char* subString) {
        int i = 0,j = 0;
        int mlen = strlen(mString);
        int slen = strlen(subString);
        int* next = (int*)malloc(sizeof(int)*strlen(subString));
        getNext(subString, next);
        while (i < mlen && j < slen) {
            if (mString[i] == subString[j]) i++, j++;
            else {
                if (j == 0)i++;
                j = next[j];//子串指针前移至最长公共前后缀的下标处
            }
        }
        if(j>=slen)
            return i - j;
        if (i >= mlen)
            return -1;
    }

 

  细心的读者可能发现,这里与蛮力算法不同的是多了一次主串适配位置之前字符与模式串前缀字符的比较操作,这样看来似乎复杂度没有改善,其实不然,由于发生失配时主串当前位置i和模式串当前位置j之前的所有字符必然相等,所以这种比较操作只取决于模式串,也就是说我们只需找出模式串每个位置的最长公共前后缀即可。我们只需在比较之前对模式串进行分析处理,将对应信息存入next[]数组来制表以供查询即可将这种操作简化为O(1)的复杂度。而这种预处理操作实际上只需O(n)的复杂度。

  next[]数组的获取我们可以使用递推的策略完成,由于next[]数组中存放的数值为当前位置最长公共前后缀的长度,也即最长公共前缀之后一个字符的下标位置,显然如果之前位置字符与之前位置的最长公共前缀的下一个字符相同,那么当前位置的最长公共前后缀的长度只需增加一即可(特别的,如果当前位置和当前位置最长公共前后缀的后一个字符相等,说明这次比较必然失败,于是应该取其此处不相等的最长公共前后缀);而如果这两个字符不同,我们希望找到之前字符稍短的一个公共前后缀,也即在之前字符的next[]值上再取一次next[]的值,再比较这个值处的字符和之前位置处字符,如果相等取此值加一即可,如果不相等,则继续循环,由于next[]数组中的值必然比数组内值小,所以循环一直继续下去必然收敛于0。当值为0时。取当前值为0即可。

void getNext(char * string, int * next) {
        int len = strlen(string);
        int i = 0,j = 0;
        next[0] = 0;
        while (j < len) {
            if (i == 0)
                next[++j] = 0;
            if (string[j] == string[i]) {
                i++; j++;
         next[j] = string[j]!=string[i]?i:next[i];//避免出现重复比较
            }
                else
                    i = next[i];
            
        }
    }

纯属个人理解,如有错误,欢迎指出

posted @ 2017-03-29 12:26  &gt;  阅读(4777)  评论(0编辑  收藏  举报