
1. 项目概述为什么暴力搜索依然是基石在C/C的算法世界里提到字符串匹配很多人第一时间会想到KMP、BM、Sunday这些听起来就很高大上的高效算法。确实在面试八股文里它们是被反复背诵的重点。但作为一个写了十几年C的老码农我必须说暴力搜索Brute-Force Search这个最原始、最“笨”的方法依然是每个开发者必须吃透的基石。它不仅是理解所有高级算法思想的起点更是在大量实际场景中最简单、最可靠的首选方案。所谓“String pattern search”就是在一个主字符串我们常称为text中寻找一个子字符串称为pattern首次出现的位置。比如在文本编辑器里按CtrlF查找一个词本质上就是在做这件事。暴力搜索的思路直白得惊人从主串的第一个字符开始尝试与模式串的每一个字符逐个比较如果中间有任何一个字符对不上就把模式串整体向后滑动一位从头再来。这个过程就像你拿着一把尺子模式串一寸一寸地在一段布料主串上比划看看哪里能完全对上。你可能会问这么“慢”的方法还有什么好讲的这里有几个关键点首先它的时间复杂度在最坏情况下是O(m*n)m和n分别是模式串和主串的长度这在理论算法课上是被“批判”的对象。但理论归理论实践是另一回事。对于日常开发中绝大多数短文本、一次性搜索的场景比如解析配置文件、处理命令行参数、验证用户输入的关键字模式串和主串的长度都很有限暴力搜索的性能开销微乎其微其实现简单、无额外内存消耗、逻辑清晰的优点就凸显出来了。其次它是所有优化算法的参照系。你不彻底理解暴力搜索为何“慢”就永远无法真正领悟KMP如何利用“失败函数”避免回溯或者BM算法如何利用“坏字符规则”实现跳跃式移动。最后在要求代码极度稳定、可读性优先的底层系统或嵌入式开发中一个没有复杂状态机、没有预处理表的暴力搜索函数往往比一个调用了高级算法库但可能引入隐晦边界错误的函数更让人安心。所以今天我们就抛开那些炫技的算法回归本源把C/C下的暴力搜索字符串匹配算法从里到外、从原理到源码、从常规实现到极致优化掰开揉碎了讲清楚。无论你是正在啃《数据结构与算法》的学生还是需要在项目中快速实现一个字符串查找功能的工程师这篇文章都能给你一份可以直接“抄作业”的可靠方案。2. 暴力搜索的核心思想与算法流程拆解2.1 算法思想可视化尺子与布料的比喻让我们把算法思想变得更形象。假设主串text hello world模式串pattern world。第一轮比对我们把尺子world的起点对准布料hello world的第一个字符h。比较pattern[0]的w和text[0]的h- 不匹配。结论本轮失败。尺子向右滑动1位。第二轮比对尺子起点对准text[1]的e。wvse- 不匹配。滑动。第三至第六轮比对同理尺子依次与l,l,o, 对齐首字符均不匹配快速滑动。第七轮比对尺子起点对准text[6]的w。pattern[0]的wvstext[6]的w- 匹配。pattern[1]的ovstext[7]的o- 匹配。pattern[2]的rvstext[8]的r- 匹配。pattern[3]的lvstext[9]的l- 匹配。pattern[4]的dvstext[10]的d- 匹配。所有字符匹配成功算法返回起始位置6假设从0开始计数。这个过程中最核心的操作就是两个嵌套循环外层循环控制尺子模式串在主串上的起始位置i内层循环控制尺子上的每个字符j并与主串上对应位置ij的字符进行比较。2.2 算法流程的伪代码与边界条件分析基于上述思想我们可以写出最朴素的伪代码函数 bruteForceSearch(text, pattern): n text的长度 m pattern的长度 对于 i 从 0 到 n-m: 对于 j 从 0 到 m-1: 如果 text[ij] ! pattern[j]: 跳出内层循环 // 本次对齐失败 如果 j m: // 内层循环完整执行完毕说明所有字符都匹配了 返回 i // 找到匹配返回起始位置 返回 -1 // 未找到匹配这里有几个极其关键的边界条件是新手甚至老手都容易栽跟头的地方外层循环的终止条件i n-m这是算法的安全边界。当主串剩余的长度已经小于模式串的长度时就绝对不可能再匹配成功了必须停止搜索。如果写成i n在i接近末尾时内层循环访问text[ij]就会发生数组越界这是未定义行为可能导致程序崩溃或产生不可预知的结果。这是暴力搜索实现中的第一个“坑”。内层循环的提前退出一旦发现不匹配应立即break跳出内层循环并尝试下一个起始位置。这是效率的关键避免了无谓的比较。匹配成功的判断条件j m内层循环如果正常执行完毕即没有因不匹配而break循环变量j的值会自增到m因为j后判断j m不成立而退出。利用这个特性我们可以简洁地判断是否完全匹配而无需引入额外的标志变量。注意在实际C/C编码中字符串通常以空字符\0结尾。我们的算法逻辑不依赖这个终止符来进行长度判断或比较而是显式地使用字符串长度n和m。这是一种更安全、更通用的做法也适用于处理二进制数据等非字符串场景。3. C/C 暴力搜索源码实现与逐行解析理解了思想我们来看代码。下面我将给出一个工业级强度的C实现它考虑了可读性、效率和一些常见的优化。3.1 基础版本实现#include cstring // for strlen, 但更推荐使用string和.size() // 使用C风格字符串的版本 int bruteForceSearch_C(const char* text, const char* pattern) { // 防御性编程检查输入指针是否有效 if (text nullptr || pattern nullptr) { return -1; // 或抛出异常根据项目约定 } size_t n strlen(text); size_t m strlen(pattern); // 边界条件1模式串为空约定俗成返回0表示在起始位置“找到”空串 if (m 0) { return 0; } // 边界条件2主串长度小于模式串不可能匹配 if (n m) { return -1; } // 核心搜索循环 // i 表示在主串text中的起始比较位置 for (size_t i 0; i n - m; i) { size_t j; // j 表示在模式串pattern中的比较位置 for (j 0; j m; j) { if (text[i j] ! pattern[j]) { break; // 发现不匹配跳出内层循环尝试下一个i } } // 判断内层循环是否完整走完 if (j m) { return static_castint(i); // 找到匹配返回起始索引 } } return -1; // 遍历所有可能起始位置未找到匹配 } // 使用C std::string的版本 (更现代更安全) int bruteForceSearch_CPP(const std::string text, const std::string pattern) { size_t n text.size(); size_t m pattern.size(); if (m 0) return 0; if (n m) return -1; for (size_t i 0; i n - m; i) { size_t j 0; for (; j m; j) { if (text[i j] ! pattern[j]) { break; } } if (j m) { return static_castint(i); } } return -1; }逐行解析与心得输入验证C风格版本开头检查了空指针。在生产代码中这很重要尤其是当参数可能来自不可信的来源时。C的std::string引用则无需此检查因为引用不能为空。长度获取C版本使用strlen其时间复杂度是O(n)。如果在一个循环中反复调用此搜索函数且字符串不变这是一个可以优化的点提前计算并传入长度。C版本的.size()是常数时间。空串处理这是一个常见的约定。寻找空串应该返回什么大多数标准库函数如strstr的行为是返回主串的起始地址。我们这里遵循类似约定返回0。明确处理它可以使函数行为更可预测。循环条件i n - m再次强调这是防止数组越界的生命线。当n和m都是size_t无符号整数时n - m在n m时会发生下溢得到一个巨大的正数导致循环访问非法内存。这就是为什么我们在循环前必须加上if (n m) return -1;这个守卫条件。类型转换返回值是int而索引是size_t。使用static_castint(i)进行显式转换避免了隐式转换的警告也明确了设计意图这个函数可能返回-1表示失败因此使用有符号整数。局部变量j的作用域将j的声明放在内层for循环之外是为了在循环结束后还能访问它以判断是否匹配成功。这是一种经典的C语言模式。3.2 性能优化版本基础版本清晰但还有优化空间。我们来看一个微优化版本它通常比基础版本快10%-30%。int bruteForceSearch_Opt(const char* text, const char* pattern) { if (!text || !pattern) return -1; const char* t text; const char* p pattern; // 手动计算长度避免重复调用strlen // 注意这里假设pattern以\0结尾是标准C字符串。 size_t m 0; while (p[m]) m; // 计算模式串长度 if (m 0) return 0; size_t n 0; while (t[n]) n; // 计算主串长度 if (n m) return -1; // 关键优化预计算循环边界 const char* end_pos t (n - m); for (const char* pos t; pos end_pos; pos) { const char* t_ptr pos; const char* p_ptr pattern; // 手动展开比较循环利用指针直接操作 while (*p_ptr) { if (*t_ptr ! *p_ptr) { break; } t_ptr; p_ptr; } // 判断是否比较到了pattern的末尾 if (*p_ptr \0) { return static_castint(pos - t); // 计算并返回索引 } } return -1; }优化点解析指针操作直接使用指针t_ptr和p_ptr遍历字符串比使用下标text[ij]在语法上更简洁在某些编译器优化下可能更高效。预计算边界end_pos t (n - m)直接计算出了最后一个可能的匹配起始地址。循环条件pos end_pos非常直观。循环终止判断内层循环通过判断*p_ptr是否为\0来结束这省去了一个循环变量j并将匹配成功的判断整合进了循环条件检查中逻辑紧凑。返回值计算通过指针相减pos - t得到整数索引这是指针运算的合法应用且效率很高。实操心得这种优化在pattern较短时效果比较明显。但对于现代编译器和优化器如GCC的-O2 MSVC的/O2来说基础版本通常也能被优化得很好。代码清晰性优先除非你是在性能关键的底层循环中比如搜索引擎的核心匹配器否则基础版本通常是更好的选择。先写对再测性能必要时才优化。4. 算法复杂度分析与实际场景评估4.1 时间复杂度最好、最坏与平均暴力搜索算法的时间复杂度分析是理解其性能局限性的关键最好情况时间复杂度 O(m)这发生在模式串就在主串的开头。只需要进行m次字符比较内层循环完整执行一次就找到了外层循环只执行了一次。例如在hello中找he。最坏情况时间复杂度 O(m * n)这是算法被诟病的主要原因。发生在两种典型场景主串是AAAA...AAAAn个A模式串是AA...ABm-1个A加一个B。每次比较都在最后一个字符失败总共需要大约(n-m1) * m次比较。主串是AAAA...AAAA模式串是AA...AA全是A。虽然最终能匹配成功但每次比较都需要走完整个模式串长度比较次数同样是(n-m1) * m量级。平均情况时间复杂度 O(n m)在随机文本和随机模式串的情况下平均比较次数与nm成正比远好于最坏情况。因为在不匹配时通常很快在前几个字符就会break。4.2 空间复杂度O(1)这是暴力搜索最大的优势之一。它只需要常数级别的额外空间用于存储几个索引或指针变量。无论主串和模式串有多长它占用的额外内存都固定不变。相比之下KMP算法需要O(m)的空间来存储“部分匹配表”next数组BM算法的好后缀和坏字符规则也需要额外的预处理空间。4.3 实际应用场景选择指南那么在实际项目中何时该用暴力搜索何时该考虑更高级的算法呢我总结了一个简单的决策流优先考虑暴力搜索如果模式串非常短通常m 10。预处理高级算法带来的开销可能已经超过了搜索本身。搜索是“一次性”或低频操作。比如解析一个命令行参数、在一个配置文件中查找某个键。实现简单、不出错比微小的性能差异更重要。主串长度有限。例如在UI中搜索用户当前输入的短文本。开发环境或运行环境受限。例如嵌入式系统内存宝贵代码空间小一个简单可靠的暴力搜索比引入复杂的算法库更合适。你需要实现的只是一个原型或演示。快速实现功能是首要目标。考虑使用KMP、BM、Sunday等算法如果模式串较长且需要在极长的文本中反复搜索同一个模式。预处理的开销被均摊高效的搜索算法能带来显著收益。例如在文本编辑器中持续查找、在大型日志文件中反复搜索特定错误码。主串和模式串具有明显的“坏字符”特征。例如在英文文本中搜索一个包含稀有字母如‘z’ ‘x’的单词BM算法能大幅跳跃。性能是核心需求并且经过 profiling 证实暴力搜索是瓶颈。不要过早优化但当工具性能分析器告诉你这里是热点时就该升级算法了。一个经验法则对于95%的日常业务逻辑中的字符串查找暴力搜索完全够用且是最佳选择。它的简单性意味着更少的bug更易维护的代码。5. 常见问题、调试技巧与边界测试即使是一个简单的算法在实现和使用的过程中也会遇到各种坑。下面是我在多年开发中总结的一些常见问题和解决技巧。5.1 编译与运行问题排查表问题现象可能原因解决方案与调试技巧程序崩溃Segmentation Fault1. 传入的text或pattern指针为NULL。2. 数组越界。外层循环条件错误如i n当i很大时text[ij]访问越界。1. 在函数入口添加空指针检查。2.重点检查循环条件是否为i n - m。使用调试器或打印i,n,m的值在循环前和循环中观察。确保n-m计算正确且不会下溢对于无符号数。返回错误的位置1. 返回值的类型或计算错误。例如使用了指针但返回了错误的偏移量。2. 匹配成功判断逻辑有误。1. 对于指针版本确认返回位置 当前指针 - 起始指针。对于索引版本确认返回的是i。2. 单步调试内层循环观察j变量在匹配成功时的值是否等于m。在应该找到时返回-11. 大小写敏感问题。‘A’和‘a’在比较中被视为不同。2. 字符串包含不可见字符如空格、制表符、换行符。3. 编码问题。例如在UTF-8多字节字符处进行比较。1. 如果需求不区分大小写实现一个专用的caseInsensitiveCompare函数或在比较前使用tolower()/toupper()转换。2. 在调试时将字符串的每个字符以整数形式ASCII码打印出来检查。3.暴力搜索算法通常只适用于单字节字符集如ASCII或已知编码的字节流。对于UTF-8你需要按字符可能多字节进行遍历而不是按字节。死循环或性能极差1. 外层或内层循环的终止条件永远无法满足。2. 遇到了最坏情况的输入如全A串中找A…AB。1. 检查循环变量i,j是否被错误地修改。2. 对于已知的、可能产生最坏情况的输入如果性能不可接受考虑换用KMP等算法。5.2 必须进行的边界测试用例编写完暴力搜索函数后务必用以下测试用例进行验证。这些是我从无数个深夜调试中总结出来的“必测清单”// 假设有一个测试函数 testSearch(func)这里用思路说明 void runTests() { const char* text hello world, this is a test string.; const char* pattern; // 1. 基础功能测试 pattern world; assert(search(text, pattern) 6); // 2. 边界测试模式串在开头 pattern hello; assert(search(text, pattern) 0); // 3. 边界测试模式串在结尾 pattern string.; assert(search(text, pattern) strlen(text) - strlen(pattern)); // 4. 边界测试模式串为空 pattern ; assert(search(text, pattern) 0); // 通常约定返回0 // 5. 边界测试主串为空且模式串非空 assert(search(, abc) -1); // 6. 边界测试主串和模式串都为空 assert(search(, ) 0); // 7. 边界测试模式串比主串长 assert(search(ab, abcd) -1); // 8. 特殊字符测试 assert(search(a\nb\tc, \n) 1); // 包含换行符 assert(search(a b c, ) 1); // 包含空格 // 9. 重复字符测试最坏情况触发 text AAAAAAAAAAAAAAAAAAAAAB; // 很多A后跟一个B pattern AAAAAC; // 前面很多A匹配最后一个C不匹配 // 这里主要测试程序不崩溃性能可以接受。返回值应为-1。 // 10. 完全匹配测试另一个最坏情况 text AAAAAAAAAAAAAAAAAAAAAA; pattern AAAAA; // 测试能正确找到所有匹配如果函数设计为找第一个则返回0 // 11. 指针安全测试 assert(search(nullptr, abc) -1); assert(search(abc, nullptr) -1); assert(search(nullptr, nullptr) -1); // 根据你的设计决定 std::cout All basic tests passed! std::endl; }测试心得第4、5、6条关于空串的测试至关重要很多边缘情况bug都源于此。第11条空指针测试在C风格版本中必不可少。对于Cstd::string版本则无需担心。5.3 在VS Code等IDE中调试C/C算法很多新手在VS Code中配置C/C环境后不知道如何有效地调试这类算法。这里分享一个快速定位暴力搜索bug的方法配置好launch.json确保能正常启动调试。在函数入口和循环开始处设置断点。使用“调试控制台”或“监视窗口”添加对text,pattern,n,m的监视。特别监视i和text[ij]以及pattern[j]的值。单步执行F10一步步执行观察内层循环是如何因为字符不匹配而break的以及外层循环i是如何递增的。当怀疑越界时在循环内添加一个条件断点例如当ij n时中断这能立刻捕捉到越界访问的瞬间。调试的核心是观察程序的实际状态是否与你设想的状态一致。暴力搜索逻辑简单通过观察几次循环几乎能定位所有实现上的错误。6. 从暴力搜索到更优算法思想延伸与对比虽然本文聚焦暴力搜索但了解其与高级算法的联系能帮助我们更好地理解这个领域。暴力搜索的“笨”在于每次匹配失败后它只将模式串向后滑动一位并且完全丢弃了这次失败匹配中获得的信息。6.1 KMP算法利用“已知信息”避免回溯KMP算法的精髓在于当某次匹配失败时它已经知道了主串中当前失败位置之前的某些字符是什么。通过一个预先计算好的next数组或称“部分匹配表”它能够确定模式串可以安全地向后滑动多远而不仅仅是一位并且主串的指针i不需要回溯。与暴力搜索的关联你可以把KMP看作是暴力搜索的“智能版”。它保留了暴力搜索中主串指针i只增不减的特点这是其高效的原因之一但通过预处理模式串本身的信息让模式串指针j在失败时能回退到一个合理的位置而不是每次都回到0。理解暴力搜索中i和j的回溯过程是理解KMP为何要计算next数组的基础。6.2 Boyer-Moore算法从后往前匹配与跳跃BM算法则采用了更激进的策略。它有两个核心规则坏字符规则当发现一个不匹配的字符坏字符时它在模式串中寻找该字符最后一次出现的位置然后将模式串对齐到这个位置。这可能导致模式串一次滑动多位。好后缀规则当发现尾部有一部分匹配好后缀时利用这部分信息进行滑动。与暴力搜索的关联BM算法通常从模式串的末尾开始比较这看起来和暴力搜索从开头比较完全不同。但这种“反向比较”的策略在实践中尤其是自然语言文本中能更快地发现不匹配从而触发更大的滑动距离。学习BM算法会让你反思暴力搜索“从前到后、逐位滑动”这个默认策略是否总是最优。6.3 如何选择一个简单的决策树面对一个具体的字符串搜索问题我的选择思路通常是开始 | V 模式串是否非常短5或搜索频率极低 |-- 是 -- 使用暴力搜索实现简单无额外开销 |-- 否 -- 进入下一步 | V 是否需要搜索多个不同的模式串 |-- 是 -- 考虑将主串预处理为更高效的数据结构如后缀树、后缀数组或使用Aho-Corasick自动机多模式匹配。 |-- 否 -- 进入下一步 | V 模式串本身是否具有显著特征如包含稀有字符 |-- 是 -- 优先尝试Boyer-Moore算法坏字符规则可能带来巨大跳跃。 |-- 否 -- 进入下一步 | V 文本和模式串是否来自特定领域如DNA序列字符集很小如{A,T,C,G} |-- 是 -- 字符集小暴力搜索最坏情况容易触发。考虑使用基于自动机的算法如KMP或Sunday等。 |-- 否 -- 通用文本模式串较长 -- 使用Boyer-Moore或经过高度优化的库实现如C标准库的strstr现代编译器对其有深度优化。 | V 实现复杂度与性能的权衡 |-- 追求极简实现和可维护性 -- 暴力搜索 |-- 追求最佳平均性能可接受预处理开销 -- Boyer-Moore |-- 追求最坏情况性能保证 -- KMP记住strstr、std::string::find这些标准库函数在背后很可能已经为你选择了当前平台和场景下最优的算法可能是暴力搜索的优化版本也可能是BM或KMP的变种。在绝大多数情况下直接使用它们是最佳实践。自己重新实现一个字符串搜索函数更多是为了学习算法原理或在某些无法使用标准库的特殊环境中。7. 实战集成到项目与性能对比实验最后我们来点实际的。假设你有一个项目需要自己实现字符串查找也许是为了教学也许是环境限制。如何优雅地集成它并如何验证它的性能呢7.1 编写一个可复用的头文件创建一个brute_force_search.h头文件提供清晰、安全的接口。// brute_force_search.h #ifndef BRUTE_FORCE_SEARCH_H #define BRUTE_FORCE_SEARCH_H #include cstddef // for size_t // C风格字符串接口 // 在text中查找pattern第一次出现的位置返回索引从0开始未找到返回-1。 // 要求text和pattern必须以\0结尾。若传入nullptr行为未定义或可添加检查返回-1。 int bf_search_cstr(const char* text, const char* pattern); // 带长度参数的通用接口更安全可用于二进制数据 // 在text的前text_len字节中查找pattern的前pattern_len字节。 int bf_search_mem(const char* text, size_t text_len, const char* pattern, size_t pattern_len); // C std::string 接口 #include string int bf_search_string(const std::string text, const std::string pattern); #endif // BRUTE_FORCE_SEARCH_H对应的实现文件brute_force_search.cpp// brute_force_search.cpp #include brute_force_search.h int bf_search_cstr(const char* text, const char* pattern) { // 使用优化版本的指针实现 if (!text || !pattern) return -1; // 简单检查生产环境可能需要更严谨 const char* t text; const char* p pattern; size_t m 0; while (p[m]) m; if (m 0) return 0; size_t n 0; while (t[n]) n; if (n m) return -1; const char* end_pos t (n - m); for (const char* pos t; pos end_pos; pos) { const char* t_ptr pos; const char* p_ptr p; while (*p_ptr *t_ptr *p_ptr) { t_ptr; p_ptr; } if (*p_ptr \0) { return static_castint(pos - t); } } return -1; } int bf_search_mem(const char* text, size_t text_len, const char* pattern, size_t pattern_len) { if (pattern_len 0) return 0; if (text_len pattern_len) return -1; for (size_t i 0; i text_len - pattern_len; i) { size_t j 0; for (; j pattern_len; j) { if (text[i j] ! pattern[j]) { break; } } if (j pattern_len) { return static_castint(i); } } return -1; } int bf_search_string(const std::string text, const std::string pattern) { // 直接调用通用内存版本避免重复逻辑 return bf_search_mem(text.data(), text.size(), pattern.data(), pattern.size()); }7.2 简单的性能对比实验想知道暴力搜索到底比标准库慢多少写个简单的测试程序。注意这是一个非常粗略的对比旨在感受量级差异。#include iostream #include string #include chrono #include brute_force_search.h #include cstring // for strstr int main() { // 构造一个较长的文本和一个中等的模式串 std::string long_text(100000, A); // 10万个A long_text THE_NEEDLE_IN_THE_HAYSTACK; long_text std::string(100000, B); // 再接10万个B std::string pattern THE_NEEDLE_IN_THE_HAYSTACK; const char* c_text long_text.c_str(); const char* c_pattern pattern.c_str(); int result_bf, result_std; auto start std::chrono::high_resolution_clock::now(); for (int i 0; i 1000; i) { // 重复多次以测量 result_bf bf_search_cstr(c_text, c_pattern); } auto end std::chrono::high_resolution_clock::now(); auto duration_bf std::chrono::duration_caststd::chrono::microseconds(end - start); start std::chrono::high_resolution_clock::now(); for (int i 0; i 1000; i) { result_std (strstr(c_text, c_pattern) - c_text); // strstr返回指针计算偏移 } end std::chrono::high_resolution_clock::now(); auto duration_std std::chrono::duration_caststd::chrono::microseconds(end - start); std::cout Brute-Force result: result_bf , time: duration_bf.count() us\n; std::cout strstr result: result_std , time: duration_std.count() us\n; std::cout Ratio (BF/std): (double)duration_bf.count() / duration_std.count() std::endl; return 0; }运行结果分析在我的测试环境Release模式编译下strstr通常比我们手写的暴力搜索快数倍甚至数十倍。这是因为标准库的实现可能是高度优化的汇编代码如x86的repne scasb等指令。编译器可能对标准库函数有内置intrinsic优化。strstr的内部实现很可能不是朴素的暴力搜索而是综合了多种策略的优化算法。这个实验告诉我们一个道理在追求性能的生产代码中优先使用标准库函数。自己实现的算法其价值在于理解原理、应对特殊需求、以及在无法使用标准库的环境下提供解决方案。暴力搜索字符串匹配就像编程世界里的扎马步。它不炫酷但扎实它不高效但通用它是一切复杂搜索算法的起点。吃透它不仅能让你在需要时快速写出可用的代码更能为你打开一扇门去理解那些精妙算法究竟在解决什么问题。下次当你顺手写下str.find()时不妨想想背后这个朴素而强大的思想。