当前位置: 首页 > news >正文

C++ 哈希+unordered_map+unordered_set+位图+布隆过滤器(深度剖析)

文章目录

  • 1. 前言
  • 2. unordered 系列关联式容器
    • 2.1 unordered_map
      • 2.1.1 unordered_map 的概念
      • 2.1.2 unordered_map 的使用
    • 2.2 unordered_set
      • 2.2.1 unordered_set 的概念
      • 2.2.2 unordered_set 的使用
  • 3. 底层结构
    • 3.1 哈希的概念
    • 3.2 哈希冲突
    • 3.3 哈希函数
    • 3.4 哈希冲突的解决
      • 3.4.1 闭散列
      • 3.4.2 开散列
  • 4. 模拟实现
    • 4.1 哈希表的改造
    • 4.2 unordered_map 的模拟实现
    • 4.3 unordered_set 的模拟实现
  • 5. 哈希的应用
    • 5.1 位图
      • 5.1.1 位图的概念
      • 5.1.2 位图的实现
      • 5.1.3 位图的应用
    • 5.2 布隆过滤器
      • 5.2.1 布隆过滤器的提出
      • 5.2.2 布隆过滤器的概念
      • 5.2.3 布隆过滤器的实现
      • 5.2.4 布隆过滤器的优点
      • 5.2.5 布隆过滤器的缺陷


1. 前言

想象一下,你有一个巨大的图书馆,里面摆满了成千上万本书。如果你想要找到其中一本特定的书,你会怎么做?如果你按照书的编号来有序地排列它们,找到特定的书本可能会很容易。但是,如果书籍是随机地摆放,你可能需要逐本地查找,这个过程会非常耗时。

而哈希函数就像是给每本书分配一个独特的编号,然后将它们放置在合适的位置,使得我们能够快速地找到并访问它们。哈希函数能够将输入数据映射到一个固定大小的哈希表中,每个元素都有一个唯一的位置。当我们需要查找特定的元素时,只需使用哈希函数计算出它的位置,然后直接访问该位置的元素,无需遍历整个数据集。

这种基于哈希的快速查找技术在现代编程中非常常见。在本篇博客中,我们将深入剖析哈希相关的知识点。

本篇文章将着重讲解 unordered 系列关联式容器(unordered_map 和 unordered_set)、底层结构(哈希的概念、哈希函数、哈希冲突)、模拟实现(unordered_map 和 unordered_set 的模拟实现)以及哈希的应用(位图和布隆过滤器)。

2. unordered 系列关联式容器

在 C++98 中,STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在 C++11 中,STL又提供了4个 unordered 系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对 unordered_map 和 unordered_set 进行介绍。

2.1 unordered_map

2.1.1 unordered_map 的概念

英文解释:

在这里插入图片描述

也就是说:

  1. unordered_map 是存储 <key, value> 键值对的关联式容器,其允许通过 key 快速的索引到与其对应的 value。

  2. 在 unordered_map 中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。

  3. 在内部,unordered_map 没有对 <key, value> 按照任何特定的顺序排序, 为了能在常数范围内找到 key 所对应的 value,unordered_map 将相同哈希值的键值对放在相同的桶中。

  4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭代方面效率较低。

  5. unordered_map 实现了直接访问操作符(operator[]),它允许使用 key 作为参数直接访问 value。

  6. 它的迭代器至少是单向迭代器。

