STL之哈希表与哈希桶的模拟实现(万字长文详解)
  1EQmf8Oo0jTP 2023年11月14日 36 0

哈希表与哈希桶的模拟实现

unordered系列关联式容器介绍

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

==但是unordered系列的底层是哈希==!——其实是应该叫做hash_map,hash_set会比较合理的!但是之所以叫做unordered是为了在功能上进行区分!==因为map与set的底层是二叉搜索树!是有序的!但是以哈希为底层的unordered_map和unordered_set是无序的!所以这就是为什么叫unordered!==

==有一些区别就是unordered系列的容器不支持反向迭代器!==

除了这两点之外unordered系列的容器和由二叉树作为底层的容器几乎没有区别!

那么究竟为什么需要unordered系列的容器呢?

==我们看如下的测试==

#include <iostream>
#include <unordered_set>
#include <set>
#include <vector>
using namespace std;
int main()
{
        const size_t N = 100000;
        unordered_set<int> us;
        set<int> s;
        vector<int> v;
        v.reserve(N);
        srand(time(0));
        for(size_t i = 0; i < N; ++i)
        {
           v.push_back(i);//大量数据不同
           //v.push_back(rand());//大量数据相同
           //v.push_back(rand()+i);//部分数据相同
        }
        size_t begin = clock();
        for(size_t i = 0; i < N; ++i)
        {
           us.insert(v[i]);
        }
        size_t end = clock();
        cout << "unordered_set: " << end - begin << endl;

        begin = clock();
        for(size_t i = 0; i < N; ++i)
        {
           s.insert(v[i]);
        }
        end = clock();
        cout << "set: " << end - begin << endl;

        size_t begin2 = clock();
        for(auto e:v)
        {
           s.find(e);
        }
        size_t end2 = clock();
        cout << "set find:" << end2 - begin2 << endl;

        size_t begin3 = clock();
        for(auto e:v)
        {
           us.find(e);
        }
        size_t end3 = clock();
        cout << "unordered_set find :" << end3 - begin3 << endl;

        size_t begin4 = clock();
        for(auto& e:v)
        {
           s.erase(e);
        }
        size_t end4 = clock();
        cout << "set: erase :" << end4 - begin4 << endl;

        size_t begin5 = clock();
        for(auto& e:v)
        {
           us.erase(e);
        }
        size_t end5 = clock();
        cout << "unordered_set: erase :" << end5 - begin5 << endl;
        return 0;
}

insert性能对比

在大量的重复数据的情况下!

image-20230424194812226

在大量不重复的数据的情况下

image-20230424200216244

我们发现了在大批量的有重复的进行插入的时候!unordered系列容器快了好几倍!——rand只能产生3w多个随机值!所以就会造成大量的重复!

如果是不同的的数据进行插入,那么set就会比会比unordered系列的快一点

find效率对比!

image-20230424201435554

==而且我们发现unordered系列的查找非常的快!这是由unordered的底层哈希决定的!无论是大量的数据重复还是大量的数据不重复!unordered系列的find都更加的快!==

erase效率对比

在有大量的数据不同的情况下

image-20230424202347630

在部分或者少量数据相同的情况下

image-20230424203536026

在大量数据相同的情况下!

image-20230424203636989

在数据量重复的情况比较多的时候我们会发现unordered系列会更加的占优势!在大量不重复的时候非unordered版本会更好一点!不过也不一定,在不同的编译器下面实现的可能会有不同的结果!

==但是我们综上我们可以发现,unordered系列的find效率极高!无论是什么场景!unordered系列的find性能都是高于非unordered系列的!所以如果对于查找性能要求比较高的!我们可以使用unordered版本!==

哈希的概念

unordered系列的容器的底层是哈希表!

什么是哈希?哈希就是一种映射!==我们一般叫做哈希映射==——key值跟存储位置建立一种关联关系!

只要知道了key就找到了存储位置!

我们一般会映射到表里面(在C++里面,数组,链表我们都可以叫做表!)

