赞
踩
哈希是一种高效用来搜索的数据结构,与传统的查找方式进行比较,发现传统的方式都需要进行元素的比较,性能高低取决于元素的比较次数。让元素在查找时不进行比较,或者减少比较次数。
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2 N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一对一映射的关系,那么在查找时通过该函数可以很快找到该元素。
1.通过函数计算插入位置,该函数成为哈希函数
2.通过计算出的存储位置进行元素的插入,通过这种方法构造出来结构称为哈希表(散列表)
1.通过哈希函数对关键码进行计算,得出元素的存储位置
2.根据存储位置在哈希表中,取出元素进行比较,若关键码相同,则查找成功。
这里使用除留余数法举例
对于两个数据元素的关键字 ki 和 kj (i != j),有 ki != kj,但有:Hash(ki) == Hash(kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
举例:
上述图片中,1的位置上已经插入了一,如果我们还想插入11
,使用除留余数法11 % 10 = 1
,插入位置还是 1 的位置,这就发生了哈希冲突。
原理:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀 缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
原理:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
注意: 一般情况下,p最好是一个不超过m的最大素数
原理:假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
场景:平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
原理:折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
场景:折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
不论一个哈希函数设计的有多么精妙,都不能完全解决哈希冲突,只能是,哈希函数越精妙,产生的哈希冲突概率越低
闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
原理
从发生冲突的位置逐个挨着一次往后查找,如果查找到空间的末尾,也没有发现空位置时,再折回空间起始位置继续查找,直到找到空位置插入进去。
缺陷
容易产生数据堆积,如果发生了冲入,冲突的元素可能连成一片,因为查找下一个空位置的方式,是逐个向后查找到。
如何避免
不要挨着依次向后查找空位置,二次探测
。
解决数据堆积的问题,每次查找空位置时,都向后偏移不同的举例进行检测。
查找下一个空位置的方法
H(i) = (H0 + i ^ 2) % cap
/H(i) = (H0 - i ^ 2) % cap
所以:H(i + 1) = H(i) + 2 * i + 1
缺陷
当表格中空位置比较少时,可能需要查找很多次
当表格中数据较多时,或者哈希表中的空位置较少时,发生冲突的概率非常高,而且找下一个空位置的效率比较低,不论是线性探测还是二次探测都需要找好多次才能找到。
当哈希表中有效元素比较多时,对哈希表的性能影响非常大。
哈希期望查找时间复杂度是 O(1),但空位置比较少时,性能远远超过O(1).
哈希使用时间换空间
解决方法
不要让表格存储太多元素,达到一定程度时就进行扩容,即: 哈希表中的元素一定不会存满
负载因子 = 填入表的有效元素的个数 / 散列表的长度
线性探测为:70%左右,二次探测为:50%~60%,在这个区间内就需要扩容了,存的多了就会导致冲突概率上升,存的少了就会空间利用率降低。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
数组中的每个元素都是链表,每条链表中挂的都是发生哈希冲突的元素
操作
插入或查找时,当中存在一个循环,即:遍历链表,检测data是否存在,表面上时间复杂度O(n),但实际认为操作时的时间复杂度为O(1),因为链表不会很长
1. 如果元素特别多,或元素特别特殊,大部分元素都集中到了某一个链表中
导致某些链表特别长,在哈希桶中查找某个元素,就退化成了在单链表中查找。
如果每个哈希桶中的元素不是特别多的情况下,哈希桶的时间复杂度为O(1).
如果哈希桶当中某些链表中挂的结点非常多,哈希桶就退化成了在链表中进行查找,时间复杂度变成了O(n)。
所以在插入元素的过程中,需要避免上述的情况,在闭散列中,元素多到一定程度通过扩容来降低冲突概率,哈希桶中也是如此,通过扩容,把链表中的结点分散开来。
2. 什么情况下进行扩容?什么情况下哈希桶最优(空间利用率高、查找性能高)?
最优状态: 每个桶只挂一个结点。当哈希桶中元素个数与桶的个数相同时就要考虑扩容。因为扩容之后,哈希桶的容量变了,哈希函数计算出来的结果就发生了变化,再将旧哈希桶中的元素往新哈希桶中插入,就可以基本到达较优。
3. 随着哈希桶中的元素不断增多,哈希桶的的容量也会不断增大,怎么解决?
扩容之后,继续往哈希桶中插入元素,或者对方在向哈希表中插入元素是,每个插入哈希桶中的元素都是发生哈希冲突的,可能又会出现某些链表特别长的场景出现,扩容之后依旧不能达到目的。(哈希桶性能又下降了,但是还没有达到扩容的时机)。
解决方案
当链表中的结点达到一定的阈值,还没有达到扩容的条件时,可以将链表转换成红黑树,当结点个数小于阈值就转换成链表。
4. 哈希函数
除留取余法:最好模上一个素数,发生冲突的概率相对而言能稍微低一些,而元素的个数与表格容量相等时,需要扩容,按照两倍的方式扩容。可以用素数表将含有两倍关系的素数存下来。
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
5. 泛型存储
哈希函数中任意类型都能存储,但是使用除留取余法,只能转换整数,所以还要传入一个模板参数,可以将其它类型进行转换,使之能够使用除留取余法。
#include <iostream> using namespace std; #include <vector> #include "Common.h" // 哈希桶:数组+链表(无头单链表) template<class T> struct HashBucketNode { HashBucketNode<T>* next; T data; HashBucketNode(const T& x = T()) : next(nullptr) , data(x) {} }; // T 如果是整形家族 template<class T> class T2IntDef { public: const T& operator()(const T& data) { return data; } }; class Str2Int { public: size_t operator()(const string& s) { const char* str = s.c_str(); unsigned int seed = 131; // 31 131 1313 13131 131313 unsigned int hash = 0; while (*str) { hash = hash * seed + (*str++); } return (hash & 0x7FFFFFFF); } }; // 假设:当前实现的哈希表中元素唯一 // T:哈希表中存储的元素的类型 // T2Int: 将T转换为整形的结果 template<class T, class T2Int = T2IntDef<T>> class HashBucket { typedef HashBucketNode<T> Node; public: HashBucket(size_t capacity = 53) : table(GetNextPrime(capacity)) // n个值为data的构造方法在构造哈希桶 , size(0) {} ~HashBucket() { Destroy(); } /* 虽然从代码层面来看,Insert当中存在一个循环,即:遍历链表,检测data是否存在---->表面上Insert的时间复杂度O(N) 但是实际认为Insert的时间复杂度仍旧为O(1)---->因为:在哈希桶中,每个桶对应的链表当中的节点的格式都不是非常多 */ bool Insert(const T& data) { // 0. 检测是否需要扩容 CheckCapacity(); // 1. 通过哈希函数计算元素所在桶号 size_t bucketNo = HashFunc(data); // 2. 检测元素是否存在 Node* cur = table[bucketNo]; while (cur) { if (data == cur->data) return false; cur = cur->next; } // 3. 插入元素 cur = new Node(data); cur->next = table[bucketNo]; table[bucketNo] = cur; ++size; return true; } bool Erase(const T& data) { // 1. 先通过哈希函数计算元素所在的桶号 size_t bucketNo = HashFunc(data); // 2. 找元素在bucketNo桶中是否处在,存在则删除 // 核心操作--->删除链表当中只为data的节点 // 该节点可能是链表中的第一个节点 // 该节点可能是链表中的非第一个节点 Node* cur = table[bucketNo]; Node* prev = nullptr; // 标记cur的前一个节点 while (cur) { if (data == cur->data) { // 删除cur节点 // 删除的节点如果是第一个节点 if (nullptr == prev) table[bucketNo] = cur->next; else prev->next = cur->next; delete cur; --size; return true; } else { // 当前节点不是要找的data,则两个指针同时往后移动 prev = cur; cur = cur->next; } } // 哈希桶中不存在值为data的元素,无法删除即删除失败 return false; } Node* Find(const T& data) { // 1. 先通过哈希函数计算元素所在的桶号 size_t bucketNo = HashFunc(data); // 2. 在检测该元素是否存在对应的链表中 Node* cur = table[bucketNo]; while (cur) { if (data == cur->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.capacity(); ++i) { Node* cur = table[i]; cout << "table[" << i << "]:"; while (cur) { cout << cur->data << "--->"; cur = cur->next; } cout << "NULL" << endl; } cout << "=========================================" << endl; } void Swap(HashBucket<T, T2Int>& ht) { table.swap(ht.table); std::swap(size, ht.size); } private: // 哈希函数---除留余数法 size_t HashFunc(const T& data) { T2Int t2Int; return t2Int(data) % table.capacity(); } void Destroy() { // 循环去销毁:table中的每个链表 for (size_t i = 0; i < table.capacity(); ++i) { Node* cur = table[i]; // 采用:头删法删除链表中的每个节点 while (cur) { table[i] = cur->next; delete cur; cur = table[i]; } } size = 0; } void CheckCapacity() { if (size == table.capacity()) { // 扩容---将表格放大----然后将桶中的元素往新桶中插入 HashBucket<T, T2Int> newHT(GetNextPrime(table.capacity())); // 将就哈希桶中的节点拆下来,移动到新哈希桶中 for (size_t i = 0; i < table.capacity(); ++i) { Node* cur = table[i]; while (cur) { // 1. 将cur节点拆下来 table[i] = cur->next; // 2. 将cur节点往newHT中插入 // 找位置 size_t bucketNo = newHT.HashFunc(cur->data); // 头插法 cur->next = newHT.table[bucketNo]; newHT.table[bucketNo] = cur; newHT.size++; cur = table[i]; } } // 已经将旧哈希桶中的元素全部移动到新哈希桶当中了 this->Swap(newHT); } } private: // table当中将来存放所有的链表--->实际只需要存放链表首节点的地址 std::vector<Node*> table; size_t size; // 有效元素的个数 };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。