暴力匹配vs KMP实测:nm到底差多少?附可运行源码+时间复杂度对比

一.串的定义

 串,即字符串(String)是由零个或多个字符组成的有限序列。一般记为

S = 'a₁a₂...aₙ' (n ≥ 0)

其中,S 是串名,单引号括起来的字符序列是串的值;aᵢ 可以是字母、数字或其他字符;串中字符的个数 n 称为串的长度。n = 0 时的串称为空串(用 ∅ 表示)。

例:

S = "Hello World!"

注:有的地方用双引号(如 Java),

T = 'iPhone 11 Pro Max?'

有的地方用单引号(如 Python)

子串:串中任意个连续的字符组成的子序列。

例如:'iPhone'、'Pro M' 是串 T 的子串。

主串:包含子串的串。

例如:T 是子串 'iPhone' 的主串。

字符在主串中的位置:字符在串中的序号。

例如:'1' 在 T 中的位置是 8(第一次出现)。

子串在主串中的位置:子串的第一个字符在主串中的位置。

例如:'11 Pro' 在 T 中的位置为 8。

空串 vs 空格串:

注意:位序从 1 开始,不是从 0 开始。

M = "" → M 是空串

N = "   " → N 是由三个空格字符组成的空格串,每个空格字符占 1B。

二.串的基本操作

三.串的顺序存储

1.定长顺序串(静态数组,长度写死)
#define MAXSIZE 255          // 最大容量
typedef struct {
    char ch[MAXSIZE];        // 连续空间
    int  length;                // 当前实际长度
} SString;
2.堆分配顺序串(动态数组,运行时可扩容)
typedef struct {
    char *ch;   // malloc/realloc 得到的连续空间
    int  length;   // 已用长度
    int  cap;   // 已分配容量
} HString;
3.不同的存储方式

四.串的链式存储

1.单字符结点
typedef struct StringNode{
	char ch;     //每个结点存1个字符
	struct StringNode* next;
}StringNode,*String;

2.多字符结点
typedef struct StringNode {
	char ch[4];   //每个结点存多个字符
	struct StringNode* next;
}StringNode,*next;

五.基本操作的实现

1.求子串

//求子串
bool SubString(SString& Sub, SString S, int pos, int len) {
	//子串范围越界
	if (pos + len - 1 > S.length)
		return false;
	for (int i = pos, i < pos + len;i++)
		Sub.ch[i - pos + 1] = S.ch[i];
	Sub.length = len;
	return true;
}
2.比较操作

S.ch和T2比较时;
扫描过的所有字符都相同,则比较长度:
S.length - T.length = 7 - 3 > 0;
则S.ch更长
 

如左图所示, 当S.ch和T1比较时 : 
从S.ch,T1字符串的第一个位置开始循环扫描:
   for (int i = 1;i <= S.length && i <= T.length;i++)
如果发现两个字符串对应位置的字符不同时:
   if (S.ch[i] != T.ch[i])
则返回两个值相减的值:
   return S.ch[i] - T.ch[i];
如S.ch和T1所示, 则是返回了a - b的值, 又因为a - b < 0;
所有S.ch字符串比T1小, 则返回值 < 0.

//比较操作
//若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0
int StringCompare(SString S, SString T) {
	for (int i = 1;i <= S.length && i <= T.length;i++) {
		if (S.ch[i] != T.ch[i])
			return S.ch[i] - T.ch[i];
	}
	//扫描过的所有字符都相同,则长度长的串更大
	return S.length - T.length;
}
3.定位操作

首先求两个字符串的长度,才可以知道子串要取多长:
    n = StrLength(S), m = StrLength(T);
子串长为T.length=m;
然后调用取子串基本操作,我们就可以取出长度为3的子串:
    SubString(sub, S, i, m);
然后使用while循环就可以从头到尾取长度为3的子串:
    while (i < n - m + 1);
取下来子串后,使用比较的操作,观察取下来的子串与T是否相等:
如果相等则返回0,如果不等则说明还没有匹配,则 i++:
    if (StringCompare(sub, T) != 0)
        i++;
最后返回子串在主串中的位置:
    else return i;
 

//定位操作
int Index(SString S, SString T) {
	int i = 1, n = StrLength(S), m = StrLength(T);
	SString sub;     //暂存子串
	while (i <= n - m + 1) {
		SubString(sub, S, i, m);
		if (StringCompare(sub, T) != 0)
			i++;
		else return i;      //返回子串在主串中的位置
	}
	return 0;     //S中不存在与T相等的子串
}

六.堆串操作

1.类型定义
typedef struct {
    char* ch;     // 指向动态分配的字符数组
    int length;   // 当前实际字符个数
} HString;

  char*  而不是  char[] :大小在编译期不确定,要到运行期才申请,所以叫“堆”串。
  如果只写  char* ch  而不初始化,它就是个野指针,访问即崩溃。