哈希是一种思想!

image-20230424212757356

  1. 直接定址法--(常用)
  • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

  • 优点:简单、均匀

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

  • 使用场景:==适合查找比较小且连续的情况(范围集中!)==

为了解决上面的缺陷,所以有了另一种方法!

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

image-20230425082328683

==模出来的余数是几就在那个位置!这样子就和范围不相干!==

但是这样子又引入了另一个问题!

万一模出来的数是相同的应该怎么办?这样子就会映射在相同的位置上!

我们将不同的值映射在相同位置的情况——称之为==哈希冲突/碰撞==

直接定址法——是不会有这个问题的!因为每一个值都会对应一个唯一的位置!

那么我们应该如何解决哈希冲突呢?

==下面我将介绍常见的两种解决办法——闭散列和开散列!==


闭散列——开放定址法

思路就是——映射的位置已经有存在值了,就按照一定的规则找到其他的位置

规则1——线性探测——如果这个位置已经发生了冲突了,那么就往下面继续找!直到找到空为止!

就是一直++下标!——这就是线性探测

但是这个方法也有问题!

  1. 查找效率低下

image-20230425091823465

  1. 导致相互冲突

image-20230425092010619

==找到最后,要从头开始寻找!不是要扩容!==

规则2——二次探测

二次探测——不是探测两次的意思!

image-20230426172215053

关于哈希表数据应该如何删除

image-20230425110451329

==所以我们既不应该将数据置空!也不应该以位置空为结束条件!——我们应该弄一个状态标识符来标记每一个位置的状态!==

状态标识符——应该有三种状态

  1. 该位置为空
  2. 该位置不为空,但是已经删除
  3. 该位置不为空,没有删除!

==遇到状态为删除的是不能停下来的!要遇到状态为空的的才能停下来!==

有的读者可能会有另一种想法!挪动行不行?删除一个然后向前挪动?

这是不可以的!首先这样子删除效率低下!其次这会导致映射位置造成偏差!比如有的数值明明映射在正确的位置上!但是一挪动就错了!这样子可能会影响查找效率!

闭散列的代码实现——线性探测的实现

哈希数据类
namespace MySTL
{
    enum State
    {
        EMPTY,
        EXIST,
        DELETE
    };//设定三种状态
    template<class K,class V>
    struct HashData
    {
        std::pair<K, V> _kv;
        State _state;
    };
}
哈希数据类的构造
namespace MySTL
{
    enum State
    {
        EMPTY,
        EXIST,
        DELETE
    };
    template<class K,class V>
    struct HashData
    {
        HashData(const std::pair<K,V>& kv = std::pair<K,V>())
            :_kv(kv)
            ,_state(EMPTY)
        {}
        std::pair<K, V> _kv;
        State _state;
    };
}

哈希类的成员变量
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
    template<class K, class V>
    class Hashtable
    {
        typedef HashData<K,V> Data;
    private:
        std::vector<Data> _table;
        size_t _n;//有效元素个数
    };
}

