文章目录
- 1.哈希概念
- 2.哈希冲突
- 3.解决哈希冲突
- 3.1.闭散列
- 3.2.开散列
- 4.字符串哈希算法
1.哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。如果一个顺序结构,有N个数据,数据之间没有顺序,暴力查找时间复杂度是O(N),但如果数据之间是有序的,就可以使用二分查找能快速的找到查找的值,但是,顺序结构保证有序来存储数据,插入和删除的代价太大!
对应平衡树来说,查找的时间复杂度O(log2(N))很优秀!
但更理想(高效)的方法是:将存储的值和存储的位置一一对应,那么一次就能找到要查找的元素;哈希(散列)方法:通过哈希函数使元素的存储位置与它的关键码之间能够建立一一映射的关系;构造出来的结构称为哈希表(Hash Table)(或者称散列表)
这样的哈希函数称为 除留余数法;常用的还有,直接定址法。
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B,在关键字分分布集中的情况的下使用比较好,但是,如果存储的key范围分散如:arr[] = {1,111,999}要存储这些数据就很麻烦!
我们来看一个直接地址法的例子:字符串中第一个只出现一次字符
题目描述:给定一个字符串 s
,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1
。如:输入: s = “leetcode” 输出: 0
题目提示:s
只包含小写字母,说明关键字集中。
class Solution {
public:
int firstUniqChar(string s)
{
int temp[256] = {0};
for(char ch : s)
{
temp[ch]++;
}
for(int i = 0; i < s.size();i++)
{
if(temp[s[i]] == 1)
{
return i;
}
}
return -1;
}
};
2.哈希冲突
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
3.解决哈希冲突
3.1.闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以key存放到冲突位置中的下一个空位置中去
下面使用线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。解决哈希冲突的问题,当然还有 二次探测(H_i = (H_0 + i^2 )% m, 或者:H_i = (H_0 - i^2 )% m。其中:i =1,2,3…,H_0是通过散列函数Hash(x)对元素的关键码key 进行计算得到的位置m是表的大小。
这两种方法,在解决哈希冲突的时候对其他元素产生影响,如:现在要插入数据8,这个位置被14占了,就需要找下一个位置;当然这种方法,也会浪费大量的空间,但这就是用空间换时间的策略,后面开散列才是常用的解决哈希冲突的方法!
下面快速的使用闭散列的线性探测,实现基于闭散列的哈希表:
代码结构:
namespace order_table
{
enum state
{
EXIST = 1,
EMPTY,
DELETE
};
template<class K, class V>
struct HashData
{
std::pair<K, V> _kv;
state _st = EMPTY;
};
template<class K,class V>
class HashTable
{
typedef HashData<K, V> Data;
public:
HashTable(const size_t size = 5)
{
_table.resize(size);
}
...
private:
std::vector<Data> _table;
size_t _n = 0; // 填入表中的元素个数
};
}
插入操作:
- 通过哈希函数,计算出待插入的位置,如果没有哈希冲突(也就是说判断这个位置有没有插入的值了)直接插入,这里定义一个枚举类型来判断状态
- 如果哈希冲突,使用线性探测的方式,寻找下一个空位置!
- 在插入之前其实有一个重要的扩容问题,哈希表什么时候扩容呢? 哈希表的载荷因子定义为:α = 填入表中的元素个数 / 哈希表的长度;α 越大说明,填入表中的元素个数越多,哈希冲突的概率就会越大,所以在开放定址法中α严格定义在0.7 - 0.8之间!
- 哈希表扩容是会重新遍历,所以在扩容的那一下会消耗大一些
bool insert(const std::pair<K, V>& kv)
{
// 查找一下,不添加重复的元素
if (find(kv.first))
{
return false;
}
// 扩容
if ((double)_n / (double)_table.size() >= 0.7)
{
size_t newsize = _table.size() * 2;
HashTable tb(newsize);
for (int i = 0; i < _table.size(); i++)
{
if (_table[i]._st == EXIST)
{
tb.insert(_table[i]._kv);
}
}
_table.swap(tb._table);
}
// 通过哈希函数,计算出待插入的位置,
int hashi = kv.first % _table.size();
// 线性探测,避免哈希冲突
while (_table[hashi]._st == EXIST)
{
hashi++;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._st = EXIST;
_n++;
return true;
}
查找操作
-
查找hash表中的元素,通过哈希函数计算初步计算出了查找的位置,EXIST存在但有可能不是查找的元素,如查找14,计算到了4这个位置,但是不是要找元素,另外,当找到状态为DELETE是不能停下来的!要找到下一个空(EMPTY)位置!
-
如果找到下一个空(EMPTY)位置说明,哈希表中不存在该元素!
Data* find(const K& key)
{
int hashi = key % _table.size();
while (_table[hashi]._st != EMPTY)
{
if (_table[hashi]._st == EXIST && _table[hashi]._kv.first == key)
{
return &_table[hashi];
}
hashi++;
hashi %= _table.size();
}
return nullptr;
}
删除操作
- 查找的逻辑,然后,将状态置成DELETE即可。
bool erase(const K& key)
{
Data* data = find(key);
if (data != nullptr)
{
data->_st = DELETE;
_n--;
return true;
}
return false;
}
3.2.开散列
开散列法又叫链地址法(开链法),或称为哈希桶开辟一个指针数组,通过哈希函数计算关键字,出现哈希冲突时,将冲突的元素通过单链表的方式链接。
代码结构:
namespace hash_backet
{
template<class K,class V>
struct HashNode
{
std::pair<K, V> _kv;
HashNode* _next;
HashNode(const std::pair<K, V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K,class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable(const size_t size = 5)
{
_table.resize(size, nullptr);
}
...
private:
std::vector<Node*> _table;
size_t _n = 0; // 填入表中的元素个数
};
}
插入操作:
-
考虑扩容,由于使用哈希桶的方式解决哈希冲突的问题,是以链表的方式,对冲突元素进行链接,冲突不对影响其他元素,所以平衡因子 = 1时扩容
-
扩容使用现代方法,即重新开辟一个哈希表对象,将旧表元素插入新表中,然后旧表和新表交换!
-
哈希冲突时使用头插法
bool insert(const std::pair<K,V> kv)
{
// 避免插入重复元素
if (find(kv.first))
{
return false;
}
if (_n / _table.size() >= 1)
{
size_t new_size = _table.size() * 2;
HashTable<K, V> new_table(new_size);
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
new_table.insert(cur->_kv );
cur = cur->_next;
}
}
_table.swap(new_table._table);
}
int hashi = kv.first % _table.size();
// 插入逻辑
Node* new_node = new Node(kv);
Node *cur = _table[hashi];
_table[hashi] = new_node;
new_node->_next = cur;
_n++;
return true;
}
查找操作
Node* find(const K& key)
{
int hashi = key % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
删除操作
- 如果为NULL不用删除,返回fasle;如果删除1,即_table[hashi]这个位置,置为NULL然后删除;如果删除4,那么4位置出为cur,prev = _table[hashi];prev-> _next = cur-> _next,然后delete删除cur
bool erase(const K& key)
{
int hashi = key % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev = cur->_next;
}
delete(cur);
_n--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
4.字符串哈希算法
如果你完整的实现了上面的代码,那么使用哈希函数:int hashi = key % _table.size();
时会发现,这个哈希函数只能对能整形进行计算!
如何能对浮点数和字符串进行哈希计算呢
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct DefaultHashFunc<std::string>
{
size_t operator()(const std::string& key)
{
size_t hash = 0;
for (char ch : key)
{
hash = hash * 131 + ch;
}
return hash;
}
};
这样就支持了!关于哈希字符串函数算法:参考博客,完整的哈希桶的实现代码
namespace hash_backet
{
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct DefaultHashFunc<std::string>
{
size_t operator()(const std::string& key)
{
size_t hash = 0;
for (char ch : key)
{
hash = hash * 131 + ch;
}
return hash;
}
};
template<class K,class V>
struct HashNode
{
std::pair<K, V> _kv;
HashNode* _next;
HashNode(const std::pair<K, V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K,class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable(const size_t size = 5)
{
_table.resize(size, nullptr);
}
bool insert(const std::pair<K,V> kv)
{
// 避免插入重复元素
if (find(kv.first))
{
return false;
}
if (_n / _table.size() >= 1)
{
size_t new_size = _table.size() * 2;
HashTable<K, V> new_table(new_size);
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
new_table.insert(cur->_kv );
cur = cur->_next;
}
}
_table.swap(new_table._table);
}
DefaultHashFunc<K> dtf;
size_t hashi = dtf(kv.first )% _table.size();
// 插入逻辑
Node* new_node = new Node(kv);
Node *cur = _table[hashi];
_table[hashi] = new_node;
new_node->_next = cur;
_n++;
return true;
}
bool erase(const K& key)
{
DefaultHashFunc<K> dtf;
size_t hashi = dtf(key )% _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev = cur->_next;
}
delete(cur);
_n--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
Node* find(const K& key)
{
DefaultHashFunc<K> dtf;
size_t hashi = dtf(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
void print()
{
for (int i = 0; i < _table.size(); i++)
{
printf("%d:", i);
Node* data = _table[i];
while (true)
{
if (data== nullptr)
{
std::cout << "NULL" << std::endl;
break;
}
else
{
std::cout << data->_kv.second << "-->";
data = data->_next;
}
}
}
}
private:
std::vector<Node*> _table;
size_t _n = 0; // 填入表中的元素个数,用于计算平衡因子
};
}