字符函数和字符串函数的模拟实现及KMP算法(下)

简介: 字符函数和字符串函数的模拟实现及KMP算法

总结

每次匹配失败后,子串回到起始位置,主串回到上次匹配的起始位置的下一个位置。

注意事项:被查找的主串和子串都不能为空串,且都要以"\0"结尾。如果查找成功则返回主串中子串所在位置的地址,查找失败则返回空指针。

KMP算法

strstr每次匹配失败子串都要回到起始位置,主串则是这个不行那就换下一个位置。这样的效率就很低,因为主串中的每一个位置都被尝试过,而子串也被遍历了很多遍。而KMP的一个优化就在于,匹配失败后主串的位置保持不动,而子串的位置回到一个特定位置(可能是起始位置也可能不是)。

引入

那么假设能在已经成功匹配的字符串中找到一个最大程度相同的串,那么如何确定子串回退的位置?

下面是两个例子:

这两个例子看起来好像很凑巧,哎,就是凑巧,就是玩~。其实关于子串回退的位置,KMP给定了一个next数组用于保存子串在某个位置匹配失败后应该回退的位置。

next数组

用next[ j ]=k 来表示子串在某个位置匹配失败应该回退的位置。其中 j 是子串匹配失败的位置,而 k 是子串在j位置匹配失败后应该回退的位置。

k值的求法

1.找到匹配成功部分两个相等的真子串(不包含本身),一个以下标0开始,一个以下标 “j-1 ”结束。

2.不管什么样的数据,规定:next[ 0 ]=-1,next[ 1 ]=0.

举例求next数组:

解析:

注意事项

这里说的相同的字符串,可以有部分内容是重叠的。但必须要是从 0 下标开始到 “j-1 ”下标结束。

k的值是以能找到的最大程度相同字符串的长度为准。

此时我们已经学会如何求next数组了,那么假设我们知道next[ i ]=k,我们如何求next[ i+1]?

这里就要用一点数学来推导了:

这里很凑巧的是子串的P[ i ]==P[ k ],那么如果子串的P[ i ]!=P[ k ]我们又如何知道next[ i+1 ]的值呢?

如果P[ i ] ! = P[ k ],那么可以说,当前的k位置的元素一定不是我们要找的,因此还要继续回退,即 k = next[ k ] ,也就是将next[ k ]的值赋给next,直到p[ i ] == p[ k ]。

值得注意的是,这些推导都是在next[ i ] = k的前提下才存在的。

next数组就是KMP算法的精髓,因为next数组的存在,子串不必每次都从头再来,主串也不必走回头路。学会了next数组的求法,就已经是掌握了KMP思想,接下来就是代码实现了。

代码实现

KMP代码:

//str代表主串
//sub代表子串
//pos代表从主串的pos位置开始查找
int KMP(char* str, char* sub, int pos)
{
  assert(str && sub);
  int lenStr = strlen(str);
  int lenSub = strlen(sub);
  if (lenStr == 0 || lenSub == 0)
    return -1;
  if (pos<0 || pos>lenStr)
    return -1;
  int* next = (int*)malloc(sizeof(int) * lenSub);
  assert(next);
  GetNext(sub,next, lenSub);
  int i = pos;//遍历主串
  int j = 0;//遍历子串
  while (i < lenStr && j < lenSub)
  {
    if (j==-1||str[i] == sub[j])//如果第一次匹配就失败,但next[0]=-1,此时j为-1,但其实只需要回退到0位置,sub[-1]越界为了避免这种情况,当j为-1时,直接放进去++。
    {
      i++;
      j++;
    }
    else
    {
      //匹配失败,i不回退,j回退到next[j]的位置
      j = next[j];
    }
  }
  if (j >= lenSub)
    return i - j;
  return -1;
}

这里需要留意的一点就是:规定next数组下标为0处的值为负一,但是子串从负一位置开始访问就是越界了。因此当 j 为负一时应该直接进入循环。

GetNext实现:

void GetNext(char* sub, int* next,int lenSub)
{
  next[0] = -1;
  next[1] = 0;
  int i = 2;
  int k = 0;//是钱一项的k
  while (i < lenSub)
  {
    if(k==-1||sub[i - 1] == sub[k])//本来是sub[i]==sub[k],但是这个i是已经自增一次后得到的i
    {
      next[i] = k + 1;
      i++;
      k++;
    }
    else
    {
      k = next[k];//sub[i-1]!=sub[k],要继续回退,此时的k应该等于当前k位置对应的next数组的值
    }
  }
}

这里同样有需要注意的地方:之前是已知next[ i ]的值求next[ i+1 ]。但这里是已知next[ i-1 ]求next[ i ]的值。

