傲渊山岳 发表于 2025-3-27 21:11:16

C++之哈希

1. unordered系列关联式容器

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

1.1.1 unordered_map的文档介绍

unordered_map在线文档说明

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


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

参见unordered_set在线文档说明
和set的区别:


[*]无序
[*]单向迭代器
[*]访问效率高一些(O(1))
性能比力:(1000w次随机数)
https://i-blog.csdnimg.cn/img_convert/6be077742d1ac75f72a7ee72eae0e5cf.png
1.3 在线OJ

重复n次的元素
代码:
class Solution {
public:
    int repeatedNTimes(vector<int>& nums)
    {
      size_t N = nums.size() / 2;
      //用unordered_map统计每个元素出现的次数
      unordered_map<int, int> m;
      for(auto e : nums)
      {
            m++;
      }
      //找出出现次数为N的元素
      for(auto& e : m)
      {
            if(e.second == N)
            {
                return e.first;
            }
      }
      return 0;
    }
};
2. 底层结构

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

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


[*] 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
[*] 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比力,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(大概称散列表)
直接创建映射题目:
1、数据范围分布很广、不集中
2、key的数据不是整数,是字符串怎么办?是自界说类型怎么办?
例如:数据聚集{1,7,6,4,5,9};capacity为存储元素底层空间总的大小。
https://i-blog.csdnimg.cn/img_convert/5ab89a39880503dde9ab8b4704e54823.png
用该方法进行搜索不必进行多次关键码的比力,因此搜索的速度比力快
题目:按照上述哈希方式,向聚集中插入元素44,会出现什么题目?
答:会出现辩论,因为44应该放到下标为4的位置上去。
2.2 哈希辩论

对于两个数据元素的关键字k_i和k_j(i != j),有k_i!=k_j,但有:Hash(k_i) == Hash(k_j),即:差别关键字通过雷同哈希哈数计算出雷同的哈希地址,该种现象称为哈希辩论或哈希碰撞。
把具有差别关键码而具有雷同哈希地址的数据元素称为“同义词”。
发生哈希辩论该怎样处置惩罚呢?
2.3 哈希函数

引起哈希辩论的一个原因可能是:哈希函数筹划不够合理。
哈希函数筹划原则:


[*]哈希函数的界说域必须包罗需要存储的全部关键码,而假如散列表答应有m个地址时,其值域必须在0到m-1之间
[*]哈希函数计算出来的地址能均匀分布在整个空间中
[*]哈希函数应该比力简朴
常见哈希函数