2.1.2 unordered_map 的使用

  1. unordered_map 的模板参数列表

    在这里插入图片描述

    说明:

    • key:键值对中 key 的类型。
    • T:键值对中 value 的类型。
    • Hash:哈希函数用于确定元素在内部数据结构中的位置。
    • Pred:键相等判断函数用于比较两个键是否相等。
    • Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。
  2. unordered_map 的构造函数

    函数声明功能介绍
    unordered_map构造不同格式的 unordered_map 对象。
  3. unordered_map 的容量操作

    函数名称函数声明功能简介
    emptybool empty () const;检测 unordered_map 中的元素是否为空,是返回 true,否则返回 false。
    sizesize_t size() const;返回 unordered_map 中有效元素的个数。
  4. unordered_map 的元素访问操作

    函数名称函数声明功能简介
    operator[]mapped_type& operator[] (const key_type& k);返回 k 对应的 value。
    atmapped_type& at (const key_type& k);
    const mapped_type& at (const key_type& k) const;
    返回 k 对应的 value。

    区分:

    在元素访问时,有一个与 operator[] 类似的操作 at 函数(该函数不常用),都是通过 key 找到与 key 对应的 value 然后返回其引用,不同的是:当 key 不存在时,operator[] 用默认 value 与 key 构造键值对然后插入,返回该默认 value;at 函数直接抛异常。

  5. unordered_map 的查找操作

    函数名称函数声明功能介绍
    finditerator find (const key_type& k);
    const_iterator find (const key_type& k) const;
    在 unordered_map 中查找 key 为 k 的元素,找到返回该元素位置的迭代器,否则返回 end。
    在 unordered_map 中查找 key 为 k 的元素,找到返回该元素位置的 const 迭代器,否则返回 cend。
    countsize_t count (const key_type& k) const;返回 unordered_map 中值为 k 的键值在 map 中的个数(这里只会返回0或1)。
  6. unordered_map 的修改操作

    函数名称函数声明功能介绍
    insertpair<iterator,bool> insert (const value_type& val);在 unordered_map 中插入键值对 val。如果插入成功,返回 <val 位置的迭代器,true>;如果插入失败,说明 val 在 unordered_map 中已经存在,返回 <val 位置的迭代器,false>。
    eraseiterator erase (const_iterator position);
    size_t erase (const key_type& k);
    删除 unordered_map 中 position 位置上的元素,并返回一个指向被删除元素之后位置的迭代器。
    删除 unordered_map 中键值为 k 的元素,返回删除的元素的个数(这里只会返回0或1)。
    swapvoid swap (unordered_map& ump);与 ump 交换元素。
    clearvoid clear();将 map 的元素清空。
  7. unordered_map 的桶操作

    函数名称函数声明功能介绍
    bucket_countsize_t bucket_count() const返回哈希桶中桶的总个数。
    bucket_sizesize_t bucket_size(size_t n) const返回 n 号桶中有效元素的总个数。
    bucketsize_t bucket(const key_type& k)返回元素 k 所在的桶号。

2.2 unordered_set

2.2.1 unordered_set 的概念

英文解释:

在这里插入图片描述

也就是说:

  1. unordered_set 是一种容器,它以无特定顺序存储唯一元素,并且允许根据 value 快速检索单个元素。

  2. 在 unordered_set 中,元素的 value 同时也是唯一标识它的 key。键是不可变的,因此,在容器中的元素一旦插入就不能修改,尽管可以插入和删除。

  3. 在内部,unordered_set 中的元素没有按照任何特定的顺序排序,而是根据它们的哈希值被组织到不同的桶中,以便能够通过值快速直接地访问单个元素(平均情况下具有常数时间复杂度)。

  4. 与集合容器相比,unordered_set 容器更快地通过 key 访问单个元素,尽管对于对子集进行范围迭代,它们通常不太高效。

  5. 容器中的迭代器至少是单向迭代器。

2.2.2 unordered_set 的使用

  1. unordered_set 的模板参数列表

    在这里插入图片描述

    说明:

    • T:unordered_set 中存放元素的类型。

    • Hash:哈希函数用于确定元素在内部数据结构中的位置。

    • Pred:键相等判断函数用于比较两个键是否相等。

    • Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。

  2. unordered_set 的构造函数

    函数声明功能介绍
    unordered_set构造不同格式的 unordered_set 对象。
  3. unordered_set 的容量操作

    函数名称函数声明功能介绍
    emptybool empty() const;检测 unordered_set 是否为空,空返回 true,否则返回 false。
    sizesize_t size() const;返回 unordered_set 中有效元素的个数。
  4. unordered_set 的查找操作

    函数名称函数声明功能介绍
    finditerator find (const value_type& val) const;在 unordered_set 中查找值为 val 的元素,如果找到则返回该元素位置的迭代器,未找到则返回 end 迭代器。
    countsize_t count (const value_type& val) const;返回 unordered_set 中值为 val 的元素的个数(这里只会返回0或1)。
  5. unordered_set 的修改操作

    函数名称函数声明功能介绍
    insertpair<iterator,bool> insert (const value_type& val);在 unordered_set 中插入元素 val。如果插入成功,返回 <val 位置的迭代器,true>;如果插入失败,说明 val 在 unordered_set 中已经存在,返回 <val 位置的迭代器,false>。
    eraseiterator erase (const_iterator position);
    size_t erase (const value_type& val);
    删除 unordered_set 中 position 位置上的元素,并返回一个指向被删除元素之后位置的迭代器。
    删除 unordered_set 中值为 val 的元素,返回删除的元素的个数(这里只会返回0或1)。
    swapvoid swap (unordered_set& ust);与 ust 交换元素。
    clearvoid clear();将 unordered_set 的元素清空。
  6. unordered_set 的桶操作

    函数名称函数声明功能介绍
    bucket_countsize_t bucket_count() const返回哈希桶中桶的总个数。
    bucket_sizesize_t bucket_size(size_t n) const返回 n 号桶中有效元素的总个数。
    bucketsize_t bucket(const key_type& k)返回元素 k 所在的桶号。