完整代码:

void GetNext(char* sub, int* next,int lenSub)
{
  next[0] = -1;
  next[1] = 0;
  int i = 2;
  int k = 0;//是钱一项的k
  while (i < lenSub)
  {
    if(k==-1||sub[i - 1] == sub[k])//本来是sub[i]==sub[k],但是这个i是已经自增一次后得到的i
    {
      next[i] = k + 1;
      i++;
      k++;
    }
    else
    {
      k = next[k];//sub[i-1]!=sub[k],要继续回退,此时的k应该等于当前k位置对应的next数组的值
    }
  }
}
int KMP(char* str, char* sub, int pos)
{
  assert(str && sub);
  int lenStr = strlen(str);
  int lenSub = strlen(sub);
  if (lenStr == 0 || lenSub == 0)
    return -1;
  if (pos<0 || pos>lenStr)
    return -1;
  int* next = (int*)malloc(sizeof(int) * lenSub);
  assert(next);
  GetNext(sub,next, lenSub);
  int i = pos;//遍历主串
  int j = 0;//遍历子串
  while (i < lenStr && j < lenSub)
  {
    if (j==-1||str[i] == sub[j])//如果第一次匹配就失败,但next[0]=-1,此时j为-1,但其实只需要回退到0位置,sub[-1]越界为了避免这种情况,当j为-1时,直接放进去++。
    {
      i++;
      j++;
    }
    else
    {
      //匹配失败,i不回退,j回退到next[j]的位置
      j = next[j];
    }
  }
  if (j >= lenSub)
    return i - j;
  return -1;
}
int main()
{
  printf("%d\n", KMP("ababcabcdabcde", "abcd", 0));//5
  printf("%d\n", KMP("ababcabcdabcde", "abcdef", 0));//-1
  printf("%d\n", KMP("ababcabcdabcde", "ab", 0));//0
  return 0;
}

注意:返回值是子串在主串中的位置。

内存操作函数

前面的strcpy,strcat,strcmp,strncpy,strncat,strncmp,strstr等都是字符串函数,也就是说它们只能对字符串进行操作。如果要对其他类型操作就需要学习内存操作函数。常见的内存操作函数有memcpy,memmove,memcmp,memset。因为内存操作函数可以操作的类型很多,我们不会知道使用者会给我们传什么类型的参数,因此我们在实现时通常将函数参数设定为泛型指针,在实现功能时以字节为单位。

memcpy

函数功能

内存拷贝,将一块内存中的num个字节拷贝到另外一块内存中。常用来处理空间不重叠的数据拷贝。

函数参数

void * memcpy ( void * destination, const void * source, size_t num );
# void* 函数返回值,返回dest内存空间的地址;
# void* destination 目标内存空间地址;
# void* source 源空间地址;
# size_t num 要拷贝的字节数;

模拟实现

void* my_memcpy(void* dest, const void* src, size_t num)
{
  assert(dest && src);
  char* ret = dest;
  //要将src中num个字节拷贝到dest中,因为不确定num的大小,所以我们一个字节一个字节的拷贝是最正确的选择
  while (num--)
  {
    *(char*)dest = *(char*)src;
    dest = (char*)dest + 1;
    src = (char*)src + 1;
  }
  return ret;
}

注意事项

在C语言标准中,memcpy只用来拷贝非重叠空间的数据。如果拷贝重叠空间的内容,由于mempy是由前向后拷贝的,这就会导致数据覆盖覆盖的情况发生,最后得到的结果不是我们想要的。C语言为了处理重叠的数据空间,给定了memmove函数。

memove

函数功能

内存移动,将一块内存数据的num个字节的内容向指定的位置移动。常用来处理重叠空间的数据拷贝。

函数参数

# memmove 函数的参数和 memcpy 函数完全相同
void * memmove ( void* destination, const void * source, size_t num );

模拟实现

memove的参数与memcpy的参数相同,那么memmove是如何实现重叠空间的拷贝的呢?

前面提到memcpy是从前向后拷贝的,而memmove之所以能处理重叠空间的拷贝,是因为memmove会根据情况的不同选择由前向后拷贝还是从后向前拷贝。

总结:src < dest就从前向后拷贝,其他都从后向前拷贝

代码实现:

void* my_memmove(void* dest, const void* src, size_t num)
{
  assert(dest && src);
  char* ret = dest;
  //总结就是分两种情况,src<dest就从前向后拷贝,其他都从后向前拷贝
  if (dest < src)
  {
    while (num--)
    {
      *(char*)dest = *(char*)src;
      dest = (char*)dest+1;
      src = (char*)src + 1;
    }
  }
  else
  {
    //从后向前拷贝,是指从最后一个字节开始,起始位置+(--num)就是最后一个字节
    while (num--)
    {
      *((char*)dest + num) = *((char*)src + num);//num在递减,每次加num就能直接实现从最后一个字节往前移动
    }
  }
}

