跟我学C++中级篇——C++中的匹配查找

一、匹配查找

在计算机的应用中,不管是互联网还是到某个专业的场景,对数据的匹配和查找是一个重要的应用。不管是电商中的商品查找还是金融系统中的资金定位,都无法离开这种搜索和查找。而在这个过程中,如何能够匹配得更准确更快捷也是一个重要的方式。那么现在就明白了,所谓查找其实就是一种遍历,而匹配则是一种带策略的查找。
无论在哪种语言中,这种应用都是基础的技术构建。下面就对C++中的相关方法进行分析说明。

二、C++中常见方法

在C++中匹配查找的方法可以分为几大类:

  1. 循环遍历处理
    这是一种何种情况下都可以使用的方法,不管是查找还是匹配都可以由开发者自行在循环中进行处理
  2. STL算法
    标准库提供的相关算法,一般来说最稳定最快捷,让开发者应用起来也最简单的一种方式,它包括std::find,std::find_if以及std::any_of, std::all_of , std::none_of等
  3. 使用正则表达式
    这个也有很多种方法,STL中也提供了std::regex这种正则的处理办法
  4. 哈希表
    这个就非常常见了,在互联网企业的面试中,经常问到哈希表的冲突等问题。包括布隆过滤器等较高级的应用。在C++的STL中,std::unordered_set,std::map等就是基于此进行应用的。哈希表的特点就是快,特别是在大数据中查找,更是如此。缺点就是效率差并且容易产生冲突。
  5. 子串及模糊查找
    对于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²)
  6. 前缀处理
    这种处理的算法在区块链、互联网大数据处理等处也比较常见。主要的算法有:
    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是匹配数量。其主要应用的场景是病毒扫描、敏感词过滤、生物学信息的匹配以及网络入侵处理等

此处不是对算法详细解析的文章,以后有机会在算法的相关文章中会对这些算法进行详细的分析说明。如果开发者有兴趣可以自己查看相关资料中的技术细节和应用场景等。

三、例程

根据上述的说明,可以与下面的实例进行结合进行对比分析:

  1. 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";
}
  1. 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';
}
  1. 哈希表——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';
}
  1. 循环查找匹配
#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;
}

  1. 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;
}
  1. 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库本身的思想,如果对底层算法有兴趣,需要开发者自行对数据结构和算法等进行深入的了解和学习。其实从这一点也可以看标准库的设计目的,这也是开发者做中间层的目的之一。从某种角度看也是抽象隔离的一种方法,不需要每个开发者都需要深入的学习和掌握相关的算法。

### 哈希查找的平均查找长度分析 哈希查找是一种高效的查找方法,其性能取决于多个因素。当讨论哈希查找的平均查找长度时,主要考虑的是冲突处理机制以及负载因子的影响。 #### 负载因子与冲突率的关系 负载因子 \( \alpha = \frac{n}{m} \),其中 \( n \) 是存储在哈希表中的元素数量,\( m \) 是哈希表槽数量。随着负载因子增加,发生冲突的概率也会增大,这会直接影响到查找效率[^1]。 #### 成功查找的平均比较次数 对于成功的查找操作,在理想情况下(即没有冲突),每次查找只需要一次访问即可完成。然而实际上由于存在碰撞,因此需要额外计算探查序列长度。如果采用开放地址法,则成功查找的期望时间复杂度可以表示为: \[ E_{\text{success}}(\alpha)=\begin{cases} \sum^{i=0}_{k}\left(1-\alpha+\alpha k\right)\cdot p(k), & \text{(线性探测)} \\ \ln{\frac{1}{1-\alpha}},& \text{(二次探测/双重散列)} \end{cases} \] 这里 \(p(k)\) 表示第 \(k\) 次尝试找到目标键值的概率分布函数[^3]。 #### 失败查找的平均比较次数 对于未存在于哈希表内的关键字进行查找称为失败查找。此时无论哪种方式实现,都需要遍历整个可能的位置直到确认不存在该记录为止。理论上讲,最坏情况下的查找成本接近于满表扫描的成本。但在实际应用中,通常假设均匀随机分布模型下估算失败查找所需的时间开销: \[E_{\text{failure}}=\frac{1}{1-\alpha}\] 此表达式适用于大多数常见的解决冲突的方法,比如链地址法、线性探测等[^2]。 ```python def expected_search_cost(alpha, method='linear'): """ 计算给定负载因子α条件下不同策略的成功和不成功查找预期代价 参数: alpha (float): 负载因子 α=n/m method (str): 解决冲突的方式 ('chain', 'linear') 返回: tuple(float,float): 分别对应成功查找和失败查找的预计花费 """ import math if method == "chain": success = 1 + alpha / 2 failure = 1 + alpha elif method == "quadratic" or method == "double_hashing": success = math.log(1/(1-alpha)) failure = 1 / (1 - alpha) else: # 默认为 linear probing from scipy.stats import geom pmf = lambda k : ((1 - alpha)**k)*alpha success = sum([(1-(alpha+(alpha*k)) * geom.pmf(k+1, alpha)) for k in range(int(1//alpha)+1)]) failure = 1 / (1 - alpha) return round(success,4),round(failure,4) print(expected_search_cost(.7,'linear')) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值