一、匹配查找
在计算机的应用中,不管是互联网还是到某个专业的场景,对数据的匹配和查找是一个重要的应用。不管是电商中的商品查找还是金融系统中的资金定位,都无法离开这种搜索和查找。而在这个过程中,如何能够匹配得更准确更快捷也是一个重要的方式。那么现在就明白了,所谓查找其实就是一种遍历,而匹配则是一种带策略的查找。
无论在哪种语言中,这种应用都是基础的技术构建。下面就对C++中的相关方法进行分析说明。
二、C++中常见方法
在C++中匹配查找的方法可以分为几大类:
- 循环遍历处理
这是一种何种情况下都可以使用的方法,不管是查找还是匹配都可以由开发者自行在循环中进行处理 - STL算法
标准库提供的相关算法,一般来说最稳定最快捷,让开发者应用起来也最简单的一种方式,它包括std::find,std::find_if以及std::any_of, std::all_of , std::none_of等 - 使用正则表达式
这个也有很多种方法,STL中也提供了std::regex这种正则的处理办法 - 哈希表
这个就非常常见了,在互联网企业的面试中,经常问到哈希表的冲突等问题。包括布隆过滤器等较高级的应用。在C++的STL中,std::unordered_set,std::map等就是基于此进行应用的。哈希表的特点就是快,特别是在大数据中查找,更是如此。缺点就是效率差并且容易产生冲突。 - 子串及模糊查找
对于C++开发者来说,子串查找最常见的就是字符串的处理。主要的算法包括Suffix Array(后缀数组),而模糊查找的算法主要有BK-Tree(Burkhard-Keller Tree)。
Suffix Array:将一个字符串的所有后缀按照字典序 进行排序后,将排序后的后缀的起始下标 存储在一个数组中,这个数组就是后缀数组。主要应用于简单的模式匹配、处理子串的各种算法及数据压缩等。其时间复杂度O(n) (最优) 或 O(n log n),而空间复杂度为O(N)
BK-Tree:是一种度量树数据结构,专门为离散度量空间设计,主要用于拼写检查、相似性搜索、模糊匹配。其核心的思想是利用三角不等式来高效地排除大量不可能匹配的候选项,从而减少需要直接计算距离的次数。其时间复杂度为O(L log n)~O(nL),L是字符串平均长度;空间复杂度为O(n)~O(n²) - 前缀处理
这种处理的算法在区块链、互联网大数据处理等处也比较常见。主要的算法有:
Trie(前缀树):Trie 是专门为字符串检索设计的多叉树结构,其插入和搜索的算法复杂度为O(N),N为字符串长度;空间复杂度为O(AN),A是字母表大小。其应用场景主要包括拼写检查、路由表处理、自动补全以及其它一些需要处理类似字符串应用的情况
Radix Tree(压缩 Trie):对 Trie 的空间优化,合并了只有一个子节点的路径,搜索的时间复杂度O(N),但空间复杂度下降很多(看结点数量减少的数量)。其主要应用场景包括内存受限的数据存储、数据库索引以及文件系统路径的处理等
Ternary Search Tree(TST):它是二叉搜索树和Trie的结合,实现了空间效率和灵活性之间的平衡。搜索的时间复杂度为O(N + log n),n是树中字符串数量,空间复杂度为O(N)。其主要的应用场景包括拼写检查、近邻搜索和某些需要在时间和空间进行平衡的字符串检索场景中等
Aho–Corasick 自动机(AC 自动机):它是Trie的扩展,用于多模式匹配,可以在单次扫描中查找多个模式。其时间复杂度为O(N + Z),N是文本长度,Z是匹配数量。其主要应用的场景是病毒扫描、敏感词过滤、生物学信息的匹配以及网络入侵处理等
此处不是对算法详细解析的文章,以后有机会在算法的相关文章中会对这些算法进行详细的分析说明。如果开发者有兴趣可以自己查看相关资料中的技术细节和应用场景等。
三、例程
根据上述的说明,可以与下面的实例进行结合进行对比分析:
- STL中find及all_of等
//find系列
#include <algorithm>
#include <array>
#include <cassert>
#include <complex>
#include <initializer_list>
#include <iostream>
#include <vector>
bool is_even(int i)
{
return i % 2 == 0;
}
void example_contains()
{
const auto haystack = {1, 2, 3, 4};
for (const int needle : {3, 5})
if (std::find(haystack.begin(), haystack.end(), needle) == haystack.end())
std::cout << "haystack does not contain " << needle << '\n';
else
std::cout << "haystack contains " << needle << '\n';
}
void example_predicate()
{
for (const auto& haystack : {std::array{3, 1, 4}, {1, 3, 5}})
{
const auto it = std::find_if(haystack.begin(), haystack.end(), is_even);
if (it != haystack.end())
std::cout << "haystack contains an even number " << *it << '\n';
else
std::cout << "haystack does not contain even numbers\n";
}
}
void example_list_init()
{
std::vector<std::complex<double>> haystack{{4.0, 2.0}};
#ifdef __cpp_lib_algorithm_default_value_type
// T gets deduced making list-initialization possible
const auto it = std::find(haystack.begin(), haystack.end(), {4.0, 2.0});
#else
const auto it = std::find(haystack.begin(), haystack.end(), std::complex{4.0, 2.0});
#endif
assert(it == haystack.begin());
}
int main()
{
example_contains();
example_predicate();
example_list_init();
}
//all_of等
#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <numeric>
#include <vector>
int main()
{
std::vector<int> v(10, 2);
std::partial_sum(v.cbegin(), v.cend(), v.begin());
std::cout << "Among the numbers: ";
std::copy(v.cbegin(), v.cend(), std::ostream_iterator<int>(std::cout, " "));
std::cout << '\n';
if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; }))
std::cout << "All numbers are even\n";
if (std::none_of(v.cbegin(), v.cend(), std::bind(std::modulus<>(),
std::placeholders::_1, 2)))
std::cout << "None of them are odd\n";
struct DivisibleBy
{
const int d;
DivisibleBy(int n) : d(n) {}
bool operator()(int n) const { return n % d == 0; }
};
if (std::any_of(v.cbegin(), v.cend(), DivisibleBy(7)))
std::cout << "At least one number is divisible by 7\n";
}
- stl::regex系列
#include <iostream>
#include <regex>
#include <string>
int main()
{
std::string text = "Quick brown fox";
std::regex vowel_re("a|o|e|u|i");
std::cout << std::regex_replace(text, vowel_re, "[$&]") << '\n';
}
- 哈希表——stl::map系列
#include <cstddef>
#include <functional>
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
using namespace std::literals;
struct string_hash
{
using hash_type = std::hash<std::string_view>;
using is_transparent = void;
std::size_t operator()(const char* str) const { return hash_type{}(str); }
std::size_t operator()(std::string_view str) const { return hash_type{}(str); }
std::size_t operator()(const std::string& str) const { return hash_type{}(str); }
};
int main()
{
// simple comparison demo
std::unordered_map<int, char> example{{1, 'a'}, {2, 'b'}};
if (auto search = example.find(2); search != example.end())
std::cout << "Found " << search->first << ' ' << search->second << '\n';
else
std::cout << "Not found\n";
// C++20 demo: Heterogeneous lookup for unordered containers (transparent hashing)
std::unordered_map<std::string, size_t, string_hash, std::equal_to<>> map{{"one"s, 1}};
std::cout << std::boolalpha
<< (map.find("one") != map.end()) << '\n'
<< (map.find("one"s) != map.end()) << '\n'
<< (map.find("one"sv) != map.end()) << '\n';
}
- 循环查找匹配
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1,2,3,4,5,6};
for (const auto& i : data) {
if (i == 3) {
std::cout << "find data!"<<std::endl;
}
}
return 0;
}
- Suffix Array
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> createSuffixArray(const std::string& s) {
int n = s.length();
std::vector<int> suff(n);
// 初始化起始索引
for (int i = 0; i < n; i++) {
suff[i] = i;
}
// 字典序排序
sort(suff.begin(), suff.end(), [&](int a, int b) {
return s.compare(a, n - a, s, b, n - b) < 0;
});
return suff;
}
void displaySuffixArray(const std::string& s, const std::vector<int>& sa) {
cout << "Suffix Array: ";
for (int i : sa) {
std::cout << i << " ";
}
std::cout << "\n\n suff:\n";
for (int i = 0; i < sa.size(); i++) {
std::cout << "SA[" << i << "] = " << sa[i] << ": \"";
std::cout << s.substr(sa[i]) << "\"" << std::endl;
}
}
int main() {
std::string t = "banana";
std::vector<int> sa = createSuffixArray(t);
displaySuffixArray(t, sa);
return 0;
}
- Trie树
#include <iostream>
#include <memory>
using namespace std;
class Trie {
private:
struct TNode {
shared_ptr<TNode> children_[26] = {nullptr};
bool isEnd_ = false;
};
shared_ptr<TNode> root_;
public:
Trie() : root_(make_shared<TNode>()) {}
void insert(const string& word) {
auto node = root_;
for (char c : word) {
int id = c - 'a';
if (!node->children_[id]) {
node->children_[id] = make_shared<TNode>();
}
node = node->children_[id];
}
node->isEnd_ = true;
}
bool search(const string& word) {
auto node = root_;
for (char c : word) {
int id = c - 'a';
if (!node->children_[id]) return false;
node = node->children_[id];
}
return node->isEnd_;
}
bool startsWith(const string& prefix) {
auto node = root_;
for (char c : prefix) {
int id = c - 'a';
if (!node->children_[id]) return false;
node = node->children_[id];
}
return true;
}
};
int main() {
Trie t;
t.insert("this");
t.insert("test");
cout << "word 'this': " << t.search("this") << endl;
cout << "word 'teet': " << t.search("teet") << endl;
cout << "Has pre 'teet': " << t.startsWith("teet") << endl;
return 0;
}
如果对其中一些代码有困惑,可以查看一下具体的算法实现,就会明白,不必担心。
四、应用选择
在上述的几类选择匹配中,一般来说,常见的就是使用自定义循环处理和使用STL中的相关算法。比如std::find_if、std::all_of等。如果与模式匹配相关,如登陆的密码、邮箱的命名等可以使用正则表达式std::regex相关的接口。对数据的精确匹配和存在性处理可以使用哈希相关如unordered_map等。至于Trie、BK-Tree等的高级用法,一般是用在比较复杂的场景下,如大数据的搜索和模糊查找等。这个在遇到实际的情况时再斟酌考虑即可。
不过,还是要注意一些应用的限制,比如std::regex的应用的C++版本以及在GCC4时的一些小问题;另外还要注意在一些算法处理应用时,如果未考虑内存大小的限制,会导致分配的失败引起异常(典型的如Trie)。
当然,最重要的还是要考虑成本,这才是所有应用的一个关键控制因素,这里不用多说大家都明白。
五、总结
本文重点强调的说明和应用,对于算法本身没有展开。这也是STL库本身的思想,如果对底层算法有兴趣,需要开发者自行对数据结构和算法等进行深入的了解和学习。其实从这一点也可以看标准库的设计目的,这也是开发者做中间层的目的之一。从某种角度看也是抽象隔离的一种方法,不需要每个开发者都需要深入的学习和掌握相关的算法。

3万+

被折叠的 条评论
为什么被折叠?



