算法笔记(一)——KMP算法

简介: 算法笔记(一)——KMP算法

暴力匹配(BF)算法


基本概念


BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。

分析BF算法


光看定义晦涩难懂,接下来我将举例子与大家一起学习:

假定我们给出字符串"ababcabcdabcde"做为主串,然后给出子串"abcd",现在我们需要查找子串是否在主串中出现,出现返回主串中第一个匹配的下标,失败返回-1.

image.png

对于这个问题我们很容易想到:从左到右依次匹配,如果字符相等,都向后移一位,如不相等, 子串从0下标重新开始,主串向右移动一位(假设原来从0下标开始,下次从1下标开始)

我们可以这样初始化:

image.png

根据我们的想法,之后我们就需要比较i指针和j指针指向的数字是否一致,如果一致都向后移动,如果不一致,如下图:

image.png

b和d不相等,那就把i指针回退到刚刚指针的下一个位置(刚刚指针是从0下标开始的),j指针退回到0下标重新开始。

image.png

代码实现


根据以上的分析,下面我们开始写代码:

C代码:

#include<stdio.h>
#include<string.h>
#include<assert.h>
int BF(char* str1, char* str2)
{
  assert(str1 != NULL && str2 != NULL);
  int len1 = strlen(str1);//主串的长度
  int len2 = strlen(str2);//子串的长度
  int i = 0;//主串的起始位置
  int j = 0;//子串的起始位置
  while (i < len1 && j < len2)
  {
    if (str1[i] == str2[j])
    {
      i++;//相等i和j都向后移动一位
      j++;
    }
    else {//不相等
      i = i - j + 1;//i回退
      j = 0;//j回到0位置
    }
  }
  if (j >= len2) {//子串遍历玩了说明已经找到与其匹配的子串
    return i - j;
  }
  else {
    return -1;
  }
}
int main()
{
  printf("%d\n", BF("ababcabcdabcde", "abcd"));//测试,为了验证代码是否正确尽量多举几个例子
  printf("%d\n", BF("ababcabcdabcde", "abcde"));
  return 0;
}

java代码:

public class Test {
public static int BF(String str,String sub) {
if(str == null || sub == null) return -1;
int strLen = str.length();
int subLen = sub.length();
int i = 0;
int j = 0;
while (i < strLen && j < subLen) {
if(str.charAt(i) == sub.charAt(j)) {
i++;
j++;
}else {
i = i-j+1;
j = 0;
}
} i
f(j >= subLen) {
return i-j;
} r
eturn -1;
} 
public static void main(String[] args) {
System.out.println(BF("ababcabcdabcde","abcd"));
System.out.println(BF("ababcabcdabcde","abcde"));
}
}

牛刀小试


通过上面的学习,对BF算法有了初步的认识,为了更深刻的了解和运用,下面和大家一起完成以下试题:

试题在这里>>实现strStr()

感兴趣的伙伴可以去做试试,下一章我们进行共同探讨;

BF算法的时间复杂度


最好的情况就是从第一次开始就匹配成功时间复杂度为O(1);

image.png

最坏的情况就是每次都匹配到最后一个才发现与主串不相同,比如"aaaaab",子串”aab"

image.png

image.png

image.png

image.png

看上图,除了最后一次,其余的都是每次匹配到最后,才发现,啊,我们不一样。


这种情况下,上图中,模式串在前 3 次,每次都要匹配 3 次,并且不匹配,直到第 4 次,全部匹配,不需要继续移动,所以匹配的次数为(6 - 3 + 1)* 3 = 12 次。


由此可知,对于主串长度为 n,模式串长度为 m ,最坏情况下的时间复杂度为 O((n - m + 1) * m) = O(n * m)。

相信善于思考的小伙伴会发现,如果是为了寻找的话,根本不需要将i移动到1的位置,因为前面几个字符都是匹配的,再将i移动到1的位置,j移动到0位置,位置错开了,显然也不会匹配,那么我们能不能丢掉以上没必要的步骤,减少指针回溯进行算法简化囊,有一个想法,i位置不动,只需要移动j位置,由此引出我们今天的主人公kmp算法.

