2025年5月22日 星期四 乙巳(蛇)年 二月廿四 设为首页 加入收藏
rss
您当前的位置:首页 > 电子 > 嵌入式系统

嵌入式算法15-KMP字符匹配算法

时间:03-17来源:作者:点击数:50

摘要:改进的字符串匹配算法–KMP算法

1、问题

嵌入式软件开发时会在某个长字符串中,检索出现指定字符串标识的需求,最常用的库函数就是strstr。其源码有很多版本,常用经典实现方式:

  • char *strstr(const char *str1, const char *str2) {
  • char *cp = (char *) str1;
  • char *s1, *s2;
  • if (!*str2)
  • return ((char *) str1);
  • while (*cp) {
  • s1 = cp;
  • s2 = (char *) str2;
  • while (*s1 && *s2 && !(*s1 - *s2))
  • s1++, s2++;
  • if (!*s2)
  • return (cp);
  • cp++;
  • }
  • return NULL;
  • }

所有以str开头的字符串处理函数,最大限制是对传入的指针内容,中间不能出现‘\0’,否则会导致字符串截止。上面的源码中出现的3处条件判断,只对ASCII码字符串有效。

实际应用中,从一个大数组中找出小数组出现的位置,两个数组的内容可能出现0x00,使用strstr就无法满足需求。

2、暴力查找

根据查找需求,不难实现暴力查找算法,在主串中逐个查找子串第一个数据,相等则匹配第二个;出现不相同的则主串位置回到第一次相等位置加1,而子串回到第一个位置重新开始匹配。

  • #include "string.h"
  • #include "stdio.h"
  • typedef unsigned char uint8_t;
  • /**
  • * @brief brute_force 字符串暴力匹配算法
  • * @param str 主串数据
  • * @param str_size
  • * @param sub 模式串数据,需匹配的关键字数据
  • * @param sub_size
  • * @return
  • * <0 :fail
  • * >=0 :success ,sub在str中出现地址的偏移量
  • */
  • static int brute_force(const uint8_t *str, int str_size,const uint8_t *sub,int sub_size)
  • {
  • int i = 0;
  • int j = 0;
  • if(sub_size==0)
  • {
  • return 0;
  • }
  • if(str_size < sub_size)
  • {
  • return -1;
  • }
  • while(i < str_size && (j < sub_size))
  • {
  • if(str[i] == sub[j])
  • {
  • i++;
  • j++;
  • }
  • else
  • {
  • i = i-j+1;
  • j = 0;
  • }
  • }
  • if(j == sub_size)
  • {
  • return i - j;
  • }
  • else
  • {
  • return -1;
  • }
  • }
  • int main(int argc, char *argv[])
  • {
  • int ret;
  • uint8_t str[]={0x00,0x11,0x00,0x11,0x33,\
  • 0x00,0x11,0x00,0x11,0x44,\
  • 0x00,0x11,0x00,0x11,0x22,\
  • 0x00};
  • uint8_t sub[]={0x00,0x11,0x00,0x11,0x22};
  • ret=brute_force(str,sizeof(str),sub,sizeof(sub));
  • printf("ret=%d\r\n",ret);
  • return 0;
  • }
  • //运行结果
  • ret=10

从上面的暴力查找算法,sub出现在str主串的地址偏移10字节的地方,功能是正常,但效率呢?2次出现比较到最后一个字节才出现不相同,然后重新开始查找,是否可以改进呢?

在字串(模式串)中有多个字符和主串中的若干个连续字符比较都相等,但最后一个字符比较不相等时,主串和模式串地址都需要回退,这就损失了效率。

分析sub子串,其前部分有重复的0x00 0x11,如果在0x22处比较不同,是不需要回退,可以只是回退1个字节继续比较,这样提高检索效率。这个思路,就是本文重点KMP算法。

3、KMP算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串(子串)与主串的匹配次数,以达到快速匹配的目的。

以下数据是16进制,就是为了表明KMP支持任意值的数组查找,不限ASCII。

假设已经查找到中间某个状态,如下表:

主串 00 11 00 11 33 00 11 00 11 -44- 00 11 00 11 22 00
模式串           00 11 00 11 -22-            

使用暴力查找,下一步比较的效果如下:

主串 00 11 00 11 33 00 -11- 00 11 44 00 11 00 11 22 00
暴力             -00- 11 00 11 22          

而使用KMP可以越过前面的:

主串 00 11 00 11 33 00 11 00 11 -44- 00 11 00 11 22 00
KMP                   -00- 11 00 11 22    

数组下标不用回退,继续往前移动比较,效率明显比暴力查找快。

对于KMP中模式串比较如何偏移,需要在检索前根据模式串的前缀生成next数组。

完整KMP算法如下:

  • //自动重建偏移关系表
  • int *get_next(const uint8_t *str1, int length, int *next)
  • {
  • int i, j;
  • i = 0;
  • j = -1;
  • next[0] = -1;
  • while(i < length)
  • {
  • if(j == -1 || str1[i] == str1[j])
  • {
  • ++i;
  • ++j;
  • if(str1[i] != str1[j])
  • {
  • next[i] = j;
  • }
  • else
  • {
  • next[i] = next[j];
  • }
  • }
  • else
  • {
  • j = next[j];
  • }
  • }
  • return next;
  • }
  • /**
  • * @brief brute_force
  • * @copyright 微信公众号【嵌入式系统】
  • * @param str 主检索数据
  • * @param str_size
  • * @param sub 需匹配的关键字数据
  • * @param sub_size
  • * @return
  • * <0 :fail
  • * >=0 :success ,substr中出现的地址偏移量
  • */
  • int kmp(const uint8_t *str, int str_size, const uint8_t *sub, int sub_size)
  • {
  • int i = 0;
  • int j = 0;
  • int next[1025];//大于sub_size
  • get_next(sub, sub_size, next);
  • while(i <= str_size && j <= sub_size)
  • {
  • if(j == -1 || str[i] == sub[j])
  • {
  • i++;
  • j++;
  • }
  • else
  • {
  • j = next[j];
  • }
  • }
  • if(j >= sub_size) //返回值为第一次匹配到该字符串的位置
  • {
  • return i - sub_size -1;//第i个,但从0开始计算下标,需要减1
  • }
  • else
  • {
  • return -1;
  • }
  • }

4、对比

为验证三种查找算法的效率,定义较长的两ASCII字符串,运行10000次,其时间如下图:

KMP的算法明显占优,因为模式串中有循环出现的片段,这种更能体现KMP的效率。

对于不同场景的算法选择,可参考下面的方式:

  • if (主串和模式串内容都是ASCII串)
  • {
  • if(两字符串都比较简短)
  • {
  • strstr
  • }
  • else
  • {
  • strstr or kmp //子串有大量重复片段的更能体现效率,如 ABCDABCDABCDE,否则差不多
  • }
  • }
  • else
  • {
  • if(两字符串都比较简短)
  • {
  • 暴力 or kmp //如果子串前面没有重复片段,而且长度小于255,暴力算法不一定差
  • }
  • else
  • {
  • kmp
  • }
  • }
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门