哈希表(Hash Table)
概念
不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
向该结构中插入元素时:根据待插入元素的关键码,以此函数计算出该元素的存储位置。
在该结构中搜索元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中寻找此位置的元素与欲查找的元素的关键码比较,若相等则代表搜索成功
上述方式即为哈希方法,此方法中用到的函数称为“哈希函数”,构造出来的这种结构称为“哈希表”(Hash Table)。
映射关系
哈希函数
把元素/键值映射到空间的一个位置
特点:
- 映射的位置范围要小于等于空间范围
- 映射的位置要尽量均匀
- 映射关系尽量简单
一些常用的哈希函数:
1.除留余数法:元素/键值 % 空间大小(通用方法)
2.直接定址法:线性函数,A*X(元素//键值)+ B(只适合范围比较紧凑的数据,例如字符)
哈希冲突
不同的数据,经过哈希函数,映射到了同一个位置
注意:只要空间小于数据范围,那么就必然会出现哈希冲突
解决哈希冲突:
- 闭散列(开放定址法)(出现哈希冲突,无法保证效率)
线性探测,二次探测
线性探测:
插入
a. 通过哈希函数计算哈希位置
b. 如果当前位置为空,则进行插入操作
c. 如果当前位置不为空,则从该位置开始,向后找到第一个为空的位置,再插入
查找
a. 通过哈希函数计算哈希位置
b. 查看当前位置的数据是否和查找的数据相同,相同则查找成功并结束
c. 如果不相同,则从当前位置开始向后查找,直到找到数据或者走到了空的位置,则查找结束
删除(不是真正的删除)
a. 先进行查找操作
b. 如果找到了需要删除的数据,把该数据所在位置标记为删除状态
二次探测
与线性探测基本相同,区别在于,每次偏移的长度是上一次的平方,可以减少数据扎堆出现的情况,从而减少哈希冲突出现的情况
闭散列代码具体如下
//定义空间位置的三种状态
enum State {
Empty,
Exist,
Delete
};
template<class K,class V>
//定义哈希节点
struct HashNode{
pair<K, V> _val;
State _state;
HashNode(const pair<K, V>& val = pair<K, V>())
:_val(val)
,_state(Empty)
{}
};
template<class K,class V>
//定义哈希表
class HashTable {
public:
HashTable(size_t n = 10) {
_table.resize(n);
_size = 0;
}
//插入
bool insert(const pair<K, V>& val) {
//检查容量
checkCapacity();
//计算哈希位置
int id = val.first%_table.size();
//检查该位置的状态,看是否可用,以及检查要插入的数据是否已经存在
while (_table[id]._state == Exist) {
if (_table[id]._val.first == val.first) {
return false;
}
id++;
//走到空间的结尾,回到空间的开始位置
if (id == _table.size()) {
id = 0;
}
}
//找到了合适位置
_table[id]._val = val;
_table[id]._state = Exist;
_size++;
return true;
}
//检查容量以及扩容
void checkCapacity() {
//当空间被占百分之80的时候进行扩容
if (_size * 10 / _table.size() >= 8) {
HashTable Newht(2 * _table.size());
//旧表元素要重新插入到扩容之后的新表之中
for (int i = 0; i < _table.size(); i++) {
if (_table[i]._state == Exist) {
Newht.insert(_table[i]._val);
}
}
//新表替换旧表,完成扩容
swap(_table, Newht._table);
}
}
//查找
HashNode<K, V>* find(const K& key) {
int id = key % _table.size();
while (_table[id]._state != Empty) {
if (id == _table.size()) {
id = 0;
}
if (_table[id]._state == Exist && _table[id]._val.first == key) {
cout << "查找成功,K值为" << key << "的数据在下标为" << id << "的位置" << endl;
return &_table[id];
}
else {
id++;
}
}
cout << "没有找到K值为" << key << "的数据" << endl;
return nullptr;
}
//删除
bool erase(const K& key) {
HashNode<K, V>* result = nullptr;
int id = key % _table.size();
while (_table[id]._state != Empty) {
if (id == _table.size()) {
id = 0;
}
if (_table[id]._state == Exist && _table[id]._val.first == key) {
result = &_table[id];
break;
}
else {
id++;
}
}
if (result) {
result->_state = Delete;
_size--;
return true;
}
return false;
}
private:
vector<HashNode<K, V>> _table;
size_t _size;
};
- 开散列(拉链法,哈希桶)
哈希桶
每个空间节点下面都会挂一个单链表或者红黑树,如下图
开散列代码具体如下
template<class K>
struct KeyofValue {
const K& operator()(const K& key) {
return key;
}
};
//开散列,哈希桶方法,指针数组 + 单链表
template<class V>
struct HashNode {
V _val;
HashNode<V>* _next;
HashNode(const V& val = V())
:_val(val)
,_next(nullptr)
{}
};
template<class K,class V,class KeyofValue>
class HashTable {
public:
typedef HashNode<V> Node;
//插入(要么是K,V键值对,要么是V)
bool insert(const V& val) {
//检查容量及扩容
checkCapacity();
//计算位置
KeyofValue kov;
int id = kov(val) % _table.size();
//检查kov是否已经存在
Node* cur = _table[id];
while (cur) {
if (kov(cur->_val) == kov(val)) {
return false;
}
cur = cur->_next;
}
//插入:使用头插
cur = new Node(val);
cur->_next = _table[id];
_table[id] = cur;
_size++;
return true;
}
//检查容量及扩容
void checkCapacity() {
if (_size == _table.size()) {
size_t newSize = _size == 0 ? 5 : 2 * _size;
vector<Node*> Newht;
Newht.resize(newSize);
KeyofValue kov;
//遍历旧表中的非空单链表
for (int i = 0; i < _table.size(); i++) {
Node* cur = _table[i];
//遍历当前单链表
while (cur) {
//记录旧表中的下一个元素
Node* next = cur->_next;
//计算在新表中的位置
int Newid = kov(cur->_val%Newht.size());
//头插
cur->_next = Newht[Newid];
Newht[Newid] = cur;
//处理下一个元素
cur = next;
}
_table[i] = nullptr;
}
_table.swap(Newht);
}
}
//查找
Node* find(const K& key) {
if (_table.size() == 0) {
return nullptr;
}
int id = key % _table.size();
Node* cur = _table[id];
KeyofValue kov;
while (cur) {
if (kov(cur->_val) == key) {
return cur;
}
cur = cur->_next;
}
return nullptr;
}
//删除
bool erase(const K& key) {
if (_table.size() == 0) {
return false;
}
int id = key % _table.size();
Node* cur = _table[id];
Node* prev = nullptr;
KeyofValue kov;
while (cur) {
if (kov(cur->_val) == key) {
//如果删除的是头结点,直接更新头结点
if (prev == nullptr) {
_table[id] = cur->_next;
}
else {
prev->_next = cur->_next;
}
delete cur;
_size--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _table;
size_t _size = 0;
};
负载因子:实际存放的元素个数/空间大小