数据结构基础之字符串匹配

字符串匹配

1.BF算法

给一个while循环,(当i和j都没有走出主串和子串的自身范围)比较i和j指向的字符,如果i和j指向的字符相同,则i++,j++;如果不同,j退回到0,i退回到这一趟匹配失败开始的下一个位置(即开始位置+1)

int BF(const char* str,const char* sub)
{
    //对两个指针断言
    assert(str != NULL);
    assert(sub != NULL);
    int i = 0, j = 0;
    while(i < strlen(str) && j < strlen(sub))
    {
        int k = i;//记录i的开始位置
        if(str[i] == sub[i])
        {
            i++;
            j++;
        }
        else
        {
            i = k + 1;
            j = 0;
        }
    }
    if(j < strlen(sub)) return -1;//循环结束如果j还小于子串(模式串)的长度,说明j没遍历完子串,那么while循环是因为i越界而推出的,也就说明没有匹配到字符串
    else return i - j;
}

2.KMP算法

在每次匹配失败后,主串指针不退回,而是让子串退回到一个合适的位置,这个位置由next数组记录,所以就要找next数组。

int KMP(const char* text, const char* pattern)
{
    //txt_idx为主串遍历索引,ptn_idx为子串遍历索引
    //index是最终返回的匹配位置,初始化-1(未找到)
    int index = -1,txt_idx = 0,ptn_idx = 0;
    int txt_len = strlen(text);
    int ptn_len = strlen(pattern);
    int* next = (int*)malloc(sizeof(int)*ptn_len);//分配next数组大小
    while((txt_inx < txt_len) && (ptn_idx < ptn_len))
    {
        if(text[txt_idx] == pattern[ptn_idx] || ptn_idx == -1)//该索引位置字符相等或者模式串索引退回到-1,则对比下一个位置,当ptn_idx == -1,说明模式串只能重头开始重新匹配了,故ptn_idx++ == 0
        {
            ptn_idx++;
            txt_idx++;
		}
        else
        {
            ptn_idx = next[ptn_idx];//匹配失败了,使用next数组退回模式串索引ptn_idx
        }
    }
    
    if(ptn_idx >= ptn_len)//匹配成功
    {
        index = txt_idx - ptn_len;
    }
    free(next);
    return index;//index初始为-1,所以如果匹配失败了,就不需要改值
}
1.next数组

next数组是KMP算法的核心,用于在模式串匹配失败时,快速确定模式串退回的位置,从而避免主串指针回溯。它的本质是记录模式串每个位置的最长公共前后缀长度。

最长公共前后缀定义

前缀:不包含最后一个字符的所有连续子串

后缀:不包含第一个字符的所有连续子串

最长公共前后缀:前缀和后缀中完全相同的最大长度

2.next数组函数实现
void get_next(const char* str,int* next,int len)
{
    int j = 0;//指向模式串中当前正在处理的字符位置
    int k = -1;//指向模式串中当前最长公共前缀的末尾索引
    //next[0]索引的字符的前面再没有字符了,所以如果next[0]还没有找到的话,-1就表示应该重新遍历模式串了。
    next[0] = -1;
    while(j < len - 1)
    {
        if(k == -1 || str[j] == str[k])
        {
            //k == -1说明没有找到,那么next[j]=k++也就是0的位置,说明退回到字符串开始位置
            k++;
            j++;
            next[j] = k;//记录当前位置的最长公共前后缀长度
        }
        else
        {
            k = next[k];
        }
    }
}

j和k指向的位置是当前正在比较的两个字符。在第三步发现 j和k所指向的字符不同,则k回退到next[0]的位置,若还不相同,则继续执行回退操作。最长公共前缀的区间在[0,j),也就是j之前的字符。注意到j每次自增时都伴随着k的自增,此时要记录下k的值,也就是更新next数组,所以next[j]的值也随之确定。

在这里插入图片描述

3.next数组优化

在原本的next数组中,如果退回后的字符等于当前字符,那么就会导致冗余比较,所以在每次更新next数组前,先检查退回后的字符是否与当前字符相等,如果相等则当前的next值赋予更早的next值。

void get_nextval(const char* str, int* nextval, int len)
{
    int j = 0;
    int k = - 1;
    nextval[0] = -1;
    while(j < len - 1)
    {
        if(k == -1 || str[j] == str[k])
        {
            k++;
            j++;
            //优化:检查回退后的字符是否与当前字符相同
            if(str[j] == str[k])
                nextval[j] = nextval[k];//如果相同,则跳回更早的位置,避免了多余的比较
            else
                nextval[j] = k;
        }
        else
        {
            k = nextval[k];
        }
    }
}

利用上述代码比较主串"aabaabaaf"和子串"aabaaf",关键流程图如下:

这是KMP代码while循环里的if条件判断:

在这里插入图片描述

在第六步发现字符不匹配,于是触发else调用next数组,使子串退回。aabaaf的next数组为{-1 ,0,1,0,1,2},所以退回到b的位置继续比较。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值