在闭散列的哈希表中,哈希表每个位置除了存储所给数据之外,还应该存储该位置当前的状态,哈希表中每个位置的可能状态如下:
我们可以用枚举定义这三个状态。
// 闭散列哈希表
enum State {
EMPTY,// 哈希表位置为NULL
EXITS,// 哈希表位置有值了
DELETE// 哈希表位置为删除标志
};
为什么需要标识哈希表中每个位置的状态?
若是不设置哈希表中每个位置的状态,那么在哈希表中查找数据的时候可能是这样的。以除留余数法的线性探测为例,我们若是要判断下面这个哈希表是否存在元素40,步骤如下:
但是我们在寻找元素40时,不可能从0下标开始将整个哈希表全部遍历一次,这样就失去了哈希的意义。我们只需要从0下标开始往后查找,直到找到元素40判定为存在,或是找到一个空位置判定为不存在即可。
因为线性探测在为冲突元素寻找下一个位置时是依次往后寻找的,既然我们已经找到了一个空位置,那就说明这个空位置的后面不会再有从下标0位置开始冲突的元素了。比如我们要判断该哈希表中是否存在元素90,步骤如下:
但这种方式是不可行的,原因如下:
我们先将上述哈希表当中的元素1000找到,并将其删除,此时我们要判断当前哈希表当中是否存在元素40,当我们从0下标开始往后找到2下标(空位置)时,我们就应该停下来,此时并没有找到元素40,但是元素40却在哈希表中存在。
因此我们必须为哈希表中的每一个位置设置一个状态,并且每个位置的状态应该有三种可能,当哈希表中的一个元素被删除后,我们不应该简单的将该位置的状态设置为EMPTY,而是应该将该位置的状态设置为DELETE。
这样一来,当我们在哈希表中查找元素的过程中,若当前位置的元素与待查找的元素不匹配,但是当前位置的状态是EXIST或是DELETE,那么我们都应该继续往后进行查找,而当我们插入元素的时候,可以将元素插入到状态为EMPTY或是DELETE的位置。
因此,闭散列的哈希表中的每个位置存储的结构,应该包括所给数据和该位置的当前状态。
template<class K, class V>
struct HashData {
pair<K, V> _kv;
State _state = EMPTY;//状态初始化为空
};
而为了在插入元素时好计算当前哈希表的负载因子,我们还应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。
//哈希表
template<class K, class V>
class HashTable{
public:
//...
private:
vector<HashData<K, V>> _tables;// 将Hash值存放在vector中
size_t _n = 0; // 存储的数据个数
};
在哈希表中查找数据的步骤如下:
注意: 在查找过程中,必须找到位置状态为EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了。
HashData<K, V> *Find(const K &key) {
//哈希表大小为0,表示哈希表为空,返回nullptr
if (this->_tables.size() == 0) {
return nullptr;
}
//哈希函数
size_t hashi = key % this->_tables.size();
size_t i = 1;
size_t index = hashi;// index是插入的位置
// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
while (this->_tables[index]._state != EMPTY) {
// 当表中值跟key相等,并且状态为存在时才返回,因为可能值的状态被改为了delete说明刚刚被删除,不可以返回
if (this->_tables[index]._kv.first == key && this->_tables[index]._state == EXITS) {
return &this->_tables[index];
}
index = hashi + i; //线性探测
//index = hashi + i * i; //二次探测
index %= this->_tables.size();// 防止index越界,绕回去
i++;
// 这里的_state可能都是存在或者删除,那么程序就可能陷入死循环,所以需要给定条件退出
// 如果已经查找一圈,那么说明全是存在+删除
if (index == hashi) {
break;
}
}
return nullptr;
}
向哈希表中插入数据的步骤如下:
其中,哈希表的调整方式如下:
注意: 在将原哈希表的数据插入到新哈希表的过程中,不能只是简单的将原哈希表中的数据对应的挪到新哈希表中,而是需要根据新哈希表的大小重新计算每个数据在新哈希表中的位置,然后再进行插入。
将键值对插入哈希表的具体步骤如下:
注意: 产生哈希冲突向后进行探测时,一定会找到一个合适位置进行插入,因为哈希表的负载因子是控制在0.7以下的,也就是说哈希表永远都不会被装满。
bool Insert(const pair<K, V> &kv) {
//1.查找值
if (Find(kv.first)) {
return false;
}
// 当我们的哈希表是空或者负载因子大于0.7的时候,我们需要给将哈希表增容
// 负载因子 = 表中有效数据个数 / 空间的大小
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {
//为空的时候,给初始10,负载因子大于0.7就扩容两倍
size_t newsize = this->_tables.size() == 0 ? 10 : this->_tables.size() * 2;
HashTable<K, V> newHashTable;// 重新创建一个HashTable类
newHashTable._tables.resize(newsize);
// 遍历旧表,重新映射到新表
for (auto &data: this->_tables) {// data是_table中的类型,对应的HashData
if (data._state == EXITS) {
newHashTable.Insert(data._kv);// 将旧的kv插入到新的类对象中
}
}
//交换
this->_tables.swap(newHashTable._tables);
}
//哈希函数
size_t hashi = kv.first % this->_tables.size();
// 线形探测
size_t i = 1;
size_t index = hashi;// index是最后要插入的位置
// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
while (this->_tables[index]._state == EXITS) {
index = hashi + i;
//index = hashi + i * i; //二次线性探测
index %= this->_tables.size();// 防止index越界,绕回去
i++;
}
this->_tables[index]._kv = kv;
this->_tables[index]._state = EXITS;
this->_n++;// 存储的数据个数+1
return true;
}
删除哈希表中的元素非常简单,我们只需要进行伪删除即可,也就是将待删除元素所在位置的状态设置为DELETE。
在哈希表中删除数据的步骤如下:
注意: 虽然删除元素时没有将该位置的数据清0,只是将该元素所在状态设为了DELETE,但是并不会造成空间的浪费,因为我们在插入数据时是可以将数据插入到状态为DELETE的位置的,此时插入的数据就会把该数据覆盖。
bool Erase(const K &key) {
HashData<K, V> *ret = Find(key);
if (ret) {
ret->_state = DELETE;
this->_n--;
return true;
} else {
return false;
}
}
#pragma once
#include
#include
using namespace std;
// 闭散列哈希表
enum State {
EMPTY,// 哈希表位置为NULL
EXITS,// 哈希表位置有值了
DELETE// 哈希表位置为删除标志
};
template<class K, class V>
struct HashData {
pair<K, V> _kv;
State _state = EMPTY;//状态初始化为空
};
template<class K, class V>
class HashTable {
public:
HashData<K, V> *Find(const K &key) {
//哈希表大小为0,表示哈希表为空,返回nullptr
if (this->_tables.size() == 0) {
return nullptr;
}
//哈希函数
size_t hashi = key % this->_tables.size();
size_t i = 1;
size_t index = hashi;// index是插入的位置
// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
while (this->_tables[index]._state != EMPTY) {
// 当表中值跟key相等,并且状态为存在时才返回,因为可能值的状态被改为了delete说明刚刚被删除,不可以返回
if (this->_tables[index]._kv.first == key && this->_tables[index]._state == EXITS) {
return &this->_tables[index];
}
index = hashi + i;//线性探测
//index = hashi + i * i; //二次探测
index %= this->_tables.size();// 防止index越界,绕回去
i++;
// 这里的_state可能都是存在或者删除,那么程序就可能陷入死循环,所以需要给定条件退出
// 如果已经查找一圈,那么说明全是存在+删除
if (index == hashi) {
break;
}
}
return nullptr;
}
bool Insert(const pair<K, V> &kv) {
//1.查找值
if (Find(kv.first)) {
return false;
}
// 当我们的哈希表是空或者负载因子大于0.7的时候,我们需要给将哈希表增容
// 负载因子 = 表中有效数据个数 / 空间的大小
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {
//为空的时候,给初始10,负载因子大于0.7就扩容两倍
size_t newsize = this->_tables.size() == 0 ? 10 : this->_tables.size() * 2;
HashTable<K, V> newHashTable;// 重新创建一个HashTable类
newHashTable._tables.resize(newsize);
// 遍历旧表,重新映射到新表
for (auto &data: this->_tables) {// data是_table中的类型,对应的HashData
if (data._state == EXITS) {
newHashTable.Insert(data._kv);// 将旧的kv插入到新的类对象中
}
}
//交换
this->_tables.swap(newHashTable._tables);
}
//哈希函数
size_t hashi = kv.first % this->_tables.size();
// 线形探测
size_t i = 1;
size_t index = hashi;// index是最后要插入的位置
// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
while (this->_tables[index]._state == EXITS) {
index = hashi + i;
//index = hashi + i * i; //二次线性探测
index %= this->_tables.size();// 防止index越界,绕回去
i++;
}
this->_tables[index]._kv = kv;
this->_tables[index]._state = EXITS;
this->_n++;// 存储的数据个数+1
return true;
}
bool Erase(const K &key) {
HashData<K, V> *ret = Find(key);
if (ret) {
ret->_state = DELETE;
this->_n--;
return true;
} else {
return false;
}
}
private:
vector<HashData<K, V>> _tables;// 将Hash值存放在vector中
size_t _n = 0; // 存储的数据个数
};
在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点。
template<class K, class V>
struct HashNode {
HashNode<K, V> *_next;
pair<K, V> _kv;
HashNode(const pair<K, V> &kv)
: _kv(kv), _next(nullptr) {}
};
与闭散列的哈希表不同的是,在实现开散列的哈希表时,我们不用为哈希表中的每个位置设置一个状态字段,因为在开散列的哈希表中,我们将哈希地址相同的元素都放到了同一个哈希桶中,并不需要经过探测寻找所谓的“下一个位置”。
哈希表的开散列实现方式,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。
//哈希表
template<class K, class V>
class HashTable{
public:
//...
private:
vector<Node *> _tables;
size_t n = 0;// 存储有效数据的个数
};
只能存储key为整形的元素,其他类型怎么解决?
使用模板特化编写仿函数
template<class K>
struct HashFunc {
size_t operator()(const K &key) {
return key;
}
};
// 特化模板,传string的话,就走这个
template<>
struct HashFunc<string> {
size_t operator()(const string &s) {
size_t hash = 0;
for (auto ch: s) {
hash += ch;
hash *= 31;
}
return hash;
}
};
这样我们的结构就变成了:
template<class K, class V, class Hash = HashFunc<K>>// Hash用于将key转换成可以取模的类型
class HashTable {
public:
//
private:
vector<Node *> _tables;
size_t n = 0;// 存储有效数据的个数
};
在哈希表中查找数据的步骤如下:
Node *Find(const K &key) {
if (this->_tables.size() == 0) {
return nullptr;
}
Hash hash; //用于处理各种类型的仿函数
size_t hashi = hash(key) % this->_tables.size();
Node *cur = this->_tables[hashi];
while (cur) {
if (cur->_kv.first == key) {
return cur;
}
cur = cur->_next;
}
return nullptr;
}
向哈希表中插入数据的步骤如下:
其中,哈希表的调整方式如下:
重点: 在将原哈希表的数据插入到新哈希表的过程中,不要通过复用插入函数将原哈希表中的数据插入到新哈希表,因为在这个过程中我们需要创建相同数据的结点插入到新哈希表,在插入完毕后还需要将原哈希表中的结点进行释放,多此一举。
实际上,我们只需要遍历原哈希表的每个哈希桶,通过哈希函数将每个哈希桶中的结点重新找到对应位置插入到新哈希表即可,不用进行结点的创建与释放。
说明一下: 下面代码中为了降低时间复杂度,在增容时取结点都是从单链表的表头开始向后依次取的
将键值对插入哈希表的具体步骤如下:
bool Insert(const pair<K, V> &kv) {
Hash hash;// 仿函数用于不能取模的值
// 已经有这个数,就不用插入了
if (Find(kv.first)) {
return false;
}
// 负载因子 == 1时扩容
if (this->n == this->_tables.size()) {
// size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newsize = this->GetNextPrime(_tables.size());
vector<Node *> newtables(newsize, nullptr);
for (auto &cur: this->_tables) {// cur是Node*
while (cur) {
// 保存下一个
Node *next = cur->_next;
// 头插到新表
size_t hashi = hash(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = hash(kv.first) % this->_tables.size();
// 头插
Node *newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
this->n++;
return true;
}
在哈希表中删除数据的步骤如下:
注意: 不要先调用查找函数判断待删除结点是否存在,这样做如果待删除不在哈希表中那还好,但如果待删除结点在哈希表,那我们还需要重新在哈希表中找到该结点并删除,还不如一开始就直接在哈希表中找,找到了就删除。
bool Erase(const K &key) {
Hash hash;
size_t hashi = hash(key) % this->_tables.size();
//删除的时候需要找到前一个节点和后一个节点进行链接
Node *prev = nullptr;
Node *cur = this->_tables[hashi];//cur初始为头结点
//遍历单链表
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) {
//要找的结点就是头结点则直接更新头
this->_tables[hashi] = cur->_next;
} else {
//链接
prev->_next = cur->_next;
}
delete cur;
return true;
} else {
//更新prev和cur
prev = cur;
cur = cur->_next;
}
}
return false;
}
在哈希表中,使用素数作为表的大小可以有效地减少哈希冲突。这主要基于以下两点:
hash(key) = key % table_size
。在这种情况下,如果table_size
是素数,那么哈希函数就能够更好地将不同的键散列在表的不同位置,从而减少哈希冲突。因此,当哈希表需要扩容时,通常选择下一个较大的素数作为新的表的大小,以优化哈希表的性能。
// 扩容优化,使用素数扩容
size_t GetNextPrime(size_t prime) {
// SGI
static const int __stl_num_primes = 28;
static const unsigned long __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};
size_t i = 0;
for (; i < __stl_num_primes; ++i) {
if (__stl_prime_list[i] > prime)
return __stl_prime_list[i];
}
return __stl_prime_list[i];
}
#pragma once
#include
#include
#include
#include
using namespace std;
template<class K, class V>
struct HashNode {
HashNode<K, V> *_next;
pair<K, V> _kv;
HashNode(const pair<K, V> &kv)
: _kv(kv), _next(nullptr) {}
};
template<class K>
struct HashFunc {
size_t operator()(const K &key) {
return key;
}
};
// 特化模板,传string的话,就走这个
template<>
struct HashFunc<string> {
size_t operator()(const string &s) {
size_t hash = 0;
for (auto ch: s) {
hash += ch;
hash *= 31;
}
return hash;
}
};
template<class K, class V, class Hash = HashFunc<K>>// Hash用于将key转换成可以取模的类型
class HashTable {
typedef HashNode<K, V> Node;
public:
~HashTable() {
for (auto &cur: this->_tables) {
while (cur) {
Node *next = cur->_next;
delete cur;
cur = next;
}
cur = nullptr;
}
}
Node *Find(const K &key) {
if (this->_tables.size() == 0) {
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % this->_tables.size();
Node *cur = this->_tables[hashi];
while (cur) {
if (cur->_kv.first == key) {
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K &key) {
Hash hash;
size_t hashi = hash(key) % this->_tables.size();
//删除的时候需要找到前一个节点和后一个节点进行链接
Node *prev = nullptr;
Node *cur = this->_tables[hashi];//cur初始为头结点
//遍历单链表
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) {
//要找的结点就是头结点则直接更新头
this->_tables[hashi] = cur->_next;
} else {
//链接
prev->_next = cur->_next;
}
delete cur;
return true;
} else {
//更新prev和cur
prev = cur;
cur = cur->_next;
}
}
return false;
}
// 扩容优化,使用素数扩容
size_t GetNextPrime(size_t prime) {
// SGI
static const int __stl_num_primes = 28;
static const unsigned long __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};
size_t i = 0;
for (; i < __stl_num_primes; ++i) {
if (__stl_prime_list[i] > prime)
return __stl_prime_list[i];
}
return __stl_prime_list[i];
}
bool Insert(const pair<K, V> &kv) {
Hash hash;// 仿函数用于不能取模的值
// 已经有这个数,就不用插入了
if (Find(kv.first)) {
return false;
}
// 负载因子 == 1时扩容
if (this->n == this->_tables.size()) {
// size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newsize = this->GetNextPrime(_tables.size());
vector<Node *> newtables(newsize, nullptr);
for (auto &cur: this->_tables) {// cur是Node*
while (cur) {
// 保存下一个
Node *next = cur->_next;
// 头插到新表
size_t hashi = hash(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = hash(kv.first) % this->_tables.size();
// 头插
Node *newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
this->n++;
return true;
}
// 获取哈希表索引最大长度(哈希桶长度)
size_t MaxBucketSize() {
size_t max = 0;
for (int i = 0; i < _tables.size(); ++i) {
auto cur = _tables[i];
size_t size = 0;
while (cur) {
++size;
cur = cur->_next;
}
printf("[%d]->%d\n", i, size);
if (size > max) {
max = size;
}
if (max == 5121) {
printf("%d", i);
break;
}
}
return max;
}
private:
vector<Node *> _tables;
size_t n = 0;// 存储有效数据的个数
};