3. 底层结构

3.1 哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素

    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

  • 搜索元素

    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

例如:

数据集合:{ 1,7,6,4,5,9 };

哈希函数设置为:hash(key) = key % capacity;(capacity 为存储元素底层空间总的大小)

图解:

在这里插入图片描述

注:用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。

3.2 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

把具有不同关键码而具有相同哈希地址的数据元素称为同义词

发生哈希冲突该如何处理呢?

3.3 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中。
  • 哈希函数应该比较简单。

常见哈希函数

  1. 直接定址法(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀。

缺点:需要事先知道关键字的分布情况。

使用场景:适合查找比较小且连续的情况。

  1. 除留余数法(常用)

设散列表中允许的地址数为 m,取一个不大于 m,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:Hash(key) = key % p(p<=m),将关键码转换成哈希地址

  1. 平方取中法(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。

平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。

  1. 折叠法(了解)

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

  1. 随机数法(了解)

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key),其中 random 为随机数函数。

随机数法通常应用于关键字长度不等时。

  1. 数学分析法(了解)

设有 n 个 d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:

假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。

拓展:字符串Hash函数链接。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

3.4 哈希冲突的解决

解决哈希冲突两种常见的方法是:闭散列开散列

3.4.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?

  1. 线性探测

    比如3.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr 为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    • 插入

      • 通过哈希函数获取待插入元素在哈希表中的位置。
      • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
    • 删除

      采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。

      比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

      像这样:

      // 哈希表每个空间给个标记
      // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
      enum State {EMPTY, EXIST, DELETE};
      

    思考:哈希表什么情况下进行扩容?如何扩容?

    在这里插入图片描述

    线性探测的实现

    template<class K>
    struct HashFunc
    {size_t operator()(const K& key){return (size_t)key;}
    };// 字符串哈希函数特化:HashFunc<string>
    template<>
    struct HashFunc<std::string>
    {size_t operator()(const string& key){// BKDRsize_t hash = 0;for (auto e : key){hash *= 31;hash += e;}return hash;}
    };namespace open_address
    {enum Status{EMPTY,EXIST,DELETE};template<class K, class V>struct HashData{pair<K, V> _kv;Status _s;          //状态};template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable(){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;// 负载因子0.7就扩容if (_n * 10 / _tables.size() == 7){size_t newSize = _tables.size() * 2;HashTable<K, V, Hash> newHT;newHT._tables.resize(newSize);// 遍历旧表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._s == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}Hash hf;// 线性探测size_t hashi = hf(kv.first) % _tables.size();while (_tables[hashi]._s == EXIST){hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._s = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();while (_tables[hashi]._s != EMPTY){if (_tables[hashi]._s == EXIST&& _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi++;hashi %= _tables.size();}return nullptr;}// 伪删除法bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_s = DELETE;--_n;return true;}else{return false;}}// 打印观察分布void Print(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._s == EXIST){cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;}else if (_tables[i]._s == EMPTY){printf("[%zd]->\n", i);}else{printf("[%zd]->D\n", i);}}cout << endl;}private:vector<HashData<K, V>> _tables;size_t _n = 0; // 存储的关键字的个数};
    }
    

    线性探测优点:实现非常简单。

    线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据堆积,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低

  2. 二次探测

    线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m,或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i = 1,2,3,…, H 0 H_0 H0 是通过散列函数 Hash(x) 对元素的关键码 key 进行计算得到的位置,m 是表的大小

    对于3.1中如果要插入44,产生冲突,使用解决后的情况为:

    在这里插入图片描述

    研究表明:当表的长度为质数且表装载因子 a 不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子 a 不超过0.5,如果超出必须考虑增容

    因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

3.4.2 开散列

  1. 开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

在这里插入图片描述

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

  1. 开散列增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

  1. 开散列实现
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};// 字符串哈希函数特化:HashFunc<string>
template<>
struct HashFunc<std::string>
{size_t operator()(const string& key){// BKDRsize_t hash = 0;for (auto e : key){hash *= 31;hash += e;}return hash;}
};namespace hash_bucket
{template<class K, class V>struct HashNode{HashNode* _next;pair<K, V> _kv;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_tables.resize(10);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t Size() const{return _n;}bool Empty() const{return _n == 0;}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;Hash hf;// 负载因子最大到1if (_n == _tables.size()){vector<Node*> newTables;newTables.resize(_tables.size() * 2, nullptr);// 遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 挪动到映射的新表size_t hashi = hf(cur->_kv.first) % newTables.size();cur->_next = newTables[i];newTables[i] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = hf(kv.first) % _tables.size();Node* newnode = new Node(kv);// 头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}bool Erase(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}Node* Find(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}size_t Count(const K& key){Hash hf;KeyOfT kot;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return 1;}cur = cur->_next;}return 0;}// 桶的总个数size_t BucketCount(){size_t bucketCount = 0;for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){++bucketCount;}}return bucketCount;}// key所在桶的大小size_t BucketSize(const K& key){Hash hf;size_t bucketSize = 0;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){++bucketSize;cur = cur->_next;}return bucketSize;}// 打印信息void Some(){size_t bucketSize = 0;size_t maxBucketLen = 0;size_t sum = 0;double averageBucketLen = 0;for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){++bucketSize;}size_t bucketLen = 0;while (cur){++bucketLen;cur = cur->_next;}sum += bucketLen;if (bucketLen > maxBucketLen){maxBucketLen = bucketLen;}}averageBucketLen = (double)sum / (double)bucketSize;printf("all bucketSize:%d\n", _tables.size());printf("bucketSize:%d\n", bucketSize);printf("maxBucketLen:%d\n", maxBucketLen);printf("averageBucketLen:%lf\n\n", averageBucketLen);}private:vector<Node*> _tables;size_t _n = 0;};
}

开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.5,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

4. 模拟实现

4.1 哈希表的改造

  1. 模板参数列表的改造

    // K:关键码类型
    // T:不同容器T的类型不同,如果是unordered_map,T代表一个键值对,如果是unordered_set,T为K。
    // KeyOfT:因为T的类型不同,通过value取key的方式就不同,详细见unordered_map/set的实现。
    // Hash:哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整型数字才能取模。
    template<class T>
    struct HashNode
    {HashNode<T>* _next;T _data;HashNode(const T& data):_data(data), _next(nullptr){}
    };
    template<class K, class T, class KeyOfT, class Hash>
    class HashTable;
    
  2. 增加迭代器操作

    template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
    struct __HTIterator
    {typedef HashNode<T> Node;typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node;const HashTable<K, T, KeyOfT, Hash>* _pht;size_t _hashi;__HTIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi):_node(node), _pht(pht), _hashi(hashi){}__HTIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi):_node(node), _pht(pht), _hashi(hashi){}Self& operator++(){if (_node->_next){// 当前桶还有节点,走到下一个节点_node = _node->_next;}else{// 当前桶已经走完了,找下一个桶开始++_hashi;while (_hashi < _pht->_tables.size()){if (_pht->_tables[_hashi]){_node = _pht->_tables[_hashi];break;}++_hashi;}if (_hashi == _pht->_tables.size()){_node = nullptr;}}return *this;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s){return _node != s._node;}
    };
    
  3. 改造后代码实现

    template<class K, class T, class KeyOfT, class Hash>
    class HashTable
    {typedef HashNode<T> Node;template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct __HTIterator;public:typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){return iterator(_tables[i], this, i);}}return end();}iterator end(){return iterator(nullptr, this, -1);}const_iterator begin() const{for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){return const_iterator(_tables[i], this, i);}}return end();}const_iterator end() const{return const_iterator(nullptr, this, -1);}HashTable(){_tables.resize(10);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t Size() const{return _n;}bool Empty() const{return _n == 0;}pair<iterator, bool> Insert(const T& data){Hash hf;KeyOfT kot;iterator it = Find(kot(data));if (it != end())return make_pair(it, false);// 负载因子最大到1if (_n == _tables.size()){vector<Node*> newTables;newTables.resize(_tables.size() * 2, nullptr);// 遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 挪动到映射的新表size_t hashi = hf(kot(cur->_data)) % newTables.size();cur->_next = newTables[i];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = hf(kot(data)) % _tables.size();Node* newnode = new Node(data);// 头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return make_pair(iterator(newnode, this, hashi), true);}bool Erase(const K& key){Hash hf;KeyOfT kot;size_t hashi = hf(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}iterator Find(const K& key){Hash hf;KeyOfT kot;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return iterator(cur, this, hashi);}cur = cur->_next;}return end();}size_t Count(const K& key){Hash hf;KeyOfT kot;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return 1;}cur = cur->_next;}return 0;}// 桶的总个数size_t BucketCount(){size_t bucketCount = 0;for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){++bucketCount;}}return bucketCount;}// key所在桶的大小size_t BucketSize(const K& key){Hash hf;size_t bucketSize = 0;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){++bucketSize;cur = cur->_next;}return bucketSize;}// 打印信息void Some(){size_t bucketSize = 0;size_t maxBucketLen = 0;size_t sum = 0;double averageBucketLen = 0;for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){++bucketSize;}size_t bucketLen = 0;while (cur){++bucketLen;cur = cur->_next;}sum += bucketLen;if (bucketLen > maxBucketLen){maxBucketLen = bucketLen;}}averageBucketLen = (double)sum / (double)bucketSize;printf("all bucketSize:%zd\n", _tables.size());printf("bucketSize:%zd\n", bucketSize);printf("maxBucketLen:%zd\n", maxBucketLen);printf("averageBucketLen:%lf\n\n", averageBucketLen);}private:vector<Node*> _tables;size_t _n = 0;
    };
    

4.2 unordered_map 的模拟实现

unordered_map 的底层结构就是哈希表,因此在 unordered_map 中直接封装一个哈希表,然后将其接口包装下即可。

代码实现如下:

#pragma once
#include"HashTable.h"namespace my_unordered_map
{template<class K, class V, class Hash = HashFunc<K>>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}size_t size() const{return _ht.Size();}bool empty() const{return _ht.Empty();}pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}bool erase(const K& key){return _ht.Erase(key);}V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}const V& operator[](const K& key) const{pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}iterator find(const K& key){return _ht.Find(key);}size_t count(const K& key){return _ht.Count(key);}size_t bucket_count(){return _ht.BucketCount();}size_t bucket_size(const K& key){return _ht.BucketSize(key);}private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;};
}

