赞
踩
目录
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
上图插入1,模10得到1,就填入到1的位置,其他元素也是同理。
如计数排序,其实就有哈希的思想。详情参考:计数排序 。
当我们向上图中再插入一个数字44的时候,44和4取10的模的余数都是4,那么都应该在4的位置。这该怎么填入数字呢?这种出现几个数字都符合一个位置的条件的情况,叫做哈希碰撞/冲突。
1. 直接定址法--(常用)取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况2. 除留余数法--(常用)设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数, 按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址3. 平方取中法--(了解)假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 位 227 作为哈希地址;再比如关键字为4321 ,对它平方就是 18671041 ,抽取中间的 3 位 671( 或 710) 作为哈希地址。平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况4. 折叠法--(了解)折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况5. 随机数法--(了解)选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中random为随机数函数。通常应用于关键字长度不等时采用此法6. 数学分析法--(了解)设有 n 个 d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
例如:
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转( 如 1234 改成 4321) 、右环位移 ( 如 1234 改成 4123) 、左环移位、前两数与后两数叠加( 如 1234 改成 12+34=46) 等方法。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的 若干位分布较均匀的情况
结构: 既然要线性存储,底层肯定要用vector来存储的。我们还需要设置一个size_t类型变量来记录哈希表中有效元素的数量。
每一个位置还需要有三种状态:空、存在、删除,以便后序的插入删除和查找。我们可以用枚举变量。
构造函数:我们给定元素个数为10。
查找:获取数据对应的hashi,找到数据对应的位置,如果不在该位置,则继续往后查找,直到位置上的状态为 EMPTY。(注意:这里是不能在状态DELETE的时候停下的,有可能会影响查找后面的元素。)在全是DELETE和EXIST的情况下,会陷入死循环,所以我们设置一个初始值 starti,每次查找完 hashi %= 哈希表元素个数,如果 hashi == starti ,那么说明回到了原点,就应该停止查找。
插入:我们首先需要判定是否已经存在了插入的元素的值。并且。我们还需要设定一个负载因子(负载因子 = 有效元素 / 总元素个数),如果负载因子超过了设定值,那么则需要扩容。由于传入的数据不一定是size_t类型的,我们需要构造一个仿函数Hashfunc来获取。通过获取数据对应的hashi,将数据插入到对应位置,若该位置已经有了数据,则++hashi。
- 扩容:创建一个新的vector,大小是旧表的两倍,把旧表中的有效数据插入到新表中,然后交换新旧表。
删除:找到对应的元素,将其状态修改为DELETE即可。
关于负载因子:
代码:
- #pragma once
- #include <vector>
-
- template<class K>
- struct HashFunc
- {
- size_t operator()(const K& key)
- {
- return (size_t)key;
- }
- };
-
- namespace closehash
- {
- enum State
- {
- EMPTY,
- EXIST,
- DELETE,
- };
-
- template<class K, class V>
- struct HashData
- {
- pair<K, V> _kv;
- State _state = EMPTY; // 默认状态为空
- };
-
- template<class K, class V, class Hash = HashFunc<K>>
- class HashTable
- {
- typedef HashData<K, V> Data;
- public:
- HashTable()
- :_n(0)
- {
- _tables.resize(10);
- }
-
- bool Insert(const pair<K, V>& kv)
- {
- if (Find(kv.first))
- {
- return false;
- }
-
- //大于标定负载因子,就需要扩容
- if (_n * 10 / _tables.size() >= 7) //负载因子 0.7
- {
- HashTable<K, V, Hash> newHT;
- newHT._tables.resize(_tables.size() * 2);
-
- //重新插入存在的节点
- for (auto e : _tables)
- {
- if (e._state == EXIST)
- {
- newHT.Insert(e._kv);
- }
- }
-
- _tables.swap(newHT._tables);
- }
- //仿函数获取pair中first
- Hash hf;
- size_t hashi = hf(kv.first) % _tables.size();
- while (_tables[hashi]._state == EXIST)
- {
- //线性探测
- hashi++;
- hashi %= _tables.size(); //保证不越界
- }
-
- _tables[hashi]._kv = kv;
- _tables[hashi]._state = EXIST;
- ++_n;
-
- return true;
- }
-
- Data* Find(const K& key)
- {
- Hash hf;
- size_t hashi = hf(key) % _tables.size();
- size_t starti = hashi; //防止死循环查询
- while (_tables[hashi]._state != EMPTY)
- {
- if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
- {
- return &_tables[hashi];
- }
- //如果是delete也要继续往下走
- ++hashi;
- hashi %= _tables.size();
-
- //防止极端环境:全是delete和exist,这时会死循环
- if (hashi == starti)
- {
- break;
- }
-
- }
- return nullptr;
- }
-
- bool Erase(const K& key)
- {
- Data* ret = Find(key);
- if (ret)
- {
- ret->_state = DELETE;
- --_n;
- return true;
- }
- else
- {
- return false;
- }
- }
-
- private:
- vector<Data> _tables;
- size_t _n = 0; //表中存储的有效数据的个数
- };
- }
开散列法又叫链地址法( 开链法 ),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
结构: 因为表中是存储的单链表,所以基础结构当然是链表节点。链表节点中存储着pair结构和状态_state。
插入: 如果有效数据个数和表大小相同的时候,需要扩容。重新创建节点插入的方法十分浪费空间,我们可以服用旧表的节点。获取对应的位置后插入节点到新表中。由于是单链表,插入节点如果是尾插,十分浪费时间,而链表头插十分方便,所以节点插入时采用头插的方式。
扩容:桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
查找: 获取元素对应位置,通过链表一直遍历下去,找到了就返回该节点,没找到返回空指针。
删除:和查找类似,找到节点,需要判断一下删除节点是不是头结点,是的话需要改变头结点。不是头结点只需要前一个节点指向该节点后一个节点,再删除该节点即可。
代码:
- #pragma once
- #include <vector>
- #include <string>
-
- template<class K>
- struct HashFunc
- {
- size_t operator()(const K& key)
- {
- return (size_t)key;
- }
- };
-
- //特化
- template<>
- struct HashFunc<string>
- {
- size_t operator()(const string& key)
- {
- size_t hash = 0;
- for (auto ch : key)
- {
- hash *= 131;
- hash += ch;
- }
- return hash;
- }
- };
-
- namespace buckethash
- {
- 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;
- public:
- //构造函数
- HashTable()
- :_n(0)
- {
- _tables.resize(10, nullptr);
- }
-
- //析构
- ~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;
- }
- }
-
- bool Insert(const pair<K, V>& kv)
- {
- if (Find(kv.first))
- {
- return false;
- }
-
- if (_n == _tables.size())
- {
- //方法一:重新开辟相同节点插入
- //缺点:消耗空间更大
- /*HashTable<K, V> newHT;
- newHT._tables.resize(2 * _n, nullptr);
- for (size_t i = 0; i < _tables.size(); ++i)
- {
- Node* cur = _tables[i];
- while (cur)
- {
- newHT.Insert(cur->_kv);
- cur = cur->_next;
- }
- }
- _tables.swap(newHT._tables); */
-
-
- //方法二:复用旧表中的节点
- vector<Node*> newTables;
- newTables.resize(_n * 2, nullptr);
- for (size_t i = 0; i < _tables.size(); ++i)
- {
- Node* cur = _tables[i];
- while (cur)
- {
- Node* next = cur->_next;
- size_t hashi = Hash()(cur->_kv.first) % newTables.size();
- cur->_next = newTables[hashi];
- newTables[hashi] = cur;
-
- cur = next;
-
- }
- }
- _tables.swap(newTables);
- }
- size_t hashi = Hash()(kv.first) % _tables.size();
- Node* newNode = new Node(kv);
- //头插新节点
- newNode->_next = _tables[hashi];
- _tables[hashi] = newNode;
- ++_n;
-
- return true;
- }
-
- Node* Find(const K& key)
- {
- size_t hashi = Hash()(key) % _tables.size();
- Node* cur = _tables[hashi];
- while (cur)
- {
- if (cur->_kv.first == key)
- {
- return cur;
- }
- cur = cur->_next;
- }
- return nullptr;
- }
-
- bool Erase(const K& key)
- {
- size_t hashi = Hash()(key) % _tables.size();
- Node* prev = nullptr;
- Node* cur = _tables[hashi];
-
- while (cur)
- {
- if (cur->_kv.first == key)
- {
- //需要判断是否为头节点
- if (cur == _tables[hashi])
- {
- _tables[hashi] = cur->_next;
- }
- else
- {
- prev->_next = cur->_next;
- }
- delete cur;
- --_n;
- return true;
- }
- prev = cur;
- cur = cur->_next;
- }
-
- return false;
- }
-
- private:
- vector<Node*> _tables;
- size_t _n = 0; //表中存储的有效数据的个数
- };
- }
-
对与能够强制转换为整形的类型,我们采用强制类型转换使其变成整形。
- template<class K>
- struct HashFunc
- {
- size_t operator()(const K& key)
- {
- return (size_t)key;
- }
- };
方法很多,我们值介绍常用的方法。
最常用的方法就是每次乘上一个数字,然后加上一个字符。返回最终获取到的数字。
不同的类型需要对应的转化方法,这点可以参考库里的实现方法。
- template<>
- struct HashFunc<string>
- {
- size_t operator()(const string& key)
- {
- size_t hash = 0;
- for (auto ch : key)
- {
- hash *= 131;
- hash += ch;
- }
- return hash;
- }
- };
在开辟哈希表的大小时,我们可以采用取一个素数的方式。
当使用素数作为除数时,能够更加均匀地散列 key 值,减少了哈希冲突的发生,而如果使用合数(即非素数)作为除数,那么就会有更多的键被映射到相同的索引上,从而增加哈希冲突的概率 – 合数有多个因子,取模后产生的余数可能比较集中,所以不能很好地散列 key 值。
另外,每个素数几乎都是接近二倍的关系,也基本符合我们的扩容两倍的要求。
源码:
- // Note: assumes long is at least 32 bits.
- static const int __stl_num_primes = 28;
- static const unsigned long __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
- };
-
- inline unsigned long __stl_next_prime(unsigned long n)
- {
- const unsigned long* first = __stl_prime_list;
- const unsigned long* last = __stl_prime_list + __stl_num_primes;
- const unsigned long* pos = lower_bound(first, last, n);
- return pos == last ? *(last - 1) : *pos;
- }
在某些极端情况下,可能会存在哈希表中某一条链表过长的情况,从而导致效率的低下。
为了解决这种问题,可能会将其转化成红黑树的结构。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。