构造函数
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
    template<class K, class V>
    class Hashtable
    {
        typedef HashData<K,V> Data;
    public: 
        Hashtable(size_t size = 10)//开始就要给表一个值!防止出现空表导致插入出问题!
            :_table(size)
            ,_n(0)
        {}
    private:
        std::vector<Data> _table;
        size_t _n;//有效元素个数
    };
}
Insert
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
    template<class K, class V>
    class Hashtable
    {
        typedef HashData<K,V> Data;
    public:
        bool Insert(const std::pair<K,V>& kv)
        {
            //大于标定的负载因	子就需要进行扩容!
            //不要使用if(_n/_table.size() > 0.7)因为_n是size_t,_table.size()是size_t
            //_n/_table.size()的结果是不会是一个浮点数的!
            if(_n*10/_table.size() >= 7)
            {
                //方法一
                /*
                size_t newsize = _table.size() * 2;
                std::vector<Data> newtable(newsize);//创建一个新的表!
                for(auto& e:_table)//一个个的拷贝!
                {
                    if(e._state != EXIST)//只有存在的才需要拷贝!一定要判断!
                        continue;
                    size_t hashi = e._kv.first%newsize;
                    while(newtable[hashi]._state == EXIST)
                    {
                        ++hashi;
                        if(hashi == newsize)
                            hashi = 0;
                    }
                    newtable[hashi] = e;
                }
                _table.swap(newtable);
                */
                //方法二
                Hashtable<K,V> NEWHT(_table.size()*2);
                for(auto& e:_table)
                {
                    if(e._state == EXIST)
                        NEWHT.Insert(e._kv);
                }
                //我们可以直接创建一个新的哈希表!
                //然后调用新的哈希表的插入函数!
                //复用代码!因为新表的负载因子一定是不会超过0.7的!
                //所以不会再次进行扩容!
                //那么就会调用下面的代码!
                //这样就不用再写一遍插入的代码了!
                //然后交换一下就好了!
                _table.swap(NEWHT._table);
            }

            size_t hashi = kv.first% _table.size();//先算要映射的位置!
            //要模上表的大小,因为可能会超出表的大小!
            //而不是capacity,因为capacity是表的容量,可能会比表的大小大!
            while(_table[hashi]._state == EXIST)//循环结束的条件就是遇到空,或者遇到删除!
            {
                if(_table[hashi]._kv.first == kv.first)//如果已经存在
                    return false;
                //线性探测就是++
                ++hashi;
                if(hashi == _table.size())
                    hashi = 0;
            }
            _table[hashi]._kv = kv;
            _table[hashi]._state = EXIST;
            ++_n;//有效元素个数加一!
        }

    private:
        std::vector<Data> _table;
        size_t _n;//有效元素个数
    };
}

image-20230425153739579

负载因子

==insert的时候!是完全不用担心是满的!为什么?==

假如有一个哈希表!表的大小是1000,里面已经有了999个值!那么假如我们这时候去插入!冲突的概率是极大的!且插入效率很低!==所以对于闭散列的哈希表我们是不可能让它满的!==

所以这里就有提出来一个新的概念负载因子!W

==负载因子/载荷因子 == 表中有效数据的个数/表的大小==

所以闭散列的负载因子是不可能达到1的!1就是意味着满了!

==一般我们认为负载因子控制在 0.7-0.8这个区间是最好的!==

所以一旦负载因子到了0.7-0.8左右的时候就要进行扩容的!

负载因子就是一种空间换时间的手段!

负载因子越小,冲突的概率越小,消耗空间越多

负载因子越大,冲突概率越大,空间利用率越高

哈希表如何进行扩容

哈希表的扩容能不能直接单纯的将数据拷贝下来呢?——答案是否定的!

为什么呢?——因为==进行了扩容之后表的大小已经发生了改变!映射关系也已经发生了改变!==

image-20230425161046545

==我们必须重新计算映射关系!==

扩容的时候,不要去使用reserve或者reszie,因为这样子我们相当于是在原表上进行处理!——会很麻烦!

因为如果我们重新算映射关系的时候,将还没计算映射关系的值给覆盖了!那就很不妙了!

==所以最好是弄一个新表!然后重新映射完后交换一下==

Find
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
 template<class K, class V>
 class Hashtable
 {
     typedef HashData<K,V> Data;
 public:
     Data* Find(const K& key)
     {
         //查找和插入的逻辑是一致的!就是线性探测的过程!
         size_t hashi = key%_table.size();
         size_t starti = hashi;
         while(_table[hashi]._state != EMPTY)//找到空才能停
         {
            if(_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//要判断是否存在!而不是删除!
                return &_table[hashi];
             ++hashi;
             if(hashi == _table.size())
                 hashi = 0;
             if(starti == hashi)//极端场景下面不存在空!全是存在或者删除!
             {
                 break;
             }             
         }
         return nullptr;
     }
 private:
     std::vector<Data> _table;
     size_t _n;//有效元素个数
 };
}