4.3 unordered_set 的模拟实现

unordered_set 的底层结构就是哈希表,因此在 unordered_set 中直接封装一个哈希表,然后将其接口包装下即可。

代码实现如下:

#pragma once
#include"HashTable.h"namespace my_unordered_set
{template<class K, class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;iterator begin() const{return _ht.begin();}iterator end() const{return _ht.end();}size_t size() const{return _ht.Size();}bool empty() const{return _ht.Empty();}pair<const_iterator, bool> insert(const K& key){auto ret = _ht.Insert(key);return pair<const_iterator, bool>(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);}bool erase(const K& key){return _ht.Erase(key);}iterator find(const K& key){auto ret = _ht.Find(key);return const_iterator(ret._node, ret._pht, ret._hashi);}size_t count(const K& key){return _ht.Count(key);}size_t bucket_count(){return _ht.BucketCount();}size_t bucket_size(const K& key){return _ht.BucketSize(key);}private:hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;};
}

5. 哈希的应用

5.1 位图

5.1.1 位图的概念

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景通常是用来判断某个数据存不存在的

来看一个问题:

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?

  1. 遍历 O ( N ) O(N) O(N)

  2. 排序 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) + 二分查找 O ( l o g 2 N ) O(log_2N) O(log2N)

  3. 位图解决

