目录
- 介绍
- 哈希概念
- 哈希冲突
- 哈希函数
- 解决哈希冲突
- 闭散列
- 介绍
- 线性探测
- 二次探测
- 负载因子
- 实现
- 哈希表结构
- 哈希函数
- 元素查找
- 插入元素
- 删除元素
- 开散列
- 介绍
- 实现
- 哈希表结构
- 元素查找
- 插入元素
- 删除元素
- 析构函数
介绍
哈希概念
了解过搜索二叉树与红黑树后,它们的结构特点主要是为了进行快速查找, O ( l o g 2 N ) O(log_2N) O(log2N)的时间复杂度,通常在几次关键码的比较后就能找到目标元素。但是最最理想的搜索,是能够像数组那样,知道元素的下标,直接就可以访问,时间复杂度达到 O ( 1 ) O(1) O(1)。
其实哈希结构就是与数组类似的,通过元素的关键码计算出下标(哈希映射)。通过这一步计算,可以将元素存储到对于位置,同样也可以在对应位置访问该元素。
示例:将6个数字存储到哈希结构中
哈希冲突
对于两个数据元素i和j,其中关键码 k i ≠ k j k_i \ne k_j ki=kj,但是hash( k i k_i ki) = hash( k j k_j kj)
即:不同关键字通过相同哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
当插入数字9时,进行哈希计算后下标为1,但是该位置以及存在其他元素。
哈希函数
引起哈希冲突的一个原因:哈希函数不够合理
哈希函数:能将任意长度的关键码映射成固定长度的数据(下标)
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
域必须在0到m-1之间 - 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该较简单
常见的哈希函数(了解):
- 直接定址法
取关键字的某个线性函数为散列地址:$Hash(Key)= A*Key + B $
- 除留余数法
散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数
H
a
s
h
(
k
e
y
)
=
k
e
y
m
o
d
p
(
p
<
=
m
)
Hash(key) = key \bmod p (p <= m)
Hash(key)=keymodp(p<=m)
选择质数作为除数:由于质数本身不存在多余的因子,所以与其他数做取模运算的结果更加分散
- 平方取中法
例如:关键字为1234,平方为1522756,选取中间3位:277作为哈希地址
- 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址
- 随机数法
H a s h ( k e y ) = r a n d o m ( k e y ) Hash(key) = random(key) Hash(key)=random(key)
解决哈希冲突
解决哈希冲突两种常见的方法是:闭散列和 开散列
闭散列
介绍
闭散列也叫做开放定址法
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中下一个空位置中
线性探测
当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止
int index = Hash(key);
while(hashTable[index].state == EXITS)
{
++index;//向后遍历
}
对于发生冲突的情况,向后遍历寻找最近的一个空位置—线性探测
线性探测的缺陷是产生冲突的数据会堆积在一起 ,寻找某个关键码可能需要多次比较,导致效率降低。
二次探测
寻找下一个空位置时,不再挨着逐个去找,选择逐步跳跃式的去找,避免冲突堆积
int index = Hash(key);
int start = index, i = 0;
while(hashTable[index].state == EXITS)
{
index = start + i * i;
++i;
}
插入数字16时,发生冲突,选择二次探测的方式查找空位置
负载因子
随着插入数据的增多, 插入的数据产生冲突的概率也增加了。冲突的增加,在查找时的效率也会降低。
装载因子(load factor) = 表中有效数据个数 / 空间大小
- 装载因子越大,数据越多,冲突的可能性越大
- 装载因子越小,数据越少,冲突的可能性越小
通常来说,当哈希表的装载因子超过0.7时就需要考虑进行扩容,否则可能会导致插入/查找操作的效率大幅下降。
实现
哈希表结构
enum State
{
EMPTY, //空状态
EXITS, //使用状态
DELETE, //删除状态
};
template<class K, class V>
struct HashDate
{
pair<K, V> _kv; // 存储数据的键值对
State _state; // 标记状态
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
private:
vector<HashDate<K, V>> _table; // 哈希数组
size_t _n = 0; // 有效数据个数,来计算装载因子
};
哈希函数
将k类型的关键码转换为整型
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化 20:12继续
template<>
struct HashFunc<string>
{
// BKDR哈希算法
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
BKDR哈希算法是由徐盛忠教授于1992年提出的一种哈希函数,它是通过将字符串视为整数来计算哈希值的。其基本思想是使用一个较小的质数seed(比如31、131、1313、13131等),将字符串中各个字符的ASCII码转化为整数后,与seed相乘并求和得到一个哈希值
元素查找
HashDate<K, V>* find(const K& key)
{
if (_table.size() == 0)//没有元素
{
return nullptr;
}
HashFunc hf; //通过仿函数将关键码转换为整型
size_t index = hf(key) % _table.size();//除留余数法
// 线性探测
while (_table[index]._state != EMPTY)
{
// 找到待查找元素,并返回它的地址
if (_table[index]._state == EXITS &&
_table[index]._kv.first == key)
{
return &_table[index];
}
++index;
index %= _table.size();
}
return nullptr;// 不存在此元素
}
插入元素
bool insert(const pair<K, V>& kv)
{
if (find(kv.first))
{
return false; // 元素已存在
}
if (_table.size() == 0)
{
_table.resize(10); //设置哈希表初始空间大小
}
// 装载因子超过阈值0.7就进行扩容
else if( _n * 1.0 /_table.size() > 0.7)
{
//创建一个新的哈希表,大小为原哈希表的2倍
HashTable<K, V> newHT;
newHT._table.resize(2 * _table.size());
//将原数据插入到新哈希表
for (auto& e : _table)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
//交换两个哈希表
_table.swap(newHT._table);
}
// 用哈希函数算出哈希表中的映射位置
HashFunc hf;
size_t index = hf(kv.first) % _table.size();
// 线性探测
while (_table[index]._state == EXITS)
{
++index;
index %= _table.size(); // 防止下标越界
}
//插入元素
_table[index]._kv = kv;
_table[index]._state = EXITS;
++_n;
return true;
}
删除元素
bool erase(const K& key)
{
//查找该元素
HashDate<K, V>* ret = find(key);
if (ret == nullptr)
return false; //不存在
else // 找到
{
ret->_state = DELETE;//删除
--_n;
return true;
}
}
开散列
介绍
开散列也叫做链地址法
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
产生哈希冲突的元素会通过链表组织起来,形成一个子集合-----哈希桶
实现
哈希表结构
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 Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
private:
vector<Node*> _tables;//哈希表
size_t _size = 0; // 存储有效数据个数
};
元素查找
Node* find(const K& key)
{
if (_table.size() == 0)
return nullptr; //哈希表为空
//哈希映射
hash hash;
size_t index = hash(key) % _table.size();
Node* cur = _table[index];
while (cur != nullptr)
{
if (cur->_kv.first == key)
return cur;
else cur = cur->_next;
}
return nullptr;
}
插入元素
bool insert(const pair<K, V>& kv)
{
// 去重
if (Find(kv.first)) return false;
if (_table.size() == 0)
{
_table.resize(53); //设置哈希表初始空间大小
}
// 装载因子超过阈值0.7就进行扩容
else if (_n * 1.0 / _table.size() > 0.7)
{
//创建一个新的哈希表,大小为原哈希表的2倍
vector<Node*> newTables;
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 旧表中节点移动映射新表
Hash hash;
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];//首节点
while (cur != nullptr)
{
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newTables.size();
//头插
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
//交换两个哈希表
_tables.swap(newTables);
}
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
// 头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
//返回质数
size_t __stl_next_prime(size_t n)
{
static const size_t __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
};
for (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
使用质数作为除数
删除元素
bool erase(const K& key)
{
//哈希表为空
if (_tables.size() == 0) return nullptr;
//find
Hash hash;
size_t hashi = hash(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;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
析构函数
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
🦀🦀观看~~