==我们使用Data* 作为返回值有一个好处!——就是可以方便进行复用!==


erase
#pragma once
#include<iostream>
#include<vector>
namespace MySTL
{
    template<class K, class V>
    class Hashtable
    {
        typedef HashData<K,V> Data;
    public:
        bool erase(const K& key)
        {
            Data* ret = Find(key);//直接复用Find我们就可以不用自己去写查找逻辑了!
            if(ret == nullptr)
                return false;
            else
            {
                ret->_state = DELETE;//这种就是伪删除!
                --_n;//减去有效数据次数
                return true;
            }
        }
    private:
        std::vector<Data> _table;
        size_t _n;//有效元素个数
    };
}
双重映射——哈希函数

上面的映射一个问题!——那就是只能映射整形!

image-20230426160939883我们想要插入一个string类型的时候!就会发生报错!

那这时候应该怎么办?——答案就是双重映射!

image-20230426161135039

==先将string映射为整形!然后再对整形取模!==——这个算法要是一个稳定的算法!让一个字符串尽可能的对应一个唯一的数字!

例如:我们可以去字符串的第一个字母(char类型的变量)——但是这样子有很大的概率就会发生冲突!因为只要第一个字母相同那么就会发生冲突!

所以我们就必须==写一个哈希函数——一个仿函数!==用来进行转换!

这个哈希函数的的作用就是将key转换为一个整形类型!

namespace MySTL
{
     enum State
     {
         EMPTY,
         EXIST,
         DELETE
     };
     template<class K,class V>
     struct HashData
     {
         HashData(const std::pair<K,V>& kv = std::pair<K,V>())
             :_kv(kv)
             ,_state(EMPTY)
         {}
         std::pair<K, V> _kv;
         State _state;
     };
     //写一个哈希函数!
     template<class K>
     struct HashFunc//这就是我们默认的哈希函数!但是这个哈希函数是不能将string类转换为size_t的!
     {
         size_t operator()(const K& key)
         {
             return (size_t)key;
         }
     };
     //多加一个模板参数!——仿函数!
     template<class K, class V,class Hash = HashFunc<K>>
     class Hashtable
     {
         typedef HashData<K,V> Data;
     public:
         Hashtable(size_t size = 10)
             :_table(size)
             ,_n(0)
         {}
         bool Insert(const std::pair<K,V>& kv)
         {
             if(_n*10/_table.size() >= 7)
             {
                 Hashtable<K,V,Hash> NEWHT(_table.size()*2);
                 for(auto& e:_table)
                 {
                     if(e._state == EXIST)
                         NEWHT.Insert(e._kv);
                 }

                 _table.swap(NEWHT._table);
             }
             Hash hf;
             size_t hashi = hf(kv.first) % _table.size();
             while(_table[hashi]._state == EXIST)
             {
                 if(_table[hashi]._kv.first == kv.first)
                     return false;
                 ++hashi;
                 if(hashi == _table.size())
                     hashi = 0;
             }
             _table[hashi]._kv = kv;
             _table[hashi]._state = EXIST;
             ++_n;
             return true;
         }