memset

函数功能

内存设置,把一块内存中num个字节的内容设定为指定的数据。

函数参数

void *memset( void *dest, int c, size_t count );
# void* 函数返回值,返回目标空间的地址;
# int c 函数参数,指定你想要初始化的数据;
# size_t count 函数参数,指定初始化的字节数

模拟实现

void* my_memset(void* dest, int k, size_t num)
{
  assert(dest);
  char* ret = dest;
  //将dest的num个字节全部初始化为k
  while (num--)
  {
    *(char*)dest = (char)k;
    dest = (char*)dest + 1;
  }
  return ret;

memcmp

函数功能

内存比较,比较两块内存中前num个字节内容的大小。

函数参数

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
# int 函数返回值;
# void* ptr1 void* ptr2 要比较的两块内存;
# size_t num 要比较的字节数;
#函数返回值
>0 : ptr1 大于 ptr2;
=0 : ptr1 等于 ptr2;
<0 : ptr1 小于 ptr2;··

模拟实现

int my_memcmp(const void* str1, const void* str2, size_t num)
{
  assert(str1 && str2);
  int count = 0;
  while (count < num&& *(char*)str1 == *(char*)str2)
  {
    count++;
    str1 = (char*)str1 + 1;
    str2 = (char*)str2 + 1;
  }
  if (count == num)
    return 0;
  else
    return (*(char*)str1 - *(char*)str2);
}

相关文章
|
1天前
|
数据采集 算法 JavaScript
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
JavaScript字符串搜索涵盖`indexOf`、`includes`及KMP算法。`indexOf`返回子字符串位置,`includes`检查是否包含子字符串。KMP是高效的搜索算法,尤其适合长模式匹配。示例展示了如何在数据采集(如网页爬虫)中使用这些方法,结合代理IP进行安全搜索。代码示例中,搜索百度新闻结果并检测是否含有特定字符串。学习这些技术能提升编程效率和性能。
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
|
14天前
|
算法 Java C语言
Java中的算法与C语言中的函数
Java中的算法与C语言中的函数
12 2
|
14天前
|
算法 调度 C++
【调度算法】共享函数vs拥挤距离
【调度算法】共享函数vs拥挤距离
21 1
|
19小时前
|
算法 Java
KMP算法详解及其在字符串匹配中的应用
KMP算法详解及其在字符串匹配中的应用
|
2天前
|
人工智能 算法 BI
一篇文章讲明白KMP算法(俗称看毛片算法)
一篇文章讲明白KMP算法(俗称看毛片算法)
|
6天前
|
算法 vr&ar
技术好文共享:遗传算法解决函数优化
技术好文共享:遗传算法解决函数优化
|
9天前
|
算法 Java
Java数据结构与算法:字符串匹配算法之KMP算法
Java数据结构与算法:字符串匹配算法之KMP算法
|
12天前
|
算法 C语言 Python
简单遗传算法优化简单一元函数(python)
简单遗传算法优化简单一元函数(python)
12 0
|
3天前
|
算法 安全 数据库
基于结点电压法的配电网状态估计算法matlab仿真
**摘要** 该程序实现了基于结点电压法的配电网状态估计算法,旨在提升数据的准确性和可靠性。在MATLAB2022a中运行,显示了状态估计过程中的电压和相位估计值,以及误差随迭代变化的图表。算法通过迭代计算雅可比矩阵,结合基尔霍夫定律解决线性方程组,估算网络节点电压。状态估计过程中应用了高斯-牛顿或莱文贝格-马夸尔特法,处理量测数据并考虑约束条件,以提高估计精度。程序结果以图形形式展示电压幅值和角度估计的比较,以及估计误差的演变,体现了算法在处理配电网状态估计问题的有效性。
|
7天前
|
机器学习/深度学习 自然语言处理 算法
m基于深度学习的OFDM+QPSK链路信道估计和均衡算法误码率matlab仿真,对比LS,MMSE及LMMSE传统算法
**摘要:** 升级版MATLAB仿真对比了深度学习与LS、MMSE、LMMSE的OFDM信道估计算法,新增自动样本生成、复杂度分析及抗频偏性能评估。深度学习在无线通信中,尤其在OFDM的信道估计问题上展现潜力,解决了传统方法的局限。程序涉及信道估计器设计,深度学习模型通过学习导频信息估计信道响应,适应频域变化。核心代码展示了信号处理流程,包括编码、调制、信道模拟、降噪、信道估计和解调。
29 8