KMP算法


基本概念


KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。


区别:KMP 和 BF 唯一不一样的地方在,我主串的 i 并不会回退,并且 j 也不会移动到 0 号位置

分析KMP算法


假定我们给出字符串"ababcabcdabcde"做为主串,然后给出子串"abcd",现在我们需要查找子串是否在主串中出现,出现返回主串中第一个匹配的下标,失败返回-1.

1.首先举例说明,为什么主串不回退

image.png

2.j的回退的位置

image.png

那么j是怎么回退到下标2的位置的囊?下面我们引出next数组

引出next数组


KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,不同的 j 来对应一个 K 值,这个 K 就是你将来要移动的 j要移动的位置。而 K 的值是这样求的:

  • 规则:找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标 0 字符开始,另一个以 j-1 下标字符结尾。
  • 不管什么数据 next[0] = -1;next[1] = 0;在这里,我们以下标来开始,而说到的第几个第几个是从 1 开始;

求next数组的练习:

练习一:举例对于”ababcabcdabcde”, 求其的 next 数组?

-1 0 0 1 2 0 1 2 0 0 1 2 0 0

image.png

练习 2: 再对”abcabcabcabcdabcde”,求其的 next 数组? "

-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0

核心的东西来了:

到这里大家对如何求next数组应该问题不大了,接下来的问题就是,已知next[i] = k;怎么求next[i+1] = ?

如果我们能够通过 next[i]的值,通过一系列转换得到 next[i+1]得值,那么我们就能够实现这部分。

那该怎么做呢?

首先假设: next[i] = k 成立,那么,就有这个式子成立:P0...Pk-1 = Px...Pi-1,得到: P0...Pk-1 = Pi-k..Pi-1;如下图分析:

image.png

然后我们假设如果 Pk = Pi;我们可以得到 P0...Pk = Pi-k..Pi;那这个就是 next[i+1] = k+1;

image.png

那么: Pk != Pi 呢?

image.png

代码实现


C代码:

#include<stdio.h>
#include<string.h>
#include<assert.h>
void GetNext(int* next, char* sub, int len2)
{
  next[0] = -1;//规定第一个为-1,第二个为0,则直接这样定义就好了;
  next[1] = 0;
  int k =0;//前一项的k
  int j = 2;//下一项
  while (j < len2)
  {
    if (k==-1||sub[j-1] == sub[k])
    {
      next[j] = k + 1;
      j++;
      k++;
    }
    else
    {
      k = next[k];
    }
  }
}
int KMP(char* str, char* sub, int pos)
{
  assert(str != NULL && sub != NULL);
  int len1 = strlen(str);
  int len2 = strlen(sub);
  assert(pos >= 0 && pos < len1);
  int i = pos;//i从指定下标开始遍历
  int j = 0;
  int* next = (int*)malloc(sizeof(int) * len2);//动态开辟next和子串一样长
  assert(next != NULL);
  GetNext(next, sub, len2);
  while (i < len1 && j < len2)
  {
    if (j == -1||str[i] == sub[j])//j==-1是防止next[k]回退到-1的情况
    {
      i++;
      j++;
    }
    else {
      j = next[j];//如果不相等,则用next数组找到j的下个位置
    }
  }
  if (j >= len2)
  {
    return i - j;
  }
  else {
    return -1;
  }
}
int main()
{
  char* str = "ababcabcdabcde";
  char* sub = "abcd";
  printf("%d\n", KMP(str, sub, 0));
  return 0;
}

java代码:

public static void getNext(int[] next, String sub){
next[0] = -1;
next[1] = 0;
int i = 2;//下一项
int k = 0;//前一项的K
while(i < sub.length()){//next数组还没有遍历完
if((k == -1) || sub.charAt(k) == sub.charAt(i-1)) {
next[i] = k+1;
i++;
k++;
}else{
k = next[k];
}
}
} 
public static int KMP(String s,String sub,int pos) {
int i = pos;
int j = 0;
int lens = s.length();
int lensub = sub.length();
int[] next= new int[sub.length()];
getNext(next,sub);
while(i < lens && j < lensub){
if((j == -1) || (s.charAt(i) == sub.charAt(j))){
i++;
j++;
}else{
j = next[j];
}
} 
if(j >= lensub) {
return i-j;
}else {
return -1;
}
} 
public static void main(String[] args) {
System.out.println(KMP("ababcabcdabcde","abcd",0));
System.out.println(KMP("ababcabcdabcde","abcde",0));
System.out.println(KMP("ababcabcdabcde","abcdef",0));
}