         Data* Find(const K& key)
         {
             Hash hf;
             size_t hashi = hf(key)%_table.size();
             while(_table[hashi]._state != EMPTY)
             {
                if(_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
                    return &_table[hashi];
                 ++hashi;
                 if(hashi == _table.size())
                     hashi = 0;
             }
             return nullptr;
         }

         bool erase(const K& key)
         {
             Data* ret = Find(key);
             if(ret == nullptr)
                 return false;
             else
             {
                 ret->_state = DELETE;
                 --_n;
                 return true;
             }
         }
     private:
         std::vector<Data> _table;
         size_t _n;//有效元素个数
     };
}

//将string类转换为size_t的一个哈希函数!
namespace MySTL
{
     struct HashFuncstring
     {
         size_t operator()(const std::string& key)
         {
             size_t hash = 0;
             for(auto& e:key)
             {
                 hash = hash*131 + e;//不一定要使用这个算法,也可以自己写一个算法
             }
             return hash;
         }
     }
     //或者我们也可以将模版进行偏特化!
     //将上面的写的hash仿函数偏特化一下!
     template<>
     struct HashFunc<std::string>
     {
         size_t operator()(const std::string& key)
         {
             size_t hash = 0;
             for(auto& e:key)
             {
                 hash = hash*131 + e;
             }
             return hash;
         }
     };

    }
    namespace MySTL
    {
     void testHash()
     {
         std::vector<std::string> fruit = {"apple","banana","orange","apple","banana","apple"};
         //使用我们自己写的这个hash函数!
         Hashtable<std::string,int,HashFuncstring> HT2;
         for(auto& e:fruit)
         {
             HashData<std::string,int>* ret = HT2.Find(e);
             if(ret == nullptr)
                 HT2.Insert(std::make_pair(e,1));
             else
                 ++(ret->_kv.second);
         }
     }
     //我们要针对这个类型写一个特定的仿函数!
     //哈希的用法就是这样的!
}

image-20230426163616082

我们可以看到我们成功的统计了水果的数量!

==不过我们哈希表一般要么是使用整形作为key,要么就是字符串作为key!其实很少用自定义类型作为key==

我们可以看一下官方接口也是如此实现的!

image-20230426185708897

我们也可以自己去传哈希函数!

字符串转整形是一种常见的情况!

有兴趣可以看看各种字符串Hash函数 这篇文章里面介绍了各种的字符串转整形的哈希算法!

==但是无论是怎么样的哈希算法,都是无法避免冲突的!因为字符串是无限的!但是整形是有限的!但是可以极大的避免冲突!==

这个哈希表是不用写拷贝构造,赋值重装和析构的!对于自定义类型,哈希会自己去调用vector的拷贝/构造/赋值重装!

开散列——哈希桶介绍

相比哈希表(由闭散列实现!)——闭散列无论如何,占用别的数据的位置是不可避免的!

但是由开散列实现的哈希桶就没有这个问题!

image-20230425093322624

==但是哈希桶有一个问题!那就是如果大量的数据发生了冲突!甚至最极端的情况!所有数据都冲突了!(就是全都挂在了同一个的单链表上!)那么就会从哈希桶退化为单链表!==

==一般哈希桶都是使用单链表作为它的子结构!==

这就是为什么unordered系列的容器只支持单向迭代器!——而不支持双向迭代器!

哈希桶的实现

哈希桶节点类

namespace MySTL
{
    template<class K,class V>
    struct HashNode
    {
        HashNode(const std::pair<K,V> kv = std::pair<K,V>())
                :_kv(kv)
                ,_next(nullptr)
        {}
        std::pair<K,V> _kv;
        HashNode<K,V>* _next;
    };
}

哈希桶

成员变量
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
     template<class K>
     struct HashFunc
     {
         size_t operator()(const K& key)
         {
             return (size_t)key;
         }
     };//默认哈希函数!
     template<class K,class V,class Hash = HashFunc<K>>
     class HashBucket
     {
         typedef HashNode<K, V> Node;
     public:
         HashBucket()
                 :_n(0)
                 ,_Bucket(10, nullptr)
         {}
     private:
         std::vector<Node> _Bucket;
         size_t _n;
     };
}

==如果不想自己写链表可以去使用STL库里面的list或者forward_list这两个容器==

构造函数
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
     template<class K>
     struct Hash
     {
         size_t operator()(const K& key)
         {
             return (size_t)key;
         }
     };
     template<class K,class V,class Hash = HashFunc<K>>
     class HashBucket
     {
         typedef HashNode<K, V> Node;
     public:
            HashBucket(size_t n = 10)
                    :_n(0)
            {
                _Bucket.resize(n);
            }
     private:
         std::vector<Node> _Bucket;
         size_t _n;
     };
}