数据是否在给定的整型数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息。如果二进制比特位为1,则代表存在;二进制比特位为0,则代表不存在。

比如:

在这里插入图片描述

5.1.2 位图的实现

namespace my_bitset
{// N是需要多少比特位template<size_t N>class bitset{public:bitset(){_bits.resize((N >> 5) + 1, 0);}void set(size_t x){size_t i = x / 32;size_t j = x % 32;_bits[i] |= (1 << j);}void reset(size_t x){size_t i = x / 32;size_t j = x % 32;_bits[i] &= ~(1 << j);}bool test(size_t x){size_t i = x / 32;size_t j = x % 32;return _bits[i] & (1 << j);}private:vector<int> _bits;};
}

5.1.3 位图的应用

  1. 快速查找某个数据是否在一个集合中。

  2. 排序 + 去重。

  3. 求两个集合的交集、并集等。

  4. 操作系统中磁盘块标记。

以下面三个题为例:

  1. 给定100亿个整数,设计算法找到只出现一次的整数?

  2. 给两个文件,分别有100亿个整数,我们只有 1G 内存,如何找到两个文件交集?

  3. 位图应用变形:1个文件有100亿个 int,1G 内存,设计算法找到出现次数不超过2次的所有整数。

思路解答:

  1. 创建两个位图,遍历给定的100亿个整数。对于每个整数,如果出现一次,则将两个位图中对应位置分别置成01;如果出现零次、二次及以上,则将两个位图中对应位置分别置成00、10或11,最后,再次遍历位图,找到位图位置对应的值为01的整数。
  2. 将两个文件中的整数分别映射到两个位图中。然后,遍历其中一个位图,对于每个整数,查询另一个位图,如果对应位置的值为1,则表示该整数存在于两个文件中,即为所求交集。
  3. 创建两个位图,初始化所有位为0。然后,遍历给定的100亿个整数,对于每个整数,将其对应的位图位置的值加1(这里加1指的是调整两个位图对应位置组合成的数加1)。最后,再次遍历位图,找到位图位置对应的值不超过2的整数。

