当前位置: 首页 > news >正文

【数据结构进阶】哈希表

🌟🌟作者主页:ephemerals__
🌟🌟所属专栏:数据结构

目录

前言

一、哈希表的概念

二、哈希函数的实现方法

1. 直接定址法

2. 除留余数法

三、哈希冲突 

1. 开放定址法(闭散列)

线性探测

二次探测

双重探测

2. 链地址法(开散列)

四、装填因子

五、哈希表的实现

1. 线性探测法实现

整形转化的支持与字符串哈希

初始大小与扩容策略

哈希表结构定义

构造函数

析构函数

哈希表的插入

哈希表的查找

哈希表的删除

总代码

2. 链地址法实现

整形转化支持、大小设置

哈希表结构定义

构造函数

析构函数

哈希表的插入

哈希表的查找

哈希表的删除

总代码

总结


前言

        之前我们学习了红黑树,其虽然通过自平衡机制提供了高效的查找、插入和删除操作,但其操作仍依赖于树的高度,最坏情况下的时间复杂度为O(log n)。当我们希望能够实现常数时间复杂度的查找操作时,哈希表便成为了一个更优的选择。哈希表虽然不再像红黑树的元素一样默认有序,但其通过 “ 哈希 ” 将数据进行映射,从而在理想情况下实现O(1)时间复杂度的操作,在某些需要高效查找的场景非常实用。本篇文章,我们将深入学习哈希表的相关知识,并尝试实现。

一、哈希表的概念

        哈希表(Hash Table),也叫做散列表,是一种特殊的数据结构。其通过一定的规则将数据进行 “ 散乱排列 ” , 并通过该规则进行快速查找。这个规则叫做 “ 哈希函数 ” ,其可以将数据转化为一个固定的索引值,后续将该数据根据索引值存储在数组的某个位置。当进行查找时,直接通过 “ 哈希函数 ” 求出数据的索引值,理想状态下就能在常数时间内完成查找。

二、哈希函数的实现方法

        在理想状态下,哈希函数会将数据均匀散列分布在哈希表当中。为了尽量逼近这个理想状态,先辈们发明了多种哈希函数的实现方法,如直接定址法、除留余数法、乘法散列法、全域散列法等。其中直接定址法和除留余数法最为常用,我们重点介绍一下。

1. 直接定址法

         当数据的范围比较集中时,优先考虑使用直接定址法。例如有一组数据的范围在0~99之间,那么我们开一个大小为100的数组,然后将数据按照数组下标来存储。比如这组数据当中有3个“6”,那么数组下标为6处的值就是3。

        那么当这组数据的下限非0呢?处理方法也很简单。例如这组数据的范围是26个小写英文字母,ascii值在97~122之间。我们开一个大小为26的数组,然后每一个字母对应存储位置的下标即为其ascii值减去'a'的ascii值,这样就可以进行存储了。

        当然直接定址法的缺陷也很明显:当数据比较分散且范围很大时,会造成空间浪费

2. 除留余数法

        除留余数法(也叫做除法散列法),是哈希函数最常用的实现方式。它的核心思想是将数据对某个数(一般是表长)进行求余运算,将运算结果作为索引值,即表示该数据存放的下标位置。假设某个哈希表的大小为M,那么哈希函数为:hash(x) = x % M

如图所示,各个元素通过除留余数法存储在哈希表当中:

注意以下两点:

1. 为了尽量减少哈希冲突出现的次数,建议M取不太接近2的整数次幂的一个素数

2. 除留余数法中,为了方便数据转化为索引值,要求哈希表的数据元素支持转换为整数

三、哈希冲突 

        当一个数据通过哈希函数产生了与另一个数据相同的哈希值,那么也就意味着它们需要存储在同一个位置,显然无法满足。此时的情况就叫做哈希冲突(哈希碰撞)。理想情况下,数据会均匀分布在哈希表中,但是实际场景当中,哈希冲突是无法避免的。所以我们要有适当的方案处理哈希冲突

处理哈希冲突的方法主要有开放定址法、链地址法、再哈希法和位图法等,这里只介绍最常用的前两种

1. 开放定址法(闭散列)

        开放定址法是按照某种策略将冲突的数据放置在表中的另一个空位当中。常见的策略有三种:线性探测、二次探测和双重探测

