• C++哈希


    1. unordered系列关联式容器

    1.1 unordered_map

    1.1.1 unordered_map

    1. unordered_map 是存储 键值对的关联式容器,其允许通过 keys 快速的索引到与
      其对应的 value。
    2. unordered_map 中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
      键关联。键和映射值的类型可能不同。
    3. 在内部 ,unordered_map 没有对 按照任何特定的顺序排序 , 为了能在常数范围内
      找到 key 所对应的 value unordered_map 将相同哈希值的键值对放在相同的桶中。
    4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭
      代方面效率较低。
    5. unordered_maps 实现了直接访问操作符 (operator[]) ,它允许使用 key 作为参数直接访问
      value
    6. 它的迭代器至少是前向迭代器。

     1.1.2 unordered_map的接口说明

    1. unordered_map的构造
    函数声明
    功能介绍
    unordered_map
    构造不同格式的unordered_map对象
    2. unordered_map的容量
    函数声明
    功能介绍
    bool empty() const
    检测unordered_map是否为空
    size_t size() const
    获取unordered_map的有效元素个数
    3. unordered_map的迭代器
    函数声明
    功能介绍
    begin
    返回unordered_map第一个元素的迭代器
    end
    返回unordered_map最后一个元素下一个位置的迭代器
    cbegin
    返回unordered_map第一个元素的const迭代器
    cend
    返回unordered_map最后一个元素下一个位置的const迭代器
    4. unordered_map的元素访问
    函数声明
    功能介绍
    operator[]
    返回与key对应的value,没有一个默认值
           注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶
    中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,
    将key对应的value返回。
    5. unordered_map的查询
    函数声明
    功能介绍
    iterator fifind(const K& key)
    返回key在哈希桶中的位置
    size_t count(const K& key)
    返回哈希桶中关键码为key的键值对的个数
       注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1
    6. unordered_map的修改操作
    函数声明
    功能介绍
    insert
    向容器中插入键值对
    erase
    删除容器中的键值对
    void clear()
    清空容器中有效元素个数
    void swap(unordered_map&)
    交换两个容器中的元素
    7. unordered_map的桶操作
    函数声明
    功能介绍
    size_t bucket_count()const
    返回哈希桶中桶的总个数
    size_t bucket_size(size_t n)const
    返回n号桶中有效元素的总个数
    size_t bucket(const K& key)
    返回元素key所在的桶号

    1.2 在线OJ

    1.重复n次的元素

    1. class Solution {
    2. public:
    3. int repeatedNTimes(vector<int>& A) {
    4.     size_t N = A.size()/2;
    5.     // 用unordered_map统计每个元素出现的次数
    6.     unordered_map<int, int> m;
    7.     for(auto e : A)
    8.         m[e]++;
    9.    
    10.     // 找出出现次数为N的元素
    11.     for(auto& e : m)
    12.     {
    13.         if(e.second == N)
    14.             return e.first;
    15.     }
    16. }
    17. };
    2.两个数组的交集I
    1. class Solution
    2. {
    3. public:
    4. vector<int> intersection(vector<int>& nums1, vector<int>& nums2)
    5. {
    6. // 用unordered_set对nums1中的元素去重
    7. unordered_set<int> s1;
    8. for (auto e : nums1)
    9. s1.insert(e);
    10. // 用unordered_set对nums2中的元素去重
    11. unordered_set<int> s2;
    12. for (auto e : nums2)
    13. s2.insert(e);
    14. // 遍历s1,如果s1中某个元素在s2中出现过,即为交集
    15. vector<int> vRet;
    16. for (auto e : s1)
    17. {
    18. if (s2.find(e) != s2.end())
    19. vRet.push_back(e);
    20. }
    21. return vRet;
    22. }
    23. };

    2. 底层结构

          unordered 系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

    2.1 哈希概念

           顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素
    时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即
    O($log_2 N$) ,搜索的效率取决于搜索过程中元素的比较次数。
          理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素
          如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
          当向该结构中:
    • 插入元素

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

    •  搜索元素
        对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
    取元素比较,若关键码相等,则搜索成功
          该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称 为哈希表 (Hash Table)( 或者称散列表 )。
          例如:数据集合 {1 7 6 4 5 9}
          哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

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

    2.2 哈希冲突

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

    2.3 哈希函数

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

           哈希函数设计原则

    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值
      域必须在 0 m-1 之间
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数应该比较简单
       常见哈希函数
    1. 直接定址法--(常用)
      取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
      优点:简单、均匀
      缺点:需要事先知道关键字的分布情况
      使用场景:适合查找比较小且连续的情况
    2. 除留余数法--(常用)
            设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数, 按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
    3. 平方取中法
         假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址;
         再比如关键字为 4321 ,对它平方就是 18671041 ,抽取中间的 3 671( 710) 作为哈希地址
         平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
    4. 折叠法
            折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
            折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
    5. 随机数法
           选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random为随机数函数。
    6. 数学分析法
         设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定
    相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
    有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
    列地址。例如:

          假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同
    的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还
    可以对抽取出来的数字进行反转 ( 1234 改成 4321) 、右环位移 ( 1234 改成 4123) 、左环移
    位、前两数与后两数叠加 ( 1234 改成 12+34=46) 等方法。
          数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
    若干位分布较均匀的情况。
          注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

    2.4 哈希冲突解决

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

    2.4.1 闭散列

           闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置
    呢?
    1. 线性探测
         线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
    •  插入
      - 通过哈希函数获取待插入元素在哈希表中的位置;
      - 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
      使用线性探测找到下一个空位置,插入新元素。 

    •  删除
          采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
    会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影
    响。因此 线性探测采用标记的伪删除法来删除一个元素
    1. // 哈希表每个空间给个标记
    2. // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
    3. enum State{EMPTY, EXIST, DELETE};
    线性探测的实现
    1. // 注意:假如实现的哈希表中元素唯一,即key相同的元素不再进行插入
    2. // 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起
    3. template<class K, class V>
    4. class HashTable
    5. {
    6. struct Elem
    7. {
    8. pair _val;
    9. State _state;
    10. };
    11. public:
    12. HashTable(size_t capacity = 3)
    13. : _ht(capacity), _size(0)
    14. {
    15. for (size_t i = 0; i < capacity; ++i)
    16. _ht[i]._state = EMPTY;
    17. }
    18. bool Insert(const pair& val)
    19. {
    20. // 检测哈希表底层空间是否充足
    21. // _CheckCapacity();
    22. size_t hashAddr = HashFunc(key);
    23. // size_t startAddr = hashAddr;
    24. while (_ht[hashAddr]._state != EMPTY)
    25. {
    26. if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first
    27. == key)
    28. return false;
    29. hashAddr++;
    30. if (hashAddr == _ht.capacity())
    31. hashAddr = 0;
    32.           /*
    33.           // 转一圈也没有找到,注意:动态哈希表,该种情况可以不用考虑,哈希表中元
    34. 素个数到达一定的数量,哈希冲突概率会增大,需要扩容来降低哈希冲突,因此哈希表中元素是
    35. 不会存满的
    36.           if(hashAddr == startAddr)
    37.               return false;
    38.           */
    39. }
    40. // 插入元素
    41. _ht[hashAddr]._state = EXIST;
    42. _ht[hashAddr]._val = val;
    43. _size++;
    44. return true;
    45. }
    46. int Find(const K& key)
    47. {
    48. size_t hashAddr = HashFunc(key);
    49. while (_ht[hashAddr]._state != EMPTY)
    50. {
    51. if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first
    52. == key)
    53. return hashAddr;
    54. hashAddr++;
    55. }
    56. return hashAddr;
    57. }
    58. bool Erase(const K & key)
    59. {
    60. int index = Find(key);
    61. if (-1 != index)
    62. {
    63. _ht[index]._state = DELETE;
    64. _size++;
    65. return true;
    66. }
    67. return false;
    68. }
    69. size_t Size()const;
    70. bool Empty() const;
    71. void Swap(HashTable&ht);
    72. private:
    73. size_t HashFunc(const K & key)
    74. {
    75. return key % _ht.capacity();
    76. }
    77. private:
    78. vector _ht;
    79. size_t _size;
    80. };
    思考:哈希表什么情况下进行扩容?如何扩容?
    1. void CheckCapacity()
    2. {
    3. if (_size * 10 / _ht.capacity() >= 7)
    4. {
    5. HashTable newHt(GetNextPrime(ht.capacity));
    6. for (size_t i = 0; i < _ht.capacity(); ++i)
    7. {
    8. if (_ht[i]._state == EXIST)
    9. newHt.Insert(_ht[i]._val);
    10. }
    11. Swap(newHt);
    12. }
    13. }
          线性探测优点 :实现非常简单,
         线性探测缺点 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据 堆积 ,即:不同
    关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
    。如何缓解呢?
    2. 二次探测
         
           线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
    置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题, 找下一个空位置的方法
    为: $H_i$ = ($H_0$ + $i^2$ )% m, 或者: $H_i$ = ($H_0$ - $i^2$ )% m 。其中: i =
    1,2,3… $H_0$ 是通过散列函数 Hash(x) 对元素的关键码 key 进行计算得到的位置, m 是表
    的大小。
      
            对于 2.1 中如果要插入 44 ,产生冲突,使用解决后的情况为:
           研究表明: 当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任
    何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子 a 不超过 0.5 如果超出必须考虑增容。
      
            因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

     2.4.2 开散列

    1. 开散列概念

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

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

    2. 开散列实现

    1. template<class V>
    2. struct HashBucketNode
    3. {
    4. HashBucketNode(const V& data)
    5. : _pNext(nullptr), _data(data)
    6. {}
    7. HashBucketNode* _pNext;
    8. V _data;
    9. };
    10. // 所实现的哈希桶中key是唯一的
    11. template<class V>
    12. class HashBucket
    13. {
    14. typedef HashBucketNode Node;
    15. typedef Node* PNode;
    16. public:
    17. HashBucket(size_t capacity = 3) : _size(0)
    18. {
    19. _ht.resize(GetNextPrime(capacity), nullptr);
    20. }
    21. // 哈希桶中的元素不能重复
    22. PNode* Insert(const V& data)
    23. {
    24. // 确认是否需要扩容。。。
    25. // _CheckCapacity();
    26. // 1. 计算元素所在的桶号
    27. size_t bucketNo = HashFunc(data);
    28. // 2. 检测该元素是否在桶中
    29. PNode pCur = _ht[bucketNo];
    30. while (pCur)
    31. {
    32. if (pCur->_data == data)
    33. return pCur;
    34. pCur = pCur->_pNext;
    35. }
    36. // 3. 插入新元素
    37. pCur = new Node(data);
    38. pCur->_pNext = _ht[bucketNo];
    39. _ht[bucketNo] = pCur;
    40. _size++;
    41. return pCur;
    42. }
    43. // 删除哈希桶中为data的元素(data不会重复),返回删除元素的下一个节点
    44. PNode* Erase(const V& data)
    45. {
    46. size_t bucketNo = HashFunc(data);
    47. PNode pCur = _ht[bucketNo];
    48. PNode pPrev = nullptr, pRet = nullptr;
    49. while (pCur)
    50. {
    51. if (pCur->_data == data)
    52. {
    53. if (pCur == _ht[bucketNo])
    54. _ht[bucketNo] = pCur->_pNext;
    55. else
    56. pPrev->_pNext = pCur->_pNext;
    57. pRet = pCur->_pNext;
    58. delete pCur;
    59. _size--;
    60. return pRet;
    61. }
    62. }
    63. return nullptr;
    64. }
    65. PNode* Find(const V& data);
    66. size_t Size()const;
    67. bool Empty()const;
    68. void Clear();
    69. bool BucketCount()const;
    70. void Swap(HashBucket& ht;
    71. ~HashBucket();
    72. private:
    73. size_t HashFunc(const V& data)
    74. {
    75. return data % _ht.capacity();
    76. }
    77. private:
    78. vector _ht;
    79. size_t _size;      // 哈希表中有效元素的个数
    80. };
    3. 开散列增容
         桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
    能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
    表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
    再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
    以给哈希表增容。
    1. void _CheckCapacity()
    2. {
    3. size_t bucketCount = BucketCount();
    4. if (_size == bucketCount)
    5. {
    6. HashBucket newHt(bucketCount);
    7. for (size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx)
    8. {
    9. PNode pCur = _ht[bucketIdx];
    10. while (pCur)
    11. {
    12. // 将该节点从原哈希表中拆出来
    13. _ht[bucketIdx] = pCur->_pNext;
    14. // 将该节点插入到新哈希表中
    15. size_t bucketNo = newHt.HashFunc(pCur->_data);
    16. pCur->_pNext = newHt._ht[bucketNo];
    17. newHt._ht[bucketNo] = pCur;
    18. pCur = _ht[bucketIdx];
    19. }
    20. }
    21. newHt._size = _size;
    22. this->Swap(newHt);
    23. }
    24. }
    4. 开散列的思考
           1. 只能存储 key 为整形的元素,其他类型怎么解决?
    1. // 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法
    2. // 整形数据不需要转化
    3. template<class T>
    4. class DefHashF
    5. {
    6. public:
    7. size_t operator()(const T& val)
    8. {
    9. return val;
    10. }
    11. };
    12. // key为字符串类型,需要将其转化为整形
    13. class Str2Int
    14. {
    15. public:
    16. size_t operator()(const string& s)
    17. {
    18. const char* str = s.c_str();
    19. unsigned int seed = 131; // 31 131 1313 13131 131313
    20. unsigned int hash = 0;
    21. while (*str)
    22. {
    23. hash = hash * seed + (*str++);
    24. }
    25. return (hash & 0x7FFFFFFF);
    26. }
    27. };
    28. // 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起
    29. template<class V, class HF>
    30. class HashBucket
    31. {
    32. // ……
    33. private:
    34. size_t HashFunc(const V& data)
    35. {
    36. return HF()(data.first) % _ht.capacity();
    37. }
    38. };
          2. 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
    1. size_t GetNextPrime(size_t prime)
    2. {
    3. const int PRIMECOUNT = 28;
    4. static const size_t primeList[PRIMECOUNT] =
    5. {
    6. 53ul, 97ul, 193ul, 389ul, 769ul,
    7. 1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
    8. 49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
    9. 1572869ul, 3145739ul, 6291469ul, 12582917ul,
    10. 25165843ul,
    11. 50331653ul, 100663319ul, 201326611ul, 402653189ul,
    12. 805306457ul,
    13. 1610612741ul, 3221225473ul, 4294967291ul
    14. };
    15. size_t i = 0;
    16. for (; i < PRIMECOUNT; ++i)
    17. {
    18. if (primeList[i] > prime)
    19. return primeList[i];
    20. }
    21. return primeList[i];
    22. }
    5. 开散列与闭散列比较
           应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销 。事实上:
    由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子 a <=
    0.7 ,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

    3. 模拟实现

    3.1 哈希表的改造

        1. 模板参数列表的改造
    1. // K:关键码类型
    2. // V: 不同容器V的类型不同,如果是unordered_map,V代表一个键值对,如果是unordered_set, V 为 K
    3. // HF: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能取模
    4. template<class K, class V, class KeyOfValue, class HF = DefHashF >
    5. class HashBucket;
         2. 增加迭代器操作
    1. // 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
    2. template<class K, class V, class KeyOfValue, class HF>
    3. class HashBucket;
    4. // 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
    5. template <class K, class V, class KeyOfValue, class HF>
    6. struct HBIterator
    7. {
    8. typedef HashBucket HashBucket;
    9. typedef HashBucketNode* PNode;
    10. typedef HBIterator Self;
    11. HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr);
    12. Self& operator++()
    13. {
    14. // 当前迭代器所指节点后还有节点时直接取其下一个节点
    15. if (_pNode->_pNext)
    16. _pNode = _pNode->_pNext;
    17. else
    18. {
    19. // 找下一个不空的桶,返回该桶中第一个节点
    20. size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode -> _data)) + 1;
    21. for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
    22. {
    23. if (_pNode = _pHt->_ht[bucketNo])
    24. break;
    25. }
    26. }
    27. return *this;
    28. }
    29. Self operator++(int);
    30. V& operator*();
    31. V* operator->();
    32. bool operator==(const Self& it) const;
    33. bool operator!=(const Self& it) const;
    34. PNode _pNode;             // 当前迭代器关联的节点
    35. HashBucket* _pHt;         // 哈希桶--主要是为了找下一个空桶时候方便
    36. };
         3. 增加通过 key 获取 value 操作
    1. template<class K, class V, class KeyOfValue, class HF = DefHashF >
    2. class HashBucket
    3. {
    4. friend HBIterator;
    5. // ......
    6. public:
    7. typedef HBIterator Iterator;
    8. //
    9. // ...
    10. // 迭代器
    11. Iterator Begin()
    12. {
    13. size_t bucketNo = 0;
    14. for (; bucketNo < _ht.capacity(); ++bucketNo)
    15. {
    16. if (_ht[bucketNo])
    17. break;
    18. }
    19. if (bucketNo < _ht.capacity())
    20. return Iterator(_ht[bucketNo], this);
    21. else
    22. return Iterator(nullptr, this);
    23. }
    24. Iterator End()
    25. {
    26. return Iterator(nullptr, this);
    27. }
    28. Iterator Find(const K& key);
    29. Iterator Insert(const V& data);
    30. Iterator Erase(const K& key);
    31. // 为key的元素在桶中的个数
    32. size_t Count(const K& key)
    33. {
    34. if (Find(key) != End())
    35. return 1;
    36. return 0;
    37. }
    38. size_t BucketCount()const
    39. {
    40. return _ht.capacity();
    41. }
    42. size_t BucketSize(size_t bucketNo)
    43. {
    44. size_t count = 0;
    45. PNode pCur = _ht[bucketNo];
    46. while (pCur)
    47. {
    48. count++;
    49. pCur = pCur->_pNext;
    50. }
    51. return count;
    52. }
    53. // ......
    54. };

    3.2 unordered_map

    1. // unordered_map中存储的是pair的键值对,K为key的类型,V为value的类型,HF哈希函数类型
    2. // unordered_map在实现时,只需将hashbucket中的接口重新封装即可
    3. template<class K, class V, class HF = DefHashF>
    4. class unordered_map
    5. {
    6. typedef pair ValueType;
    7. typedef HashBucket HT;
    8. // 通过key获取value的操作
    9. struct KeyOfValue
    10. {
    11. const K& operator()(const ValueType& data)
    12. {
    13. return data.first;
    14. }
    15. };
    16. public:
    17. typename typedef HT::Iterator iterator;
    18. public:
    19. unordered_map() : _ht()
    20. {}
    21. iterator begin()
    22. {
    23. return _ht.Begin();
    24. }
    25. iterator end()
    26. {
    27. return _ht.End();
    28. }
    29. // capacity
    30. size_t size()const
    31. {
    32. return _ht.Size();
    33. }
    34. bool empty()const
    35. {
    36. return _ht.Empty();
    37. }
    38. ///
    39. // Acess
    40. V& operator[](const K& key)
    41. {
    42. return (*(_ht.InsertUnique(ValueType(key, V())).first)).second;
    43. }
    44. const V& operator[](const K& key)const;
    45. //
    46. // lookup
    47. iterator find(const K& key)
    48. {
    49. return _ht.Find(key);
    50. }
    51. size_t count(const K& key)
    52. {
    53. return _ht.Count(key);
    54. }
    55. /
    56. // modify
    57. pairbool> insert(const ValueType& valye)
    58. {
    59. return _ht.Insert(valye);
    60. }
    61. iterator erase(iterator position)
    62. {
    63. return _ht.Erase(position);
    64. }
    65. // bucket
    66. size_t bucket_count()
    67. {
    68. return _ht.BucketCount();
    69. }
    70. size_t bucket_size(const K& key)
    71. {
    72. return _ht.BucketSize(key);
    73. }
    74. private:
    75. HT _ht;
    76. };

    4. 哈希的应用

    4.1 位图

    4.1.1 位图概念

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

    4.1.2 位图的实现

    1. class bitset
    2. {
    3. public:
    4. bitset(size_t bitCount)
    5. : _bit((bitCount >> 5) + 1), _bitCount(bitCount)
    6. {}
    7. // 将which比特位置1
    8. void set(size_t which)
    9. {
    10. if (which > _bitCount)
    11. return;
    12. size_t index = (which >> 5);
    13. size_t pos = which % 32;
    14. _bit[index] |= (1 << pos);
    15. }
    16. // 将which比特位置0
    17. void reset(size_t which)
    18. {
    19. if (which > _bitCount)
    20. return;
    21. size_t index = (which >> 5);
    22. size_t pos = which % 32;
    23. _bit[index] &= ~(1 << pos);
    24. }
    25. // 检测位图中which是否为1
    26. bool test(size_t which)
    27. {
    28. if (which > _bitCount)
    29. return false;
    30. size_t index = (which >> 5);
    31. size_t pos = which % 32;
    32. return _bit[index] & (1 << pos);
    33. }
    34. // 获取位图中比特位的总个数
    35. size_t size()const { return _bitCount; }
    36. // 位图中比特为1的个数
    37. size_t Count()const
    38. {
    39. int bitCnttable[256] = {
    40. 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,
    41. 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,
    42. 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,
    43. 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,
    44. 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,
    45. 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,
    46. 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
    47. 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,
    48. 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,
    49. 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,
    50. 6, 7, 6, 7, 7, 8 };
    51. size_t size = _bit.size();
    52. size_t count = 0;
    53. for (size_t i = 0; i < size; ++i)
    54. {
    55. int value = _bit[i];
    56. int j = 0;
    57. while (j < sizeof(_bit[0]))
    58. {
    59. unsigned char c = value;
    60. count += bitCntTable[c];
    61. ++j;
    62. value >>= 8;
    63. }
    64. }
    65. return count;
    66. }
    67. private:
    68. vector<int> _bit;
    69. size_t _bitCount;
    70. };

    4.1.3 位图的应用

    1. 快速查找某个数据是否在一个集合中
    2. 排序 + 去重
    3. 求两个集合的交集、并集等
    4. 操作系统中磁盘块标记

     4.2 布隆过滤器

    4.2.1 布隆过滤器提出

          我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
    些已经存在的记录。 如何快速查找呢?
    1. 用哈希表存储用户记录,缺点:浪费空间
    2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
      了。
    3. 将哈希与位图结合,即布隆过滤器

     4.2.2布隆过滤器概念

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

    4.2.3 布隆过滤器的插入 

     向布隆过滤器中插入:"baidu"

     

    1. struct BKDRHash
    2. {
    3. size_t operator()(const string& s)
    4. {
    5. // BKDR
    6. size_t value = 0;
    7. for (auto ch : s)
    8. {
    9. value *= 31;
    10. value += ch;
    11. }
    12. return value;
    13. }
    14. };
    15. struct APHash
    16. {
    17. size_t operator()(const string& s)
    18. {
    19. size_t hash = 0;
    20. for (long i = 0; i < s.size(); i++)
    21. {
    22. if ((i & 1) == 0)
    23. {
    24. hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
    25. }
    26. else
    27. {
    28. hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
    29. }
    30. }
    31. return hash;
    32. }
    33. };
    34. struct DJBHash
    35. {
    36. size_t operator()(const string& s)
    37. {
    38. size_t hash = 5381;
    39. for (auto ch : s)
    40. {
    41. hash += (hash << 5) + ch;
    42. }
    43. return hash;
    44. }
    45. };
    46. template<size_t N,
    47. size_t X = 5,
    48. class K = string,
    49. class HashFunc1 = BKDRHash,
    50. class HashFunc2 = APHash,
    51. class HashFunc3 = DJBHash>
    52. class BloomFilter
    53. {
    54. public:
    55. void Set(const K& key)
    56. {
    57. size_t len = X * N;
    58. size_t index1 = HashFunc1()(key) % len;
    59. size_t index2 = HashFunc2()(key) % len;
    60. size_t index3 = HashFunc3()(key) % len;
    61. /* cout << index1 << endl;
    62. cout << index2 << endl;
    63. cout << index3 << endl<
    64. _bs.set(index1);
    65. _bs.set(index2);
    66. _bs.set(index3);
    67. }
    68. bool Test(const K& key)
    69. {
    70. size_t len = X * N;
    71. size_t index1 = HashFunc1()(key) % len;
    72. if (_bs.test(index1) == false)
    73. return false;
    74. size_t index2 = HashFunc2()(key) % len;
    75. if (_bs.test(index2) == false)
    76. return false;
    77. size_t index3 = HashFunc3()(key) % len;
    78. if (_bs.test(index3) == false)
    79. return false;
    80. return true;  // 存在误判的
    81. }
    82. // 不支持删除,删除可能会影响其他值。
    83. void Reset(const K& key);
    84. private:
    85. bitset _bs;
    86. };

    4.2.4 布隆过滤器的查找

         布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1 。所以可以按照以下方式进行查找: 分别计算每个哈希值对应的比特位置存储的是否为 零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中
         注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可 能存在,因为有些哈希函数存在一定的误判。
         比如:在布隆过滤器中查找 "alibaba" 时,假设 3 个哈希函数计算的哈希值为: 1 3 7 ,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

    4.2.5 布隆过滤器删除

         布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
          比如:删除上图中 "tencent" 元素,如果直接将该元素所对应的二进制比特位置 0 “baidu” 元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
         一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给 k 个计数器(k 个哈希函数计算出的哈希地址 ) 加一,删除元素时,给 k 个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
         缺陷:
    1. 无法确认元素是否真正在布隆过滤器中
    2. 存在计数回绕

     4.2.6 布隆过滤器优点

    1. 增加和查询元素的时间复杂度为 :O(K), (K 为哈希函数的个数,一般比较小 ) ,与数据量大小无
    2. 哈希函数相互之间没有关系,方便硬件并行运算
    3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
    4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
    5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。
    6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

     4.2.7 布隆过滤器缺陷

    1. 有误判率,即存在假阳性 (False Position) ,即不能准确判断元素是否在集合中 ( 补救方法:再
      建立一个白名单,存储可能会误判的数据 )
    2. 不能获取元素本身。
    3. 一般情况下不能从布隆过滤器中删除元素。
    4. 如果采用计数方式删除,可能会存在计数回绕问题。
  • 相关阅读:
    C++如何不通过语法调用成员函数
    社交媒体营销策略——如何病毒式传播:增加受众范围的9个技巧
    mysql字符集浅谈
    一文搞懂 ARM 64 系列: 寄存器
    2024.6.17 作业 xyt
    [架构之路-19]:目标系统 - 硬件平台 - 案例1 - 用单片机STM8/STM32搭建目标系统(以无线传感器LoRa终端为例)
    基于JAVA网络作业提交与批改系统计算机毕业设计源码+数据库+lw文档+系统+部署
    java-net-php-python-springboot教学评价系统计算机毕业设计程序
    同态加密:分圆多项式简介
    vue3.2的特性
  • 原文地址:https://blog.csdn.net/zhao19971014/article/details/127809372