2. 初始化(分配空间)
HString HS;
HS.ch = (char*)malloc(MAXLEN * sizeof(char));
HS.length = 0;

 malloc(MAXLEN):

向操作系统要  MAXLEN  字节的** raw 内存**,返回  void* 。

 (char*)  :

明确告诉编译器“我要当字符数组用”,不写也可以,但 C++ 推荐强转。 

 HS.length = 0 : 

当前还没存任何有效字符,不是容量,而是实际长度。 

不写这行  malloc  → 后面  HS.ch[?]  就是野指针写入,直接 Segmentation Fault。

3. 使用(演示级,未真正存字符)

示例里并没有往堆里放字符,只是打印了  length ,所以不会访问非法内存。
如果真要存字符,典型流程是:

const char* src = "heap";
int len = strlen(src);
HS.ch = (char*)malloc(len + 1); // +1 留给 '\0'(可选)
for (int i = 0; i < len; ++i) HS.ch[i] = src[i];
HS.length = len;
4. 释放内存
free(HS.ch);

 1.malloc  得到的内存不会自动还,必须手动  free 。
 2.只写  free  一次;二次 free = 未定义行为。
 3.养成“谁 malloc 谁 free”的习惯,防止内存泄漏。

5.堆串完整代码
//堆串定义
typedef struct {
    char* ch;   // 动态区,ch[1..length] 有效,ch[0] 闲置
    int  length;
} HString;	

/* 演示堆串初始化 */
	HString HS;
	HS.ch = (char*)malloc(MAXLEN * sizeof(char));
	HS.length = 0;
	cout << "堆串 HS 已初始化,length = " << HS.length << endl;
	free(HS.ch); // 避免内存泄漏
6.与定长顺序串对比
定长顺序串堆串
定长顺序串  SString  堆串  HString  
数组大小固定  MAXLEN  运行期随意扩/缩
栈或静态存储 内存来自堆 
无内存泄漏风险 必须手动  free
下标可以从 1 开始玩 通常仍从 0 开始,但你想从 1 也行 

七.完整代码实现

1.顺序存储 - - 定长存储
#include <iostream>
using namespace std;

#define MAXLEN 255

//定长顺序串
typedef struct {
	char ch[MAXLEN+1];
	int length;
}SString;

 
//求串长
int StrLength(SString S) {
	return S.length;
}

//求子串
bool SubString(SString& Sub, SString S, int pos, int len) {
	//子串范围越界
	if (pos + len - 1 > S.length)
		return false;
	for (int i = pos; i < pos + len;i++)
		Sub.ch[i - pos + 1] = S.ch[i];
	Sub.length = len;
	return true;
}

//比较操作
//若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0
int StringCompare(SString S, SString T) {
	for (int i = 1;i <= S.length && i <= T.length;i++) {
		if (S.ch[i] != T.ch[i])
			return S.ch[i] - T.ch[i];
	}
	//扫描过的所有字符都相同,则长度长的串更大
	return S.length - T.length;
}

//定位操作
int Index(SString S, SString T) {
	int i = 1, n = StrLength(S), m = StrLength(T);
	SString sub;     //暂存子串
	while (i <= n - m + 1) {
		SubString(sub, S, i, m);
		if (StringCompare(sub, T) != 0)
			i++;
		else return i;      //返回子串在主串中的位置
	}
	return 0;     //S中不存在与T相等的子串
}
 
//C风格字符串 -> SString (下标从1开始)
void InitString(SString& S, const char* str) {   // 辅助:C字符串->SString
	int len = 0;
	while (str[len]) 
		len++;
	S.length = len;
	for (int i = 1; i <= len; i++) 
		S.ch[i] = str[i - 1];
}

int main() {
	/* 1. 初始化主串与模式串 */
	SString S, T;
	InitString(S, "hello world");
	InitString(T, "world");

	/* 2. 测试 SubString */
	SString sub;
	if (SubString(sub, S, 7, 5)) {
		cout << "子串:";
		for (int i = 1; i <= sub.length; i++) cout << sub.ch[i];
		cout << endl;
	}

	/* 3. 测试 StringCompare */
	cout << "Compare 结果 (S,T): " << StringCompare(S, T) << endl;

	/* 4. 测试 Index */
	int pos = Index(S, T);
	cout << "Index 结果: " << pos << endl;

 
	return 0;
}


2.顺序存储 - - 堆串
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

typedef struct {
    char* ch;   // 动态区,ch[1..length] 有效,ch[0] 闲置
    int  length;
} HString;

/* 工具函数:下标从 1 开始 */
int  StrLength(HString S) { return S.length; }
void InitString(HString& S, const char* str) {
    S.length = strlen(str);
    S.ch = (char*)malloc(S.length + 1);      // +1 给 ch[0] 留空
    for (int i = 1; i <= S.length; ++i) S.ch[i] = str[i - 1];
}

