【C++】哈希——unordered系列容器&哈希概念&哈希冲突

1. unordered系列的关联式容器

1.1 引言

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

注意:实际上,unordered系列容器的使用和map/set基本一致,区别就在于unordered系列只支持单向迭代器,并且由于结构的限制,遍历unordered系列容器的顺序是无序

1.2 unordered_map的使用说明

使用文档

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lh9sfZMp-1684930297303)(https://pic-bed123.oss-cn-nanjing.aliyuncs.com/%E6%88%AA%E5%B1%8F2023-05-23%2013.41.16.png)]

void Test_unordered_map()
{
    
    
    unordered_map<string, int> countMap;
    string arr[] = {
    
     "苹果", "西瓜", "香蕉","苹果", "西瓜", "西瓜"};
    for(const auto& str : arr)
    {
    
    
        //countMap[str]++;
        auto it = countMap.find(str);
        if(it == countMap.end())
        {
    
    
            countMap.insert(make_pair(str, 1));
        }
        else
        {
    
    
            it->second++;
        }
    }
    for(const auto& kv : countMap)
    {
    
    
        cout << kv.first << ":" << kv.second << endl;
    }
    cout << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zQ3hv30X-1684930297304)(https://pic-bed123.oss-cn-nanjing.aliyuncs.com/%E6%88%AA%E5%B1%8F2023-05-23%2013.48.12.png)]

除了map的接口之外,由于unordered_map的底层是哈希桶,所以,有一些只属于哈希桶的接口

函数原型 功能介绍
size_t bucket_count() const 返回哈希桶中桶的总个数
size_t max_bucket_count() const 返回哈希桶中能够容纳的最大的桶个数
size_t bucket_size(size_t n) const 返回哈希桶中有效元素个数
**size_t bucket(const K& key) ** 返回元素key所在的桶号

1.3 unordered_set的使用说明

使用文档

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l4MBpovo-1684930297304)(https://pic-bed123.oss-cn-nanjing.aliyuncs.com/%E6%88%AA%E5%B1%8F2023-05-23%2013.33.08.png)]

void Test_unordered_set()
{
    
    
    unordered_set<int> us;
    us.insert(10);
    us.insert(2);
    us.insert(4);
    us.insert(5);
    us.insert(3);
    us.insert(1);
    us.insert(10);
    unordered_set<int>::iterator it = us.begin();
    while(it != us.end())
    {
    
    
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YZHaJAyB-1684930297304)(https://pic-bed123.oss-cn-nanjing.aliyuncs.com/%E6%88%AA%E5%B1%8F2023-05-23%2014.04.56.png)]

扫描二维码关注公众号,回复: 16969970 查看本文章

1.4 unordered_set和unordered_map的应用

在长度为2*N的数组中找到重复出现N次的数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xTk0r0sx-1684930297305)(https://pic-bed123.oss-cn-nanjing.aliyuncs.com/%E6%88%AA%E5%B1%8F2023-05-23%2014.44.00.png)]

1.5 性能比较

void Test_efficient()
{
    
    
	const size_t N = 1000000;//测试的数据个数
    //构造两个数组,一个有序一个无序,用于测试
	vector<int> SortV;
    vector<int> UnsortV;
	SortV.reserve(N);
	UnsortV.reserve(N);
	srand(time(0));
	for (int i = 0; i < N; ++i)
	{
    
    
		UnsortV.push_back(rand());
        SortV.push_back(i);
	}
    //开始测试
    unordered_set<int> SortUs;
    unordered_set<int> UnsortUs;
	set<int> SortS;
	set<int> UnsortS;
    cout << "有序插入效率对比" << endl;
	size_t begin1 = clock();
	for (auto e : SortV)
	{
    
    
		SortS.insert(e);
	}
	size_t end1 = clock();
	cout << "set insert sorted:" << end1 - begin1 << endl;
	size_t begin2 = clock();
	for (auto e : SortV)
	{
    
    
		SortUs.insert(e);
	}
	size_t end2 = clock();
	cout << "unordered_set insert sorted:" << end2 - begin2 << endl;
    cout << "无序插入效率对比" << endl;
	size_t begin3 = clock();
	for (auto e : UnsortV)
	{
    
    
		UnsortS.insert(e);
	}
	size_t end3 = clock();
	cout << "set insert unsort:" << end3 - begin3 << endl;
	size_t begin4 = clock();
	for (auto e : UnsortV)
	{
    
    
		UnsortUs.insert(e);
	}
	size_t end4 = clock();
	cout << "unordered_set insert unsort:" << end4 - begin4 << endl;
	
    cout << "有序查找效率对比" << endl;
    size_t begin5 = clock();
	for (auto e : SortV)
	{
    
    
		SortS.find(e);
	}
	size_t end5 = clock();
	cout << "set find sorted:" << end5 - begin5 << endl;
	size_t begin6 = clock();
	for (auto e : SortV)
	{
    
    
		SortUs.find(e);
	}
	size_t end6 = clock();
	cout << "unordered_set find sorted:" << end6 - begin6 << endl;
    cout << "无序查找效率对比" << endl;
    size_t begin7 = clock();
	for (auto e : UnsortV)
	{
    
    
		UnsortS.find(e);
	}
	size_t end7 = clock();
	cout << "set find unsorted:" << end7 - begin7 << endl;
	size_t begin8 = clock();
	for (auto e : UnsortV)
	{
    
    
		UnsortUs.find(e);
	}
	size_t end8 = clock();
	cout << "unordered_set find unsorted:" << end8 - begin8 << endl;
    cout << "有序删除效率对比" << endl;
	size_t begin9 = clock();
	for (auto e : SortV)
	{
    
    
		SortS.erase(e);
	}
	size_t end9 = clock();
	cout << "set erase sorted:" << end9 - begin9 << endl;
	size_t begin10 = clock();
	for (auto e : SortV)
	{
    
    
		SortUs.erase(e);
	}
	size_t end10 = clock();
	cout << "unordered_set erase:" << end10 - begin10 << endl;
    cout << "无序删除效率对比" << endl;
    size_t begin11 = clock();
	for (auto e : UnsortV)
	{
    
    
		UnsortS.erase(e);
	}
	size_t end11 = clock();
	cout << "set erase sorted:" << end11 - begin11 << endl;
	size_t begin12 = clock();
	for (auto e : UnsortV)
	{
    
    
		UnsortUs.erase(e);
	}
	size_t end12 = clock();
	cout << "unordered_set erase:" << end12 - begin12 << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wXEUyCsZ-1684930297305)(https://pic-bed123.oss-cn-nanjing.aliyuncs.com/%E6%88%AA%E5%B1%8F2023-05-23%2015.07.18.png)]

可以看到随机数下unordered系列效率更高,但是有序数的情况下就不太行

2. 哈希概念

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

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较顺序查找时间复杂度为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为存储元素底层空间总的大小

按照这种存放方式,如果需要查找4,就直接通过4 % 10 = 4找到hash(key) = 4的位置,直接取出来即可,省略了多次key的比较,节约了时间。

请添加图片描述

3. 哈希函数

哈希结构最关键的点就是哈希函数的设置

哈希函数的设置原则

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

常见的哈希函数设置方法

  1. 直接定址法常用

    直接定址法是最常用的哈希函数,就是根据key直接取到存储位置,这里的位置可能是绝对位置也可能是相对位置。

    哈希函数:Hash(Key)= A*Key + B

    例如对于统计字符串中某种字符出现的次数,key的范围比较小,所以这里可以直接映射

    Hash(key) = 1 * ‘a’ - ‘a’,这里就把a映射给0,所以显而易见z映射给了26。

    但是如果数据比较分散的话,就不适合使用直接定址法了,比如对于集合{1, 2, 3, 4,99, 999},就会造成很大的空间浪费

  2. 除留余数法常用

    为了应对直接定址法中的数据较为分散造成空间浪费的情况,有人设计出了除留余数法,用于集中数据。设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的素数p作为除数,按照哈希函数,将关键码转换成哈希地址。

    **哈希函数:**Hash(key) = key % p (p<=m)

  3. 平方取中法了解

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

    适合:不知道关键字的分布,而位数又不是很大的情况

  4. 折叠法了解

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

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

  5. 随机数法了解

    选择一个随机函数,取关键字的随机函数值为它的哈希地址,

    **哈希函数:**Hash(key) = random(key),其中 random为随机数函数。

    通常应用于关键字长度不等时采用此法

  6. 数学分析法了解

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

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

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

4. 哈希冲突

上面那个例子,使用哈希的方式解决,看起来这样的算法非常棒,但是,如果数据集中还有一个44需要被插入,怎么办呢?44对应的hashkey也是4,和4产生了冲突。这就是哈希冲突,也叫哈希碰撞

对于两个数据元素的关键字 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),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突/哈希碰撞把具有不同关键码而具有相同哈希地址的数据元素称为同义词”。

哈希函数的设置决定了哈希冲突的产生可能性,哈希函数设置的越巧妙,越能够减小哈希冲突,但是哈希冲突是不可能被完全避免的

5. 哈希冲突的解决——开散列和闭散列

由于哈希冲突是不可避免的,所以当然要总结哈希冲突的解决方案,一般来说,解决方案分为两种——闭散列开散列

5.1 闭散列

闭散列也叫开放定址法:当发生哈希冲突的时候,如果哈希表还没有被装满,那么就有空余的位置存放,那么就可以把key存放到冲突位置的“下一个空位置”中

那么,怎么寻找下一个空位置呢?

这里同样有很多种方式,最经典的就是线性探测

同样的,针对上述的哈希冲突的例子,现在需要插入元素44,通过哈希函数计算出哈希地址为4,因此44理论上插入的位置是4,但是由于该位置已经存放了值为4的元素,出现哈希冲突,所以依次向后探测,直到寻找到下一个空位置为止

所以,针对线性探测的插入和删除算法如下:

  1. 插入

    • 通过哈希函数获取到待插入元素在哈希表中的为止

    • 如果该位置没有元素就直接插入新元素,如果该位置有元素就继续找下一个空位置,插入新元素

      image-20230523221935153

  2. 删除

    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

    所谓伪删除法就是用一个状态标志来代表此位置的状态

    enum State {
           
           EMPTY, EXIST, DELETE};
    // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
    

闭散列线性探测的哈希表代码实现插入删除查找

首先对于闭散列的哈希表,有以下的结构设计

  1. 由于伪删除法的存在,所以需要让表里面存放的数据中增加一个状态变量,这里使用一个枚举来给出状态情况

    enum State {
          
           EMPTY, EXIST, DELETE };
    
  2. 表中的元素类型是一个KV结构的pair和一个状态变量state,所以哈希数据结构体设计如下

    //HashData数据结构体
    template<class K, class V>
    struct HashData
    {
          
          
        std::pair<K, V> _kv;
        State _state = EMPTY;
    }
    
  3. 由于KV结构中的key是一个泛型,当我们在进行哈希映射的时候,需要先让其映射成为整型,然后才能够映射到哈希地址,所以这里提供一个仿函数,用于将key映射到整型家族

    //仿函数,映射到整型
    template<class K>
    struct HashFunc
    {
          
          
        size_t operator()(const K& key)
        {
          
          
            return (size_t)key;
        }
    };
    

由于string类型在哈希映射中使用的频率非常高,所以有人对string的哈希算法做了一些研究与总结,这里附上链接,有兴趣的小伙伴可以去看一看 [字符串哈希算法](各种字符串Hash函数 - clq - 博客园 (cnblogs.com)),下面是hash映射的string类型的特化

//模版特化,针对string类型
template<>
struct HashFunc<std::string>
{
    
    
    size_t operator()(const std::string& key)
    {
    
    
        //采用了特殊方法把各元素的值放在一起
        size_t hash = 0;
        for (auto ch : key)
        {
    
    
            hash *= 131;
            hash += ch;
        }
        return hash;
    }
};
  1. 闭散列哈希表的结构

    • 由于哈希表是KV模型,所以模板参数中肯定要有KV,除此之外,由于哈希映射的key要求是整型,所以一定需要提供一个仿函数来把key映射给整型

    • 哈希表本身使用一个vector来管理,再加上一个整型的n用来存放哈希表中的有效数据个数

    所以哈希表的结构就显而易见了

    template<class K, class V, class Hash = HashFunc<K>>
    class HashTable
    {
          
          
    public:
        HashTable()//由于哈希表需要根据容量来判断哈希地址,所以_tables必须要先初始化,所以这里显示写构造函数
            :_n(0)
    	{
          
          
    		_tables.resize(10);
    	}
    private:
        std::vector<Data> _tables;//表里面存储的是HashData,HashData内部是一个KV结构和一个状态
        size_t _n = 0;//存储表中的有效数据个数
    };
    
  2. 插入

    //插入
    bool Insert(const std::pair<K,V>& kv)
    {
          
          
        if(Find(kv.first))//如果已经存在,插入失败返回false
            return false;
        //扩容:判断是否扩容的方式是判断负载因子大小 负载因子 = 存放有效个数/哈希表容量(一般对于线性探测来说都是小于1的)
        if(_n * 10 / _tables.size() >= 7)//负载因子大于0.7时扩容
        {
          
          
            //这里采用二倍的方式扩容,实际上不是这样扩容的,在上文中说明按照
            HashTable<K, V, Hash> newTable;
            newTable._tables.resize(2 * _tables.size());//重新创建一个哈希表,大小是原表的二倍
            for(auto& e : _tables)//遍历原表,如果有数据的话就在新表中插入
            {
          
          
                if(e._state == EXIST)
                {
          
          
                    newTable.Insert(e._kv);
                }
            }
            _tables.swap(newTable._tables);//交换二者的表(vector对象),这里调用的是vecotr库里的swap
        }
    
        //插入数据
        size_t hashi = Hash()(kv.first) % _tables.size();//通过Hash的匿名对象映射出一个整形,通过这个整型除留余数从而定址
        while(_tables[hashi]._state == EXIST)//映射的位置已经有值,出现哈希冲突,进行线性探测
        {
          
          
            ++hashi;
            hashi %= _tables.size();//++之后可能大于size,所以需要 模等一下
        }
    
        _tables[hashi]._kv = kv;
        _tables[hashi]._state = EXIST;
        ++_n;
        return true;
    }
    
  3. 删除

    //删除
    bool Erase(const K& key)
    {
          
          
        //由于直接删除该位置的值会引发后面的值的映射错误(会导致在找的时候提前找到空,所以不能直接删除,要使用伪删除法删除,即给一个DELETE状态)
        Data* ret = Find(key);
        if(ret)//找到值
        {
          
          
            //将该位置的值状态置为DELETE,然后n--
            ret->_state = DELETE;
            --_n;
            return true;//返回true表示删除成功
        }
        else//哈希表中没有该值,返回false
        {
          
          
            return false;
        }
    }
    
  4. 查找

    //查找
    Data* Find(const K& key)
    {
          
          
        //按照哈希函数的方式计算,得到哈希地址
        size_t hashi = Hash()(key) % _tables.size();
        //从该地址向后寻找,由于线性探测的问题,所以该地址不一定是实际存放要找的位置的值,所以需要继续向后找,直到遇到EMPTY为止
        while(_tables[hashi]._state != EMPTY)
        {
          
          
            if(_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
                return &_tables[hashi];//找到了返回地址
            else//否则++hashi继续寻找
            {
          
          
                ++hashi;
                hashi %= _tables.size();
            }
        }
        return nullptr;//最终遇到EMPTY都没找到,返回空指针
    }
    

闭散列二次探测的哈希表代码实现插入删除查找

实际上,线性探测也是具有一定的缺陷的,线性探测的缺陷就是会将产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: 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是表的大小
那么,对于2.1中如果要插入44,产生冲突,使用解决后的情况为

image-20230524183116900

由于闭散列不管使用什么探测方式,都是治标不治本,所以这里就不再继续代码实现二次探测了,下面我们看一看开散列的实现方式

5.2 开散列

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

image-20230524192605818

image-20230524192618547

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

开散列哈希表代码实现插入删除查找

  1. 首先由于结构的原因,要把数据链接起来需要一个节点类:

    template<class K, class V>
    struct HashNode
    {
          
          
        std::pair<K, V> _kv;
        HashNode* _next;
        HashNode(const std::pair<K, V>& kv)
            :_kv(kv)
                ,_next(nullptr)
            {
          
          }
    };
    
  2. 同样的,由于KV结构中的key是一个泛型,当我们在进行哈希映射的时候,需要先让其映射成为整型,然后才能够映射到哈希地址,所以这里提供一个仿函数,用于将key映射到整型家族

    template<class K>
    struct HashFunc
    {
          
          
        size_t operator()(const K& key)
        {
          
          
            return (size_t)key;
        }
    };
    template<>//模板的特化
    struct HashFunc<std::string>
    {
          
          
        size_t operator()(const std::string& key)
        {
          
          
            size_t hash = 0;
            for (auto ch : key)
            {
          
          
                hash *= 131;
                hash += ch;
            }
            return hash;
        }
    };
    
    1. 开散列的哈希表结构

      template<class K, class V, class Hash = HashFunc<K>>
      class HashTable
      {
              
              
          typedef HashNode<K, V> Node;
      public:
          HashTable()
              :_n(0)
              {
              
              
                  _tables.resize(10);
              }
      private:
          std::vector<Node*> _tables;//本质上是一个指针数组,存放节点指针。
          size_t _n = 0;
      };
      
    2. 插入

      bool Insert(const std::pair<K,V>& kv)
      {
              
              
          if(Find(kv.first))
              return false;
          //扩容
          //负载因子为1的时候就扩容
          if(_n == _tables.size())
          {
              
              
              //扩容方式有两种,一种是遍历,然后创建新节点挂在新表上
              //由于方案一造成的消耗较大,所以这里就不实现了
              //                HashTable<K, V, Hash> newTable;
              //                newTable._tables.resize(2 * _tables.size());
              //另一种是直接更改节点的指向
              std::vector<Node*> newTables;
              newTables.resize(2* _tables.size(), nullptr);//这里暂时使用2倍的方式扩容
              for(size_t i = 0; i < _tables.size(); ++i)//遍历旧表,依次拿到每个桶的头节点
              {
              
              
                  Node* cur = _tables[i];
                  while(cur)
                  {
              
              
                      Node* next = cur->_next;//先使用一个指针保存next,以免更改cur指向之后找不到其他节点
                      size_t hashi = Hash()(cur->_kv.first) % newTables.size();//计算哈希位置
                      //头插到新表中
                      cur->_next = newTables[hashi];
                      newTables[hashi] = cur;
      
                      cur = next;//迭代到next
                  }
                  _tables[i] = nullptr;//将旧表的内容置空,以免出现自动析构旧表的时候释放节点
              }
              _tables.swap(newTables);//交换旧表和新表
          }
          //插入
          size_t hashi = Hash()(kv.first) % _tables.size();//定址
          //头插
          Node* newnode = new Node(kv);
          newnode->_next = _tables[hashi];
          _tables[hashi] = newnode;
          ++_n;
      
          return true;
      }
      
    3. 删除

      bool Erase(const K& key)
      {
              
              
          size_t hashi = Hash()(key) % _tables.size();
          Node* prev = nullptr;//用prev存放当前节点的上一个节点,从而链接cur的前后节点
          Node* cur = _tables[hashi];
          while(cur)
          {
              
              
              if(cur->_kv.first == key)//找到了,准备删除
              {
              
              
                  if(_tables[hashi] == cur)//删除桶的头节点
                  {
              
              
                      _tables[hashi] = cur->_next;
                  }
                  else//删除非头节点
                  {
              
              
                      prev->_next = cur->_next;
                  }
      
                  delete cur;
                  --_n;
                  return true;
              }
              else//没找到
              {
              
              
                  prev = cur;
                  cur = cur->_next;
              }
          }
      
          return false;
      }
      
    4. 查找

      Node* Find(const K& key)
      {
              
              
          size_t hashi = Hash()(key) % _tables.size();//找到key对应的哈希地址
          Node* cur = _tables[hashi];//遍历该地址对应的哈希桶
          while(cur)
          {
              
              
              if(cur->_kv.first == key)//找到了
              {
              
              
                  return cur;
              }
              else//没找到
              {
              
              
                  cur = cur->_next;
              }
          }
          return nullptr;
      }
      

    本节完……

猜你喜欢

转载自blog.csdn.net/weixin_63249832/article/details/130854553