5.2 布隆过滤器

5.2.1 布隆过滤器的提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

  1. 用哈希表存储用户记录,缺点:浪费空间。

  2. 用位图存储用户记录,缺点:位图一般只能处理整型,如果内容编号是字符串,就无法处理了。

  3. 将哈希与位图结合,即布隆过滤器。

5.2.2 布隆过滤器的概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你某样东西一定不存在或者可能存在,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间

在这里插入图片描述

5.2.3 布隆过滤器的实现

  1. 布隆过滤器的插入

    往布隆过滤器增加元素,添加的 key 需要根据 k 个无偏 hash 函数计算得到多个 hash 值,然后对数组长度进行取模得到数组下标的位置,然后将对应数组下标的位置的值置为1。

  2. 布隆过滤器的查找

    布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:

    分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。

    注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。

    比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

  3. 布隆过滤器的删除

    在这里插入图片描述

    布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

    比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

    一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器。插入元素时给 k 个计数器(k 个哈希函数计算出的哈希地址)加一;删除元素时,给 k 个计数器减一。这样通过多占用几倍存储空间的代价来增加删除操作。

  4. 具体实现代码

    #pragma once
    #include"bitset.h"struct BKDRHash
    {size_t operator()(const string& key){// BKDRsize_t hash = 0;for (auto e : key){hash *= 31;hash += e;}return hash;}
    };struct APHash
    {size_t operator()(const string& key){size_t hash = 0;for (size_t i = 0; i < key.size(); i++){char ch = key[i];if ((i & 1) == 0){hash ^= ((hash << 7) ^ ch ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));}}return hash;}
    };struct DJBHash
    {size_t operator()(const string& key){size_t hash = 5381;for (auto ch : key){hash += (hash << 5) + ch;}return hash;}
    };template<size_t N,class K = string,class HashFunc1 = BKDRHash,class HashFunc2 = APHash,class HashFunc3 = DJBHash>
    class BloomFilter
    {
    public:void Set(const K& key){size_t hash1 = HashFunc1()(key) % N;size_t hash2 = HashFunc2()(key) % N;size_t hash3 = HashFunc3()(key) % N;_bs.set(hash1);_bs.set(hash2);_bs.set(hash3);}bool Test(const K& key){// 判断不存在是准确的size_t hash1 = HashFunc1()(key) % N;if (_bs.test(hash1) == false)return false;size_t hash2 = HashFunc2()(key) % N;if (_bs.test(hash2) == false)return false;size_t hash3 = HashFunc3()(key) % N;if (_bs.test(hash3) == false)return false;// 存在误判的return true;}private:my_bitset::bitset<N> _bs;
    };
    

5.2.4 布隆过滤器的优点

  1. 增加和查询元素的时间复杂度为: O ( K ) O(K) O(K), ( K K K为哈希函数的个数,一般比较小),与数据量大小无关。

  2. 哈希函数相互之间没有关系,方便硬件并行运算。

  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。

  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。

  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。

  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。

5.2.5 布隆过滤器的缺陷

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中。(补救方法:再建立一个白名单,存储可能会误判的数据)

  2. 不能获取元素本身。

  3. 一般情况下不能从布隆过滤器中删除元素。

  4. 如果采用计数方式删除,可能会存在计数回绕问题。

相关文章:

C++ 哈希+unordered_map+unordered_set+位图+布隆过滤器(深度剖析)

文章目录 1. 前言2. unordered 系列关联式容器2.1 unordered_map2.1.1 unordered_map 的概念2.1.2 unordered_map 的使用 2.2 unordered_set2.2.1 unordered_set 的概念2.2.2 unordered_set 的使用 3. 底层结构3.1 哈希的概念3.2 哈希冲突3.3 哈希函数3.4 哈希冲突的解决3.4.1 …...

深入理解Netty及核心组件使用—下

目录 ChannelHandler ChannelHandler 接口 ChannelInboundHandler 接口 ChannelHandler 的适配器 Handler 的共享和并发安全性 资源管理和 SimpleChannelInboundHandler Bootstrap ChannelInitializer ChannelOption ChannelHandler ChannelHandler 接口 从开发人员的…...

vscode 突然连接不上服务器了(2024年版本 自动更新从1.85-1.86)

vscode日志 ll192.168.103.5s password:]0;C:\WINDOWS\System32\cmd.exe [17:09:16.886] Got some output, clearing connection timeout [17:09:16.887] Showing password prompt [17:09:19.688] Got password response [17:09:19.688] "install" wrote data to te…...

