串和 KMP 模式串匹配算法

按书本定义,串为字符串的简称,可以在 C 中以字符数组的方式进行存储。

结构体定义

typedef struct Str Str;

struct Str{
    int len; /* 字符串长度 */
    char *ch; /* 定义为指针而非固定大小的数组可以在程序中按实际情况分配,更加具有普遍性 */
};

基本操作

/* 创建空串 */
Str *createStr(void)
{
    Str *s = (Str *)malloc(sizeof(Str));
    
    if (!s) /* 当 s == NULL时,代表内存不足,此时不能对 s 进行初始化操作 */
        return NULL;
    else{
        s->len = 0;
        s->ch = NULL;
    	return s;
    }
}

/* 判空 */
int isEmpty(Str *s)
{
	return s->len == 0;
}

/* 拷贝,string -> s */
int strCpy(Str *s, char *string)
{
    int i, len;
    
    for (len = 0; string[len] != '\0'; len++)
        ;
    s->len = len;
    if (len){
        if (str->ch)
            free(str->ch);
		s->ch = (char *)malloc(sizeof(char) * (len+1)); /* len+1 才能最后放'\0' */ 
        if (!s->ch){
			return 0;
        }
        else{
            i = 0;
            do{
                s->ch[i] = string[i];
            }while (string[i++] != '\0');
    	}
    }
    return 1;
}

int getLength(Str *s)
{
    return s->len;
}

模式串匹配算法

题:现有一主串和一模式串,分别命名为 text 和 pattern,要求编写一函数:在模式串首次匹配时返回对应下标,若没有匹配,则返回 -1。

暴力解法

算法思路:用 i,j 分别指向主串和模式串,初始化指向串的起点,每次从 i 处开始和模式串进行匹配,这里使用 k 在匹配的循环中替代 i,以保证 i 始终指向开始匹配的位置。

k 和 j 所指向的字符若相等,则二者均后移一位继续匹配,当发生越界 or 不匹配时停止,停止后检查匹配的字符数(j 的大小同时也是匹配的个数)是否等于模式串长度,若等于,则代表匹配成功,返回 i。

若匹配失败,j 回到起点(j = 0),i 相对于初始匹配的位置向后移(其实在匹配完后 i == k - j,可以利用这点少设置一个变量 k),重新匹配直到主串末尾。

该算法可以想象成主串固定不动,刚开始时主串和模式串的左端对齐,模式串每次匹配失败后,整个串向右移动一位继续从最开始重新匹配。以主:“aaaab”,模式:"aab"为例,见下图。

image-20211021210143998

可优化的点:

  • 主串不必到末尾停止,若主串长度为10,模式串长度为3,则 i 指向 10-3+1 时必定不匹配(i 从 0 开始),因为剩余的字符只有两个。
  • 形式上可以更简洁一点。
int matchedIndex(Str *text, Str *pattern)
{
    int i; /* 主串当前匹配位置 */
    int j; /* 模式串当前匹配位置 */
    int k; /* 用于主串匹配成功时移动 */
    
    if (!text->len || !pattern->len)
        return -1;
    
    for (i = 0; i < text->len - pattern->len + 1; i++){
        k = i;
        for (j = 0; k < text->len && j < pattern->len && text->ch[k] == pattern->ch[j]; k++, j++)
			;
        if (j == pattern->len)
			return i;
        // i == k-j
    }
    return -1;
}

/* 原理相同,实现形式有些许差别,主要在 i 和 k 的角色上 */
int matchedIndex(Str *text, Str *pattern)
{
    int i = 0; /* 主串当前匹配位置 */
    int j = 0; /* 模式串当前匹配位置 */
    int k = i; /* 用于主串匹配失败后重置 i */
    
    if (!text->len || !pattern->len)
        return -1;
    
    while (i < text->len && j < pattern->len){
        if (text->ch[i] == pattern->ch[j]){
            i++;
            j++;
        }else{
            j = 0;
            i = ++k; /* 若想提前终止则需要判断 k < text->len - pattern->len + 1,此处省去 */
        }
    }
    
    if (j == pattern->len)
        return k;
    else
        return -1;
}

KMP 算法

关于暴力解法,举一例说明:

主串:“aaaab”,模式串:“aab”,按照暴力算法进行匹配,会产生如下结果(序号对应轮次,为了方便讲解,这里下标从 1 开始):

  1. 从第 1 个开始匹配,在模式串的第 3 个位置发生不匹配,此时主’a’,模式 ‘b’ ,重置,i 后移为 2,j 置为 1。
  2. 从第 2 个开始匹配,依旧在模式串的第 3 个位置发生不匹配,此时主’a’,模式 ‘b’ ,重置,i 后移为 3,j 置为 1。
  3. 从第 3 个开始匹配,匹配成功,返回 3。

思考一下上方有什么可以优化的地方。

可以看到,每一次匹配失败,主串和模式串的对应“指针”都需要重置,且每次模式串都需要从头开始,时间复杂度为o(mn)。而每次从头开始会出现:在主串已经比较过的地方多次比较同一字符,这一现象的主要原因归于在模式串的第 k (k>2) 项发生不匹配时,前 k-1 项存在公共前后缀。PS:虽说“前”字在前,但这里的重点在于“后”。

这里举例说明一下公共前后缀(注意,不要理解为回文字符串 & 前缀不包括最后一个字符,后缀不包括第一个字符)

  • “aba” 的前缀为 “a”,“ab”,后缀为 “ba”,“a”。公共最大前后缀为 “a”。

  • “abab” 的前缀为 “a”,“ab”,“aba”,后缀为 “bab”,“ba”,“a”。公共最大前后缀为 “ab”