bool SubString(HString& Sub, HString S, int pos, int len) {
    if (pos < 1 || len < 0 || pos + len - 1 > S.length) return false;
    Sub.ch = (char*)malloc(len + 1);
    for (int i = 1; i <= len; ++i) Sub.ch[i] = S.ch[pos + i - 1];
    Sub.length = len;
    return true;
}

int StringCompare(HString S, HString T) {
    int i = 1;
    while (i <= S.length && i <= T.length) {
        if (S.ch[i] != T.ch[i]) return S.ch[i] - T.ch[i];
        ++i;
    }
    return S.length - T.length;
}

int Index(HString S, HString T) {
    int n = StrLength(S), m = StrLength(T);
    if (m == 0) return 0;
    HString sub;
    for (int i = 1; i <= n - m + 1; ++i) {
        SubString(sub, S, i, m);
        if (StringCompare(sub, T) == 0) {
            free(sub.ch);   // 记得释放临时堆区
            return i;
        }
        free(sub.ch);
    }
    return 0;
}

void PrintHString(HString S) {
    for (int i = 1; i <= S.length; ++i) cout << S.ch[i];
}

int main() {
    HString S, T;
    InitString(S, "hello world");
    InitString(T, "world");

    HString sub;
    if (SubString(sub, S, 7, 5)) {
        cout << "子串:"; PrintHString(sub); cout << endl;
        free(sub.ch);
    }
    cout << "Compare 结果:" << StringCompare(S, T) << endl;
    cout << "Index 结果:" << Index(S, T) << endl;

    free(S.ch);
    free(T.ch);
    return 0;
}

八.字符串模式匹配

1.朴素模式匹配算法

主串长度为n,模式串长度为m

朴素模式匹配算法:将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的子串,或所有的子串都不匹配为止. (最多匹配n-m+1个子串)

1.字符串基本操作实现匹配

Index(S,T):定位操作。若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。

int Index(SString S, SString T) {
	int i = 1, n = StrLength(S), m = StrLength(T);
	SString sub;
	while (i <= n - m + 1) {
		SubString(sub, S, i, m);    //取出从位置i开始,长度为m的子串
		if (StrCompare(sub, T) != 0)    //子串与模式串对比,若不匹配,则匹配下一个子串
			i++;
		else
			return i;    //返回子串在主串中的位置
	}
	return 0;     //S中不存在与T相等的子串
}
2.数组下标实现匹配
1.图解代码

首先设计两个指针指向各个串的第一个位置,如果这两个字符相等,则指针后移

现在i和j指向的字符依旧相等,则继续后移,即i++,j++

到第六个位置时,i和j指向的字符不相等,则没有匹配上

若当前子串匹配失败,则主串指针i指向下一个子串的第一个位置,模式串指针j回到模式串的第一个位置,即:

i = i - j + 2;
j = 1;

如上图所示:

主串S的指针 i 回到2的位置,模式串T的指针 j 依旧指向1的位置

现在两个指针指向的位置依旧不匹配,则 i 指针继续往后移动,j 指针依旧指向1;

i = i - j + 2;        i = 2-1+2 = 3
j = 1;                 j = 1

如上图所示:

每当发现 i 和 j 所指向的字符一样时,都让 i 和 j 往后移一位,如图,此时 j 指向的位置已经超越了模式串的长度,那这种情况下我们就说明当前子串匹配成功 ,即 j > T.length ,返回当前子串第一个字符的位置,即 i -T.length = 10-6=4

2.完整代码
int Index(SString S, SString T) {
	int i = 1, j = 1;
	while (i < S.length && j < T.length) {
		if (S.ch[i] == T.ch[j]) {
			i++;
			j++;
		}
		else {
			i = i - j + 2;
			j = 1;
		}
	}
	if (j > T.length)
		return i - T.length;
	else
		return 0;
}
3.时间复杂度

最坏的情况,每个子串都要对比 m 个字符,共 n-m+1 个子串,复杂的 = O((n-m+1)m) = O(nm);

最坏时间复杂度 = O(nm)

2.KMP算法
1.图解代码

如图所示,

当第六个字符发生失配的时候,那我们可以确定第六个字符之前的字符是和模式串一致的

此时,就没有必要检查已2位置,3位置开头的子串,而是直接从4位置开始检查;

而对于4位置开头的子串,也没有必要和模式串1位置,2位置的字符比较(因为确定这两个元素是和模式串匹配的)

所有可以直接从如上图所示位置开始匹配,

可令主串指针 i 不变,模式串指针 j =3

对于模式串 T = “abaabc”:

