C++的哈希 哈希表 哈希桶

目录

Unordered系列关联式容器

什么是哈希

哈希表

闭散列

载荷因子α

扩容

查找

删除 

字符串哈希算法

最终代码

开散列

插入

查找

删除

最终代码

完整代码


Unordered系列关联式容器

C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可以达到log_2 N,即最差情况下要比较红黑树的高度次树中的结点特别多且有序

        但又因为键值对与其存储位置之间没有对应的关系,查找一个元素时必须要多次比较键值对的键,而我们理想中的搜索方法应该是“可以不经过任何比较,一次直接从表中得到要搜索的元素”,而实现这一搜索方法应该需要构造一种存储结构,通过某种函数使元素的存储位置与它的键值对的键之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素,所以C++11中又提供了4个unordered系列的关联式容器unordered_map、unordered_set、unordered_multimap、unordered_multiset,它们与之前的map、set等容器的使用方式基本类似,只是底层结构不同,unordered序列的关联式容器的底层结构是哈希表,这使得它们在查询时的时间复杂度可以达到O(1)

什么是哈希

基本概念:哈希是一种将任意大小的数据映射到固定大小的值(通常为整数)的思想(建立值和值存储间的映射关系)

注意事项:哈希是一种思想,哈希表是基于哈希表的实现的一种数据结构 

哈希表

