本文共 6710 字,大约阅读时间需要 22 分钟。
对比之前博客讨论的二叉排序树 二叉平衡树 红黑树,它们的查找都是先从根节点进行查找,从节点取出数据或索引与查找值进行比较。那么,有没有一种函数H,根据这个函数和查找关键字key,可以直接确定查找值所在位置,而不需要一个个比较。这样就预先知道key所在的位置,直接找到数据,提升效率
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表
一个哈希函数的好不好,取决于以下三点
除留余数法(最常用)
函数:Hash(key)=key MOD p (p<=m m为表长),求出来的hash(key)就为存储该key的下标例如有一下数据{2, 4, 6, 8, 9}
表长为10,也就是数组容量为10直接定制法(常用)
取关键字的某个线性函数为散列地址(A、B为常数):Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 适用场景:适合查找较小数据范围且连续的情况平方取中法(少)
如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址。 使用举例 比如key=1234 1234^2=1522756 取227作hash地址 比如key=4321 4321^2=18671041 取671作hash地址 适用场景:事先不知道数据并且数据长度较小的情况即不同的key通过同一哈希函数产生了相同的哈希位置,H(key1)=H(key2),例如我们在除留余数法中的例子,如果此时插入一个12,其hash(12)为2,此时下标为2的位置已经有元素,此时就会产生哈希冲突
解决哈希冲突主要有两个方案:闭散列 和 开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那 么可以把key存放到冲突位置中的“下一个” 空位置中去
闭散列中主要处理方法有 线性探测 和 二次探测
思想:从计算的哈希位置开始,往后找到第一个空闲的位置存放数据
删除:当我们要删除一个元素时,不能物理上直接删除,例如我们把15删除了,此时下标为8的位置为空,当我们要查找25这个元素时,也是会从下标为5这个位置开始查找,当5这个位置不是25时,说明产生了哈希冲突,且该插入是使用的是线性探测,也就是第一个空位置插入。我们往后查找时,如果该数据存在,则在空位置之前一定存在该数。但是此时我们物理上把15删除了。查找会查找到下标为8的位置就结束查找,此时也就不会找到25这个数据了。
此时我们想要删除8这个位置上的数据时,就将该位置的状态置为DELETE,我们再次查找25这个数字时,遇到8位置就不会停止搜索,会继续往后搜索,直至遇到状态为EMPTY的位置为止。但是次方法会造成一个问题,就是有可能数据满了,如果此时还一直搜索,就不会找到空的位置,会一直搜索下去。而且如果数据比较极端且数据越来越多,产生的哈希冲突会越来越多。这就不符合我们的哈希要求的高效率的插入与查找。解决办法就是进行扩容
扩容:扩容并不是一定要等到数据满了才扩容。我们知道当数据越来越多,产生哈希冲突的次数就越多,所以我们要设定一个阈值,也就是当数据达到一定的数量时,就有必要进行扩容。而这决定这个阈值的高低的是一个叫负载因子。负载因子 = 实际存放元素 / 数组容量,范围在0~1之间,我们通常将负载因子置为[0.6, 0.8]之间。例如我们数组大小有10个,负载因为为0.7,则当插入第8个元素的时候就需要进行扩容,因为8/10=0.8>.07,也就是大于我们的负载因子就需要进行扩容。扩容的时候要注意,我们需要将原来的数据移动到新的表中,但是如果是单纯的赋值获取,那哈希冲突并没有解决,而此时我们应该将旧表中的数据重新以插入的方式插入到新的表中,从而减少哈希冲突的次数
二次探测和线性探测都属于闭散列,其原理都一样,两者的主要区别就是探测的方式不同,线性探测是如果要插入的位置已有元素,会一个一个往后查找到新的空位置。而二次探测是通过该位置的哈希冲突次数的平方来向后查找新的位置
开散列方法又叫链地址法,哈希表中存储的是链表的头结点。具有相同的哈希地址会存放在同一链表中,每个链表中的元素都具有相同的哈希地址。
插入:当我们插入时,计算出哈希地址,就可以直接定位到该key对应的链表的头结点,但是由于不能存放相同的key,我们需要遍历该链表中是否存在相同元素,如果不存在才进行插入。插入时我们可以进行头插或者尾插,这里头插会更简单些,创建key的一个新的结点cur,让该结点指向该链表的头结点,并将该链表的头结点更新为新创建的新结点cur
代码:闭散列----线性探测
enum STATE{ EXIST, DELETE, EMPTY};//哈希表:线性探测解决哈希冲突templatestruct HashNode{ pair _kv;//数据 STATE _state = EMPTY;//状态};//顺序表实现哈希template class HashTable{ public: typedef HashNode Node; HashTable(size_t n = 10) :_hTable(n) ,_size(0) { } bool insert(const pair & kv) { //0.检查容量 checkCapacity(); //1.当前元素的哈希位置 int idx = kv.first % _hTable.size(); //2.判断key是否存在 while (_hTable[idx]._state != EMPTY)//当前位置已有数据或者为删除位置,都不能存放 { //当前位置存在数据且key相同,则不能插入 if (_hTable[idx]._state == EXIST && _hTable[idx]._kv.first == kv.first) { return false; } //继续往后搜索空位置 ++idx; //走到末尾,需要从头开始找 if (idx == _hTable.size()) idx = 0; } _hTable[idx]._kv = kv; _hTable[idx]._state = EXIST; ++_size; return true; } void checkCapacity() { //负载因子[0, 1],这里定负载因子为0.7 if (_hTable.size() == 0 || _size * 10 / _hTable.size() >= 7) { //创建新表 int newC = _hTable.size() == 0 ? 10 : 2 * _hTable.size(); HashTable newHt(newC); for (size_t i = 0; i < _hTable.size(); ++i) { //将原先的表的数据插入到新的表中, if (_hTable[i]._state == EXIST) { newHt.insert(_hTable[i]._kv); } } //交换两个表的内容 Swap(newHt); } } void Swap(HashTable & Ht) { swap(_hTable, Ht._hTable); swap(_size, Ht._size); } Node* find(const K& key) { //计算位置 int idx = key % _hTable.size(); while (_hTable[idx]._state != EMPTY) { //找到 if (_hTable[idx]._state == EXIST && key == _hTable[idx]._kv.first) { return &_hTable[idx]; } ++idx; if (idx == _hTable.size()) idx = 0; } //如果遇到空格则表示没找到,返回空 return nullptr; } bool erase(const K& key) { Node* node = find(key); if (node) { //伪删除 --_size; node->_state = DELETE; return true; } return false; }private: vector _hTable;//表 size_t _size;//有效元素个数};
测试:
代码:开散列法
#include#include using namespace std;template struct HashNode{ typedef HashNode Node; K _val; Node* _next; HashNode(const K& val) :_val(val) , _next(nullptr) { }};template class HTable{ public: typedef HashNode Node; HTable(int n = 10) :_ht(n) , _size(0) { } bool insert(const K& val) { //0.检查容量 checkCapacity(); //1.计算hash位置 int idx = val % _ht.size(); //2.查找 Node* cur = _ht[idx]; while (cur) { if (cur->_val == val) return false; cur = cur->_next; } //3.插入--头插 cur = new Node(val); cur->_next = _ht[idx]; _ht[idx] = cur; ++_size; return true; } void checkCapacity() { if (_size == _ht.size()) { int newC = _size == 0 ? 10 : 2 * _size; //创建新的指针数组 vector newHt(newC); //遍历旧表 for (size_t i = 0; i < _ht.size(); ++i) { Node* cur = _ht[i]; //遍历单链表 while (cur) { Node* next = cur->_next; //计算新的位置 int idx = cur->_val % newHt.size(); //头插 cur->_next = newHt[idx]; newHt[idx] = cur; cur = next; } //旧表指针置空 _ht[i] = nullptr; } //交换新表和旧表 swap(_ht, newHt); } } Node* find(const K& val) { int idx = val % _ht.size(); Node* cur = _ht[idx]; while (cur) { if (cur->_val == val) return cur; cur = cur->_next; } return nullptr; } bool erase(const K& val) { Node* node = find(val); if (node) { int idx = val % _ht.size(); Node* cur = _ht[idx]; Node* prev = nullptr; while (cur != node) { prev = cur; cur = cur->_next; } Node* next = cur->_next; if (prev) prev->_next = next; else _ht[idx] = next; --_size; delete node; return true; } return false; }private: //指针数组 vector _ht; int _size;};
测试:
转载地址:http://tqxkk.baihongyu.com/