一、C语言中字符串的本质
1.1 字符串的存储
在C语言中,字符串本质上是以\0结尾的字符数组。
c
char str1[] = "hello"; // 编译器自动添加\0,长度6
char str2[] = {'h','e','l','l','o','\0'}; // 等价
char *str3 = "world"; // 字符串常量,只读
1.2 定长顺序存储
我们用结构体来封装串的定长顺序存储:
c
#define MAXLEN 255 // 串的最大长度
typedef struct {
char ch[MAXLEN]; // 存储字符
int length; // 串的实际长度
} SString;
这种存储方式的缺点:
-
最大长度固定,可能浪费空间或不够用
-
操作时需要处理长度
1.3 常用操作
c
// 初始化
void initString(SString *s) {
s->length = 0;
s->ch[0] = '\0';
}
// 从字符数组赋值
void strAssign(SString *s, const char *chars) {
int i = 0;
while (chars[i] != '\0' && i < MAXLEN - 1) {
s->ch[i] = chars[i];
i++;
}
s->ch[i] = '\0';
s->length = i;
}
// 打印
void printString(SString *s) {
printf("%s (length=%d)\n", s->ch, s->length);
}
二、朴素模式匹配算法(BF算法)
2.1 问题描述
给定一个主串 S 和一个模式串 T,在主串中找到模式串第一次出现的位置(下标从1开始),找不到返回0。
例如:
-
主串 S = "hello world"
-
模式串 T = "world"
-
匹配位置 = 7
2.2 算法思想
BF(Brute-Force)算法的核心是暴力穷举:
-
用指针 i 指向主串,j 指向模式串,都从1开始
-
如果 S[i] == T[j],i++,j++
-
如果不相等:
-
i 回溯到本轮开始位置的下一个位置:
i = i - j + 2 -
j 重置为1
-
-
如果 j > T.length,说明匹配成功,返回 i - T.length
-
如果 i > S.length,说明匹配失败,返回0
画个图(S="abcabd",T="abd"):
text
第一轮:i=1,j=1
S: a b c a b d
T: a b d
↑ ↑
a==a, b==b, c!=d → 失败,i回溯到2,j=1
第二轮:i=2,j=1
S: a b c a b d
T: a b d
↑
b!=a → 失败,i回溯到3,j=1
第三轮:i=3,j=1
S: a b c a b d
T: a b d
↑
c!=a → 失败,i回溯到4,j=1
第四轮:i=4,j=1
S: a b c a b d
T: a b d
↑ ↑ ↑
a==a, b==b, d==d → 成功,返回4
2.3 代码实现
c
#include <stdio.h>
#include <string.h>
#define MAXLEN 255
typedef struct {
char ch[MAXLEN];
int length;
} SString;
void initString(SString *s) {
s->length = 0;
s->ch[0] = '\0';
}
void strAssign(SString *s, const char *chars) {
int i = 0;
while (chars[i] != '\0' && i < MAXLEN - 1) {
s->ch[i] = chars[i];
i++;
}
s->ch[i] = '\0';
s->length = i;
}
// BF算法:返回模式串T在主串S中的位置(从1开始)
int index_BF(SString *S, SString *T) {
int i = 1, j = 1; // 注意:我们使用1-based索引
while (i <= S->length && j <= T->length) {
if (S->ch[i-1] == T->ch[j-1]) { // 字符匹配
i++;
j++;
} else {
// 回溯
i = i - j + 2;
j = 1;
}
}
if (j > T->length) {
return i - T->length; // 匹配成功
}
return 0; // 匹配失败
}
// 另一种实现:直接用字符数组
int index_BF2(const char *S, const char *T) {
int i = 0, j = 0;
int lenS = strlen(S);
int lenT = strlen(T);
while (i < lenS && j < lenT) {
if (S[i] == T[j]) {
i++;
j++;
} else {
i = i - j + 1;
j = 0;
}
}
if (j == lenT) {
return i - lenT + 1; // 返回位置(从1开始)
}
return 0;
}
void printString(SString *s) {
printf("%s (length=%d)\n", s->ch, s->length);
}
int main() {
SString S, T;
initString(&S);
initString(&T);
strAssign(&S, "hello world");
strAssign(&T, "world");
printf("主串: ");
printString(&S);
printf("模式串: ");
printString(&T);
int pos = index_BF(&S, &T);
printf("匹配位置: %d\n", pos);
// 测试其他情况
printf("\n--- 更多测试 ---\n");
printf("'abcabd' 中找 'abd': %d\n",
index_BF2("abcabd", "abd"));
printf("'aaaaa' 中找 'aaa': %d\n",
index_BF2("aaaaa", "aaa"));
printf("'abc' 中找 'def': %d\n",
index_BF2("abc", "def"));
return 0;
}
运行结果:
text
主串: hello world (length=11)
模式串: world (length=5)
匹配位置: 7
--- 更多测试 ---
'abcabd' 中找 'abd': 4
'aaaaa' 中找 'aaa': 1
'abc' 中找 'def': 0
三、BF算法的复杂度分析
3.1 最好情况
模式串的第一个字符就匹配失败,每次只比较一次就移动:
text
S = "abcdefgh"
T = "xyz"
每次比较一次就失败,总共比较 n 次。
时间复杂度:O(n)
3.2 最坏情况
每次比较都在最后一个字符才失败,或者最后才匹配成功:
text
S = "aaaaaaaaab"
T = "aaab"
匹配过程:
-
第1轮:比较4次,失败
-
第2轮:比较4次,失败
-
...
-
第(n-m+1)轮:比较m次,成功
总比较次数 ≈ (n-m+1) × m ≈ n × m
时间复杂度:O(n × m)
3.3 平均情况
一般情况下,BF算法的时间复杂度是 O(n × m)。
3.4 空间复杂度
O(1),只用了几个变量。
四、BF算法的可视化理解
以 S="abcabd",T="abd" 为例,画出每一轮的比较过程:
text
第1轮:
S: a b c a b d
↑ ↑ ↑
T: a b d
↑ ↑ ✗ (c != d)
回溯:i回到2,j回到1
第2轮:
S: a b c a b d
↑
T: a
↑ ✗ (b != a)
第3轮:
S: a b c a b d
↑
T: a
↑ ✗ (c != a)
第4轮:
S: a b c a b d
↑ ↑ ↑
T: a b d
↑ ↑ ↑ (全部匹配)
成功!
五、BF算法的优化方向
BF算法的缺点是:当匹配失败时,主串指针 i 会回溯到之前的位置,可能重复比较已经比较过的字符。
改进思路:
-
利用已匹配部分的信息,避免 i 回溯
-
这就是 KMP算法 的核心思想
例如 S="abcabd",T="abd":
-
当匹配到
c和d不匹配时 -
我们已经知道
"ab"是匹配的 -
模式串的前缀
"ab"和后缀是否有重叠? -
利用这个信息,可以跳过一些不必要的比较
KMP算法会在下一篇详细讲解。
六、串的其他存储方式
| 存储方式 | 说明 | 优缺点 |
|---|---|---|
| 定长顺序存储 | 固定长度数组 | 简单,但长度受限 |
| 堆分配存储 | 动态分配内存 | 灵活,需手动管理 |
| 块链存储 | 链表存储,每节点存多个字符 | 适合插入删除,但存储密度低 |
七、小结
这一篇我们学习了串的定长顺序存储和朴素模式匹配:
| 要点 | 说明 |
|---|---|
| C语言字符串 | 以\0结尾的字符数组 |
| 定长存储 | 固定数组,用length记录长度 |
| BF算法 | 暴力匹配,逐个字符比较 |
| 回溯 | 匹配失败时,i回溯,j重置 |
| 时间复杂度 | 最好O(n),最坏O(n×m) |
BF算法的核心代码:
text
while (i <= S.len && j <= T.len) {
if (S[i] == T[j]) { i++; j++; }
else { i = i - j + 2; j = 1; }
}
下一篇我们会讲KMP算法,这是模式匹配的经典优化,能避免不必要的回溯。
八、思考题
-
BF算法中,为什么要用1-based索引?用0-based索引有什么不同?
-
如果主串是
"0000000001",模式串是"0001",BF算法需要比较多少次? -
尝试实现一个函数,统计模式串在主串中出现的所有位置(不只是第一个)。
-
BF算法有哪些实际应用场景?什么时候它其实已经够用了?
欢迎在评论区讨论你的答案。