目录
unordered系列关联式容器
介绍
虽然红黑树效率很高,但数据量达到一定程度时,查找效率也不够理想
所以就有了unordered系列的容器
- 它们提供了快速的查找操作,平均时间复杂度为O(1)
- 但是它们不会维护元素的顺序,因此不适合需要按顺序访问元素的情况
而unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构,就像set和map的底层是红黑树一样
哈希(散列)
引入
顺序结构/平衡树中
- 元素关键码与其存储位置之间没有对应的关系
- 因此在查找一个元素时,必须要经过关键码的多次比较
- 顺序查找时间复杂度为O(N)
- 平衡树中为树的高度,即 O(logN) ,他可以通过结构特点,将要比较的元素个数,压缩到了高度次
- 他们搜索的效率取决于搜索过程中元素的比较次数
元素关键码
元素的关键码(key)是数据结构中用于标识和访问元素的重要属性或值
关键码通常用于确定元素在数据结构中的位置,以便能够对元素进行查找、插入、删除等操作
理想的搜索方法
可以不经过任何比较,一次直接从表中得到要搜索的元素(也就是将 元素的存储位置与它的关键码之间建立一一 映射的关系)
如果构造一种存储结构,通过某种函数(hashFunc),可以实现这种功能那么在查找时,通过该函数可以很快找到该元素这种方法,也就是哈希方法
介绍
哈希是计算机科学中的一个重要概念
是一种将 任意大小的输入数据 映射为 固定大小的哈希值的技术
哈希值特点
- 通常是一个整数或二进制序列
唯一性:不同的输入数据应该映射到不同的哈希值,确保哈希值能够唯一标识输入数据。(但不能保证一个哈希值不能对应多个数据)
一致性:相同的输入数据始终映射到相同的哈希值,保证数据的一致性
高效性:计算哈希值的过程应该是高效的,通常在常量时间内完成,以便在实际应用中能够快速执行
不可逆性:从哈希值不能反推出原始输入数据,确保数据的安全性
哈希函数/算法
定义了如何将输入数据转换为哈希值,并满足哈希函数的各种特性
插入元素
根据待插入元素的关键码,用哈希函数计算出该元素的存储位置,并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的哈希值作为元素的存储位置
分类
哈希表
介绍
大概就是:在一个数组中,用该数组可使用空间的大小作为除数,余数就是该元素存放位置的下标
但是,如果此时,有11插入,他就会被放在2的位置
因为1坑位已经被占用了,但总不能不让他插入吧,所以该怎么办呢?
哈希冲突/碰撞
上面提到的,当1已经有元素时,又有一个11插入进来,这种情况就被叫做哈希冲突/碰撞
也就是 -- 不同关键字通过相同的哈希函数,计算出相同的哈希地址
- 通过设计更好的哈希函数,可以让产生哈希冲突的可能性降低
- 但是无法避免哈希冲突
同义词
这些具有不同关键码,而具有相同哈希地址的数据元素
解决方法
(毕竟不能一冲突代码就罢工了吧)
闭散列和开散列
闭散列/开放定址法
介绍
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去以上方法也被叫做"线性探测法"
插入元素
- 根据待插入元素的关键码,用哈希函数计算出该元素的存储位置,并按此位置进行存放
- 如果该位置已有元素(因为无法真正实现哈希值和数据一对一,所以一个哈希值可以对应多个数据),就往后寻找空位
搜索元素
- 对元素的关键码进行同样的计算,把求得的哈希值作为元素的存储位置
- 在结构中按此位置取元素比较,若关键码相等,则搜索成功
- 如果该位置不是,也是一样,继续往后寻找,直到遍历完整个结构
删除元素
不能简单的将元素删除
- 比如,如果用vector的erase,就会让后面的数据往前挪一个位置,但是这样势必会影响那些数据的搜索!
- 要记住,哈希里面,位置和数据是有关联的, 不能随意挪动!!!
也不能将该数据赋值为某个值
- 比如,我们平常一般就可能把这个值赋值为0/-1,但是,如果真的有0/-1的元素该怎么办,这样就会产生混淆
- 所以要额外设置一个状态变量,其中有三种状态,存在/空/删除
为什么会有删除状态呢?
- 如果只有存在/空状态,那么删除元素后,该位置状态就被设置为空
- 就会导致该位置之后的位置无法被搜索到,因为搜索是遇到空就停下的!
- 所以,存在三种状态是必要的
扩容
还记得我们是用数组来组织这些元素的吗?所以,我们一般使用vector作为底层
- 但是!
- vector中的size是实际插入的元素个数,capacity虽然是容量,但比size多的那部分我们是没法直接使用的!
- 因为vector的[ ]内部是会检查传入下标的,一定要<size()才行
- 所以,我们得让size()充当容量,于是,resize成为我们的最佳选择,通过resize,我们就可以让size成为实际的"容量",而在哈希表内部再定义一个变量,作为实际的元素个数
- 当元素个数达到一定程度时,我们必须得扩容了(因为,如果哈希表太满,一定会让插入时的冲突增大,最后搜索元素的复杂度很可能会变成O(N),这不符合我们的预期,所以,一定要扩容!而且不能满时再扩!!!)
- 我们这里使用0.7作为载荷因子
- 而且扩容可以重新分配元素的位置,以前冲突的元素,可能有一部分就不冲突了
- 所以,为了减少冲突的元素,将元素个数=存在的元素+已删除的元素,因为插入元素就有可能会导致后面的元素产生冲突,删除后,这个冲突依然存在,所以,扩容的判断也要包括删除的元素
二次探测
线性探测会让冲突越堆越多,因为它占用了别人的坑,所以就有其他方法的出现,比如二次探测
介绍
- 当发生哈希冲突时,不仅线性地查找下一个可用位置,还使用二次探测来计算下一个桶的位置
- 首先计算一个初始位置(也就是哈希值)
- 如果初始位置已经被占用,则计算下一个位置(它不是简单地加一个固定的偏移量,而是使用一个二次函数来计算下一个位置)
- 这个二次探测函数中的i会逐渐增加,直到找到一个空的桶,然后将键值对插入到该桶中。
优点
能够减少桶之间的聚集,因为不同键的冲突通常会导致它们在哈希表中分布均匀
然而,它仍然可能在高负载时产生聚集,因此仍然需要适当的哈希表大小和负载因子控制
模拟实现
注意点
string的处理
- 整型家族可以直接计算出哈希值,但是, string是无法被直接计算的
-> 所以,要对string做出特殊处理
- 如果仅仅是将首字符作为哈希值的话,非常容易冲突
-> 所以可以将整个字符串的字符都加起来
- 但是这样也不够,字符加起来也很容易撞(比如同样字符不同顺序)
-> 所以,每次相加前可以将这个数字*某个数字,这样可以避免出现不同顺序的问题
模板
- 为了让这个哈希表具有泛性,所以采用仿函数
- 直接将传入的不同类型的参数转换成整型值,而不是在insert内部转换
- 在其内部直接把得到的值转成哈希值就行
哈希表元素类型
因为unordered系列的set和map底层是哈希表,所以类比红黑树版本的set和map,哈希表也应该是key-value版本的
这里就直接将pair<K,V>作为元素类型
扩容
这里扩容时,可以选择复用insert代码(也就是创建一个新对象,将已有元素插入到新对象,然后和自己互换即可)
代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 哈希表
namespace my_hash_table
{
// func用于拿到数据对应的整型值->然后可以得到对应的hashi
template <class T>
class HSFunc
{
public:
size_t operator()(const T& val)
{
return val;
}
};
template <>
class HSFunc<string>
{
public:
size_t operator()(const string& s)
{
int size = s.size();
unsigned int seed = 131; // 31 131 1313 13131 131313 都可以
unsigned int hashi = 0;
for (size_t i = 0; i < size; ++i)
{
hashi = hashi * seed + s[i];
}
return hashi;
}
};
enum State
{
EMPTY,
EXIST,
DELETE
};
template <class K, class V, class HF = HSFunc<K>>
class HashTable
{
struct Elem
{
pair<K, V> _val;
State _state;
};
typedef HashTable<K, V> Self;
public:
HashTable(size_t capacity = 5)
: _ht(capacity), _size(0), _totalSize(0)
{
for (size_t i = 0; i < capacity; ++i)
{
_ht[i]._state = EMPTY;
}
}
// 插入
bool Insert(const pair<K, V>& val)
{
if (Find(val.first) != -1)
{
return false;
}
if (CheckCapacity()) // 设置当因子超过0.7就进行扩容
{
size_t newsize = _ht.size() * 2;
Self newht(newsize);
for (size_t i = 0; i < _ht.size(); ++i)
{
if (_ht[i]._state == EXIST)
{
newht.Insert(_ht[i]._val);
}
}
Swap(newht);
}
size_t hashi = HashFunc(val.first);
while (_ht[hashi]._state == EXIST)
{
++hashi;
hashi %= _ht.size();
}
_ht[hashi]._val = val;
_ht[hashi]._state = EXIST;
++_size;
++_totalSize;
return true;
}
// 查找
size_t Find(const K& key)
{
size_t hashi = HashFunc(key);
for (size_t i = 0; i < _size; ++i)
{
if (_ht[i]._state == EXIST && _ht[i]._val.first == key)
{
return i;
}
}
return -1;
}
// 删除
bool Erase(const K& key)
{
size_t i = Find(key);
if (i == -1)
{
return false;
}
else
{
_ht[i]._state = DELETE;
--_size;
return true;
}
}
size_t Size() const
{
return _size;
}
bool Empty() const
{
return _size == 0;
}
void Print()
{
for (size_t i = 0; i < _ht.size(); ++i)
{
if (_ht[i]._state != EXIST)
{
continue;
}
else
{
Elem cur = _ht[i];
printf("[%d]->", i);
cout << cur._val.first << ":" << cur._val.second << endl;
}
}
cout << endl;
}
void Swap(Self& ht)
{
swap(_size, ht._size);
swap(_totalSize, ht._totalSize);
_ht.swap(ht._ht);
}
private:
size_t HashFunc(const K& key)
{
HF hf;
return hf(key) % _ht.size();
}
bool CheckCapacity()
{
if (_totalSize * 10 / _ht.size() >= 7)
{
return true;
}
return false;
}
private:
vector<Elem> _ht;
size_t _size;
size_t _totalSize; // 哈希表中的所有元素:有效和已删除, 扩容时候要用到
};
}
开散列/开链法
介绍
是哈希表的一种常见实现方式
特点是在哈希表的每个位置维护一个链表或其他数据结构,用于存储具有相同哈希值的键值对
这样的话,冲突的数据就不会影响别的元素了
插入元素
还是一样的,先算出哈希值
- 如果该位置是空,就直接赋值
- 如果不为空,直接头插即可(这里使用单链表,因为不需要来回找,只需要一次遍历即可)
- 这样就让冲突的元素都在自己应该的位置上,而不会污染其他位置
搜索元素
- 把求得的哈希值作为元素的存储位置
- 然后在该位置上的链表进行遍历
删除元素
- 这里的删除就变成了单链表的删除
- 因为实际上元素之间的组织方式是链表
扩容
- 虽然引入链表之后,解决了会不断累积的哈希冲突
- 但随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响哈希表的性能
- 所以,还是需要扩容,然后让位置上挂的结点分散一点
- 扩容的话,和闭散列不同,它可以直接挂结点
- 用新设置的容量计算出哈希值,将原对象的结点直接复用,头插到新对象里就行
模拟实现
注意点
基本上其实和闭散列一样,只是组织数据的方式不同
也没啥注意点,只要方法对就行
代码
namespace my_hash
{
// func用于拿到数据对应的整型值->然后可以得到对应的hashi
template <class T>
class HashFunc
{
public:
size_t operator()(const T& val)
{
return val;
}
};
template <>
class HashFunc<string>
{
public:
size_t operator()(const string& s)
{
int size = s.size();
unsigned int seed = 131; // 31 131 1313 13131 131313 都可以
unsigned int hashi = 0;
for (size_t i = 0; i < size; ++i)
{
hashi = hashi * seed + s[i];
}
return hashi;
}
};
template <class V>
struct HashBucketNode // 每个位置下链接的结点
{
HashBucketNode(const V& data)
: _next(nullptr), _data(data)
{
}
HashBucketNode<V>* _next;
V _data;
};
// 这里的哈希桶中key是唯一的
template <class V, class HF = HashFunc<V>>
class HashBucket // 哈希桶
{
typedef HashBucketNode<V> Node;
typedef Node* PNode;
typedef HashBucket<V, HF> Self;
public:
HashBucket(size_t capacity)
: _size(0)
{
_table.resize(capacity, nullptr);
}
~HashBucket()
{
Clear();
}
// 哈希桶中的元素不能重复
PNode Insert(const V& data)
{
if (Find(data))
{
return nullptr;
}
if (CheckCapacity()) // 需要扩容了
{
size_t newsize = _size * 2;
HashBucket<V> newhsb(newsize);
for (size_t i = 0; i < _size; ++i)
{
PNode cur = _table[i];
while (cur) // 把桶上的结点挂在新位置
{
PNode next = cur->_next;
size_t hashi = HashFunc(cur->_data, newsize);
cur->_next = newhsb._table[hashi];
newhsb._table[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
Swap(newhsb);
}
PNode newnode = new Node(data);
size_t hashi = HashFunc(data, _table.size());
// 头插
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_size;
return newnode;
}
// 删除哈希桶中为data的元素(data不会重复)
bool Erase(const V& data)
{
PNode del = Find(data);
if (del == nullptr) // 如果没找着
{
return false;
}
size_t hashi = HashFunc(data, _table.size());
PNode prev = _table[hashi];
if (prev == del) // 如果删除的是第一个结点
{
_table[hashi] = del->_next;
}
else
{
while (prev && prev->_next->_data != data) // 找到上一个结点
{
prev = prev->_next;
}
prev->_next = del->_next;
}
delete del;
del = nullptr;
--_size;
return true;
}
PNode Find(const V& data)
{
size_t hashi = HashFunc(data, _table.size());
PNode cur = _table[hashi];
while (cur)
{
if (cur->_data == data)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
size_t Size() const
{
return _size;
}
bool Empty() const
{
return 0 == _size;
}
void Print()
{
for (size_t i = 0; i < _table.size(); ++i)
{
PNode cur = _table[i];
printf("[%d]:", i);
while (cur)
{
PNode next = cur->_next;
cout << cur->_data << " ";
cur = next;
}
cout << endl;
}
cout << endl;
}
void Clear()
{
for (size_t i = 0; i < _table.size(); ++i)
{
PNode cur = _table[i];
while (cur)
{
PNode next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
size_t BucketCount() const
{
return _table.size();
}
void Swap(Self& ht)
{
_table.swap(ht._table);
swap(_size, ht._size);
}
private:
size_t HashFunc(const V& data, size_t size) // 将数据转换成hashi
{
HF hf;
return hf(data) % size;
}
bool CheckCapacity()
{
if (_size == _table.size())
{
return true;
}
else
{
return false;
}
}
private:
vector<PNode> _table;
size_t _size; // 哈希表中有效元素的个数
};
}