字符串匹配
引入
StrDelete
(char* S,char* T,char* V)
初始条件:串 S、串T和串 V存在,且串T是非空串。
操作结果:用串 V替换串 S 中出现的所有与串T相等的不重叠的子串。
思路(1)
- 运用
StrIndex
函数找到 S 中的 T 子串 - 运用
StrDelete
函数将子串 T 删除 - 最后再将
StrDelete
函数再将子串 V 插入 S中
暴力实现
(不用string.h
库进行编写)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
int StrLength(char* S){
int count = 0;
while (*S) {
count++;
S++;
}
return count;
}
int StrIndex(char* S, int pos, char* T) {
int lenS = StrLength(S);
int lenT = StrLength(T);
for (int i = pos - 1; i <= lenS - lenT; i++) {
int j;
for (j = 0; j < lenT; j++) {
if (S[i + j] != T[j]) {
break;
}
}
if (j == lenT) {
return i; // 返回子串在原字符串中的实际位置
}
}
return -1; // 找不到子串,返回-1
}
void StrInsert(char* S, int pos, char* T) {
int len_s = StrLength(S);
int len_t = StrLength(T);
if (pos<1 || pos>len_s + 1) {
return;
}
char temp[1000];
int i;
//将s中pos后面的字符存入临时数组中
for (i = pos - 1; i < len_s; i++) {
temp[i - pos + 1] = S[i];
}
temp[i - pos + 1] = '\0';
for (i = 0; i < len_t; i++) {
S[pos + i - 1] = T[i];
}
for (i = 0; i < len_s - pos + 1; i++) {
S[pos - 1 + len_t + i] = temp[i];
}
S[len_s + len_t] = '\0';
}
void StrDelete(char* S, int pos, int len) {
int len_s = StrLength(S);
char* src = S + pos - 1;
char* dst = S + pos - 1 + len;
while ((*src = *dst)) {
src++;
dst++;
}
}
void StrReplace(char* S, char* T, char* V) {
int pos = 0;
int lenT = StrLength(T);
int lenV = StrLength(V);
while (( pos = StrIndex(S, pos+1 ,T)) != -1) {
StrDelete(S, pos+1 , lenT);
StrInsert(S, pos+1 , V);
pos += lenV;
}
}
int main() {
char S[1000] = "This is a sample string.";
char T[] = "sample";
char V[] = "example";
StrReplace(S, T, V);
printf("%s\n", S); // 输出结果为 "This is a example string."
return 0;
}
让我们使用string.h
库进行实现,简化一下代码长度
思路(2)
- 获取字符串S、T和V的长度。
- 创建一个临时数组
temp
,用于存储替换后的结果。假设结果不会超过1000个字符,因此数组大小设为1000。 - 初始化两个指针i和j,i用于遍历字符串S,j用于遍历
temp
。 - 进入循环,循环条件是i小于字符串S的长度。
- 在循环中,首先判断从位置i开始的长度为
len_t
的子串是否与字符串T相等。可以使用strcmp
函数来进行比较。 - 如果相等,则将字符串V复制到
temp
数组中的位置j,并更新j的值为j加上字符串V的长度。 - 同时,更新i的值为i加上字符串T的长度,表示已经匹配到了一个子串并完成替换。
- 如果不相等,则将字符串S中的当前字符复制到 temp 数组中的位置j,并同时更新i和j的值为i+1和j+1,继续下一个字符的比较和复制。
- 循环结束后,添加字符串结尾标志’\0’到 temp 数组的末尾。
- 最后,使用
strcpy
函数将 temp 数组中的内容拷贝回字符串 S,完成替换操作。
void StrReplace(char* S, char* T, char* V) {
int lenS = strlen(S);
int lenT = strlen(T);
int lenV = strlen(V);
char temp[1000]; // 假设结果不会超过1000个字符
int i = 0, j = 0; // i用于遍历串S,j用于遍历temp
while (i < lenS) {
if (strncmp(&S[i], T, lenT) == 0) { // 找到了与串T相等的子串
strcpy(&temp[j], V); // 用串V替换
j += lenV;
i += lenT;
} else {
temp[j] = S[i];
++i;
++j;
}
}
temp[j] = '\0'; // 加上字符串结尾标志
strcpy(S, temp); // 将替换后的结果拷贝回串S
}
算法的存在的问题
在以上程序中,我们使用了暴力匹配字符串的方法。
while (i < lenS) {
if (strncmp(&S[i], T, lenT) == 0) { // 逐步比较,找到了与串T相等的子串
strcpy(&temp[j], V); // 用串V替换
j += lenV;
i += lenT;
} else {
temp[j] = S[i];
++i;
++j;
}
}
即假设我们的字符串主串 S
为 ABABC
,而模式串 T
为 ABC
,程序将会进行以下运行。
1.从主串的第一个字符开始,依次与模式串的第一个字符进行比较。
2.如果相等,则继续比较主串和模式串的下一个字符,直到模式串结束或出现不匹配的字符。
3.若出现不匹配的字符,则将指针1指向指向主串的下一个单位,指针2则重新指向模式串的开头,重新从步骤1开始匹配。。
匹配失败
指针一后移,指针二复原后,重回步骤一
4.如果模式串匹配完成(即所有字符都匹配),则说明找到了匹配的子串
匹配成功,结束程序
在以上程序的实现过程中,我们不难发现,若相匹配的主串子串如果处在主串靠后的位置,可能我们需要将 S 中的每个字符都进行遍历,该算法的效率较低,时间复杂度为O(m*n),其中m为主串长度,n为模式串长度。如此高的时间复杂度,让我不禁开始思考我们是否有更好的方式去实现字符串的匹配呢?
kmp算法
观察暴力算法的运算过程,指向主串的指针需要回溯两次,那我们应该怎么办来减少主串指针的回动次数呢?
当我们要实现一个StrReplace
函数时,我们可以使用KMP
算法来实现快速的字符串替换。kmp
算法是一种高效的字符串匹配算法该算法的核心思想是利用已匹配部分的信息来避免无效的回溯,从而提高字符串匹配的效率,在 O(n+m) 的时间复杂度内完成字符串匹配操作。其中 n 为目标串 S 的长度,m 为模式串 T 的长度。
KMP算法的基本步骤:
- 构建部分匹配表(next数组):遍历模式串,对每个位置i,求出模式串的前缀子串和后缀子串的最长公共前缀长度,保存在next[i]中。
- 匹配过程:遍历主串和模式串,如果当前字符匹配,则继续比较下一个字符;如果不匹配,则根据部分匹配表移动模式串的位置,而不是直接回到模式串的起始位置。
kmp算法内容的解释:
next
数组,也称为部分匹配表(Partial Match Table),是KMP算法中的关键数据结构之一,用于指示在匹配失败时应该如何移动模式串,从而避免重复比较。
在构建 next
数组时,我们遍历模式串,对每个位置 i
,计算模式串的前缀子串和后缀子串的最长公共前缀长度,然后将该长度保存在 next[i]
中。
以“ABC”为例,它的前缀分别为“A”,”AB“,后缀则为”C“,”BC“。
具体来说,next[i]
的含义是:当模式串中的第 i+1
个字符与主串中的某个字符不匹配时,下一次应该将模式串的位置移动到哪里。这个移动位置是根据模式串的自身特性决定的,而 next
数组提供了这种移动的信息。
下面我们以主串 “ABCDABD” 计算 next
数组。
-
对于第一个字符 ‘A’,它前面没有子串,因此
next[0] = 0
。 -
对于第二个字符 ‘B’,它前面的子串只有 ‘A’,因此
next[1] = 0
。 -
对于第三个字符 ‘C’,前面的子串为 ‘AB’,但没有公共前缀,因此
next[2] = 0
。 -
对于第四个字符 ‘D’,前面的子串为 ‘ABC’,但 ‘A’ 不等于 ‘D’,因此
next[3] = 0
。 -
对于第五个字符 ‘A’,前面的子串为 ‘ABCD’,其中 ‘A’ 与模式串的第一个字符匹配,因此最长公共前缀长度为1,即
next[4] = 1
。 -
对于第六个字符 ‘B’,前面的子串为 ‘ABCDA’,但 ‘AB’ 与模式串的前两个字符匹配,因此最长公共前缀长度为2,即
next[5] = 2
。 -
对于第七个字符 ‘D’,前面的子串为 ‘ABCDAB’,但 ‘AB’ 与模式串的前两个字符匹配,因此最长公共前缀长度为2,即
next[6] = 0
。#include <stdio.h> #include <string.h> void calculateNext(char *pattern, int *next, int len) { int i = 0, j = -1; next[0] = -1; while (i < len - 1) { if (j == -1 || pattern[i] == pattern[j]) { i++; j++; next[i] = j; } else { j = next[j]; } } } int main() { char pattern[] = "ABCDABD"; int len = strlen(pattern); int next[len]; calculateNext(pattern, next, len); printf("Pattern: %s\n", pattern); printf("Next array: "); for (int i = 0; i < len; i++) { printf("%d ", next[i]); } printf("\n"); return 0; }
在KMP算法中,当模式串的第 i+1
个字符与主串中的某个字符不匹配时,我们根据 next[i]
的值来移动模式串的位置,而不是直接回到模式串的起始位置,这样可以避免重复比较,提高了匹配效率。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 计算模式串 T 的 next 数组
void getNext(char* T, int* next) {
int i = 0, j = -1;
int len = strlen(T);
next[0] = -1;
while (i < len - 1) {
if (j == -1 || T[i] == T[j]) {
i++;
j++;
next[i] = j;
} else {
j = -1;//将j指针逐步回退至开头
}
}
}
// 使用 KMP 算法进行字符串匹配,并替换匹配的子串
char* StrReplace(char* S, char* T, char* V) {
int slen = strlen(S);
int tlen = strlen(T);
int* next = (int*)malloc(sizeof(int) * tlen);
// 计算模式串 T 的 next 数组
getNext(T, next);
int i = 0, j = 0;
int k = 0; // 记录新字符串的位置
char* result = (char*)malloc(sizeof(char) * (slen + 1)); // 申请足够大的空间保存结果
while (i < slen && j < tlen) {
if (j == -1 || S[i] == T[j]) { // 匹配成功或者 j == -1 则继续比较下一个字符
i++;
j++;
} else {
j = next[j]; // 失配时,模式串右移
}
if (j == tlen) { // 找到了匹配的子串
for (int l = 0; l < strlen(V); l++) {
result[k++] = V[l]; // 将替换串 V 复制到 result 中
}
j = 0; // 重新开始匹配下一个子串
} else if (i < slen) {
result[k++] = S[i]; // 将 S 中未匹配的字符复制到 result 中
}
}
result[k] = '\0'; // 添加字符串结尾符
free(next); // 释放内存
return result;
}
int main() {
char S[] = "abcdeabb";
char T[] = "ab";
char V[] = "xyz";
char* result = StrReplace(S, T, V);
printf("Result: %s\n", result);
free(result); // 释放内存
return 0;
}