[*] 直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
长处:简朴、均匀
缺点:需要事先知道关键字的分布环境
使用场景:恰当查找比力小且连续的环境
口试题:
字符串中第一个只出现一次字符
[*] 除留余数法–(常用)
设散列表中答应的地址数为m,取一个不大于m,但最接近大概等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
[*] 平方取中法–(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。平方取中法比力恰当:不知道关键字的分布,而位数又不是很大的环境
[*] 折叠法–(了解)
折叠法是将关键字从左到右分割成位数相等的几部门(末了一部门位数可以短些),然后将这几部门叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法恰当事先不需要知道关键字的分布,恰当关键字位数比力多的环境
[*] 随机数法–(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。
通常应用于关键字长度不等时采取此法
[*] 数学分析法–(了解)
设有n个d位数,每一位可能有r种差别的符号,这r种差别的符号在各位上出现的频率不一定 雷同,可能在某些位上分布比力均匀,每种符号出现的时机均等,在某些位上分布不均匀只有某几种符号常常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。例如:
https://i-blog.csdnimg.cn/img_convert/a406ed6b4251ae4130ce629e31fa1886.png
假设要存储某家公司员工登记表,假如用手机号作为关键字,那么极有可能前7位都是雷同的,那么我们可以选择后面的四位作为散列地址,假如这样的抽取工作还轻易出现 辩论,还 可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。
数字分析法通常恰当处置惩罚关键字位数比力大的环境,假如事先知道关键字的分布且关键字的若干位分布较均匀的环境
注意:哈希函数筹划的越精妙,产生哈希辩论的可能性就越低,但是无法避免哈希辩论
2.4 哈希辩论办理

办理哈希辩论两种常见的方法是:闭散列和开散列
2.4.1 闭散列

闭散列:也叫开放定址法,当发生哈希辩论时,假如哈希表未被装满,说明在哈希表中一定还有空位置,那么可以把key存放到辩论位置中的“下一个” 空位置中去。那怎样探求下一个空位置呢?

[*] 线性探测
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希辩论。
线性探测:从发生辩论的位置开始,依次向后探测,直到探求到下一个空位置为止。
缺点:会占用别的元素该占用的位置,出现拥堵现象。

[*] 插入

[*]通过哈希函数获取待插入元素在哈希表中的位置
[*]假如该位置中没有元素则直接插入新元素,假如该位置中有元素发生哈希辩论, 使用线性探测找到下一个空位置,插入新元素
https://i-blog.csdnimg.cn/img_convert/a58bdf49a06606260458224320121e49.png

[*] 删除
采取闭散列处置惩罚哈希辩论时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,假如直接删除掉,44查找起来可能会受影响。因此线性探测采取标记的伪删除法来删除一个元素。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

线性探测的实现:
#pragma once
#include<iostream>
#include<vector>
using namespace std;
enum State
{
        EMPTY,
        EXITS,
        DELETE
};

template<class K, class V>
struct HashData
{
        pair<K, V> _kv;
        State _state = EMPTY;
};

template<class K>
struct DefaultHash
{
        size_t operator()(const K&key)
        {
                return (size_t)key;
        }
};
//特化
template<>
struct DefaultHash<string>
{
        size_t operator()(const string& key)
        {
                size_t hash = 0;
                for (auto e : key)
                {
                        hash = hash * 131 + e;
                }
                return hash;
        }
};

template<class K, class V, class HashFunc = DefaultHash<K>>
class HashTable
{
        typedef HashData<K, V> Data;
public:
        bool Insert(const pair<K, V>& kv)
        {
                if (Find(kv.first))
                        return false;
                //负载因子到0.7及以上就扩充数据
                if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
                {
                        size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
                        //扩容以后就要重新映射
                        HashTable<K, V, HashFunc> newHT;
                        newHT._tables.resize(newSize);
                        for (auto& e : _tables)
                        {
                                if (e._state == EXITS)
                                {
                                        newHT.Insert(e._kv);
                                }
                        }
                        newHT._tables.swap(_tables);

                }
                HashFunc hf;
                size_t starti = hf(kv.first);
                starti %= _tables.size();//问:为什么此处不是%capacity?答:因为[]会对下标进行检查
                //线性探测/二次探测
                size_t hashi = starti;

                int i = 1;
                while (_tables._state == EXITS)
                {
                        hashi = starti + i;
                        ++i;
                        hashi %= _tables.size();
                }
                _tables._kv = kv;
                _tables._state = EXITS;
                _n++;
                return true;
        }
        Data* Find(const K& key)
        {
                if (_n == 0)
                {
                        return nullptr;
                }
                HashFunc hf;
                size_t starti = hf(key);
                starti %= _tables.size();
                //线性探测/二次探测
                size_t hashi = starti;
                int i = 1;

                while (_tables._state != EMPTY)
                {
                        if (_tables._state != DELETE && _tables._kv.first == key )
                        {
                                return &_tables;
                        }

                        hashi = starti + i;
                        ++i;
                        hashi %= _tables.size();
                }
                return nullptr;
        }
        bool Erase(const K& key)
        {
                Data* ret = Find(key);
                if (ret)
                {
                        ret->_state = DELETE;
                        _n--;
                        return true;
                }
                else
                {
                        return false;
                }
        }
private:
        vector<Data> _tables;
        size_t _n = 0;//有效数据的个数
};
思索:哈希表什么环境下进行扩容?怎样扩容?
散列表的载荷因子界说为:α = 填入表中的元素个数 / 散列的长度
https://i-blog.csdnimg.cn/img_convert/02faa68512a2626a6b18e95218b214a5.png
线性探测长处:实现非常简朴。
线性探测缺点:一旦发生哈希辩论,所有的辩论连在一起,轻易产生数据“堆积”,即:差别关键码占据了可利用的空位置,使得探求某关键码的位置需要许多次比力,导致搜索效率低落。

[*] 二次探测
线性探测的缺陷是产生辩论的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该题目,找下一个空位置的方法 为:H_i = (H_0 + i2 )% m, 大概:H_i = (H_0 - i2 )% m。其中:i = 1,2,3…, H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
对于2.1中假如要插入44,产生辩论,使用办理后的环境为:
https://i-blog.csdnimg.cn/img_convert/6661ee8de8114b84a11640c9f42b7528.png
研究表明:当表的长度为质数且表装载因子a不高出0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的题目。在搜索时可以不考虑表装满的环境,但在插入时必须确保表的装载因子a不高出0.5,假如超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比力低,这也是哈希的缺陷。
2.4.2 开散列


[*] 开散列概念
开散列法又叫链地址法(开链法),首先对关键码聚集用散列函数计算散列地址,具有雷同地址的关键码归于同一子聚集,每一个子聚集称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
注意:
https://i-blog.csdnimg.cn/img_convert/0e7e9d5724d90bb9582f860b8a0fdbdb.png
从上图可以看出,开散列中每个桶中放的都是发生哈希辩论的元素。
[*] 开散列实现
//默认存储为整型的情况
struct DefaultHash
{
        size_t operator()(const K& key)
        {
                return key;
        }
};
//存储的数据不是整数类型而是字符串类型时就要使用字符串哈希算法,即将字符串类型转换成整型
//同样的,其它自定义类型也要如此,即将自定义类型按照一定的哈希算法将自定义类型转换成整型
template<>
struct DefaultHash<string>
{
        size_t operator()(const string& key)
        {
                size_t hash = 0;
                for (auto e : key)
                {
                        hash = hash * 131 + e;
                }
                return hash;
        }
};
namespace Bucket
{
        template<class K, class V>
        struct HashNode
        {
                HashNode(const pair<K,V> kv)
                        :_kv(kv)
                        ,_next(nullptr)
                {}
                pair<K, V> _kv;
                HashNode<K, V>* _next;
        };


        template<class K, class V, class HashFunc = DefaultHash<K>>
        class HashTable
        {
                typedef HashNode<K, V> Node;
        public:
                ~HashTable()
                {
                        for (size_t i = 0; i < _tables.size(); ++i)
                        {
                                Node* cur = _tables;
                                while (cur)
                                {
                                        Node* next = cur->_next;
                                        delete cur;
                                        cur = next;
                                }
                                _tables = nullptr;
                        }
                }
                HashTable()
                {}
                HashTable<K, V>& operator=(const HashTable<K, V>& ht)
                {
                        HashTable<K, V> tmp(ht);
                        _tables.swap(tmp._tables);
                        return *this;
                }

                HashTable(const HashTable<K, V>& ht)
                {
                        _tables.resize(ht._tables.size(), nullptr);
                        for (size_t i = 0; i < ht._tables.size(); i++)
                        {
                                Node* cur = ht._tables;
                                while (cur)
                                {
                                        Insert(cur->_kv);
                                        cur = cur->_next;
                                }
                        }
                }

                bool Insert(const pair<K, V>& kv)
                {
                        if (Find(kv.first))
                        {
                                return false;
                        }
                        HashFunc hf;
                        //空间不够,进行扩容
                        if (_tables.size() == _n)
                        {
                                size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();

                                //思路一:拷贝节点
                                //HashTable newHT;
                                //newHT._tables.resize(newSize, nullptr);
                                //for (size_t i = 0; i < _tables.size(); i++)
                                //{
                                //        Node* cur = _tables;
                                //        while (cur)
                                //        {
                                //                newHT.Insert(cur->_kv);
                                //                cur = cur->_next;
                                //        }
                                //}
                                //newHT._tables.swap(_tables);

                                //思路二:转移节点
                                vector<Node*> newTables;
                                newTables.resize(newSize, nullptr);
                                for (size_t i = 0; i < _tables.size(); i++)
                                {
                                        Node* cur = _tables;
                                        while (cur)
                                        {
                                                Node* next = cur->_next;
                                                size_t hashi = hf(cur->_kv.first);
                                                hashi %=newSize;
                                                cur->_next = newTables;
                                                newTables = cur;
                                                cur = next;
                                        }
                                        _tables = nullptr;
                                }
                                newTables.swap(_tables);
                        }
                        size_t hashi = hf(kv.first);
                        hashi %= _tables.size();
                        Node* newNode = new Node(kv);
                        newNode->_next = _tables;
                        _tables = newNode;
            ++_
                        return true;
                }


                Node* Find(const K& key)
                {
                        if (_tables.size() == 0)
                        {
                                return nullptr;
                        }
                        HashFunc hf;
                        size_t hashi = hf(key);
                        hashi %= _tables.size();
                        Node* cur = _tables;
                        while (cur)
                        {
                                if (cur->_kv.first == key)
                                {
                                        return cur;
                                }
                                cur = cur->_next;
                        }
                        return nullptr;
                }
                bool Erase(const K& key)
                {
                        //问:如果不用prev指针,还能用什么方法?
                        //答:有另外两种方法,但是依旧只推荐使用prev指针的方法
                        //1.判断cur->_next->kv.first是否等于key
                        //2.替换法删除,将cur节点和_tables替换(替换kv即可),然后删除_tables
                        if (_tables.size() == 0)
                                return false;
                        HashFunc hf;
                        size_t hashi = hf(key);
                        hashi %= _tables.size();
                        Node* cur = _tables;
                        Node* prev = nullptr;
                        while (cur)
                        {
                                if (cur->_kv.first == key)
                                {
                                        //删除节点
                                        if (prev == nullptr)//也可以写成prev == nullptr的情况
                                        {
                                                _tables = cur->_next;
                                        }
                                        else
                                        {
                                                prev->_next = cur->_next;
                                        }
                                        delete cur;
                                        return true;
                                }
                                prev = cur;
                                cur = cur->_next;
                        }
                        return false;
                }

        private:
                //指针数组
                vector<Node*> _tables;
                size_t _n = 0;
        };
}

[*] 开散列与闭散列比力
应用链地址法处置惩罚溢出,需要增设链接指针,好像增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节流存储空间。
[*] 字符串哈希算法
字符串哈希算法
3. 模拟实现

哈希表的改造
Hash.h文件
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template<class K>
struct DefaultHash
{
        size_t operator()(const K& key)
        {
                return key;
        }
};

template<>
struct DefaultHash<string>
{
        size_t operator()(const string& key)
        {
                size_t hash = 0;
                for (auto e : key)
                {
                        hash = hash * 131 + e;
                }
                return hash;
        }
};



template<class T>
struct HashNode
{
        HashNode(const T& data)
                :_data(data)
                , _next(nullptr)
        {}
        T _data;
        HashNode<T>* _next;
};
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable;

template<class K, class T, class KeyOfT, class HashFunc>
class __HTIterator
{
        typedef HashNode<T> Node;
        typedef __HTIterator<K, T, KeyOfT, HashFunc> Self;

public:
        Node* _node;
        HashTable<K, T, KeyOfT, HashFunc>* _pht;
        __HTIterator(){}
        __HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht)
                :_node(node)
                ,_pht(pht)
        {}
        Self& operator++()
        {
                if (_node->_next)
                {
                        _node = _node->_next;
                }
                else
                {
                        KeyOfT kot;
                        HashFunc hf;
                        size_t hashi = hf(kot(_node->_data));
                        hashi %= _pht->_tables.size();
                        ++hashi;
                       
                        //找下一个不为空的桶
                        for (; hashi < _pht->_tables.size(); ++hashi)
                        {
                                if (_pht->_tables)
                                {
                                        _node = _pht->_tables;
                                        break;
                                }
                        }
                        //没有找到不为空的桶,用nullptr标识
                        if (hashi >= _pht->_tables.size())
                        {
                                _node = nullptr;
                        }
                }
                return *this;
        }
        bool operator!=(const Self& s )const
        {
                return _node != s._node;
        }


        bool operator==(const Self& s)const
        {
                return _node == s._node;
        }

        T& operator*()
        {
                return _node->_data;
        }

        T* operator->()
        {
                return &_node->_data;
        }
};

//set:HashTableK, K, SetKeyOfT, HashFunc = DefaultHash<K>>
//map:HashTable<K, pari<K, V>, MapKeyOfT, HashFunc = DefaultHash<K>>
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{
        template<class K, class T, class KeyOfT, class HashFunc>
        friend class __HTIterator;
public:
        typedef HashNode<T> Node;
        typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator;
        iterator begin()
        {
                for (size_t i = 0; i < _tables.size(); i++)
                {
                        Node* cur = _tables;
                        if (cur)
                        {
                                return iterator(cur, this);
                        }
                }
                return end();
        }
        iterator end()
        {
                return iterator(nullptr, this);
        }

        ~HashTable()
        {
                for (size_t i = 0; i < _tables.size(); ++i)
                {
                        Node* cur = _tables;
                        while (cur)
                        {
                                Node* next = cur->_next;
                                delete cur;
                                cur = next;
                        }
                        _tables = nullptr;
                }
        }
        HashTable()
        {}


        HashTable<K, T, KeyOfT, HashFunc>& operator=(const HashTable<K, T, KeyOfT, HashFunc>& ht)
        {
                HashTable<K, T, KeyOfT, HashFunc> tmp(ht);
                _tables.swap(tmp._tables);
                return *this;
        }

        HashTable(const HashTable<K, T, KeyOfT, HashFunc>& ht)
        {
                _tables.resize(ht._tables.size(), nullptr);
                for (size_t i = 0; i < ht._tables.size(); i++)
                {
                        Node* cur = ht._tables;
                        while (cur)
                        {
                                Insert(cur->_kv);
                                cur = cur->_next;
                        }
                }
        }
        size_t GetNextPrime(size_t prime)
        {
                const int PRIMECOUNT = 28;
                static const size_t primeList =
                {
                53ul, 97ul, 193ul, 389ul, 769ul,
                1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
                49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
                1572869ul, 3145739ul, 6291469ul, 12582917ul,
           25165843ul,
                50331653ul, 100663319ul, 201326611ul, 402653189ul,
           805306457ul,
                1610612741ul, 3221225473ul, 4294967291ul
                };
                //获取比当前值(prime)大的素数
                size_t i = 0;
                for (; i < PRIMECOUNT; ++i)
                {
                        if (primeList > prime)
                                return primeList;
                }
                return primeList;
        }

        pair<iterator, bool> Insert(const T& data)
        {
                HashFunc hf;
                KeyOfT kot;
                iterator pos = Find(kot(data));
                if (pos != end())
                {
                        return make_pair(pos, false);
                }

                //空间不够,进行扩容
                if (_tables.size() == _n)
                {
                        //size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
                        size_t newSize = GetNextPrime(_tables.size());
                        //用素数来作为哈希表的容量大小可以一定的减少哈希冲突
                        vector<Node*> newTables;
                        newTables.resize(newSize, nullptr);
                        for (size_t i = 0; i < _tables.size(); i++)
                        {
                                Node* cur = _tables;
                                while (cur)
                                {
                                        Node* next = cur->_next;
                                        size_t hashi = hf(kot(cur->_data));
                                        hashi %= newSize;
                                        cur->_next = newTables;
                                        newTables = cur;
                                        cur = next;
                                }
                                _tables = nullptr;
                        }
                        newTables.swap(_tables);
                }


                size_t hashi = hf(kot(data));
                hashi %= _tables.size();
                Node * newNode = new Node(data);
                newNode->_next = _tables;
                _tables = newNode;
                ++_n;
                return make_pair(iterator(newNode, this), true);
        }


        iterator Find(const K& key)
        {
               
                if (_tables.size() == 0)
                {
                        return iterator(nullptr, this);
                }
                HashFunc hf;
                KeyOfT kot;
                size_t hashi = hf(key);
                hashi %= _tables.size();
                Node* cur = _tables;
                while (cur)
                {
                        if (kot(cur->_data) == key)
                        {
                                return iterator(cur, this);
                        }
                        cur = cur->_next;
                }
                return iterator(nullptr, this);
        }
        bool Erase(const K& key)
        {
                //问:如果不用prev指针,还能用什么方法?
                //答:有另外两种方法,但是依旧只推荐使用prev指针的方法
                //1.判断cur->_next->kv.first是否等于key
                //2.替换法删除,将cur节点和_tables替换(替换kv即可),然后删除_tables
                if (_tables.size() == 0)
                        return false;
                HashFunc hf;
                size_t hashi = hf(key);
                hashi %= _tables.size();
                Node* cur = _tables;
                Node* prev = nullptr;
                while (cur)
                {
                        if (cur->_kv.first == key)
                        {
                                //删除节点
                                if (prev == nullptr)//也可以写成prev == nullptr的情况
                                {
                                        _tables = cur->_next;
                                }
                                else
                                {
                                        prev->_next = cur->_next;
                                }
                                delete cur;
                                return true;
                        }
                        prev = cur;
                        cur = cur->_next;
                }
                return false;
        }

private:
        //指针数组
        vector<Node*> _tables;
        size_t _n = 0;
};
UnorderedMap.h文件
#pragma once
#include"Hash.h"
namespace Test
{
        template<class K, class V, class HashFunc = DefaultHash<K>>
        class UnorderedMap
        {
       
                struct MapKeyOfT
                {
                        const K& operator()(const pair<K, V>& key)
                        {
                                return key.first;
                        }
                };
        public:
                typedef typename HashTable<K, pair<K, V>, MapKeyOfT, HashFunc>::iterator iterator;
                pair<iterator, bool> insert(const pair<K, V>& kv)
                {
                        return _ht.Insert(kv);
                }
                iterator find(const K& key)
                {
                        return _ht.Find(key);
                }
                iterator begin()
                {
                        return _ht.begin();
                }
                iterator end()
                {
                        return _ht.end();
                }
                iterator erase(const K& key)
                {
                        return _ht.Erase(key);
                }
                V& operator[](const K& key)
                {
                        pair<iterator, bool> ret = insert(make_pair(key, V()));
                        return ret.first->second;
                }

        private:
                HashTable<K, pair<K, V>, MapKeyOfT, HashFunc> _ht;
        };
}
UnorderedSet.h文件
#pragma once
#include"Hash.h"
namespace Test
{
        template<class K, class HashFunc = DefaultHash<K>>
        class UnorderedSet
        {
        public:
               
                struct SetKeyOfT
                {
                        const K& operator()(const K& key)
                        {
                                return key;
                        }
                };
                typedef typename HashTable<K, K, SetKeyOfT, HashFunc>::iterator iterator;
                pair<iterator, bool> insert(const K& key)
                {
                        return _ht.Insert(key);
                }
                //问:此处为什么要加typename?
                //答:因为编译器无法区分这个是变量还是内嵌类型
       
                iterator begin()
                {
                        return _ht.begin();
                }
                iterator end()
                {
                        return _ht.end();
                }
                iterator find(const K& key)
                {
                        return _ht.Find(key);
                }
                iterator erase(const K& key)
                {
                        return _ht.Erase(key);
                }
        private:
                HashTable<K, K, SetKeyOfT, HashFunc> _ht;
        };
}
4. 哈希的应用

4.1 位图

4.1.1 位图概念


[*] 口试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,怎样快速判定一个数是否在这40亿个数中。

[*] 遍历,时间复杂度O(N)
[*] 排序(O(NlogN)),利用二分查找: logN(外排序不能很好的支持随机访问)
[*] 位图办理
数据是否在给定的整形数据中,结果是在大概不在,刚好是两种状态,那么可以使用一 个二进制比特位来代表数据是否存在的信息,假如二进制比特位为1,代表存在,为0代表不存在。比如:
https://i-blog.csdnimg.cn/img_convert/75d28955e4634bfd9a3012389f2bfe32.png

[*] 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判定某个数据存不存在的。
4.1.2 位图的模拟实现

1GB = 1024MB
1024MB = 1024 * 1024 KB
1024 * 1024KB = 1024 * 1024 * 1024Byte = 10亿字节 = 230字节 = 80亿比特位
即最大无符号整数个比特位需要512M内存。
namespace Test
{
        //N个比特位的位图
        template<size_t N>
        class bitset
        {
        public:
                bitset()
                {
                        //+1保证足够的特位,最多浪费8个比特位
                        _bits.resize(N / 8 + 1, 0);
                }
                //x映射的比特位标记成1
                void set(size_t x)
                {
                        //i是x映射的比特位在第几个字节
                        size_t i = x / 8;

                        //x在char的第几个比特位
                        size_t j = x % 8;
                        _bits |= (1 << j);
                }
                void reset(size_t x)
                {
                        //i是x映射的比特位在第几个字节
                        size_t i = x / 8;

                        //x在char的第几个比特位
                        size_t j = x % 8;
                        _bits &= (~(1 << j));
                }
                bool test(size_t x)
                {
                        size_t i = x / 8;
                        size_t j = x % 8;
                        return _bits & (1 << j);
                }
        private:
                vector<char> _bits;
        };
}
注意:INT_MAX宏是整数的最大值(+2147483647)。INT_MIN是整数的最小值(-2147483648)。
   问:怎样表示无符号整数的最大值?
答:unsigned(-1)大概0XFFFFFFFF
4.1.3 位图的应用

   
[*]快速查找某个数据是否在一个聚集中
[*]排序 + 去重
[*]求两个聚集的交集、并集等
[*]操作系统中磁盘块标记
4.1.4 C++库中的位图

https://i-blog.csdnimg.cn/img_convert/311220d70f1147d4213f414b35de574f.png
接口函数:
https://i-blog.csdnimg.cn/img_convert/8033f443b72360598ac2ed74b666b6c1.png
当然,最关键的接口还是set、reset和test。
4.2 布隆过滤器

4.2.1 布隆过滤器提出

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

[*]用哈希表存储用户记录,缺点:浪费空间
[*]用位图存储用户记录,缺点:位图一般只能处置惩罚整形,假如内容编号是字符串,就无法处置惩罚 了。
[*]将哈希与位图团结,即布隆过滤器
https://i-blog.csdnimg.cn/img_convert/5d08a297f93dd81d8254fa72461f4d43.png
布隆过滤器对上面的这种办理方案进行了改进。
https://i-blog.csdnimg.cn/img_convert/4ea4055a44edb0434815fc325ebde776.png
4.2.2布隆过滤器概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比力奇妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在大概可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节流大量的内存空间。
https://i-blog.csdnimg.cn/img_convert/e2ad833603dbad60c4e53e3b8bf30240.png
https://zhuanlan.zhihu.com/p/43263751/
4.2.3 布隆过滤器的模拟实现

https://i-blog.csdnimg.cn/img_convert/36523cbdd653c3e2c4ad0171f674ee8b.png
#include<bitset>
using namespace std;

struct BKDRHash
{
        size_t operator()(const string& s)
        {
                // BKDR
                size_t value = 0;
                for (auto ch : s)
                {
                        value *= 31;
                        value += ch;
                }
                return value;
        }
};
struct APHash
{
        size_t operator()(const string& s)
        {
                size_t hash = 0;
                for (long i = 0; i < s.size(); i++)
                {
                        if ((i & 1) == 0)
                        {
                                hash ^= ((hash << 7) ^ s ^ (hash >> 3));
                        }
                        else
                        {
                                hash ^= (~((hash << 11) ^ s ^ (hash >> 5)));
                        }
                }
                return hash;
        }
};

struct DJBHash
{
        size_t operator()(const string& s)
        {
                size_t hash = 5381;
                for (auto ch : s)
                {
                        hash += (hash << 5) + ch;
                }
                return hash;
        }
};

template<class K, size_t M, class HashFunc1, class HashFunc2, class HashFunc3>
class BloomFilter
{
public:
        void Set(const K& key)
        {
                size_t hash1 = HashFunc1()(key) % M;
                size_t hash2 = HashFunc2()(key) % M;
                size_t hash3 = HashFunc3()(key) % M;
                _bs.set(hash1);
                _bs.set(hash2);
                _bs.set(hash3);
        }
        bool Test(const K& key)
        {
                size_t hash1 = HashFunc1()(key) % M;
                size_t hash2 = HashFunc2()(key) % M;
                size_t hash3 = HashFunc3()(key) % M;
                return _bs.test(hash1) && _bs.test(hash2) && _bs.test(hash3);
        }
private:
        bitset<M> _bs;
};
4.2.4 布隆过滤器的查找

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

   布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计 数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作(此时布隆过滤器的每一个位上存在的不是在与不在,而是计数,即出现的次数)。
注意:删除后这个值仍旧存在,这是误判造成的,而不是删除造成的。
缺陷:

[*]无法确认元素是否真正在布隆过滤器中
[*]存在计数回绕
4.2.6 布隆过滤器长处

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

   
[*]有误判率,即存在假阳性(False Position),即不能正确判定元素是否在聚集中(补救方法:再创建一个白名单,存储可能会误判的数据)
[*]不能获取元素本身
[*]一般环境下不能从布隆过滤器中删除元素
[*]假如采取计数方式删除,可能会存在计数回绕题目
4.2.8 布隆过滤器的应用场景

   布隆过滤器重要是作为过滤器快速判定一个数据是否不存在。

[*]注册用户名昵称的时候,快速判定一个昵称大概用户名是否不存在,判定存在时会出现误判,所以假如判定已经存在的环境,还需要到数据库中重新筛查判定
[*]黑名单,判定某个用户是否不在黑名单中,决定是否放行某个用户,假如不在就放行,假如在就再次去系统进行确认
5. 海量数据口试题

5.1 哈希切割

给一个高出100G大小的log file, log中存着IP地址, 筹划算法找到出现次数最多的IP地址? 与上题条件雷同,怎样找到top K的IP?怎样直接用Linux系统命令实现?
https://i-blog.csdnimg.cn/img_convert/7605a5b6ca42b6a2775e8315aaa3c6e5.png
5.2 位图应用


[*] 给定100亿个整数,筹划算法找到只出现一次的整数?
注意:虽然给定的是100亿个整数,但是位图的应用并不是将100亿个整数存储在内存中,而是用1G的空间来表示0到最大无符号整数出现次数的三种状态:出现0次,出现1次,出现2次及两次以上。
https://i-blog.csdnimg.cn/img_convert/9eedd248456ff53cb2d497512416751c.png
00表示未出现过,01表示出现一次,10表示出现一次及一次以上。
模拟实现:
template<size_t N>
class two_bitset
{
public:

        void set(size_t x)
        {
                bool in1 = _bs1.test(x);
                bool in2 = _bs2.test(x);
                if (in1 == false && in2 == false)
                {
                        _bs2.set(x);
                }
                else if (in1 == false || in2 == true)
                {
                        _bs1.set(x);
                        _bs2.reset(x);
                }
        }

        bool is_once(size_t x)
        {
                return _bs1.test(x) == false && _bs2.test(x) == true;
        }
private:
        //用两个位图去表示两个比特位,实现对前面位图的复用
        bitset<N> _bs1;
        bitset<N> _bs2;
};

[*] 给两个文件,分别有100亿个整数,我们只有1G内存,怎样找到两个文件交集?
https://i-blog.csdnimg.cn/img_convert/74cf4c08037fecca58eac6c8fccceae8.png
[*] 位图应用变形:1个文件有100亿个int,1G内存,筹划算法找到出现次数不高出2次的所有整数
位图应用1的变形,出现0次为0,出现1次为01,两次为10,三次及以上为11,实现与位图的应用1类似。
5.3 布隆过滤器


[*] 给两个文件,分别有100亿个query,我们只有1G内存,怎样找到两个文件交集?分别给出 精确算法和近似算法
query:SQL网络请求,是一个字符串
近似算法:布隆过滤器
精确算法:
假设每个query是30个字节,100亿query就是300G(10亿字节是1G)的空间。
使用哈希切分的思想:
https://i-blog.csdnimg.cn/img_convert/a7016ba342f3198567522dfe88f44e1d.png
[*] 怎样扩展BloomFilter使得它支持删除元素的操作
计数器。
6. 分布式存储(哈希的应用)

https://i-blog.csdnimg.cn/img_convert/e2697285d1f5499ca097253b9b396392.png
一致性哈希:
一致性hash算法正是为了办理此类题目的方法,它可以保证当呆板增加大概减少时,节点之间的数据迁徙只限于两个节点之间,不会造成全局的网络题目。
1. 环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有232次方个桶的空间中,即0~(232)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图:
https://i-blog.csdnimg.cn/img_convert/770b89c18b05ffb3746f7e7700dd70fd.webp?x-oss-process=image/format,png
2. 将数据通过hash算法映射到环上

将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;
https://i-blog.csdnimg.cn/img_convert/fd088b5cf67a03631106958eecc472e3.webp?x-oss-process=image/format,png
3. 将呆板通过hash算法映射到环上

假设现在有NODE1,NODE2,NODE3三台呆板,通过Hash算法(呆板IP或呆板的唯一的名称作为输入)得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
https://i-blog.csdnimg.cn/img_convert/10a05feb0ff1df6d144d4e833883abed.webp?x-oss-process=image/format,png
4. 将数据存储到呆板上

通过上图可以看出对象与呆板处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。
https://i-blog.csdnimg.cn/img_convert/0465f683becda4f995a8049b5c996cd4.webp?x-oss-process=image/format,png
5. 呆板的添加与删除


[*] 向集群中添加一台新呆板
向集群中增加呆板c4,c4颠末hash函数后映射到呆板c2和c3之间。这时根据顺时针存储的规则,数据m4从呆板c2迁徙到呆板c4。数据的移动仅发生在c2和c4之间,其他呆板上的数据并未受到影响。
https://i-blog.csdnimg.cn/img_convert/adf2252343334bc1ea5bdf2a92d2cac0.webp?x-oss-process=image/format,png
[*] 从集群中删除一台呆板
从集群中删除呆板c1,这时只有c1原有的数据需要迁徙到呆板c3,其他数据并未受到影响。
https://i-blog.csdnimg.cn/img_convert/79dc8f48fe45a861dcaf6d4ee731fa0c.webp?x-oss-process=image/format,png
相比于之前的简朴取模方法中动态增删集群中呆板的数量时,造成全局的数据迁徙,使用一致性哈希算法将大大改善这种环境,减轻了网络通信的压力。
存在的题目:

当集群中的节点数量较少时,可能会出现节点在哈希空间中分布不均衡的题目。如下图所示,图中节点A、B、C分布较为集中,造成hash环的倾斜。数据1、2、3、4、6全部被存储到了节点A上,节点B上只存储了数据5,而节点C上什么数据都没有存储。A、B、C三台呆板的负载极其不均衡。
https://i-blog.csdnimg.cn/img_convert/10d5ccc0de6f0d95184850e5eb2f0973.webp?x-oss-process=image/format,png
在极端环境下,假如A节点出现故障,存储在A上的数据要全部转移到B上,大量的数据导可能会导致节点B的崩溃,之后A和B上所有的数据向节点C迁徙,导致节点C也崩溃,由此导致整个集群宕机。这种环境被称为雪崩效应。
办理方法——虚拟节点

办理哈希环偏斜题目的方法就是,让集群中的节点尽可能的多,从而让各个节点均匀的分布在哈希空间中。在实际情境下,呆板的数量一般都是固定的,所以我们只能将现有的物理节通过虚拟的方法复制多个出来,这些由实际节点虚拟复制而来的节点被称为虚拟节点。加入虚拟节点后的环境如下图所示:
https://i-blog.csdnimg.cn/img_convert/86e799d237fff15e5d1ed6c2598aae41.webp?x-oss-process=image/format,png
从上图可得:加入虚拟节点后,节点A存储数据1、3;节点B存储5、4;节点C存储2、6。节点的负载很均衡。
https://i-blog.csdnimg.cn/img_convert/24c56c11ff9de0f8ac035e054cf7b196.webp?x-oss-process=image/format,png

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: C++之哈希