析构函数
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
    template<class K>
    struct HashFunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
        }
    };
    template<class K,class V,class Hash = HashFunc<K>>
    class HashBucket
    {
        typedef HashNode<K, V> Node;
    public:
        ~HashBucket()
        {
            for(auto cur : _Bucket)
            {
               while(cur)
               {
                   Node* next = cur->_next;
                   delete cur;
                   cur = next;
               }
            }
        }
    private:
        std::vector<Node*> _Bucket;
        size_t _n;
    };
}

==闭散列是不需要写析构函数的!但是开散列是需要的!如果不写析构函数!因为_Bucket只会释放每个桶的头节点!而不会释放每个桶的所有节点!会造成存泄露!==

Find
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
    template<class K>
    struct HashFunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
        }
    };
    template<class K,class V,class Hash = HashFunc<K>>
    class HashBucket
    {
        typedef HashNode<K, V> Node;
    public:
        
        Node* Find(const K& key)
        {
            size_t hashi = Hash()(key) % _Bucket.size();
            Node cur = _Bucket[hashi];
            while(cur)
            {
                if(cur->_kv.first == key)
                    return cur;
                cur = cur->_next;
            }
            return nullptr;
        }
    private:
        std::vector<Node> _Bucket;
        size_t _n;
    };
}

Insert
#include <iostream>
#include <vector>
namespace MySTL
{
 template<class K,class V,class Hash = HashFunc<K>>
 class HashBucket
 {
     typedef HashNode<K, V> Node;
 public:
     bool Insert(const std::pair<K,V> &kv)
     {
         if(Find(kv.first))
             return false;

         //还要考虑扩容!要考虑一下负载因子
         //闭散列的负载因子是不会大于一的,但是开散列的负载因子是可以大于一的,因为理论上开散列可以无限挂
         //一般的负载因子控制在1
         if(_n == _Bucket.size())//我们这边就简单写死
         {
             //方法一
             //像闭散列一样创建一个新的哈希表,然后把旧的哈希表的元素全部插入到新的哈希表中
             //然后交换一下根节点就结束!
             //这样子的前提是要写析构函数保证旧的哈希表的元素都能被释放!
             //因为vector只会只会去释放每个位置的根节点元素!但是根节点后面的元素都不会被释放!
             //HashBucket<K,V,Hash> newHT;
             //newHT._Bucket.resize(_Bucket.size()*2);
             //for(auto cur :_Bucket)
             //{
             //    while(cur)
             //    {
             //        newHT.Insert(cur->_kv);
             //        cur = cur->_next;
             //    }
             //}
             //_Bucket.swap(newHT._Bucket);

             //方法二,将旧表的节点全部拿出来,然后重新插入到新表中
             std::vector<Node*> newBucket;
             newBucket.resize(_Bucket.size()*2, nullptr);
             for(int i = 0;i< _Bucket.size();i++)
             {
                 Node* cur = _Bucket[i];
                 while(cur)
                 {
                     Node* next = cur->_next;
                     size_t hashi = Hash()(cur->_kv.first) % newBucket.size();

                     //cur是新插入的节点!要进行头插!变成新的头结点
                     //nweBucket[hashi]是原来的头结点!要变成新的头结点的next
                     cur->_next = newBucket[hashi];

                     //不用担心第一次是空的,即使第一次是nullptr,也不会出错!
                     newBucket[hashi] = cur;//变成新的头结点
                     cur = next;
                 }
                 _Bucket[i] = nullptr;
             }
             _Bucket.swap(newBucket);//交换两个表
         }

         size_t hashi = Hash()(kv.first) % _Bucket.size();
         //要放在扩容的后面!否则会导致扩容之后用的是扩容前的映射!        
         //哈希桶和哈希表不一样!哈希桶不用担心哈希冲突,因为哈希桶的每个位置都是一个链表
         
         //直接进行头插
         //不用判断开始是不是空!
         Node* newhead = new Node(kv);
         newhead->_next = _Bucket[hashi];
         _Bucket[hashi] = newhead;
         ++_n;
         return true;
     }
 private:
     std::vector<Node*> _Bucket;
     size_t _n;
 };
}

