【数据结构与算法】第16篇:串(String)的定长顺序存储与朴素模式匹配

一、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)算法的核心是暴力穷举

  1. 用指针 i 指向主串,j 指向模式串,都从1开始

  2. 如果 S[i] == T[j],i++,j++

  3. 如果不相等:

    • i 回溯到本轮开始位置的下一个位置:i = i - j + 2

    • j 重置为1

  4. 如果 j > T.length,说明匹配成功,返回 i - T.length

  5. 如果 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"

  • 当匹配到 cd 不匹配时

  • 我们已经知道 "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算法,这是模式匹配的经典优化,能避免不必要的回溯。


八、思考题

  1. BF算法中,为什么要用1-based索引?用0-based索引有什么不同?

  2. 如果主串是 "0000000001",模式串是 "0001",BF算法需要比较多少次?

  3. 尝试实现一个函数,统计模式串在主串中出现的所有位置(不只是第一个)。

  4. BF算法有哪些实际应用场景?什么时候它其实已经够用了?

欢迎在评论区讨论你的答案。

相关推荐
2401_827499992 小时前
python核心语法01-数据存储与运算
java·数据结构·python
AI科技星2 小时前
基于v≡c公设的理论优化方案
c语言·开发语言·算法·机器学习·数据挖掘
江不清丶2 小时前
垃圾收集算法深度解析:从标记-清除到分代收集的演进之路
java·jvm·算法
副露のmagic2 小时前
链表章节 leetcode 思路&实现
数据结构·leetcode·链表
拒朽2 小时前
51单片机学习(六)模块化编程和LCD调试工具
嵌入式硬件·学习·51单片机
自然常数e2 小时前
预处理讲解
java·linux·c语言·前端·visual studio
@菜菜_达2 小时前
Vue 入门学习
前端·vue.js·学习
jllllyuz2 小时前
小型物联网系统——家居网关设计(C语言实现)
c语言·物联网·struts