基本概念:本质是一个数组。这个数组的每个位置(称为桶或槽)用于存储键值对,哈希表需要使用哈希函数依据键值对中key将该键值对映射到数组的某个位置,因为如果直接映射会出现“值不易映射”和“值分散”的问题

  • 值不易映射:键的类型是string、结构体类型的对象
  • 值分散:多个值之间的差值过大导致的空间浪费(存放并建立10001、11、55、24、19与存放位置间的映射关系,如果不做特殊处理就需要开辟很大的数组来存放这些数

“除留余数法”可以解决值分散的问题(使得值的大小和空间无关),但会出现哈希冲突问题(此外引起哈希冲突的一个原因可能是:哈希函数设计不够合理)

哈希函数: 将key转换为数组(哈希表)的索引(可能是一行代码,也可能是一个函数)

哈希函数的设计理念:

  • 均匀性一个好的哈希函数应该能够尽可能地将不同的键映射到不同的哈希值,以确保数据在哈希表中分布均匀。这样可以减少哈希冲突的可能性,提高哈希表的效率

  • 确定性哈希函数应该是确定性的,即对于相同的输入键,哈希函数应该始终返回相同的哈希值。这是保证哈希表中数据一致性的重要特性

  • 简单性:尽量设计简单的哈希函数,以降低计算成本和实现复杂度,计算哈希值所需的时间应该是常量级的,这样可以确保哈希表的操作效率高,不会成为性能瓶颈

  • 有效性:如果散列表有 m 个地址(桶),则哈希函数的值域(输出范围)必须在 0 到 m-1 之间,以确保哈希值可以映射到散列表的有效地址范围内

常见的哈希函数:

  • 除留余数法:hash = key % table_size
  • 直接定址法:
  • Multiplicative Hash Function:hash = floor(table_size * (key * A % 1))
  • 平方取中法:key = 1234key^2 = 1522756,取中间的 227 作为哈希值
  • FNV Hash (Fowler-Noll-Vo):
uint32_t fnv1a_hash(const std::string& key) {uint32_t hash = 2166136261u; // FNV offset basisfor (char c : key) {hash ^= c;hash *= 16777619;}return hash;
}
  • DJB2 Hash:
uint32_t djb2_hash(const std::string& key) {uint32_t hash = 5381;for (char c : key) {hash = ((hash << 5) + hash) + c; // hash * 33 + c}return hash;
}

注意事项:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

哈希冲突:当不同的键被哈希函数映射到相同的索引时,称为哈希冲突,哈希表需要处理这些冲突,常见的方法有链地址法(开散列)开放地址法(闭散列)

闭散列

基本概念:发生冲突时,通过探测(如线性探测、二次探测或双重哈希等方法)来寻找下一个键可用的位置

优点:不需要额外的存储空间,访问速度快

缺点:探测过程中可能出现“聚集”现象使得存放效率降低,且它的删除操作复杂

删除操作复杂的原因:

1、寻找一个数字时,先去该数字的映射位置寻找,找不到就继续向后寻找,如果遇到空时还没找到就停止寻找(因为如果映射位置没有,那么该数字一定是在映射位置后的某个位置插入了,插入时应该是数字、新数字 ...而不是空 新数字 ...)

2、如果按照上述的方式寻找一个数字,那么当删除目标数字前的某个数字时就会导致虽然目标数字存在但是找不到目标数字的问题

删除操作:

1、映射位置除了要存放数值外,还要存放一个标志位,用于标识当前映射位置的状态

enum State
{EMPTY,//为空EXIST,//存在DELETE//删除
};
  • 当55被删除后,它的状态被设为删除而不是空,这样就可以接着继续向后寻找

进行插入操作前先进行依据上面的内容写出插入操作(负载因子后面有描述):

bool Insert(const pair<K, V>& kv)
{//防止冗余if (Find(kv.first))return false;//当负载因子大于等于0.7时进行扩容,以空间换时间if (_n * 10 / _table.size() >= 0.7)//采用/取整得不到小数,所以直接令_n * 10 {//新建一个哈希表HashTable<K,V, Hash> newHT;//确定新哈希表的大小newHT._table.resize(_table.size() * 2);//将旧表中的键重新映射到新表中for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state ==  EXIST)//原表中某下标的状态为存在时,将原表某下标中存放的键值对插入新表中{newHT.Insert(_table[i]._kv);//直接复用Insert操作}}_table.swap(newHT._table);}size_t hashi = kv.first % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数//线性探测while (_table[hashi]._state == EXIST)//探查到目标映射位置存在数字时就进行探测{++hashi;hashi %= _table.size();//取模的方式防止hashi越界}//确定好位置后进行插入_table[hashi]._kv = kv;//在合适的映射位置插入_table[hashi]._state = EXIST;//然后后将该位置的状态标识为EXIST++_n;//哈希表中有效数据元素+1return true;
}

注意事项:哈希表的大小(table_size)和底层数组的容量(capacity)是两个不同的概念, 假设我们有一个哈希表,初始大小为 7(即 table_size = 7),底层数组的容量为 10(即 capacity = 10)。我们有一组键需要插入到哈希表中:{10, 20, 30, 40, 50}:

哈希函数为 hash = key % table_size

  • 10 % 7 = 3,键 10 存储在桶 3
  • 20 % 7 = 6,键 20 存储在桶 6
  • 30 % 7 = 2,键 30 存储在桶 2
  • 40 % 7 = 5,键 40 存储在桶 5
  • 50 % 7 = 1,键 50 存储在桶 1

哈希函数为 hash = key % capacity

  • 10 % 10 = 0,键 10 存储在桶 0
  • 20 % 10 = 0,键 20 存储在桶 0(冲突)
  • 30 % 10 = 0,键 30 存储在桶 0(冲突)
  • 40 % 10 = 0,键 40 存储在桶 0(冲突)
  • 50 % 10 = 0,键 50 存储在桶 0(冲突)

        可以看出,使用 capacity 作为模数会导致所有键都存储在桶 0,导致严重的冲突和性能下降。而使用 table_size 作为模数可以确保键均匀地分布在不同的桶中,从而提高查找效率,此外,如果table_size 满了也会出现陷入死循环问题,为此引入了闭散列的另一个重要概念:载荷因子α(一定要分清底层容器大小capacity、哈希表的大小hash_table.size、哈希表中存放的有效数据个数n,一般情况下哈希表的大小和底层容器大小的设定是分开的,但也可以让它们两个相等)
 

载荷因子α

基本概念:载荷因子α表示哈希表中已存储元素数量与哈希表容量的比值,即哈希表的填充程度,载荷因子是开放定址法中十分重要的因素

计算公式:α = 填入表中的元素个数 / 哈希表的长度

α 越高,冲突率越高,插入等操作的效率越低,空间利用率越高

α 越低,冲突率越低,插入等操作的效率越高,空间利用率越低

α 的取值应该严格控制在0.7~0.8之间

扩容

中心思想:扩容时需要将原表中参与映射的键在新表中重新映射一次

vector的swap方法:vector::swap - C++ 参考 (cplusplus.com)

问题:_table.swap(newHT._table)时都做了什么?

  1. 交换容量:当前可以存储的元素数量被交换
  2. 交换大小:当前已存储的元素数量被交换
  3. 结论:交换了双方的数据结构,_table 现在持有新表的数据结构,而 newHT 持有旧表的数据结构,函数结束后 newHT 会被自动销毁,从而实现了高效的扩容操作

  交换不会改变新旧表的地址:

cout <<"交换前旧表的地址为:" <<  & _table << endl;
cout <<"交换前新表的地址为:" <<  &newTable << endl;_table.swap(newTable);//交换cout << "交换后旧表的地址为:" << &_table << endl;
cout << "交换后新表的地址为:" << &newTable << endl;

查找

HashDate<K, V>* Find(const K& key)
{//计算要查早的键的映射位置size_t hashi = key % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数//线性探测while (_table[hashi]._state != EMPTY)//到映射位置上去查找,如果该位置上不为空,{if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以//错误的:if (_table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以{return &_table[hashi];//返回该位置的地址}++hashi;hashi %= _table.size();//取模的方式防止hashi越界}return nullptr;//找不到就返回空}

删除 

bool Erase(const K& key)
{HashDate<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;//这里只是将删除键所处位置的状态标识符改为了DELETE--_n;return true;}
}

字符串哈希算法

基本概念:key不支持强转为整型后取模,需要自己提供转换为整型的仿函数,仿函数中的()重载一般为将字符串映射成固定长度的哈希值的算法(stoi只能处理"1112"类型的字符串,但是除了不了"2115few"甚至是由汉字组成的字符串"样非分"),常见的字符串哈希算法如下:

1、DJB2

unsigned long djb2(const std::string& str) {unsigned long hash = 5381;for (char c : str) {hash = ((hash << 5) + hash) + c; // hash * 33 + c}return hash;
}

2、SDBM

unsigned long sdbm(const std::string& str) {unsigned long hash = 0;for (char c : str) {hash = c + (hash << 6) + (hash << 16) - hash;}return hash;
}

3、BKDR Hash

unsigned long bkdrHash(const std::string& str) {unsigned long seed = 131; // 31, 131, 1313, 13131, 131313, etc.unsigned long hash = 0;for (char c : str) {hash = hash * seed + c;}return hash;
}

4、ELF Hash 

unsigned long elfHash(const std::string& str) {unsigned long hash = 0;unsigned long x = 0;for (char c : str) {hash = (hash << 4) + c;if ((x = hash & 0xF0000000L) != 0) {hash ^= (x >> 24);}hash &= ~x;}return hash;
}

5、FNV-1a Hash

unsigned long fnv1aHash(const std::string& str) {const unsigned long fnv_prime = 0x100000001b3;const unsigned long offset_basis = 0xcbf29ce484222325;unsigned long hash = offset_basis;for (char c : str) {hash ^= c;hash *= fnv_prime;}return hash;
}

6、MD5

#include <openssl/md5.h>std::string md5Hash(const std::string& str) {unsigned char digest[MD5_DIGEST_LENGTH];MD5((unsigned char*)str.c_str(), str.size(), (unsigned char*)&digest);    char mdString[33];for(int i = 0; i < 16; i++)sprintf(&mdString[i*2], "%02x", (unsigned int)digest[i]);return std::string(mdString);
}

7. SHA-1 

#include <openssl/sha.h>std::string sha1Hash(const std::string& str) {unsigned char hash[SHA_DIGEST_LENGTH];SHA1((unsigned char*)str.c_str(), str.size(), hash);char buf[SHA_DIGEST_LENGTH*2];for (int i = 0; i < SHA_DIGEST_LENGTH; i++)sprintf((char*)&(buf[i*2]), "%02x", hash[i]);return std::string(buf);
}

 参考网址:各种字符串Hash函数 - clq - 博客园 (cnblogs.com)

BKDR Hash的综合评分是最高的,ELF Hash的综合评分是最低的

为了泛型编程一般需要两种仿函数:处理可以强转为int的仿函数 和 处理不能强转为int的仿函数

//可以直接强转的类型,将它们强转为size_t类型,因为即使是整数也可能为负
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};
//这里采用的是BKDR Hash字符串哈希算法写的仿函数
template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto ch : key){hash = hash * 131 + ch;}return hash;}
};