关键代码讲解


else{
   j=next[j]
}

image.png

if (j == -1||str[i] == sub[j])
        {
            i++;
            j++;
        }

问题:为啥还有个j==-1?

如下图所示:当第一个字符就不匹配,i,j此时都是0,j=next[j]  >> j=next[0] >> j=-1; 此时j是-1,如果不添加j==-1这种情况,那么这个程序将结束返回没有匹配,但你仔细观察下图,P[5]~P[8]与子串相匹配,故答案显然错误.所以我们应该加上j==-1这种情况,让其从头再遍历;

image.png

   next[0] = -1;
    next[1] = 0;
    int k =0;//前一项的k
    int j = 2;//下一项

根据我们的规定next数组第一个和第二个数为-1和0,故没啥问题。k=0是前一项k的值,j=2是下一项.

image.png

if (k==-1||sub[j-1] == sub[k])
        {
            next[j] = k + 1;
            j++;
            k++;
        } 

根据上面的内容我们可知,p[j]==p[k],next[i]=k;则能推出next[i+1]=k+1;如下图所示,不过这里i是j-1,大家要注意这一点, p[j]==p[k]>>sub[j-1]==sub[k];next[i+1]=k+1>>next[j]=k+1;

image.png

else
        {
            k = next[k];
        } 

这个知识点上面讲过,当p[j]!=p[k]时候,k回退,一直找到p[j]==p[k]然后用这个next[i+1]=k+1;

牛刀小试


题目在这里>>重复的子字符串

感兴趣的伙伴可以去做试试,下一章我们进行共同探讨;

KMP算法的时间复杂度


假设在M字符串中找N字符串的起始位置,长度分别为m和n,使用KMP算法,一般认为时间复杂度是O(m+n),也就是计算next数组的时间复杂度是O(n),而匹配的时候是O(m).

以上是KMP算法的讲解,有不足的地方或者对代码有更好的见解,欢迎评论区留言共同商讨,共同进步!!

image.png

相关文章
|
1月前
|
算法 搜索推荐 Java
数据结构与算法(Java篇)笔记--希尔排序
数据结构与算法(Java篇)笔记--希尔排序
|
1月前
|
机器学习/深度学习 存储 算法
【算法沉淀】刷题笔记:并查集 带权并查集+实战讲解
【算法沉淀】刷题笔记:并查集 带权并查集+实战讲解
|
10天前
|
存储 算法
图解Kmp算法——配图详解(超级详细)
图解Kmp算法——配图详解(超级详细)
|
15天前
|
算法 测试技术 C#
【字典树】【KMP】【C++算法】3045统计前后缀下标对 II
【字典树】【KMP】【C++算法】3045统计前后缀下标对 II
|
15天前
|
算法
【算法学习--字符串】(不含KMP算法)
【算法学习--字符串】(不含KMP算法)
|
1月前
|
算法 搜索推荐 Java
数据结构与算法(Java篇)笔记--快速排序
数据结构与算法(Java篇)笔记--快速排序
|
1月前
|
机器学习/深度学习 算法 搜索推荐
数据结构与算法(Java篇)笔记--归并排序
数据结构与算法(Java篇)笔记--归并排序
|
1月前
|
算法 搜索推荐 Java
数据结构与算法(Java篇)笔记--选择排序
数据结构与算法(Java篇)笔记--选择排序
|
1月前
|
算法
白话 KMP 算法
白话 KMP 算法
13 2
|
1月前
|
算法 Java 索引
算法基础:KMP算法详细详解
算法基础:KMP算法详细详解