线性探测

        线性探测指的是从冲突位置开始,依次向后一个一个查找,直到找到空位,再将冲突元素放置在当前空位。 当查找到表尾还没有找到空位,那么从表头位置开始重新查找

        线性探测的缺点是容易造成表中的冲突数据聚集,且会占用其他元素的原本位置,从而影响效率。

二次探测

        二次探测对线性探测进行了优化。它的查找方式是依次左右按照二次方跳跃式探测。例如先探测冲突位置的右边 1^{2} 个位置,然后左边 1^{2} 个位置,右边 2^{2} 个位置,左边 2^{2} 个位置......依次类推。

        二次探测有效地减少了数据聚集问题。

双重探测

        双重探测指的是使用另一个哈希函数计算探测步长,使探测序列更加分散。

2. 链地址法(开散列)

        链地址法,也叫做拉链法,它改变了传统的哈希表元素储存策略。在链地址法当中,所有数据不再直接存储在哈希表当中,而是将哈希表的每一个元素设置为指针,用于维护一个链表。此时哈希表的每一个元素就叫做哈希桶。当某个元素位置不存储元素时,该指针为空;若有元素需要存储在该位置,则在其维护的链表中添加该元素。因此,一条链表中存储的元素互相之间就是发生哈希冲突的。

链地址法在发生冲突时,就不会占用其他元素的原本位置相比开放定址法效率较高。但当某个位置的冲突元素过多时,链表过长,会使查找效率接近O(N),此时可以考虑将链表换为红黑树

Java8以及之后的HashMap实现当中,就采用了如上优化:当某个哈希桶维护的链表长度超过8时,将该链表换成红黑树,提升总体性能

四、装填因子

        装填因子(负载因子)是一个数值,表示已存储元素与哈希表容量的比值当装填因子过大时,哈希表的空间利用率就越高,发生哈希冲突的概率就越高,哈希表的总体性能就越低

        在实际实现当中,为了保证哈希表的高效,我们将装填因子作为哈希表是否需要扩容的标准当装填因子到达一定阈值时,哈希表就需要扩容。若哈希表使用开放定址法处理冲突,那么该阈值通常在0.7~0.8之间;若使用链地址法,则该阈值通常为1。

五、哈希表的实现

        接下来我们将选用除留余数法的哈希函数,并用线性探测链地址法两种解决冲突的策略分别实现一个哈希表。至于其他方法,这里就不再一一实现。

与之前实现AVL树和红黑树相同,我们的实现当中将键值对作为数据元素,其哈希值由键Key来决定

1. 线性探测法实现

整形转化的支持与字符串哈希

        之前提到哈希表的数据元素要支持转化为整形,所以在这里我们需要实现一个仿函数模板,用仿函数对象将内置类型进行转化,并且对于不能转化为整形的类型,可以用特化来支持

代码如下:

//支持内置类型转化为整形的仿函数
template<class K>
class ToInt
{
public:size_t operator()(const K& key)//这里转换为size_t类型更符合数组下标范围{return (size_t)(key);}
};

除了内置类型,字符串(string)作为键值存储在哈希表中的场景较多,我们实现一个特化版本支持字符串转换为整形。

        传统的字符串转整形可以将所有字符的ASCII值相加,但这种方法转换出的整形值可能重复率较高(例如"abc"和"acb"),更容易导致冲突。所以这里我们使用BKDR哈希的思路:每一个字符累加之后,总累加值乘以一个素数(通常是131或31),这样就能对每个字符进行加权。最后的累加值即为转化后的整形值

注意:当字符串过长时,多次相乘可能会导致溢出,但无需考虑溢出问题,因为我们只要确定能得到唯一的整形值即可。

代码如下:

//针对string的特化版本
template<>
class ToInt<string>
{
public:size_t operator()(const string& s){size_t ret = 0;for (auto& c : s){ret += c;ret *= 131;}return ret;}
};

初始大小与扩容策略

        使用除留余数法时,为了降低哈希冲突的概率,哈希表的大小(也就是除数M)的选择十分重要,应取不太接近2的整数次幂的一个素数。那么应该如何设置哈希表的初始大小及其扩容后的大小呢?这里我们直接采用SGI STL哈希表源码中的大小设置策略