**闭散列的负载因子不能大于一!**但是==开散列可以!==因为理论上来说,开散列是可以无限挂下去的!

==但是一般我们会将负载因子控制在1左右!==

在STL库的unordered系列的容器中给我们提供了一个接口!

image-20230426190504314

==这个接口允许我们控制负载因子的大小!==

开链表如何进行扩容

image-20230427112241888

    if(_n == _Bucket.size())//我们这边就简单写死
    {
        //方法一
        //像闭散列一样创建一个新的哈希表,然后把旧的哈希表的元素全部插入到新的哈希表中
        //然后交换一下根节点就结束!
        //这样子的前提是要写析构函数保证旧的哈希表的元素都能被释放!
        //因为vector只会只会去释放每个位置的根节点元素!但是根节点后面的元素都不会被释放!
        HashBucket<K,V,Hash> newHT;
        newHT._Bucket.resize(_Bucket.size()*2);
        for(auto cur :_Bucket)
        {
            while(cur)
            {
                newHT.Insert(cur->_kv);
                cur = cur->_next;
            }
        }
        _Bucket.swap(newHT._Bucket);
    }

==但是这种方法扩容的消耗十分的大!因为我们要去new新的节点!然后再调用析构函数去释放所有旧表的节点!==

==所以我们可以想办法利用一下旧的节点!——但是绝不是像上面的一样!直接将新表指向桶!这样子旧导致一个桶被指向连词,如果不将旧表置空还会导致节点析构两次!==

    if(_n == _Bucket.size())//我们这边就简单写死
    {
        //方法二,将旧表的节点全部拿出来,然后重新插入到新表中
        std::vector<Node*> newBucket;
        newBucket.resize(_Bucket.size()*2, nullptr);
        for(int i = 0;i< _Bucket.size();i++)
        {
            Node* cur = _Bucket[i];
            while(cur)
            {
                Node* next = cur->_next;
                size_t hashi = Hash()(cur->_kv.first) % newBucket.size();

                //cur是新插入的节点!要进行头插!变成新的头结点
                //nweBucket[hashi]是原来的头结点!要变成新的头结点的next
                cur->_next = newBucket[hashi];

                //不用担心第一次是空的,即使第一次是nullptr,也不会出错!
                newBucket[hashi] = cur;//变成新的头结点
                cur = next;
            }
            _Bucket[i] = nullptr;
        }
        _Bucket.swap(newBucket);
    }

==这样我们既避免了重新new新的节点!也避免了去调用哈希表的析构函数!==

关于表扩容的大小

有一个说法,就是如果表的大小是一个素数,那么就可以在取模的时候让数据更加的分散

而在STL库的早期实现中也确实采用了这个说法

image-20230427192138053

==我们可以看到因为每一次扩容如果要算下一个素数很麻烦!所以STL库干脆给了一个素数表!然后通过lower_bound每次去找到比n更大的素数然后返回,而到最后如果到最大值了,就不再进行扩容!而是返回最大值!==

inline unsigned long __stl_next_prime(unsigned long n)
{
    static const int __stl_num_primes = 28;
    static const unsigned long __stl_prime_list[__stl_num_primes] =
            {
                    53,         97,         193,       389,       769,
                    1543,       3079,       6151,      12289,     24593,
                    49157,      98317,      196613,    393241,    786433,
                    1572869,    3145739,    6291469,   12582917,  25165843,
                    50331653,   100663319,  201326611, 402653189, 805306457,
                    1610612741, 3221225473, 4294967291
            };//是一种
    for(int i = 0;i<__stl_num_primes;i++)
    {
        if(__stl_prime_list[i] > n)
        {
            return __stl_prime_list[i];
        }
    }
    return __stl_prime_list[__stl_num_primes-1];//如果已经最大了就返回最后的值!
}