最终代码

#pragma once#include <iostream>
#include <vector>
#include <utility>
using namespace std;//将该仿函数放在了命名空间外,使得开散列和闭散列都可以使用该仿函数
//可以直接强转的类型,将它们强转为size_t类型,因为即使是整数也可能为负
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//对HashFunc采用特化处理,如果是int等就会用上面的仿函数,如果是string就会用该特化版本的HashFunc
template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto ch : key){hash = hash * 131 + ch;}return hash;}
};namespace open_address
{enum State{EMPTY,EXIST,DELETE};template<class K, class V>struct HashDate{pair<K, V> _kv;State _state = EMPTY;//默认情况下为EMPTY};//处理字符串类型的强制类型转换struct StringHashFunc{size_t operator()(const string& key){size_t hashi = 0;for (auto ch : key){hashi = hashi * 131 + ch;}return hashi;}};template <class K, class V, class Hash = HashFunc<K>>//强转方式的缺省值是强转为整型class HashTable{public://实例化哈希表时,将哈希表的大小和底层数组的容量均调整为10HashTable(){_table.resize(10);} bool Insert(const pair<K, V>& kv){//防止冗余if (Find(kv.first))return false;//当负载因子大于等于0.7时进行扩容,以空间换时间if (_n * 10 / _table.size() >= 0.7)//采用/取整得不到小数,所以直接令_n * 10 {//新建一个哈希表HashTable<K,V, Hash> newHT;//确定新哈希表的大小newHT._table.resize(_table.size() * 2);//将旧表中的键重新映射到新表中for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state ==  EXIST)//原表中某下标的状态为存在时,将原表某下标中存放的键值对插入新表中{newHT.Insert(_table[i]._kv);//直接复用Insert操作}}_table.swap(newHT._table);}Hash sh;//处理强转为整型的仿函数size_t hashi = sh(kv.first) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数//线性探测while (_table[hashi]._state == EXIST)//探查到目标映射位置存在数字时就进行探测{++hashi;hashi %= _table.size();//取模的方式防止hashi越界}//确定好位置后进行插入_table[hashi]._kv = kv;//在合适的映射位置插入_table[hashi]._state = EXIST;//然后后将该位置的状态标识为EXIST++_n;//哈希表中有效数据元素+1return true;}HashDate<K, V>* Find(const K& key){Hash hs;//处理强转为整型的仿函数//计算要查早的键的映射位置size_t hashi = hs(key) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数//线性探测while (_table[hashi]._state != EMPTY)//到映射位置上去查找,如果该位置上不为空,{if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以//错误的:if (_table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以{return &_table[hashi];//返回该位置的地址}++hashi;hashi %= _table.size();//取模的方式防止hashi越界}return nullptr;//找不到就返回空}bool Erase(const K& key){HashDate<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;//删除时只是将该位置的标识符置为了DELETE该位置上还存放着一个键,只不过该键可以被随时替换--_n;return true;}}private:vector<HashDate<K, V>> _table;//哈希表的大小和底层数组的容量都是通过 _table来管理的,正常情况下哈希表的大小_table.size和底层数组的容量是要分开的size_t _n;//哈希表中的有效数据个数};void TeshHT1(){int a[] = { 10001,11,55,24,19,12,31 };HashTable<int, int>  ht;//使用缺省的强转方式for (auto e : a){ht.Insert(make_pair(e, e));}cout << ht.Find(55) << endl;cout << ht.Find(31) << endl;ht.Erase(55);cout << ht.Find(55) << endl;cout << ht.Find(31) << endl;}void TeshHT2(){使用StringHashFunc//HashTable<string, int, StringHashFunc> ht;//ht.Insert(make_pair("sort", 1));//ht.Insert(make_pair("left", 1));//ht.Insert(make_pair("insert", 1));//查看是否可以用仿函数StringHashFunc将string转换为整型(与上面的内容无关)StringHashFunc()实例化一个匿名的仿函数类对象,然后("bacd")向该仿函数传参//cout << StringHashFunc()("bacd") << endl;//cout << StringHashFunc()("abcd") << endl;//cout << StringHashFunc()("aadd") << endl;//使用特化版本HashTable<string, int> ht;ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("left", 1));ht.Insert(make_pair("insert", 1));//查看是否可以用仿函数的特化版本HashFunc<string>将string转换为整型cout << HashFunc<string>()("bacd") << endl;cout << HashFunc<string>()("abcd") << endl;cout << HashFunc<string>()("aadd") << endl;}}

开散列

基本概念:将所有映射到相同哈希值的元素存储在一个链表或其他结构中,冲突发生时就将冲突位置的链表结点++,这样每个哈希表的桶实际上是一个指向链表头部的指针

优点:冲突处理简单,扩展容易,删除操作高效

缺点:需要额外的存储空间来存储链表节点,可能导致内存碎片

  • 可以是尾插但是尾插还要去寻找尾,而且发生冲突存入同一位置的结点也是有顺序的双向循环差入也行,但是没必要

插入

//采用头插的方式
bool Insert(const pair<K, V>& kv)
{//防止冗余if(Find(kv.first))return false;//如果还是重新建立映射关系,如果结点个数过多,在新表创建结点和移动后删除旧表的结点需要很多的资源//因为是队列是一个个挂在数组上的,所以当存放的有效元素个数 == 数组大小时才需要扩容//if (_n == _table.size())//{//	//新建一个哈希表//	HashTable<K, V> newHT;//	//确定新哈希表的大小//	newHT._table.resize(_table.size() * 2);//	//将旧表中的键重新映射到新表中//	for (size_t i = 0; i < _table.size(); i++)//每一次的for循环都是将某一个数组下标中的结点都链接到新表中//	{//		Node* cur = _table[i];//		while (cur)//		{//			newHT.Insert(_table[i]._kv);//直接复用Insert操作//			cur = cur->_next;//		}//	}//	_table.swap(newHT._table);//使用交换新旧表名中代表的地址信息//}//应该在确认了映射关系后,直接将旧表中的所有结点移动到新表中,这样移动后旧表中只用析构一个vector,且不用创建新的结点if (_n == _table.size()){vector<Node*> newTable(_table.size() * 2, nullptr);//设定新表大小及初始化各个位置for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];//cur指向旧表的下标为i位置的结点(该结点是一条链表的头结点)//遍历一条链表while (cur){Node* next = cur->_next;//next存放cur的下一个结点的位置//确定将旧表cur指向的结点要头插新表的哪个位置size_t hashi = cur->_kv.first % newTable.size();cur->_next = newTable[hashi];//头插(忘了回去看外面的注释,这点就是有点会让人发懵)newTable[hashi] = cur;cur = next;//cur指向自己的下一个结点}_table[i] = nullptr;//置空}/*cout <<"交换前旧表的地址为:" <<  & _table << endl;cout <<"交换前新表的地址为:" <<  &newTable << endl;*/_table.swap(newTable);//交换/*	cout << "交换后旧表的地址为:" << &_table << endl;cout << "交换后新表的地址为:" << &newTable << endl;*/}size_t hashi = kv.first % _table.size();Node* newnode = new Node(kv);//匿名结点对象//头插(数组中某个位置没有结点插入时_table[hashi] == nullptr)newnode->_next = _table[hashi];//_新结点的next结点指向当前链表的头指针指向的结点_table[hashi] = newnode;//令链表头指针指向新结点++_n;return true;//哈希桶的头插类似于://Node* head = nullptr; // 初始化一个空的链表,现在数组中每个位置都是一个空指针//void insertAtHead(int value) {//	Node* newNode = new Node(value); // 创建一个新节点//	newNode->next = head; // 将新节点的后继节点指向当前的头节点//	head = newNode; // 更新头节点指针,使其指向新节点//}
}

假设我们有以下插入过程:

  1. 初始状态

    • _tables 被初始化为大小为 10 的向量,每个元素都是 nullptr
    • 这意味着 _tables[0]_tables[9] 都是 nullptr
  2. 第一次插入

    • 插入键值对 (key1, value1)
    • 计算哈希值 hashi = key1 % 10,假设 hashi 为 2。
    • 创建新节点 newnode,其 _kv(key1, value1),且 _next 初始化为 nullptr
    • newnode->_next = _tables[hashi]; 此时 _tables[2]nullptr,因此 newnode->_next 也指向 nullptr
    • newnode 赋值给 _tables[hashi],即 _tables[2] = newnode
  3. 第二次插入

    • 插入键值对 (key2, value2)
    • 计算哈希值 hashi = key2 % 10,假设 hashi 仍然为 2。
    • 创建新节点 newnode,其 _kv(key2, value2),且 _next 初始化为 nullptr
    • newnode->_next = _tables[hashi]; 此时 _tables[2] 已经指向第一个节点(即存储 (key1, value1) 的节点),所以 newnode->_next 指向这个第一个节点。
    • newnode 赋值给 _tables[hashi],即 _tables[2] = newnode

通过这种方式,链表中的节点按插入顺序逆序排列,因为每次新节点都插入到链表的头部。

查看扩容过程的视频:20240526_213552-CSDN直播

结论:交换过程中结点的地址不发生改变,存放结点存放的位置一直在改变,且重新插入后一条链表中结点的先后顺序发生改变,原本的头变成了尾;交换结束后新旧表的地址不变使用&新表对象得到的依然是原地址,而不是旧表的地址,实际交换的是两个表的各个数据结构

查找

Node* Find(const K& key)
{size_t hashi = kv.first % _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)
{Hash hs;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];//直接去映射位置上查找while (cur){if (cur->_kv.first == key){delete cur;}	else{prev = cur->_next;cur = prev;}cur = cur->_next;}return nullptr;
}

最终代码

namespace hash_bucket
{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 T>//struct HashNode//{//	T _data;//	HashNode<T>* _next;//	HashNode(const T& data)//		:_data(data)//		, _next(nullptr)//	{}//};template<class K,class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_table.resize(10, nullptr);//初始化时,数组中有十个位置,每个位置都存放的是空指针_n = 0;}//哈希表的底层容器vector函数结束后自动析构,但是vector中各个位置存放的都是自定义类型Node,自定义类型的析构时会调用它们的析构函数,所以还要重新写一个析构函数~HashTable() {//遍历删除for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;//删除一个位置上的一整条链表就将该位置存放的指针置空}}//采用头插的方式bool Insert(const pair<K, V>& kv){//防止冗余if (Find(kv.first))return false;Hash hs;//如果还是重新建立映射关系,如果结点个数过多,在新表创建结点和移动后删除旧表的结点需要很多的资源//因为是队列是一个个挂在数组上的,所以当存放的有效元素个数 == 数组大小时才需要扩容//if (_n == _table.size())//{//	//新建一个哈希表//	HashTable<K, V> newHT;//	//确定新哈希表的大小//	newHT._table.resize(_table.size() * 2);//	//将旧表中的键重新映射到新表中//	for (size_t i = 0; i < _table.size(); i++)//每一次的for循环都是将某一个数组下标中的结点都链接到新表中//	{//		Node* cur = _table[i];//		while (cur)//		{//			newHT.Insert(_table[i]._kv);//直接复用Insert操作//			cur = cur->_next;//		}//	}//	_table.swap(newHT._table);//使用交换新旧表名中代表的地址信息//}//应该在确认了映射关系后,直接将旧表中的所有结点移动到新表中,这样移动后旧表中只用析构一个vector,且不用创建新的结点if (_n == _table.size()){vector<Node*> newTable(_table.size() * 2, nullptr);//设定新表大小及初始化各个位置for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];//cur指向旧表的下标为i位置的结点(该结点是一条链表的头结点)//遍历一条链表while (cur){Node* next = cur->_next;//next存放cur的下一个结点的位置//确定将旧表cur指向的结点要头插新表的哪个位置size_t hashi = hs(cur->_kv.first) % newTable.size();cur->_next = newTable[hashi];//头插(忘了回去看外面的注释,这点就是有点会让人发懵)newTable[hashi] = cur;cur = next;//cur指向自己的下一个结点}_table[i] = nullptr;//置空}/*cout <<"交换前旧表的地址为:" <<  & _table << endl;cout <<"交换前新表的地址为:" <<  &newTable << endl;*/_table.swap(newTable);//交换/*	cout << "交换后旧表的地址为:" << &_table << endl;cout << "交换后新表的地址为:" << &newTable << endl;*/}size_t hashi = hs(kv.first) % _table.size();Node* newnode = new Node(kv);//匿名结点对象//头插(数组中某个位置没有结点插入时_table[hashi] == nullptr)newnode->_next = _table[hashi];//_新结点的next结点指向当前链表的头指针指向的结点_table[hashi] = newnode;//令链表头指针指向新结点++_n;return true;//哈希桶的头插类似于://Node* head = nullptr; // 初始化一个空的链表,现在数组中每个位置都是一个空指针//void insertAtHead(int value) {//	Node* newNode = new Node(value); // 创建一个新节点//	newNode->next = head; // 将新节点的后继节点指向当前的头节点//	head = newNode; // 更新头节点指针,使其指向新节点//}}//链表的遍历Node* Find(const K& key){Hash hs;size_t hashi = hs(key) % _table.size();Node* cur = _table[hashi];//直接去映射位置上查找while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];//直接去映射位置上查找while (cur){if (cur->_kv.first == key){delete cur;}else{prev = cur->_next;cur = prev;}cur = cur->_next;}return nullptr;}private:vector<Node*> _table;//数组中的每个位置存放的都是结点的指针size_t _n;};void HashTest1(){int a[] = { 10001,11,55,24,19,12,31,4,34,44 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}//ht.Insert(make_pair(32, 32));//ht.Insert(make_pair(32, 32));}void HashTest2(){HashTable<string, int> ht;ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("left", 1));ht.Insert(make_pair("insert", 1));}
}

完整代码

#pragma once#include <iostream>
#include <vector>
#include <utility>
using namespace std;//将该仿函数放在了命名空间外,使得开散列和闭散列都可以使用该仿函数
//可以直接强转的类型,将它们强转为size_t类型,因为即使是整数也可能为负
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//对HashFunc采用特化处理,如果是int等就会用上面的仿函数,如果是string就会用该特化版本的HashFunc
template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto ch : key){hash = hash * 131 + ch;}return hash;}
};namespace open_address
{enum State{EMPTY,EXIST,DELETE};template<class K, class V>struct HashDate{pair<K, V> _kv;State _state = EMPTY;//默认情况下为EMPTY};//处理字符串类型的强制类型转换struct StringHashFunc{size_t operator()(const string& key){size_t hashi = 0;for (auto ch : key){hashi = hashi * 131 + ch;}return hashi;}};template <class K, class V, class Hash = HashFunc<K>>//强转方式的缺省值是强转为整型class HashTable{public://实例化哈希表时,将哈希表的大小和底层数组的容量均调整为10HashTable(){_table.resize(10);} bool Insert(const pair<K, V>& kv){//防止冗余if (Find(kv.first))return false;//当负载因子大于等于0.7时进行扩容,以空间换时间if (_n * 10 / _table.size() >= 0.7)//采用/取整得不到小数,所以直接令_n * 10 {//新建一个哈希表HashTable<K,V, Hash> newHT;//确定新哈希表的大小newHT._table.resize(_table.size() * 2);//将旧表中的键重新映射到新表中for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state ==  EXIST)//原表中某下标的状态为存在时,将原表某下标中存放的键值对插入新表中{newHT.Insert(_table[i]._kv);//直接复用Insert操作}}_table.swap(newHT._table);}Hash sh;//处理强转为整型的仿函数size_t hashi = sh(kv.first) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数//线性探测while (_table[hashi]._state == EXIST)//探查到目标映射位置存在数字时就进行探测{++hashi;hashi %= _table.size();//取模的方式防止hashi越界}//确定好位置后进行插入_table[hashi]._kv = kv;//在合适的映射位置插入_table[hashi]._state = EXIST;//然后后将该位置的状态标识为EXIST++_n;//哈希表中有效数据元素+1return true;}HashDate<K, V>* Find(const K& key){Hash hs;//处理强转为整型的仿函数//计算要查早的键的映射位置size_t hashi = hs(key) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数//线性探测while (_table[hashi]._state != EMPTY)//到映射位置上去查找,如果该位置上不为空,{if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以//错误的:if (_table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以{return &_table[hashi];//返回该位置的地址}++hashi;hashi %= _table.size();//取模的方式防止hashi越界}return nullptr;//找不到就返回空}bool Erase(const K& key){HashDate<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;//删除时只是将该位置的标识符置为了DELETE该位置上还存放着一个键,只不过该键可以被随时替换--_n;return true;}}private:vector<HashDate<K, V>> _table;//哈希表的大小和底层数组的容量都是通过 _table来管理的,正常情况下哈希表的大小_table.size和底层数组的容量是要分开的size_t _n;//哈希表中的有效数据个数};void TeshHT1(){int a[] = { 10001,11,55,24,19,12,31 };HashTable<int, int>  ht;//使用缺省的强转方式for (auto e : a){ht.Insert(make_pair(e, e));}cout << ht.Find(55) << endl;cout << ht.Find(31) << endl;ht.Erase(55);cout << ht.Find(55) << endl;cout << ht.Find(31) << endl;}void TeshHT2(){使用StringHashFunc//HashTable<string, int, StringHashFunc> ht;//ht.Insert(make_pair("sort", 1));//ht.Insert(make_pair("left", 1));//ht.Insert(make_pair("insert", 1));//查看是否可以用仿函数StringHashFunc将string转换为整型(与上面的内容无关)StringHashFunc()实例化一个匿名的仿函数类对象,然后("bacd")向该仿函数传参//cout << StringHashFunc()("bacd") << endl;//cout << StringHashFunc()("abcd") << endl;//cout << StringHashFunc()("aadd") << endl;//使用特化版本HashTable<string, int> ht;ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("left", 1));ht.Insert(make_pair("insert", 1));//查看是否可以用仿函数的特化版本HashFunc<string>将string转换为整型cout << HashFunc<string>()("bacd") << endl;cout << HashFunc<string>()("abcd") << endl;cout << HashFunc<string>()("aadd") << endl;}}namespace hash_bucket
{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 T>//struct HashNode//{//	T _data;//	HashNode<T>* _next;//	HashNode(const T& data)//		:_data(data)//		, _next(nullptr)//	{}//};template<class K,class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_table.resize(10, nullptr);//初始化时,数组中有十个位置,每个位置都存放的是空指针_n = 0;}//哈希表的底层容器vector函数结束后自动析构,但是vector中各个位置存放的都是自定义类型Node,自定义类型的析构时会调用它们的析构函数,所以还要重新写一个析构函数~HashTable() {//遍历删除for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;//删除一个位置上的一整条链表就将该位置存放的指针置空}}//采用头插的方式bool Insert(const pair<K, V>& kv){//防止冗余if (Find(kv.first))return false;Hash hs;//如果还是重新建立映射关系,如果结点个数过多,在新表创建结点和移动后删除旧表的结点需要很多的资源//因为是队列是一个个挂在数组上的,所以当存放的有效元素个数 == 数组大小时才需要扩容//if (_n == _table.size())//{//	//新建一个哈希表//	HashTable<K, V> newHT;//	//确定新哈希表的大小//	newHT._table.resize(_table.size() * 2);//	//将旧表中的键重新映射到新表中//	for (size_t i = 0; i < _table.size(); i++)//每一次的for循环都是将某一个数组下标中的结点都链接到新表中//	{//		Node* cur = _table[i];//		while (cur)//		{//			newHT.Insert(_table[i]._kv);//直接复用Insert操作//			cur = cur->_next;//		}//	}//	_table.swap(newHT._table);//使用交换新旧表名中代表的地址信息//}//应该在确认了映射关系后,直接将旧表中的所有结点移动到新表中,这样移动后旧表中只用析构一个vector,且不用创建新的结点if (_n == _table.size()){vector<Node*> newTable(_table.size() * 2, nullptr);//设定新表大小及初始化各个位置for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];//cur指向旧表的下标为i位置的结点(该结点是一条链表的头结点)//遍历一条链表while (cur){Node* next = cur->_next;//next存放cur的下一个结点的位置//确定将旧表cur指向的结点要头插新表的哪个位置size_t hashi = hs(cur->_kv.first) % newTable.size();cur->_next = newTable[hashi];//头插(忘了回去看外面的注释,这点就是有点会让人发懵)newTable[hashi] = cur;cur = next;//cur指向自己的下一个结点}_table[i] = nullptr;//置空}/*cout <<"交换前旧表的地址为:" <<  & _table << endl;cout <<"交换前新表的地址为:" <<  &newTable << endl;*/_table.swap(newTable);//交换/*	cout << "交换后旧表的地址为:" << &_table << endl;cout << "交换后新表的地址为:" << &newTable << endl;*/}size_t hashi = hs(kv.first) % _table.size();Node* newnode = new Node(kv);//匿名结点对象//头插(数组中某个位置没有结点插入时_table[hashi] == nullptr)newnode->_next = _table[hashi];//_新结点的next结点指向当前链表的头指针指向的结点_table[hashi] = newnode;//令链表头指针指向新结点++_n;return true;//哈希桶的头插类似于://Node* head = nullptr; // 初始化一个空的链表,现在数组中每个位置都是一个空指针//void insertAtHead(int value) {//	Node* newNode = new Node(value); // 创建一个新节点//	newNode->next = head; // 将新节点的后继节点指向当前的头节点//	head = newNode; // 更新头节点指针,使其指向新节点//}}//链表的遍历Node* Find(const K& key){Hash hs;size_t hashi = hs(key) % _table.size();Node* cur = _table[hashi];//直接去映射位置上查找while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];//直接去映射位置上查找while (cur){if (cur->_kv.first == key){delete cur;}else{prev = cur->_next;cur = prev;}cur = cur->_next;}return nullptr;}private:vector<Node*> _table;//数组中的每个位置存放的都是结点的指针size_t _n;};void HashTest1(){int a[] = { 10001,11,55,24,19,12,31,4,34,44 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}//ht.Insert(make_pair(32, 32));//ht.Insert(make_pair(32, 32));}void HashTest2(){HashTable<string, int> ht;ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("left", 1));ht.Insert(make_pair("insert", 1));}
}

~over~

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

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

相关文章

大学校园广播“录编播”与IP广播系统技术方案

一、项目概述 1、校园IP网络广播系统概述 大学校园广播系统是学校整个弱电系统中的子系统&#xff0c;它是每个学校不可缺少的基础设施之一&#xff0c;在传递校园文化、传播校园新闻资讯方面发挥着重要的作用。近几年来&#xff0c;虽然视频技术和网络技术在飞速发展&#xf…

行为设计模式之策略模式

文章目录 概述原理结构图 代码实现小结 概述 策略模式(strategy pattern)的原始定义是&#xff1a;定义一系列算法&#xff0c;将每一个算法封装起来&#xff0c;并使它们可以相互替换。策略模式让算法可以独立于使用它的客户端而变化。 在软件开发中也会遇到相似的情况&…

基于双差分值和RR间隔处理的心电信号R峰检测算法(MATLAB R2018A)

心电信号中的R峰是确定心率和节律、以及检测其它波形特征点&#xff08;图1A&#xff09;的基础。R峰的准确检测是心率变异性分析、心拍分割和心律失常识别重要的处理步骤。 现有的心电信号R峰检测方法主要为基于规则的决策法和基于深度学习的检测方法。基于规则的决策法通常对…

中国低调海外巨头,实力超乎想象!

在全球化的浪潮中&#xff0c;中国公司正以前所未有的速度和规模走向世界。他们或许低调&#xff0c;但却实力非凡&#xff0c;在国际市场上掀起了一股不可小觑的“中国风暴”。今天&#xff0c;就让我们揭开那些在国外牛逼到爆炸的中国公司的神秘面纱&#xff0c;深度解析他们…

mysql中InnoDB的表空间--系统表空间

大家好。上篇文章我们讲了InnoDB的独立表空间&#xff0c;我们知道了表空间被划分为许多连续的区&#xff0c;对于大小为16KB的页面来说&#xff0c;每个区默认由64个页组成&#xff0c;每256个区为一组&#xff0c;每个组最开始的几个页的类型是固定的。&#xff08;在这里强烈…

HCIP-Datacom-ARST自选题库__BGP判断【20道题】

1.传统的BGP-4只能管理IPV4单播路由信息&#xff0c;MP-BGP为了提供对多种网络层协议的支持&#xff0c;对BGP-4进行了扩展。其中MP-BGP对IPv6单播网络的支持特性称为BGP4&#xff0c;BGP4通过Next Hop属性携带路由下一跳地址信息。 2.BGP4通过Update报文中的Next Hop属性携带…

RK3568笔记二十六:音频应用

若该文为原创文章&#xff0c;转载请注明原文出处。 一、介绍 音频是我们最常用到的功能&#xff0c;音频也是 linux 和安卓的重点应用场合。 测试使用的是ATK-DLR3568板子&#xff0c;板载外挂RK809 CODEC芯片&#xff0c;RK官方驱动是写好的&#xff0c;不用在自己重新写。…

智慧城市运维可视化:透视未来城市高效管理的新视窗

行业痛点 现代城市运维是一个复杂而庞大的系统&#xff0c;涉及到诸多方面&#xff0c;包括交通、环境、能源等等。然而&#xff0c;在城市运维中&#xff0c;存在着一些现实的痛点&#xff0c;给城市管理者带来了不小的压力和困扰&#xff1a; 1、交通拥堵 随着城市化进程的…

帝国cms自定义专题列表模板list.var中获取对应专题下的信息、信息数量及信息所属栏目名称

帝国cms自定义专题列表模板list.var中获取对应专题下的信息、信息数量及信息所属栏目名称 代码如下&#xff1a; $rr $empire->fetch1("SELECT GROUP_CONCAT(id) from phome_enewsztinfo where ztid$r[id]"); $ff $rr[0]; $ga explode(",", $ff); …

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑分布式光伏高效消纳与负荷损失最小的区域配电网应急资源协同配置策略》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

一款网站源码下载开源工具 Website Downloader

一、简介 Website Downloader 是一款免费的网站源码下载器&#xff0c;下载任何网站的完整源代码&#xff0c;包括 JavaScript、样式表、图像等等&#xff0c;而且使用也非常简单&#xff0c;只需要粘贴网址&#xff0c;用户即可将网页链接内容下载到本地&#xff0c;而且自动…

堆(建堆算法,堆排序)

目录 一.什么是堆&#xff1f; 1.堆 2.堆的储存 二.堆结构的创建 1.头文件的声明&#xff1a; 2.向上调整 3.向下调整 4.源码&#xff1a; 三.建堆算法 1.向上建堆法 2.向下建堆法 四.堆排序 五.在文件中Top出最小的K个数 一.什么是堆&#xff1f; 1.堆 堆就…

Docker之xfs文件系统下安装报错解决方案

一、需求说明 centos系统下安装docker最新版的时候&#xff0c;安装成功&#xff0c;启动的时候报错。报错信息“failed to start daemon: error initializing graphdriver: overlay2: the backing xfs filesystem is formatted without d_type support, which leads to incorr…

ROS添加GDB调试

文章目录 一、问题描述二、配置步骤1. debug 模式编译2. rosrun 添加GDB指令3. launch 添加GDB指令 三、GDB基本命令1. 基本2. 显示被调试文件信息3. 查看/修改内存4. 断点5. 调试运行 一、问题描述 在享受ROS带来便利的同时&#xff0c;但因每运行出现错误&#xff0c;ROS不会…

风电机组的振动控制

文章目录 0. 背景1. 原文记录 0. 背景 混塔机组的频率大概是目前业内遇见的比较普遍的通病。最近在了解风电机组振动控制的知识&#xff0c;看到一篇科普性质的文章&#xff0c;感觉不错&#xff0c;所以记录下来。想要看原文的点击这里。感谢原作者。 1. 原文记录

一文带你了解.NET能做什么?

前言 在DotNetGuide技术社区微信交流群经常看到有小伙伴问&#xff1a;.NET除了能写桌面应用和Web应用还能做什么&#xff1f;今天大姚将通过本篇文章来简单讲讲.NET能做哪些开发&#xff0c;对.NET感兴趣的小伙伴也可以自行领取文末附带的.NET相关学习资料。 .NET简单介绍 .…

多层派生时的构造函数和派生类的析构函数

一、多层派生时的构造函数 目录 一、多层派生时的构造函数 二、派生类的析构函数 析构函数的作用&#xff1a; 例&#xff1a;多级派生情况下派生类的构造函数 #include <iostream> #include<string> using namespace std; class Student { public:Student(int…

【SpeedAI科研小助手】2分钟解决知网维普AIGC检测

2分钟搞定AIGC率&#xff1f;还能降到0%&#xff1f; 使用方法&#xff1a; 打开SpeedAI科研小助手&#xff0c;将功能模式换成降AIGC率&#xff0c;后面可以一段一段自己改&#xff0c;也可以直接上传论文文件&#xff0c;SpeedAI直接帮你全文修改&#xff08;主打一个用户友…

HTML5 基本框架

HTML5基本的内容 文章目录 系列文章目录前言一、HTML5 基本框架二、具体框架结构三、知识补充总结 前言 HTML5的介绍&#xff1a; HTML5 是一种用于构建网页内容的标准化语言。它是 HTML&#xff08;超文本标记语言&#xff09;的第五个版本&#xff0c;引入了许多新的功能和特…

Jmeter-使用手册(_5.5版本)

JMeter是一个Java桌面应用程序&#xff0c;具有使用Swing图形API的图形界面。可以进行接口、性能等测试&#xff0c;也可以对任何数据库进行同样的测试&#xff0c;具有可移植性&#xff0c;可跨平台支持Windows&#xff0c;Linux&#xff0c;Mac上使用。 JMeter运行场景不仅可…