inline size_t __stl_next_prime(size_t n)
{// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const size_t __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};const size_t* first = __stl_prime_list;const size_t* last = __stl_prime_list + __stl_num_primes;const size_t* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}

可以看到,函数创建了一个大小为28的数组,其中的数据即是哈希表每次扩容的大小(53、97、193...以此类推)。函数接收一个参数n,并返回数组中第一个不小于n的元素。而第一个元素53就可以作为我们哈希表的初始大小,接下来实现构造函数时调用该函数即可。

哈希表结构定义

        接下来是线性探测法哈希表及其数据元素的结构定义: 

//元素的状态表示
enum State
{EMPTY,//空EXIST,//存在DELETE//已删除
};//数据元素
template<class K,class V>
struct HashData
{pair<K, V> _kv;//键值对State _state = EMPTY;//状态
};//哈希表
template<class K, class V, class ToInteger = ToInt<K>>
class HashTable
{
public:
private:vector<HashData<K, V>> _tables;//使用vector表示哈希表size_t _n = 0;//记录已有元素个数ToInteger _toi;//仿函数对象
};

结构定义当中,我们使用了vector用于存储数据,其中元素是一个HashData,HashData包含一个键值对和状态表示(空、存在和已删除)。另外成员n和toi分别用于计算装填因子和整形转化。

构造函数

        构造函数当中,我们只需要设置哈希表的初始大小。代码如下:

//构造函数
HashTable()
{_tables.resize(__stl_next_prime(1));
}

传入参数1,则函数__stl_next_prime返回不小于1的第一个数组元素,也就是53,然后用它调整vector的大小。

析构函数

        析构函数不需要我们显式实现,编译器默认生成的析构函数会调用vector的析构,进行数据销毁。 

哈希表的插入

        哈希表插入的大致过程如下:

1. 首先判断表中是否已有相同键值元素,若有则插入失败(不允许键值重复)。

2. 判断装填因子的值是否过大(这里我们判断是否超过0.7),若过大,哈希表需要扩容。

3. 根据哈希函数进行插入,并处理冲突。 

需要注意的是:扩容之后,哈希表的大小(即除数M)会发生变化,之后进行插入或查找操作时,根据新的M进行插入/查找原有元素就会出现问题。所以当哈希表扩容之后,需要将其中的所有元素都拿出来重新根据哈希函数插入在哈希表当中,使其符合新哈希表的除数M。(数据重排)

代码实现:

//插入
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))//如果表中已有相同键,则插入失败返回false{return false;}if ((double)_n / (double)_tables.size() >= 0.7)//当装填因子大于0.7,则需要扩容{size_t newSize = __stl_next_prime(_tables.size() + 1);//先确定扩容后的大小//创建新的哈希表,并调整大小HashTable<K, V, ToInteger> newHT;newHT._tables.resize(newSize);//遍历原哈希表,将有效数据插入到新哈希表中for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST)//存在说明是有效数据{//直接调用Insert函数本身进行插入//此时已经扩容,装填因子一定不会超过阈值,不会再次走到这里newHT.Insert(_tables[i]._kv);}}//最后交换两个哈希表的tables_tables.swap(newHT._tables);}//开始插入//除留余数法计算哈希值size_t hash0 = _toi(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while(_tables[hashi]._state == EXIST)//该位置已有元素,说明发生了哈希冲突,循环向后寻找{hashi = (hash0 + i) % _tables.size();//相比直接++,这样设置能确保找到末尾还能从头开始寻找i++;}//找到了不出现冲突的位置,进行插入_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}

可以看到,在进行扩容时,我们巧妙地创建了一个新的哈希表,并调用Insert方法本身进行数据的重新插入,减少了代码冗余。

哈希表的查找

        查找时的逻辑与插入过程大体相同,同样使用哈希函数求出索引值,然后用线性探测法进行查找(注意按键查找)。代码如下:

HashData<K, V>* Find(const K& key)
{//除留余数法计算哈希值size_t hash0 = _toi(key) % _tables.size();size_t i = 1;size_t hashi = hash0;//没有走到空,则一直查找while (_tables[hashi]._state != EMPTY){//存在且键值相同,返回if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &(_tables[hashi]);}hashi = (hash0 + i) % _tables.size();i++;}//没找到,返回空return nullptr;
}

这里需要注意:由于线性探测法一定会使得冲突的数据在表中连续排列,所以我们查找时,如果在线性探测的过程中出现了空位,说明一定查找失败,就无需遍历整个哈希表了

