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

news2024/9/21 12:39:03

文章目录

  • 1. unordered系列的关联式容器
    • 1.1 引言
    • 1.2 unordered_map的使用说明
    • 1.3 unordered_set的使用说明
    • 1.4 unordered_set和unordered_map的应用
    • 1.5 性能比较
  • 2. 哈希概念
  • 3. 哈希函数
  • 4. 哈希冲突
  • 5. 哈希冲突的解决——开散列和闭散列
    • 5.1 闭散列
    • 5.2 开散列

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)]

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;
      }
      

    本节完……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/564738.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++:征服C指针:关于指针

关于指针 1.看一个简单的程序&#xff0c;来接触下指针二级目录三级目录 本章节&#xff0c;我们重点分析C指针&#xff0c;首先我们从指针的概念谈起 &#x1f49a;&#x1f49a;&#x1f49a; 指针是一种保存变量地址的变量&#xff0c;并在C中频繁使用在C语言标准中&#xf…

数据结构与算法01:时间复杂度

目录 【复杂度分析】 【降低时间复杂度】 降低时间复杂度的必要性 【每日一练】 不管是使用什么编程语言或者哪种数据库&#xff0c;不管是解决项目中的什么问题&#xff0c;都离不开数据结构与算法。所谓数据结构就是指某一种数据的存储结构&#xff0c;所谓算法就是操作这…

【ClickHouse】

文章目录 一、表引擎1、表引擎的作用2、TinyLog3、Memory4、MergeTree二、数据库引擎1、作用--跨种类交换数据2、示例 三、MergeTree引擎1、简单使用2、分区partition by3、主键primary key4、order by&#xff08;必填&#xff09; 一、表引擎 1、表引擎的作用 CK表引擎决定…

U盘无法格式化?试试这几种快速有效的方法!

我们在通过Windows文件资源管理器、磁盘管理或Diskpart命令格式化U盘过程中&#xff0c;会遇到Windows无法格式化U盘。导致出现该问题的原因可能有以下几种&#xff1a; U盘被病毒和恶意软件感染&#xff1b; U盘有坏道&#xff1b; U盘已经物理损坏&#xff1b; 1.消除可能…

数据类型的陷进,从表象看本质!

哪些值转为布尔值为false 1、undefined&#xff08;未定义&#xff0c;找不到值时出现&#xff09; 2、null&#xff08;代表空值&#xff09; 3、false&#xff08;布尔值的false&#xff0c;字符串"false"布尔值为true&#xff09; 4、0&#xff08;数字0&…

LeetCode每日一题之209长度最小的子数组

文章目录 问题描述方法一&#xff1a;暴力求解方法二&#xff1a;滑动窗口 问题描述 方法一&#xff1a;暴力求解 暴力求解法&#xff1a;时间复杂度O(n^2)&#xff0c;空间复杂度O(1)。 暴力求解法的思想&#xff1a;每一次遍历数组&#xff0c;然后更新result的值&#xff0…

【Linux基本指令(3)】几十条指令快速入手Linux/到底什么是日志?/指令的运行原理到底是什么?

本文思维导图&#xff1a; 文章目录 13.head指令14.tail指令15.时间相关指令到底什么是日志&#xff1f;时间戳 16.cal指令17.find指令&#xff08;非常重要&#xff09;18.grep指令19.zip/unzip指令20.tar指令&#xff08;非常重要&#xff09;21.bc指令22.uname指令23.几个重…

香橙派4和树莓派4B构建K8S集群实践之六:虚拟主机

1. 说明 根据之前设置好的基础&#xff0c;我打算设置两种不同的虚拟主机运行在这个K8s集群上面&#xff0c;一个是LNMP体系的WebApp&#xff0c;一个是Java SpringBoot体系的WebApp。 1.1 设置表格 - wwwroot 是之前做pvc定义的文件目录&#xff0c;形如&#xff1a;/data0…

外包没有前途的,已经被替换了....

我25岁的时候&#xff0c;外包测试&#xff0c;薪资13.5k&#xff0c;人在深圳。 内卷什么的就不说了&#xff0c;而且人在外包那些高级精英年薪大几十的咱也接触不到&#xff0c;就说说外包吧。假设以我为界限&#xff0c;25岁一线城市13.5k&#xff0c;那22-24大部分情况下是…