==也不用担心到了最大了但是不扩容的问题!因为最大了有42亿9千万多个数据!即使我们按最小的字节来计算也就是42亿9千万多个字节——也就是 4G==——而如果每个数据是整形(4字节)那么就是16G的数据!——而且不止有这个这个16G的数据还有每个节点还有一个指针!(我们按32位来计算)——那么就是**(4+4)*4 = 32G**的数据!那么光是节点的数据就是非常的恐怖了,何况还有挂节点的数组的大小

所以几乎不用担心,哈希表可能会怎么大!

#include <iostream>
#include <vector>
namespace MySTL
{
     template<class K,class V,class Hash = HashFunc<K>>
     class HashBucket
     {
         typedef HashNode<K, V> Node;
     public:
         bool Insert(const std::pair<K,V> &kv)
         {
             if(Find(kv.first))
                 return false;

             size_t hashi = Hash()(kv.first) % _Bucket.size();
         
             if(_n == _Bucket.size())//我们这边就简单写死
             {
                 std::vector<Node*> newBucket;
                 //然后就简单的修一下一下扩容
                 newBucket.resize( __stl_next_prime(_Bucket.size()), nullptr);
                 for(int i = 0;i< _Bucket.size();i++)
                 {
                     Node* cur = _Bucket[i];
                     while(cur)
                     {
                         //.....
                     }
                 }
                 _Bucket.swap(newBucket);//交换两个表
             }
             //......
         }
     private:
         std::vector<Node*> _Bucket;
         size_t _n;
     };
}

如果实在害怕链表挂的太长了!情况过于极端!还可以在超过一定长度之后将链表改成红黑树!我们既然可以挂单链表,自然也是可以挂红黑树的!(像是java的处理就是在链表长度超过8就将链表改成红黑树!)这样子就是最极端的情况就是退化为log2(N)

拷贝构造
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
    template<class K>
    struct HashFunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
        }
    };
    template<class K,class V,class Hash = HashFunc<K>>
    class HashBucket
    {
        typedef HashNode<K, V> Node;
    public:
        HashBucket(const HashBucket<K,V,Hash>& ht)//拷贝构造
        {
            _Bucket.resize(ht._Bucket.size());
            for(size_t i = 0;i < ht._Bucket.size();++i)
            {
                Node* cur = ht._Bucket[i];
                while(cur)//不断插入的这个链表的节点!
                {
                    Insert(cur->_kv);
                    cur = cur->_next;
                }
            }
        }
    private:
        std::vector<Node*> _Bucket;
        size_t _n;
    };
   }
   
Erase
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
    template<class K,class V,class Hash = HashFunc<K>>
    class HashBucket
    {
        typedef HashNode<K, V> Node;
    public:
        bool Erase(const K& key)
        {
           //开散列不能像闭散列一样Find然后删除!因为这是一个单链表!要找到前一个节点才能删除!
           size_t hashi = Hash()(key) % _Bucket.size();
           Node* cur = _Bucket[hashi];
           Node* curprev = nullptr;
           while(cur)
           {
               //找到后开始准备删除
               if(cur->_kv.first == key)
               {
                  if(cur == _Bucket[hashi]) //如果删除的是头结点!
                  {
                      _Bucket[hashi] = cur->_next;
                  }
                  else
                  {
                      curprev->_next = cur->_next;
                  }
                  delete cur;
                   _n--;
                  return true;
               }
               cur = cur->_next;
               curprev = cur;
           }
           return false;
        }
    private:
        std::vector<Node*> _Bucket;
        size_t _n;
    };
}

12

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

上一篇: python 题目:查找字符串。 下一篇: 直方图
  1. 分享:
最后一次编辑于 2023年11月14日 0

暂无评论

推荐阅读
1EQmf8Oo0jTP