哈希表的删除

        删除的大致过程也很简单(注意按键删除):

如果表中不存在该元素,则删除失败。

如果存在该元素,直接将元素状态修改为DELETE,然后调整已有元素个数即可。

代码如下:

//删除
bool Erase(const K& key)
{//首先进行查找HashData<K, V>* ret = Find(key);//没找到,删除失败if (ret == nullptr){return false;}else//找到了,删除{ret->_state = DELETE;//修改元素状态_n--;//修改已有元素个数return true;}
}

总代码

哈希表线性探测版本的全部代码如下:

#include <iostream>
#include <utility>
#include <vector>
#include <string>
using namespace std;//支持内置类型转化为整形的仿函数
template<class K>
class ToInt
{
public:size_t operator()(const K& key)//这里转换为size_t类型更符合数组下标范围{return (size_t)(key);}
};//针对string的特化版本
template<>
class ToInt<string>
{
public:size_t operator()(const string& s){size_t ret = 0;for (auto& c : s){ret += c;ret *= 131;}return ret;}
};//元素的状态表示
enum State
{EMPTY,//空EXIST,//存在DELETE//已删除
};//数据元素
template<class K, class V>
struct HashData
{pair<K, V> _kv;//键值对State _state = EMPTY;//状态
};//哈希表
template<class K, class V, class ToInteger = ToInt<K>>
class HashTable
{
public://构造函数HashTable(){_tables.resize(__stl_next_prime(1));}//插入bool Insert(const pair<K, V>& kv){if (Find(kv.first))//如果表中已有相同键,则插入失败返回false{return false;}if ((double)_n / (double)_tables.size() >= 0.7)//当装填因子大于0.7,则需要扩容{size_t newSize = __stl_next_prime(_tables.size() + 1);//先确定扩容后的大小//创建新的哈希表,并调整大小HashTable<K, V, ToInteger> newHT;newHT._tables.resize(newSize);//遍历原哈希表,将有效数据插入到新哈希表中for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST)//存在说明是有效数据{//直接调用Insert函数本身进行插入//此时已经扩容,装填因子一定不会超过阈值,不会再次走到这里newHT.Insert(_tables[i]._kv);}}//最后交换两个哈希表的tables_tables.swap(newHT._tables);}//开始插入//除留余数法计算哈希值size_t hash0 = _toi(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while(_tables[hashi]._state == EXIST)//该位置已有元素,说明发生了哈希冲突,循环向后寻找{hashi = (hash0 + i) % _tables.size();//相比直接++,这样设置能确保找到末尾还能从头开始寻找i++;}//找到了不出现冲突的位置,进行插入_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;}//查找HashData<K, V>* Find(const K& key){//除留余数法计算哈希值size_t hash0 = _toi(key) % _tables.size();size_t i = 1;size_t hashi = hash0;//没有走到空,则一直查找while (_tables[hashi]._state != EMPTY){//存在且键值相同,返回if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &(_tables[hashi]);}hashi = (hash0 + i) % _tables.size();i++;}//没找到,返回空return nullptr;}//删除bool Erase(const K& key){//首先进行查找HashData<K, V>* ret = Find(key);//没找到,删除失败if (ret == nullptr){return false;}else//找到了,删除{ret->_state = DELETE;//修改元素状态_n--;//修改已有元素个数return true;}}
private:inline size_t __stl_next_prime(size_t n){// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const size_t __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};const size_t* first = __stl_prime_list;const size_t* last = __stl_prime_list + __stl_num_primes;const size_t* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}vector<HashData<K, V>> _tables;//使用vector表示哈希表size_t _n = 0;//记录已有元素个数ToInteger _toi;//仿函数对象
};

2. 链地址法实现

整形转化支持、大小设置