花指令问题

前言 想起之前打题的时候经常会遇到一些关乎花指令的问题&#xff0c;但是没有系统地总结归纳花指令去除的姿势&#xff0c;浅浅开一个坑慢慢来写 题1&#xff1a;简单jmp 可以骗过dbg&#xff0c;但是放在ida中就很容易看出来&#xff0c;无效跳转 题目来源&#xff1a;[HD…

七牛云图床设置

文章目录 七牛云图床设置下面是用picgo配置图床SSL证书申请https网站显示http图片解决方案 原文链接图床设置&#xff0c;免费SSL证书申请&#xff0c;https网站显示http链接的图片 七牛云图床设置 登录七牛云官网并进行个人注册&#xff0c;然后找到对象存储 点击空间管理&a…

一种适用于大量租户大量角色权限系统设计

前言 权限管理是每个系统不可缺少的一部分&#xff0c;大部分开发者应该都设计过权限管理系统&#xff0c;很多开发者学习的第一个项目可能就是权限管理系统。但是常见的权限设计在租户量非常大、角色数量非常多时会存在角色权限表数据量指数增长的情况&#xff0c;本文介绍一…

c++代码手撕红黑树

企业里永远是技术驱动理论发展 比起理解红黑树的原理&#xff0c;更重要的是理解红黑树的应用场景&#xff0c;因为某些应用场景的需要&#xff0c;红黑树才会应运而生。 红黑树的特点&#xff1a; 插入&#xff0c;删除&#xff0c;查找都是O(logn)的复杂度。 红黑树的应用…

【lager】日志系统2:测试程序调试

design 官方设计文档 design cmakelist增加plog构建 cmake_minimum_required(VERSION 3.1.3 FATAL_ERROR) project(Lager)set(CMAKE_CXX_STANDARD 11) set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)find_package(

课时八——进程同步(二)

1、信号量 信号量机制是一种功能较强的机制&#xff0c;可用来解决互斥和同步问题&#xff0c;它只能被两个标准的原语wait(S)&#xff08;P操作&#xff09;和signal(S)&#xff08;V操作 &#xff09;访问。 注意&#xff1a;原语是一种特殊的程序段&#xff0c;其执行只能一…

EV 电动汽车远程监控系统

EV 电动汽车远程监控系统 远程监控系统简介 目录 EV 电动汽车远程监控系统 1、远程监控系统是什么&#xff1f; 2、远程监控系统包含什么&#xff1f; 2.1车载终端 2.2、数据服务器 ​​​​​​​ 2.3、监控页面 3、远程监控系统有什么用&#xff1f; 4、车载终端…

中性市场观下,贝壳驶入长期价值之海

&#xff08;图片来源于网络&#xff0c;侵删&#xff09; 文 | 螳螂观察 作者 | 易不二 2023年一季度&#xff0c;房地产市场实现了久违的回暖。 国家统计局公布的数据显示&#xff0c;2023年1-4月&#xff0c;全国商品房销售额39750亿元&#xff0c;增长8.8%&#xff0c;…

Atlassian数据迁移攻略:迁移前必备须知

到2024年2月&#xff0c;Atlassian将终止对Server产品及插件的所有支持。是时候制定您的迁移计划了——Atlassian为您提供两种迁移选择&#xff0c;一是本地部署的数据中心版本&#xff0c;中国用户25人以上即可使用&#xff0c;二是云版。作为Atlassian全球白金合作伙伴&#…

es Elasticsearch 六 java api spirngboot 集成es

目录 Java restApi Springboot 集成es 新增-同步 新增-异步 增删改查流程 _bulk 批量操作 Java restApi Springboot 集成es 新增-同步 Testpublic void te2() throws IOException {System.out.println(1);IndexRequest ir new IndexRequest("test");ir.id(&qu…

STM8 模拟iic接口调试温湿度传感器SHT3x驱动

背景 项目实际使用SHT3x进行温湿度测量&#xff0c;主控芯片采用STM8S003F3P6&#xff0c;并且使用模拟IIC接口的硬件连接。 原理图 如下图所示&#xff0c;使用STM8S003F3P6管脚PB4/PB5进行SHT3x数据接口 SHT3x-DIS是Sensirion新一代的温湿度传感器&#xff0c;精度为2%RH和…