C++——哈希表
目录
一、误区
二、哈希结构:
三、哈希冲突
四、哈希函数
(1)哈希函数设计原则:
(2)常见哈希函数
五、哈希冲突解决
(1)闭散列
① 线性探测
编辑 ② 二次探测编辑
(2) 开散列
① 插入元素(不能有重复元素出现)
② 插入元素(元素不唯一)
③ 查找元素
④ 删除元素(元素唯一)
⑤ 删除元素(不唯一)
⑥ 哈希扩容
如果哈希桶中某条链非常长,但是没有达到扩容的时机,如何处理?编辑
一、误区
首先要纠正一下自己以前的误区,map和set底层使用的红黑树进行封装,而unordered_map unordered_set底层使用的是哈希表结构中的哈希桶来进行封装。
二、哈希结构:
一种理想的搜索方法:可以不经过一次次的遍历,可以直接从表中找到需要检索的元素。如果为这种方法构造一种存储结构,通过某种函数可以使元素和其再表中对应的位置进行建立一种一一映射的关系,那么就能很快找到该元素。
哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
三、哈希冲突
不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
四、哈希函数
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
(1)哈希函数设计原则:
① 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
② 哈希函数计算出来的地址能均匀分布在整个空间中
③ 哈希函数应该比较简单
(2)常见哈希函数
1. 直接定址法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
3. 平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4. 折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法--(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。
通常应用于关键字长度不等时采用此法
6. 数学分析法--(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址。
五、哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列 和 开散列
(1)闭散列
① 线性探测
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

② 二次探测
(2) 开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
使用一个存放指针的vector来模拟这个哈希桶,这个是哈希桶中每个节点的结构,每个结点中要保存当前节点元素内容和下一个节点地址。
#include<vector>
// using namespace std;
template<class T>
struct HashBucketNode
{HashBucketNode(const T& data): _pNext(nullptr), _data(data){}HashBucketNode<T>* _pNext;T _data;
};
下面代码是哈希桶结构,桶是不断维护一个存放HashBucketNode的vector数组_table,用_size来记录当前已经插入多少元素。
template<class T>
class HashBucket
{typedef HashBucketNode<T> Node;
public:HashBucket(size_t capacity = 10): _size(0) {_table.resize(capacity, nullptr);}bool Insert_Unique(const T& data)bool InsertEqual(const T& data)bool Find(const T& data)bool EraseUnique(const T& data)bool EraseEqual(const T& data)private: // 我们的哈希函数必须私有化 避免哈希表被攻击size_t HashFunc(const T& data){return data % _table.capacity();}private:size_t _size; // 记录有效元素个数std::vector<Node*> _table; // 给vector中放的链表结点
};
① 插入元素(不能有重复元素出现)
元素的插入使用的是头插法,因为我们首先通过哈希函数获取到插入元素的插入位置,但是在插入之前首先要遍历一下链表中是否已经有这个元素了,有了直接进行返回即可。
// 哈希桶中的元素是唯一的bool Insert_Unique(const T& data){// 1. 计算哈希桶号size_t bucketNo = HashFunc(data);// 2. 确保bucketNo桶中不存在dataNode* cur = _table[bucketNo];while (cur){if (data == cur->_data)return false;cur = cur->_pNext;}// 3、插入新结点 这里使用头插法cur = new Node(data);cur->_pNext = _table[bucketNo];_table[bucketNo] = cur;++_size;return true;}
② 插入元素(元素不唯一)
bool InsertEqual(const T& data){// 1、计算找到桶号size_t bucketNo = HashFunc(data);// 2、直接进行头插即可Node* cur = new Node(data);cur->_pNext = _table[bucketNo];_table[bucketNo] = cur;_size++;return true;}
③ 查找元素
bool Find(const T& data){// 1. 计算哈希桶号size_t bucketNo = HashFunc(data);Node* cur = _table[bucketNo];while (cur){if (data == cur->_data)return true;cur = cur->_pNext;}return false;}
④ 删除元素(元素唯一)
bool EraseUnique(const T& data){// 1. 计算哈希桶号size_t bucketNo = HashFunc(data);// 2、再这个桶里面找到值为data的节点并删除Node* cur = _table[bucketNo];Node* pre = nullptr;while (cur){if (cur->_data == data){// 找到了 但也要分情况 是否桶中第一个节点就是dataif (_table[bucketNo]._data == data){_table[bucketNo] = cur->_pNext;//--_size;//delete cur;//return true;}else{pre->_pNext = cur->_pNext;//--_size;//delete cur;//return true;}--_size;delete cur;return true;}else{// 不断遍历 直到找到data或者为空pre = cur;cur = cur->_pNext;}}// 找到最后都没有找到 桶中没有datareturn false;}
⑤ 删除元素(不唯一)
bool EraseEqual(const T& data){// 1、找到对应的桶号size_t bucketNo = HashFunc(data);Node* cur = _table[bucketNo];Node* pre = nullptr;// 2、遍历访问当前桶号链表,依次删除data元素while (cur){if (cur->_data == data){if (pre == nullptr) // 看是否为第一个元素{_table[bucketNo] = cur->_pNext;delete cur;cur = _table[bucketNo];--_size;return true;}else{pre->_pNext = cur->_pNext;delete cur;cur = pre->_pNext;--_size;return true;}}else{pre = cur;cur = cur->_pNext;}}return false;}
⑥ 哈希扩容
另外,最初的错误想法是让新桶直接将旧桶中的vector进行一次拷贝,把每个链表的第一个节点指针拷入就行,最后发现肯定不能这样,因为后续对桶进行扩容,取模的值也会发生改变,所以旧桶中的元素在新的模值下就会映射到新的结点中。
void My_Swap(HashBucket<T>& new_Bucket){_table.swap(new_Bucket._table);std::swap(new_Bucket._size);}void Check_capacity(){if (_size == _table.capacity()){HashBucket<T> new_Bucket(Get_prime(_size));for (size_t i = 0; i < _size; i++){Node* cur = _table[i];while (cur){// 1、将cur从旧桶中拿出来,并维护好旧桶_table[i] = cur->_pNext;// 2、将cur接入新桶位置 size_t NewHashBucketNo = new_Bucket.HashFunc(cur->_data);cur->_pNext = new_Bucket._table[NewHashBucketNo];new_Bucket._table[NewHashBucketNo] = cur;new_Bucket._size++;// 3、更新cur处理当前链表下一个位置 直到为空跳出cur = _table[i];_size--;}}this->My_Swap(new_Bucket);}}为了获取扩容后的更大素数size_t Get_prime(size_t prime){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul, 1610612741ul, 3221225473ul, 4294967291ul};for (int i = 0; i < PRIMECOUNT; ++i){if (primeList[i] > prime)return primeList[i];}return primeList[PRIMECOUNT - 1];
}
⑦ 清除和打印
void Print()const{for (int i = 0; i < _table.capacity(); i++){Node* cur = _table[i];cout << "Node [" << i << "]";while (cur){cout << cur->_data << "->";}cout << "NULL" << endl;}}void Clear(){for (int i = 0; i < _table.capacity(); i++){Node* cur = _table[i];while (cur){_table[i] = cur->_pNext;delete cur;cur = _table;_size--;}}}
如果哈希桶中某条链非常长,但是没有达到扩容的时机,如何处理?
#pragma once
#include<vector>
// using namespace std;
template<class T>
struct HashBucketNode
{HashBucketNode(const T& data): _pNext(nullptr), _data(data){}HashBucketNode<T>* _pNext;T _data;
};template<class T>
class HashBucket
{typedef HashBucketNode<T> Node;
public:HashBucket(size_t capacity = 10): _size(0) {_table.resize(capacity, nullptr);}// 哈希桶中的元素是唯一的bool Insert_Unique(const T& data){// 1. 计算哈希桶号size_t bucketNo = HashFunc(data);// 2. 确保bucketNo桶中不存在dataNode* cur = _table[bucketNo];while (cur){if (data == cur->_data)return false;cur = cur->_pNext;}// 3、插入新结点 这里使用头插法cur = new Node(data);cur->_pNext = _table[bucketNo];_table[bucketNo] = cur;++_size;return true;}bool InsertEqual(const T& data){// 1、计算找到桶号size_t bucketNo = HashFunc(data);// 2、直接进行头插即可Node* cur = new Node(data);cur->_pNext = _table[bucketNo];_table[bucketNo] = cur;_size++;return true;}bool Find(const T& data){// 1. 计算哈希桶号size_t bucketNo = HashFunc(data);Node* cur = _table[bucketNo];while (cur){if (data == cur->_data)return true;cur = cur->_pNext;}return false;}bool EraseUnique(const T& data){// 1. 计算哈希桶号size_t bucketNo = HashFunc(data);// 2、再这个桶里面找到值为data的节点并删除Node* cur = _table[bucketNo];Node* pre = nullptr;while (cur){if (cur->_data == data){// 找到了 但也要分情况 是否桶中第一个节点就是dataif (_table[bucketNo]._data == data){_table[bucketNo] = cur->_pNext;//--_size;//delete cur;//return true;}else{pre->_pNext = cur->_pNext;//--_size;//delete cur;//return true;}--_size;delete cur;return true;}else{// 不断遍历 直到找到data或者为空pre = cur;cur = cur->_pNext;}}// 找到最后都没有找到 桶中没有datareturn false;}bool EraseEqual(const T& data){// 1、找到对应的桶号size_t bucketNo = HashFunc(data);Node* cur = _table[bucketNo];Node* pre = nullptr;// 2、遍历访问当前桶号链表,依次删除data元素while (cur){if (cur->_data == data){if (pre == nullptr) // 看是否为第一个元素{_table[bucketNo] = cur->_pNext;delete cur;cur = _table[bucketNo];--_size;return true;}else{pre->_pNext = cur->_pNext;delete cur;cur = pre->_pNext;--_size;return true;}}else{pre = cur;cur = cur->_pNext;}}return false;}private: // 我们的哈希函数必须私有化 避免哈希表被攻击size_t HashFunc(const T& data){return data % _table.capacity();}private:size_t _size; // 记录有效元素个数std::vector<Node*> _table; // 给vector中放的链表结点
};void Test1()
{HashBucket<int> hash;hash.Insert_Unique(5);hash.Insert_Unique(5);hash.Insert_Unique(6);hash.Insert_Unique(7);}