        对于进行整形转化的仿函数和哈希表的初始大小设置,我们继续沿用之前线性探测法的实现。

//支持内置类型转化为整形的仿函数
template<class K>
class ToInt
{
public:size_t operator()(const K& key)//这里转换为size_t类型更符合数组下标范围{return (size_t)(key);}
};//针对string的特化版本
template<>
class ToInt<string>
{
public:size_t operator()(const string& s){size_t ret = 0;for (auto& c : s){ret += c;ret *= 131;}return ret;}
};
inline size_t __stl_next_prime(size_t n)
{// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const size_t __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};const size_t* first = __stl_prime_list;const size_t* last = __stl_prime_list + __stl_num_primes;const size_t* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}

哈希表结构定义

        接下来是链地址法哈希表的结构定义:

//链表节点
template<class K, class V>
struct HashNode
{pair<K, V> _kv;//数据域HashNode<K, V>* _next;//指针域//节点构造HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};//哈希表
template<class K, class V, class ToInteger = ToInt<K>>
class HashTable
{typedef HashNode<K, V> Node;
public:
private:vector<Node*> _tables;size_t _n = 0;//记录已有元素个数ToInteger _toi;//仿函数对象
};

这里我们创建了一个指针数组,每一个指针都用于维护一个链表。其次,成员n与toi的作用和线性探测实现中相同。由于链地址法中的数据都是通过链表存储,所以这里就无需对每个元素进行状态表示。

构造函数

        构造函数的实现与线性探测法相同,注意初始化时将表中的所有指针制为空。

代码如下:

//构造函数
HashTable()
{_tables.resize(__stl_next_prime(1), nullptr);
}

析构函数

        与线性探测法实现不同,由于每个哈希桶都维护一个单链表,所以析构时需要我们手动释放这些链表所占内存。 遍历哈希表,逐个销毁即可。代码如下:

//析构函数
~HashTable()
{//循环遍历所有哈希桶for (size_t i = 0; i < _tables.size(); i++){//使cur指向当前哈希桶Node* cur = _tables[i];//循环销毁当前链表中的所有节点while (cur){Node* next = cur->_next;delete cur;cur = next;}//将当前哈希桶制空_tables[i] = nullptr;}
}

哈希表的插入

        链地址法哈希表的插入大致过程:

1. 与线性探测一样,首先判断是否已有相同键值。

2. 判断装填因子是否大于1,如果大于1就需要扩容。(扩容仍需注意数据重排

3. 通过哈希函数找到对应的哈希桶,进行插入。 

需要注意两点:

1. 这里的插入过程涉及单链表的插入,为了提高效率,我们将使用头插法。 

2. 由于数据存储在链表节点当中,扩容之后,进行数据重排时释放节点再创建节点就过于麻烦,所以我们遍历所有链表,将节点连接到新的哈希表中对应位置即可。

代码实现:

//插入
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))//已有相同键,插入失败{return false;}if (_n >= _tables.size())//装填因子的值超过1,需要扩容{size_t newSize = __stl_next_prime(_tables.size() + 1);//先确定扩容后大小//创建新的vector,然后进行数据重排vector<Node*> newV(newSize, nullptr);for (size_t i = 0; i < _tables.size(); i++)//遍历所有哈希桶{Node* cur = _tables[i];//cur指向当前哈希桶while (cur){Node* next = cur->_next;//先记录下一个节点size_t hashi = _toi(cur->_kv.first) % newV.size();//计算当前节点在新表中的索引值//将当前节点连接到新桶中cur->_next = newV[hashi];newV[hashi] = cur;//继续操作下一个节点cur = next;}//一个哈希桶遍历结束后,及时制空_tables[i] = nullptr;}//重排结束,交换两个vector_tables.swap(newV);}//进行插入size_t hashi = _toi(kv.first) % _tables.size();//计算索引值Node* newnode = new Node(kv);//创建新节点//头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;_n++;return true;
}

可以看到,与刚才的线性探测实现不同,这里我们在扩容时创建的是一个vector,相比直接创建HashTable再调用Insert方法,能够有效地控制数据重排时节点之间的连接。

对于不同的结构实现,同一种操作所使用的方法也会有所不同,因此在实际场景中,我们应抓住问题本质,灵活运用各种方法,而非死抓一种应对措施。

哈希表的查找

        相比线性探测法,链地址法的查找中,我们只需要确定索引值,然后顺着链表直接查找即可,总体过程相对简洁。代码实现如下:

//查找
Node* Find(const K& key)
{size_t hashi = _toi(key) % _tables.size();//确定索引值Node* cur = _tables[hashi];//开始查找while (cur){if (cur->_kv.first == key)//找到了,返回当前节点{return cur;}cur = cur->_next;}return nullptr;//没找到,返回空
}

哈希表的删除