element-ui link 组件源码分享

link 组件的 api 涉及的内容不是很多&#xff0c;源码部分的内容也相对较简单&#xff0c;下面从以下这三个方面来讲解&#xff1a; 一、组件结构 1.1 组件结构如下图&#xff1a; 二、组件属性 2.1 组件主要有 type、underline、disabled、href、icon 这些属性&#xff0c;…...

序列化和反序列化、pytest-DDT数据驱动

序列化 序列化就是将对象转化成文件 python转成json import jsondata {"数字": [1, 1.1, -1],"字符串": ["aaaa", bbbb],"布尔值": [True, False],"空值": None,"列表": [[1, 2, 3], [4, 5, 6], [7, 8, 9]],&…...

Spring Boot整合MyBatis Plus实现基本CRUD与高级功能

文章目录 1. 引言2. 项目搭建与依赖配置2.1 添加MyBatis Plus依赖2.2 配置数据源与MyBatis Plus 3. 实现基本CRUD功能3.1 创建实体类3.2 创建Mapper接口3.3 实现Service层3.4 控制器实现 4. 高级功能实现4.1 自动填充功能4.2 乐观锁功能4.3 逻辑删除功能 5. 拓展&#xff1a;My…...

CSS 闪电按钮效果

<template><view class="const"><div class="voltage-button"><button>闪电按钮</button><svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox=&q…...

【Go-Zero】Error: only one service expected goctl一键转换生成rpc服务错误解决方案

【Go-Zero】Error: only one service expected goctl一键转换生成rpc服务错误解决方案 大家好 我是寸铁&#x1f44a; 总结了一篇Error: only one service expected goctl一键转换生成rpc服务错误解决方案的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 问题背景 今天寸铁在…...

从头开始构建和训练 Transformer(上)

1、导 读 2017 年&#xff0c;Google 研究团队发表了一篇名为《Attention Is All You Need》的论文&#xff0c;提出了 Transformer 架构&#xff0c;是机器学习&#xff0c;特别是深度学习和自然语言处理领域的范式转变。 Transformer 具有并行处理功能&#xff0c;可以实现…...

JVM-JVM内存结构(一)

程序计数器 Program Counter Register程序计数器(寄存器) 程序计数器在物理层上是通过寄存器实现的 作用&#xff1a;记住下一条jvm指令的执行地址特点 是线程私有的(每个线程都有属于自己的程序计数器)不会存在内存溢出 虚拟机栈 每个线程运行时所需要的内存称为虚拟机栈…...

React Emotion 如何优雅的使用样式(一)

简介 Emotion 是一个专为使用 JavaScript 编写 css 样式而设计的库。它提供了强大且可预测的样式组合&#xff0c;以及源映射、标签和测试实用程序等功能为开发人员提供了出色的体验&#xff0c;并且支持字符串和对象样式。 与框架无关的样式应用包 Emotion中提供了一个与框…...

1+X运维试题样卷A卷(初级)

云计算A卷 单选题(200分) 1.在OSI模型中,HTTP协议工作在第()层,交换机工作在第()层。(10分) (答案正确:10分) A、7/3 B、7/2 (正确答案) C、6/3 D、6/2 2.Linux有三个查看文件的命令,若希望在查看文件内容过程中可以用光标上下移动来查看文件内容,应使用命令。(10分…...

QT QDialog 中的按钮,如何按下后触发 accepted 消息?

QT 作为跨平台的系统&#xff0c;对话框并没有采用 Windows API 那种模式&#xff0c;通过返回 mrOK、mrCancel 等结果告诉调用方结果&#xff0c;而是采用了 accepted、rejected 等信号确定执行结果。下面介绍几种出发这些信号的方法。 1. 在按钮的 clicked 槽函数中触发 acc…...

seata分布式事务

文章目录 1、分布式事务1.1 事务的ACID原则原子性一致性隔离性持久性 1.2 分布式事务的问题示例代码准备环境1. seata_demo数据库2. 启动nacos seata-demo父工程pom.xml order-servicepom.xmlapplication.ymlOrderApplicationOrderControllerOrderServiceImplAccountClientStor…...

Python HttpServer 之 简单快速搭建本地服务器,并且使用 requests 测试访问下载服务器文件

Python HttpServer 之 搭建本地服务器,并且使用requests访问下载服务器文件测试 目录 Python HttpServer 之 搭建本地服务器,并且使用requests访问下载服务器文件测试...

【Python 实战】---- 实现批量给 pdf 插入 excel 动态生成的印章

1. 需求 想要能否实现批量自动为多个pdf加盖不同六格虚拟章(不改变pdf原有分辨率和文字可识别性);改在pdf首页上方空白位置,一般居中即可;如可由使用者自主选择靠页边距更好,以便部分首页上方有字的文件时人工可微调位置;从上而下,自左往右分别对应 excel 中各个字段;…...

51单片机实验课二

实验任务一&#xff1a; 用C语言设计实现8个led灯左右移动显示效果。具体要求如下&#xff1a; 左移时&#xff0c;8个灯中的奇数位灯依次点亮&#xff1b; 右移时&#xff0c;8个灯中的偶数灯依次点亮&#xff1b; 如此循环往 #include <REGX52.H> void Delay(unsi…...

1-4 动手学深度学习v2-线性回归的简洁实现-笔记

通过使用深度学习框架来简洁地实现 线性回归模型 生成数据集 import numpy as np import torch from torch.utils import data # 从torch.utils中引入一些处理数据的模块 from d2l import torch as d2ltrue_w torch.tensor([2,-3.4]) true_b 4.2 features, labels d2l.syn…...

SQL如何实现数据表行转列、列转行?

SQL行转列、列转行可以帮助我们更方便地处理数据&#xff0c;生成需要的报表和结果集。本文将介绍在SQL中如何实现数据表地行转列、列转行操作&#xff0c;以及实际应用示例。 这里通过表下面三张表进行举例 SQL创建数据库和数据表 数据表示例数据分别如下&#xff1a; data_…...

【React】redux状态管理、react-redux状态管理高级封装模块化

【React】react组件传参、redux状态管理 一、redux全局状态管理1、redux概述2、redux的组成1.1 State-状态1.2 Action-事件1.3 Reducer1.4 Store 3、redux入门案例1.1 前期准备1.2 构建store1.2.1 在src下新建store文件夹1.2.2 在store文件夹下新建index.ts文件1.2.3 在index.t…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

Unit 1 深度强化学习简介

Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库&#xff0c;例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体&#xff0c;比如 SnowballFight、Huggy the Do…...

多模态图像修复系统:基于深度学习的图片修复实现

多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...

数据结构:递归的种类(Types of Recursion)

目录 尾递归&#xff08;Tail Recursion&#xff09; 什么是 Loop&#xff08;循环&#xff09;&#xff1f; 复杂度分析 头递归&#xff08;Head Recursion&#xff09; 树形递归&#xff08;Tree Recursion&#xff09; 线性递归&#xff08;Linear Recursion&#xff09;…...

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...

TJCTF 2025

还以为是天津的。这个比较容易&#xff0c;虽然绕了点弯&#xff0c;可还是把CP AK了&#xff0c;不过我会的别人也会&#xff0c;还是没啥名次。记录一下吧。 Crypto bacon-bits with open(flag.txt) as f: flag f.read().strip() with open(text.txt) as t: text t.read…...

stm32进入Infinite_Loop原因(因为有系统中断函数未自定义实现)

这是系统中断服务程序的默认处理汇编函数&#xff0c;如果我们没有定义实现某个中断函数&#xff0c;那么当stm32产生了该中断时&#xff0c;就会默认跑这里来了&#xff0c;所以我们打开了什么中断&#xff0c;一定要记得实现对应的系统中断函数&#xff0c;否则会进来一直循环…...

CSS 工具对比:UnoCSS vs Tailwind CSS,谁是你的菜?

在现代前端开发中&#xff0c;Utility-First (功能优先) CSS 框架已经成为主流。其中&#xff0c;Tailwind CSS 无疑是市场的领导者和标杆。然而&#xff0c;一个名为 UnoCSS 的新星正以其惊人的性能和极致的灵活性迅速崛起。 这篇文章将深入探讨这两款工具的核心理念、技术差…...