先走一遍KMP的匹配流程,不同于暴力算法,其更像是模式串每次匹配失败后根据当前匹配的结果选择向右移动的次数。

数据与图部分截取于Donald E. Knuth**, James H. Morris, Jr.**, and Vaughan R. Pratt的论文 FAST PATTERN MATCHING IN STRINGS*,他们正是KMP算法的创始者,以下是可供在线查看的论文地址:
https://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Knuth77.pdf

设输入的文本"abcbabcabcaabcabcabcacabc"在数组 text[1:n] 中,模式串"abcabcacab"位于 pattern[1:m],text[k]表示当前文本字符,pattern[j]表示对应的模式字符(注:下标从 1 开始):

image-20211023203413435

(时间原因,因为考纲中不含串,就先直接走代码了。

next 数组是根据模式串生成的,next[i]意义为若第 i 个元素不匹配,则可以跳至 next[i] 处再进行匹配,原理是next[i] = 前 i-1 项的最大公共字符串的长度(设为 len)+1,为了描述的直观性,下标从 1 开始(若从 0 开始,则不需要+1,因为此时next[i] = len就刚好指向下次需要匹配的位置),所以想求得 next[i+1],只需要求出前i项得最大公共前后缀即可。遵循以下三个规则:

  1. 首个元素为特殊位置,设置为0(注意:j 始终指向前 i-1 项中最长公共前缀的后一位)。

  2. 第 2 个位置处发生不匹配时,必然不存在公共前后缀,len 为 0,根据 next[2] = len+1 赋值为 1。

  3. 根据上面两条,便已求得next[]数组的前两位,不妨讨论更普遍的情况。

    因为是从左到右去求的,根据之前求得的 next[i] ,可以获取前 i-1 个字符的最大公共前后缀长度 len 的信息。j = next[i],在已知 next[i] 的情况下求 next[i+1] 可以遵循下面两步:

    (1) 只需要对比 pattern[j] 是否等于 pattern[i],若相等,则 next[i+1] = j+1。

    (2)若不等,则令 j = next[j],重复(1),若 j = 0,直接令 next[i+1] = 1。

    依据是,如果前 i 项存在公共前后缀,j = next[i],其最长公共前缀和后缀分别为:p1p2…pj-1和pi-j+2pj-r+3pi,若pj和pi+1相同,则代表前 i+1 项的最长公共前后缀长度 len 为 j(也即 next[i]),故next[i+1] = len+1 = next[j]+1 = j+1。若不同,取第二长公共前后缀进行对比(对应于代码就是令 j = next[j]),直至 j=0。

/* 获取next数组 */
void getNext(Str *pattern, int *next){
    int i = 1, j = 0;
    next[1] = 0; /* 1 */
    
    while (i < pattern->len){
        if (j == 0 || pattern->ch[i] == pattern->ch[j]){
            i++;
            j++;
            next[i] = j;
        }else{
            j = next[j];
        }
    }
}

int KMP(Str *text, Str *pattern)
{
    int i,j;
    int *next = (int *)malloc(sizeof(int) * (pattern->len+1)); // +1 是因为从 1 开始存储
    
    i = j = 1;
    getNext(pattern, next);
    while (i <= text->len && j <= pattern->len){
        if (j == 0 || text->ch[i] == pattern->ch[j]){
            i++;
            j++;
        }else{
            j = next[j];
        }
    }
    if (j > pattern->len)
        return i-j+1;
    else
        return 0;
    
}

附 main 实现

两个下标不同写起来还挺混乱的,我更倾向于 0 开始,不过既然 KMP 论文从 1 开始,也就从善如流了。

int main(void){
    Str *text = createStr();
    Str *pattern = createStr();
    
    strCpy(text, "1235423221321");
    strCpy(pattern, "321");
    printf("暴力算法(下标从0开始):%d\n", matchedIndex(text, pattern));
    
    strCpy(text, "01235423221321"); 
    strCpy(pattern, "0321"); /* 折中的一种方法,只是为了演示,其实不严谨,因为根据结构体定义,此时的pattern->ch等于4,会导致next数组长度多1,可以修改strCpy()使从下标1开始拷贝 */
    printf("KMP(下标从1开始):%d\n", KMP(text, pattern));
}

KMP 的改进

感觉考研辅导书中这种标题描述不太严谨,毕竟人家论文本来就是这么写的 : )

原理就是对比过的字符不需要再次对比,也就是说 pattern[j] 若等于 pattern[next[j]],则可以跳过这次比较,用nextval数组记录下次比较的位置。

/* 基于 pattern 和 next */
void getNextval(Str *pattern, int *next, int *nextval)
{
    int i, j;
    
    nextval[1] = 0;
    j = ;
    while (j <= pattern->len){
        if (pattern->ch[j] == pattern->ch[next[j]])
            nextval[j] = nextval[next[j]];
        else
            nextval[j] = next[j];
        j++;
    }
}

/* 直接基于 pattern */
void getNextval(Str *pattern, int *next, int *nextval)
{
    int i = 1, j = 0;
    nextval[1] = 0;

    while (i < pattern->len){
        if (j == 0 || pattern->ch[i] == pattern->ch[j]){
            i++;
            j++;
            if (pattern->ch[i] != pattern->ch[j])
	            nextval[i] = j;
            else
                nextval[i] = nextval[j];
        }else{
            j = nextval[j];
        }
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hoper.J

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值