        删除的逻辑也比较简单,确定哈希值之后,进行单链表的指定位置删除。代码如下:

//删除
bool Erase(const K& key)
{size_t hashi = _toi(key) % _tables.size();Node* prev = nullptr;//记录前驱指针Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key)//找到了,删除{if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;//没找到,删除失败
}

总代码

链地址法实现哈希表的全部代码如下:

#include <iostream>
#include <utility>
#include <vector>
#include <string>
using namespace std;//支持内置类型转化为整形的仿函数
template<class K>
class ToInt
{
public:size_t operator()(const K& key)//这里转换为size_t类型更符合数组下标范围{return (size_t)(key);}
};//针对string的特化版本
template<>
class ToInt<string>
{
public:size_t operator()(const string& s){size_t ret = 0;for (auto& c : s){ret += c;ret *= 131;}return ret;}
};//链表节点
template<class K, class V>
struct HashNode
{pair<K, V> _kv;//数据域HashNode<K, V>* _next;//指针域//节点构造HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};//哈希表
template<class K, class V, class ToInteger = ToInt<K>>
class HashTable
{typedef HashNode<K, V> Node;
public://构造函数HashTable(){_tables.resize(__stl_next_prime(1), nullptr);}//析构函数~HashTable(){//循环遍历所有哈希桶for (size_t i = 0; i < _tables.size(); i++){//使cur指向当前哈希桶Node* cur = _tables[i];//循环销毁当前链表中的所有节点while (cur){Node* next = cur->_next;delete cur;cur = next;}//将当前哈希桶制空_tables[i] = nullptr;}}//插入bool Insert(const pair<K, V>& kv){if (Find(kv.first))//已有相同键,插入失败{return false;}if (_n >= _tables.size())//装填因子的值超过1,需要扩容{size_t newSize = __stl_next_prime(_tables.size() + 1);//先确定扩容后大小//创建新的vector,然后进行数据重排vector<Node*> newV(newSize, nullptr);for (size_t i = 0; i < _tables.size(); i++)//遍历所有哈希桶{Node* cur = _tables[i];//cur指向当前哈希桶while (cur){Node* next = cur->_next;//先记录下一个节点size_t hashi = _toi(cur->_kv.first) % newV.size();//计算当前节点在新表中的索引值//将当前节点连接到新桶中cur->_next = newV[hashi];newV[hashi] = cur;//继续操作下一个节点cur = next;}//一个哈希桶遍历结束后,及时制空_tables[i] = nullptr;}//重排结束,交换两个vector_tables.swap(newV);}//进行插入size_t hashi = _toi(kv.first) % _tables.size();//计算索引值Node* newnode = new Node(kv);//创建新节点//头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;_n++;return true;}//查找Node* Find(const K& key){size_t hashi = _toi(key) % _tables.size();//确定索引值Node* cur = _tables[hashi];//开始查找while (cur){if (cur->_kv.first == key)//找到了,返回当前节点{return cur;}cur = cur->_next;}return nullptr;//没找到,返回空}//删除bool Erase(const K& key){size_t hashi = _toi(key) % _tables.size();Node* prev = nullptr;//记录前驱指针Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key)//找到了,删除{if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;//没找到,删除失败}
private:inline size_t __stl_next_prime(size_t n){// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const size_t __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};const size_t* first = __stl_prime_list;const size_t* last = __stl_prime_list + __stl_num_primes;const size_t* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}vector<Node*> _tables;size_t _n = 0;//记录已有元素个数ToInteger _toi;//仿函数对象
};

总结