- 当第 6 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 3;
- 当第 5 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 2;
- 当第 4 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 2;
- 当第 3 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 1;
- 当第 2 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 1;
- 当第 1 个元素匹配失败时,匹配下一个相邻子串,令 j = 0,i++,j++。

2.示例代码图解

⇖  当第 5 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 2; 

⇖ 当第 2 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 1; 

⇖ 当第 1 个元素匹配失败时,匹配下一个相邻子串,令 j = 0,i++,j++; 

当第 2 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 1 ;  

⇖ 当第 3 个元素匹配失败时,可令主串指针 i 不变,模式串指针 j = 1;⇙

⇖ 当第 1 个元素匹配失败时,匹配下一个相邻子串,令 j = 0,i++,j++;⇙

最终因为 j 超过了模式串的范围而停止匹配.

优化后,主串指针不"回溯",相比于朴素模式.

3.完整代码
int Index_KMP(SString S, SString T, int next[]) {
	int i = 1, j = 1;
	while (i <= S.length && j <= T.length) {
		if (j == 0 || S.ch[i] == T.ch[j]) {     //如果主串和模式串的当前元素相等,则继续往后匹配
			i++;
			j++;
		}
		else {
			j = next[j];
		}
	}
	if (j > T.length)
		return i - T.length;
	else
		return 0;
}
4.时间复杂度

KMP算法

最坏时间复杂度:O(m + n)

其中:  
- 求next数组时间复杂度:O(m)  
- 模式匹配过程最坏时间复杂度:O(n)

九.求模式串的next数组

next数组的作用:当模式串的第 j 个字符失配时,从模式串的第 next[ j ] 的继续往后匹配

1.图解代码

如上图所示:

当第1个字符匹配失败,则令 j =0, i++, j++;

此逻辑对于任何一个模式串都一样,第一个字符不匹配时,只能匹配下一个子串;

因此,所有模式串的 next[ 1 ] 都写0.

如上图所示:

当第2个字符不匹配时,应尝试匹配模式串的第一个字符;

适用于所有模式串,next [ 2 ] 都为1

得到下图:

如上图所示:

现在在不匹配的位置的前边,划一根分界线;

模式串一步一步往后退;

知道分界线之前可以对的上;

或模式串完全跨过分界线为止.

如上图所示:

第3个字符不匹配时,将字符往后移;

直到分界线之前可以对的上,但是上图在分界线之前对不上;

所以让模式串完全跨过分界线;

得到下图:

如上图所示:

第4个字符不匹配,则在第4个字符前面画一个分界线;

再将模式串往后移,直到字符匹配;

得到下图:

如上图所示:

第5个字符不匹配也是此做法;

将模式串依次往后退,直到出现对应字符;

得到下图:

如上图为第6个字符不匹配;

按照上述方法类比;

如下图:

最后得到google的next数组:

2.示例代码图解

求该模式串的next数组:

首先根据结论:

next [ 1 ] = 0;

next [ 2 ] = 1;

其他 next : 在不匹配的位置前,划一根分界线,模式串一步一步往后退,直到分界线之前可以对的上,或模式串完全跨过分界线为止.此时 j 指向哪里, next 数组值就为多少.

第3个字符不匹配时:
next [3] = 1.

第4个字符不匹配时:
next [4] = 2.

第5个字符不匹配时 :
next [5] = 3.

第6个字符不匹配时 :
next [6] = 4.

十.KMP算法的进一步优化(求 nextval 数组)

1.示例代码图解

手算解题:先求 next 数组,再由 next 数组求 nextval 数组

nextval[1] = 0;
for (int j = 2;j < T.length;j++) {
    if (T.ch[next[j]] == T.ch[j])
        nextval[j] = nextval[next[j]];
    else
        nextval[j] = next[j];
}

首先默认 nextval[1] = 0,然后继续求后面的 nextval 值;

方法:如果当前 next[ j ] 所指向的字符和目前 j 所指的字符他们两个不相等,那我们就让 nextval 的值 = next 的值.

如图:
当前 next[2] 指向的字符为1, j = 1时所指向的模式串为a;
此时 a≠b;
所以 nextval[2]=next[2]=1.

如图:
当前 next[3] 指向的字符为1, j = 1时所指向的模式串为a;
此时 a=a;
所以 nextval[3] = nextval[next[1]] = 0.

如图:
当前 next[4] 指向的字符为2, j = 2时所指向的模式串为b;
此时 b = b;
所以 nextval[4] = nextval[next[2]] = 1.

如图:
当前next[5]指向的字符为3, j = 3时所指向的模式串为a;
此时a=a;
所以nextval[5] = nextval[next[3]] = 0.

如图:
当前next[6]指向的字符为4, j = 4时所指向的模式串为b;
此时a≠b;
所以nextval[6] = next[6] = 4.

2.例题

next数组:

nextval数组:

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

遗憾是什么.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值