第20章:字符串中查找子串位置(暴力匹配):BF算法实现

各位同学,今天我们来聊聊字符串查找中最基础、最直观的算法——BF算法。

BF算法,全称是Brute-Force,翻译过来就是「暴力匹配」。别被「暴力」这俩字吓到,它其实是最朴素的思路:拿着子串,在主串里一个一个位置去试。说白了,就是最笨的办法,但也是最容易理解的办法。

20.1 什么是BF算法?

假设你有一个主串S,比如"hello world",你想知道子串T"world"在不在里面,在的话在哪个位置。

BF算法的做法很简单:

  • 从主串的第一个字符开始,和子串的第一个字符比较
  • 如果相等,继续比较下一个字符
  • 如果不相等,主串回退到第二个字符,子串回退到开头,重新比较
  • 一直重复,直到找到匹配或者主串遍历完

我刚开始学C语言的时候,第一次接触字符串匹配,老师问我们怎么实现。我当时的想法就是:两个循环嵌套,外层遍历主串,内层遍历子串。嗯,这就是BF算法的核心思想。

20.2 BF算法的代码实现

直接上代码。我个人习惯把函数命名为str_index_bf,清晰明了。

#include <stdio.h>
#include <string.h>

/**
 * BF算法:在字符串S中查找子串T第一次出现的位置
 * @param S 主串
 * @param T 子串
 * @param pos 从主串的哪个位置开始查找(从0开始)
 * @return 成功返回子串起始下标,失败返回-1
 */
int str_index_bf(const char *S, const char *T, int pos) {
    int i = pos;   // 主串当前比较位置
    int j = 0;     // 子串当前比较位置
    int s_len = strlen(S);
    int t_len = strlen(T);

    // 边界检查
    if (pos < 0 || pos >= s_len) {
        return -1;
    }
    if (t_len == 0) {
        return 0;  // 空串约定返回0
    }

    while (i < s_len && j < t_len) {
        if (S[i] == T[j]) {
            // 字符相等,继续往后比较
            i++;
            j++;
        } else {
            // 字符不相等,主串回溯,子串重置
            i = i - j + 1;
            j = 0;
        }
    }

    // 判断是否匹配成功
    if (j == t_len) {
        return i - j;  // 返回子串起始位置
    } else {
        return -1;
    }
}

int main() {
    char S[] = "hello world, this is a test";
    char T[] = "world";

    int index = str_index_bf(S, T, 0);
    if (index != -1) {
        printf("子串 \"%s\" 在主串中的位置是:%d\n", T, index);
    } else {
        printf("未找到子串 \"%s\"\n", T);
    }

    return 0;
}

核心逻辑解析:

关键就在那个i = i - j + 1。当匹配失败时,主串指针i要回退到本轮匹配开始位置的下一个字符。而i - j正好是本次匹配的起始位置,再加1就是下一个位置。

20.3 图解BF算法执行过程

光说理论不够直观,我画了一张流程图,帮你理解BF算法的执行过程。

BF算法执行流程图 开始 初始化 i=pos, j=0 i < s_len 且 j < t_len ? j == t_len ? 返回 i - j 返回 -1 S[i] == T[j] ? i++, j++ i = i-j+1, j=0

从流程图可以看出,BF算法的核心就是一个循环嵌套。外层循环控制主串的遍历,内层循环控制子串的比较。匹配成功就继续,匹配失败就回溯。

20.4 时间复杂度分析

情况 时间复杂度 说明
最好情况 O(n + m) 第一次比较就匹配成功,或者每次第一个字符就不匹配
最坏情况 O(n × m) 每次匹配到子串最后一个字符才失败,比如主串"aaaaa...b",子串"aaa...b"
平均情况 O(n + m) 实际应用中,字符分布比较随机,平均性能还不错

其中n是主串长度,m是子串长度。

⚠️ 避坑指南:

我曾经在一个日志分析工具里用了BF算法,日志文件有几百兆,子串长度也不短。结果跑一次要好几秒,用户直接投诉了。后来才发现,日志里大量重复字符,触发了最坏情况。所以,如果你的数据有大量重复模式,BF算法要慎用。

20.5 BF算法的优缺点

优点:

  • 实现简单,代码量少,容易理解
  • 不需要预处理,直接开干
  • 在大多数实际场景下,性能可以接受

缺点:

  • 最坏情况时间复杂度高,O(n×m)
  • 没有利用已经匹配过的信息,每次失败都要从头开始
  • 对于大规模文本处理,效率偏低

20.6 实际项目中的经验

我记得有一次做嵌入式开发,要在单片机里实现一个简单的命令解析器。用户输入命令字符串,程序要匹配预定义的命令列表。那时候内存紧张,不能引入复杂的算法库。我就用BF算法手写了一个匹配函数。

为什么选BF?因为命令都很短,最长不超过20个字符,而且命令列表也就几十条。BF算法在这种场景下完全够用,而且代码简单,不容易出bug。

💡 我的建议:

BF算法适合以下场景:

  • 子串长度较短(比如小于100个字符)
  • 主串和子串的字符分布比较随机
  • 对性能要求不高,或者数据量不大
  • 需要快速实现,不想引入复杂算法

如果数据量大,或者有大量重复字符,建议用KMP、BM等更高效的算法。

20.7 代码优化小技巧

虽然BF算法简单,但也有一些优化空间。我分享几个小技巧:

  1. 提前计算长度:在循环外调用strlen,避免每次循环都计算。
  2. 边界剪枝:主串剩余长度小于子串长度时,直接返回-1。
  3. 使用指针:在性能敏感的场合,可以用指针操作代替数组下标。

来看一个优化版的代码:

int str_index_bf_opt(const char *S, const char *T, int pos) {
    int s_len = strlen(S);
    int t_len = strlen(T);

    if (pos < 0 || pos >= s_len || t_len == 0) {
        return (t_len == 0) ? 0 : -1;
    }

    // 边界剪枝:主串剩余长度不够,直接返回
    if (s_len - pos < t_len) {
        return -1;
    }

    int i, j;
    for (i = pos; i <= s_len - t_len; i++) {
        for (j = 0; j < t_len; j++) {
            if (S[i + j] != T[j]) {
                break;
            }
        }
        if (j == t_len) {
            return i;
        }
    }
    return -1;
}

这个版本用了两层for循环,逻辑更清晰。外层循环只遍历到s_len - t_len,避免了不必要的比较。

20.8 总结

BF算法是字符串匹配的入门算法,它简单、直观、容易实现。虽然效率不是最高的,但它是理解更高级算法的基础。你想想看,KMP算法就是在BF的基础上,加入了「部分匹配表」来避免回溯。没有BF,你怎么理解KMP为什么要那样优化?

我个人建议,初学者一定要亲手写一遍BF算法。不要复制粘贴,一个字一个字敲出来。调试过程中遇到的各种问题——比如数组越界、死循环、返回值错误——都是宝贵的经验。

好了,BF算法就讲到这里。代码不多,但值得你反复琢磨。下一章我们会讲KMP算法,到时候你会感谢今天认真学BF的自己。


公众号:蓝海资料掘金营,微信deep3321