        哈希表是一种基于哈希函数的高效数据结构,其通过数据映射,支持 O(1) 级别的查找、插入和删除操作。哈希表有多种实现方法,我们实现了开放地址法(线性探测)链地址法两种。开放地址法适用于低负载、节省空间,但处理冲突时可能导致探测成本增加;链地址法适用于高负载,扩展性更强,但需额外存储指针。合理选择哈希函数和冲突解决策略是优化哈希表性能的关键。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章:

【数据结构进阶】哈希表

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;数据结构 目录 前言 一、哈希表的概念 二、哈希函数的实现方法 1. 直接定址法 2. 除留余数法 三、哈希冲突 1. 开放定址法&#xff08;闭散列&#xff0…...

STM32内存五区及堆栈空间大小设置(启动文件浅析)

前言 嘿&#xff0c;朋友们&#xff01;今天咱们来聊聊STM32的内存五区和堆栈空间大小设置。这可是嵌入式开发里的“必修课”&#xff0c;要是没整明白&#xff0c;程序说不定就“翻车”了。别担心&#xff0c;我这就带你一步步搞懂这事儿&#xff0c;让你轻松上手&#xff0c…...

微信小程序调用火山方舟(字节跳动火山引擎)中的DeepSeek大模型

微信小程序的轻量化特性与DeepSeek大模型的AI能力结合&#xff0c;可快速构建智能问答、内容生成等场景化服务。通过火山方舟平台提供的标准化接口&#xff0c;开发者无需深入算法细节即可调用模型能力。 一、注册火山引擎账号&#xff0c;创建API Key和model&#xff08;接入…...

(八)Java-Collection

一、Collection接口 1.特点 Collection实现子类可以存放多个元素&#xff0c;每个元素可以是Object&#xff1b; 有些Collection的实现类&#xff0c;可以存放重复的元素&#xff0c;有些不可以&#xff1b; 有些Collection的实现类&#xff0c;有些是有序的&#xff08;Li…...

从单片机的启动说起一个单片机到点灯发生了什么下——使用GPIO点一个灯

目录 前言 HAL库对GPIO的抽象 核心分析&#xff1a;HAL_GPIO_Init 前言 我们终于到达了熟悉的地方&#xff0c;对GPIO的初始化。经过漫长的铺垫&#xff0c;我们终于历经千辛万苦&#xff0c;来到了这里。关于GPIO的八种模式等更加详细的细节&#xff0c;由于只是点个灯&am…...

C++ | 哈希表

前言 &#x1f493; 个人主页&#xff1a;普通young man-CSDN博客 ⏩ 文章专栏&#xff1a;C_普通young man的博客-CSDN博客 ⏩ 本人giee: 普通小青年 (pu-tong-young-man) - Gitee.com 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 —…...

leetcode_动态规划/递归 70. 爬楼梯

70. 爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 思路&#xff1a; 考虑: 假设现在已经爬到了某一阶台阶&#xff0c;那是如何到达这里的呢&#xff1f;可能是从前一阶台阶爬上来的&am…...

基于Rook的Ceph云原生存储部署与实践指南(上)

#作者&#xff1a;任少近 文章目录 1 Ceph环境准备2 rook部署ceph群集2.1 Rook 帮助地址2.2 安装ceph2.3 获取csi镜像2.4 Master参加到osd2.5 设置默认存储 3 Rook部署云原生RBD块存储3.1 部署storageclass资源3.2 部署WordPress使用RBD3.3 WordPress访问 4 Rook部署云原生RGW…...

C++ Qt常见面试题(4):Qt事件过滤器

在 Qt 中,事件过滤器(Event Filter)提供了一种机制,可以拦截并处理对象的事件(如鼠标事件、键盘事件等),在事件到达目标对象之前对其进行预处理。事件过滤器通常用于以下场景: 捕获和处理特定的事件(如鼠标点击、按键等);对事件进行筛选或修改;实现全局的事件监听功…...

regionserver实例僵住问题分析

问题现象: 应用提交超时,发现regionserver实例异常。hbase原生页面这个实例dead,业务连接到这个rs的进程超时8个regionserver实例。 D08在18:30分后显示warning,应用提交任务到这个rs节点超时,hbase控制台不显示d08的rs信息了。19:30在页面停止rs实例失败,然后kill进程…...

服务器离线部署DeepSeek

目标 本次部署的目标是在本地服务器上部署DeepSeek。但是该服务不能连接外网&#xff0c;因此只能使用离线部署的方式。为了一次完成部署。现在云服务器上进行尝试。 云服务器部署尝试 云服务器配置 CentOS72080Ti 11GB 安装准备 1、上传iso并配置为本地yum源 安装前先将…...

QT mac系统下qml实现的菜单栏,标准快捷键Delete无作用或失灵的处理

一.在mac系统下,QT官方提供的删除快捷键无作用。 1.下面这一段代码,最后一个menuItem采用的是QT自带的标准快捷键,但是在使用的过程中,快捷键无响应。大家可以将代码带入简单的demo尝试一下: MenuBar {id: rootMenu {id: editMenutitle: TransText.titleBar_EditMenuItem…...

redis序列化设置

redis序列化设置 redis序列化设置序列化对象里有org.joda.time.DateTime1&#xff09;、报错内容如下2&#xff09;、解决方案&#xff1a;分别自定义时间的序列化和反序列化&#xff0c;以对象形式关联到redisTemplate redis序列化设置 redis序列化设置&#xff0c;通过自定义…...

浅谈C++/C命名冲突

前言 在这里我会简要地介绍产生命名冲突的原因&#xff0c;和C中处理命名冲突的方法&#xff0c;同时和C语言的解决办法进行比较。 相信你在阅读完之后一定会有收获。对于我们来说&#xff0c;了解编译器的编译链接过程才能更好的理解编译器是如何报错的&#xff0c;更能让我们…...

【语音编解码】常用的基于神经网络的语音编解码方案对比

引言 随着实时通信与多媒体应用的爆炸式增长&#xff0c;传统语音编解码技术正面临带宽效率与音质保真的双重挑战。近年来&#xff0c;基于深度学习的神经编解码器突破性地将端到端架构、动态码率控制与可解释信号处理相结合&#xff0c;在3kbps以下超低码率场景仍能保持自然语…...

PVE 配置显卡直通

博客地址&#xff1a;PVE 配置显卡直通 配置 Device: Dell PowerEdge T630CPU: Intel Xeon E5-2696 v4 x2GPU 1: Matrox Electronics Systems Ltd. G200eR2GPU 2: NVIDIA GeForce GTX 1060 3GBOS: Proxmox VE bookworm 8.3.1 x86_64 注意事项 硬件需支持并在 BIOS 中开启 I…...

Kronecker分解(K-FAC):让自然梯度在深度学习中飞起来

Kronecker分解&#xff08;K-FAC&#xff09;&#xff1a;让自然梯度在深度学习中飞起来 在深度学习的优化中&#xff0c;自然梯度下降&#xff08;Natural Gradient Descent&#xff09;是一个强大的工具&#xff0c;它利用Fisher信息矩阵&#xff08;FIM&#xff09;调整梯度…...

ArcGIS Pro技巧实战:高效矢量化天地图地表覆盖图

在地理信息系统&#xff08;GIS&#xff09;领域&#xff0c;地表覆盖图的矢量化是一项至关重要的任务。天地图作为中国国家级的地理信息服务平台&#xff0c;提供了丰富且详尽的地表覆盖数据。然而&#xff0c;这些数据通常以栅格格式存在&#xff0c;不利于进行空间分析和数据…...

React + TypeScript 数据模型驱动数据字典生成示例

React TypeScript 数据模型驱动数据字典生成示例 引言&#xff1a;数据字典的工程价值 在现代化全栈开发中&#xff0c;数据字典作为业务实体与数据存储的映射桥梁&#xff0c;直接影响系统可维护性与团队协作效率。传统手动维护字典的方式存在同步成本高和版本管理混乱两大痛…...

道可云人工智能每日资讯|深圳将设立人工智能和机器人产业基金

道可云元宇宙每日简报&#xff08;2025年2月26日&#xff09;讯&#xff0c;今日元宇宙新鲜事有&#xff1a; 上海青浦发布国际产业协作元宇宙平台 近日&#xff0c;“2025出海企业与跨境专业服务论坛”在上海青浦区徐泾镇举行。论坛上重磅发布三大全球化服务平台&#xff0c…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互

物理引擎&#xff08;Physics Engine&#xff09; 物理引擎 是一种通过计算机模拟物理规律&#xff08;如力学、碰撞、重力、流体动力学等&#xff09;的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互&#xff0c;广泛应用于 游戏开发、动画制作、虚…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解

【关注我&#xff0c;后续持续新增专题博文&#xff0c;谢谢&#xff01;&#xff01;&#xff01;】 上一篇我们讲了&#xff1a; 这一篇我们开始讲&#xff1a; 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下&#xff1a; 一、场景操作步骤 操作步…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

苍穹外卖--缓存菜品

1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询操作。 缓存逻辑分析&#xff1a; ①每个分类下的菜品保持一份缓存数据…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习

禁止商业或二改转载&#xff0c;仅供自学使用&#xff0c;侵权必究&#xff0c;如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...