当前位置:   article > 正文

<C++> 哈希及其模拟实现_实现hash函数的程序

实现hash函数的程序

  目录   

 一、哈希概念

1.哈希思想

2.哈希表 

3.常见的哈希函数

1.直接定址法(常用)

2.除留余数法(常用) 

 3.平方取中法(了解)

4.折叠法(了解)

5.随机数法(了解)

二、哈希冲突

1.冲突的原因

2.解决方法

1.用闭散列解决哈希冲突:

1.线性探测法介绍

2.线性探测的实现

(1)状态

(2)定义HashData

(3)哈希表 

(4)查找

(5)插入

(6)删除

2.用开散列解决哈希冲突

(1)哈希仿函数

(2)存储节点结构的定义

(3)析构函数

(4)查找

(5)插入

(6)删除

(7)桶的长度

三、完整代码


 一、哈希概念

        顺序结构以及平衡树中,元素key与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过key的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即,搜索的效率取决于搜索过程中元素的比较次数。

效率最高的搜索方法:不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的key之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

1.哈希思想

哈希(Hash) 是一种 映射 思想,规定存在一个唯一的 哈希值 与 键值 之前建立 映射 关系,由这种思想而构成的数据结构称为 哈希表(散列表)

 

 

哈希表中的数据查找时间可以做到 O(1)

这是非常高效的,比 AVL树 还要快

哈希表 中 插入数据 和 查找数据 的步骤如下:

  • 插入数据:根据当前待插入的元素的键值,计算出哈希值,并存入相应的位置中
  • 查找数据:根据待查找元素的键值,计算出哈希值,判断对应的位置中存储的值是否与 键值 相等

假设此时 数组 的大小 capacity 为 8哈希函数 计算哈希值:HashI = key % capacity

数据存储如下:

显然,这个哈希表并没有把所有位置都填满,数据分布无序且分散

因此,哈希表 又称为 散列表

2.哈希表 

哈希方法中使用的转换函数称为哈希函数(也叫散列函数),来建立映射关系,构造出来的结构称为哈希表 (Hash Table)(也叫散列表)。

如有数据集合{ 5,2,6,8,9,7},假如哈希函数设置为:

hash(key) = key % capacity

其中capacity为存储元素底层空间总大小

按照这种方法查找不用拿key多次比较,因此查找的速度比较快。

不同关键字通过 相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突。当再插入别的元素时,有可能发生哈希冲突,比如插入22,hashFunc(22) = 22%10 = 2,2的位置已经存了数据2了,那么22该如何存储呢?

引起哈希冲突的原因:哈希函数设计不合理。哈希函数设计原则包括:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应比较简单

3.常见的哈希函数

1.直接定址法(常用)

函数原型:HashI  =  A  *  key  +  B

  • 优点:简单、均匀、节省空间,查找key O(1)的时间复杂度
  • 缺点:需要提前知道键值的分布情况
  • 适用场景:范围比较集中,每个数据分配一个唯一位置

例如计数排序,统计字符串中出现的用26个英文字符统计,给数组分配26个空间,遍历到的字符是谁,就把相应的元素值++

2.除留余数法(常用) 

假设 哈希表 的大小为 m

函数原型:HashI =  key  %  p  (p < m)

把数据映射到有限的空间里面。设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将key转换成哈希地址。如第2节哈希表的例子。

  • 优点:简单易用,性能均衡
  • 缺点:容易出现哈希冲突,需要借助特定方法解决
  • 适用场景:范围不集中,分布分散的数据

 3.平方取中法(了解)

函数原型:Hash(Key) = mid(key * key)

  • 适用场景:不知道键值的分布,而位数又不是很大的情况

假设键值为 1234,对其进行平方后得到 1522756,取其中间的三位数 227 作为 哈希值

4.折叠法(了解)

折叠法是将键值从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按 哈希表 表长,取后几位作为散列地址

  • 适用场景:事先不需要知道键值的分布,且键值位数比较多

假设键值为 85673113,分为三部分 85673113,求和:1600,根据表长(假设为 100),哈希值 就是 600

5.随机数法(了解)

选择一个随机函数,取键值的随机函数值为它的 哈希值

函数原型:Hash(key)  =  rand(key)
其中 rand 为随机数函数

  • 适用场景:键值长度不等时

哈希函数 还有很多很多种,最终目的都是为了计算出重复度低的 哈希值

最常用的是 直接定址法 和 除留余数法


二、哈希冲突

哈希冲突(哈希碰撞) 是面试中的常客,可以通过一个 哈希冲突 间接引出 哈希 中的其他知识点

1.冲突的原因

哈希值 是 键值 通过 哈希函数 计算得出的 位置标识符,难以避免重复问题

比如在下面的 哈希表 中,存入元素 20哈希值  HashI = 20 % 8 = 4,此时 4 位置中并没有元素,可以直接存入

 但是如果继续插入元素 36哈希值 HashI = 36 % 8 = 4,此时 4 位置处已经有元素了,无法继续存入,此时就发生了 哈希冲突

 不同的 哈希函数 引发 哈希冲突 的概率不同,但最终都会面临 哈希冲突 这个问题,因此需要解决一些方法,解决哈希冲突

2.解决方法

主要的解决方法有两种:闭散列 与 开散列

1.用闭散列解决哈希冲突:

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。 下一个位置怎样找呢?有以下两种常见方式:

1.线性探测法介绍

如下场景,要插入22,通过哈希函数hashfunc(22) = 22%10=2计算出的地址为2,2的位置已经有数据2了,现在发生了冲突: 规定:当哈希表中存储的数据量 与 哈希表的容量 比值(负载因子)过大时,扩大哈希表的容量,并重新进行映射

线性探测:因为有 负载因子 的存在,所以 哈希表是一定有剩余空间的,当发生 哈希冲突 时,从冲突位置向后探测,直到找到可用位置,直到寻找到下一个空位置为止。

 负载因子 = 存储的有效数据个数/空间的大小 

负载因子越大,冲突的概率越高,增删查改效率越低

负载因子越小,冲突的概率越低,增删查改的效率越高,但是空间利用率低,浪费多。 

负载因子 <1,就能保证发生哈希冲突时一定能找到空位置

2.线性探测的实现

存储数据结构的定义:

闭散列中存储的数据至少包含两个信息:键值对、状态表示

键值对既可以是 K 也可以是 K / V,我们这里实现的是 K / V

(1)状态

区分哈希表的一个位置有没有数据,如果用两种状态表示,在(1)或不在(0),那么就会带来两个问题:

  1. 0表示不在,那么如何存数据0呢?
  2. 如果数据发生冲突,当前位置和后面位置都存放的是冲突数据,加入当前位置的数据被删除了,那么查找key时发现当前位置状态为不在,那么就不会再向后查找了。

因此要用3个状态位分别表示空、已存在、已删除,用枚举表示状态位:

  1. 空 EMPTY 初始状态
  2. 存在 EXIST 插入数据后的状态
  3. 删除 DELETE 删除数据后的状态

其实简单分为 [可用 / 不可用] 两种状态也行,细分出 EMPTY 与 DELETE 是为了在进行 探测 时,提高效率

  • 探测 时,如果探测到空,就不必再探测,因为后面必然不存在我们想找的数据,如果存在,这里就不会为空了

所以闭散列中的 存储数据结构 可以用一个结构体定义

  1. enum State
  2. {
  3. EMPTY,
  4. EXIST,
  5. DELETE
  6. };
(2)定义HashData

哈希数据应包含两个成员:数据和状态  

  1. //存储数据结构类
  2. template<class K, class V>
  3. struct HashData
  4. {
  5. pair<K, V> _kv; //键值对
  6. Status _status = EMPTY; //状态
  7. };
(3)哈希表 

哈希表包含两个成员:哈希数据、存储的有效数据的个数,模板有3个参数K、V、HashFunc。

①由于不知道 keyK还是pair,所以需要定义两个模板参数K、V来包含key是Kpair的两种情况

②由于不知道key的数据类型是int还是string、pair、struct,计算key的映射位置时需要取模,但是只能对int型取模,string、struct、pair无法取模HashFunc作为仿函数,它的实例可以分别应对这些类型的取模。

  1. template<class K, class V, class HashFunc>
  2. class HashTable
  3. {
  4. pubilc:
  5. // ...
  6. private:
  7. vector<HashData<K, V>> _table;//哈希表
  8. size_t _n = 0;//存储有效数据的个数
  9. };
(4)查找

①无论传给哈希表的数据是K还是pair,查找时,都需要用K做key来进行查找

②计算元素位置

③如果当前位置元素为key,那么就返回该元素,否则可能发生了冲突,继续向后探测

  1. HashData<K, V>* Find(const K& key)
  2. {
  3. if (_tables.size() == 0)
  4. return nullptr;
  5. size_t hashi = key % _tables.size();// 取余找位置
  6. // 线性探测
  7. size_t i = 1;
  8. size_t index = hashi;
  9. while (_tables[index]._state != EMPTY)
  10. {
  11. if (_tables[index]._state == EXIST// 如果不添加这段代码,是删除状态也会被找到,然后返回数据,所以只能是存在状态
  12. && _tables[index]._kv.first == key) {
  13. return &_tables[index];//该位置存在且值为key返回地址方便对该数据进行修改
  14. }
  15. //冲突时,向后查找
  16. index = hashi + i;//线性探测 //index = start + i*i;//二次探测
  17. index %= _tables.size();// 取余防止越界
  18. ++i;
  19. // 如果已经查找一圈,那么说明全是存在+删除
  20. if (index == hashi)
  21. {
  22. break;
  23. }
  24. }
  25. return nullptr;
  26. }
(5)插入

①根据键值计算出下标(哈希值)

②线性探测,找到可用的位置 [空 / 删除]

③插入数据,有效数据量+1

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. // 存在就不允许插入
  4. if (Find(kv.first))
  5. return false;
  6. // 扩容
  7. // ...
  8. size_t hashi = kv.first % _tables.size();// 取余找位置
  9. // 线性探测
  10. size_t i = 1;
  11. size_t index = hashi;
  12. //状态为空就插入
  13. while (_tables[index]._state == EXIST)
  14. {
  15. index = hashi + i;
  16. index %= _tables.size();// 取余防止越界
  17. ++i;
  18. }
  19. _tables[index]._kv = kv;
  20. _tables[index]._state = EXIST;// 插入后状态置为存在
  21. _n++;
  22. return true;
  23. }

如果想降低踩踏发生的概率,可以将 线性探测 改为 二次探测

HashI += (i * i);	//二次探测

以上就是 插入操作 的具体实现,此时面临着一个棘手的问题:扩容问题

当数据 第一次插入负载因子达到临界值 时,需要进行 扩容因为 vector<Data> _table 一开始为空,所以第一次插入也要扩容

当空间扩容后,容量发生了改变,这也就意味着 数据与下标 的映射关系也发生了改变,需要更新数据,即重新进行线性探测

扩容可分为 传统写法 和 现代写法

传统:

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. // 存在就不允许插入
  4. if (Find(kv.first))
  5. return false;
  6. //考虑扩容问题(存储数据超过容量的 70% 就扩容)
  7. if (_table.size() == 0 || _n * 10 / _table.size() == 7)
  8. {
  9. size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
  10. vector<Data> newTable(newSize); //创建新表
  11. //将老表中的数据,挪动至新表(需要重新建立映射关系)
  12. for (auto& e : _table)
  13. {
  14. //获取下标(哈希值)
  15. size_t HashI = e._kv.first % newTable.size();
  16. int i = 0; //探测时的权值,方便改为二次探测
  17. //如果为空,进行探测
  18. while (newTable[HashI]._status == EXIST)
  19. {
  20. HashI += i; //向后移动
  21. HashI %= newTable.size();
  22. i++;
  23. }
  24. //找到空位置了,进行插入
  25. newTable[HashI]._kv = e._kv;
  26. newTable[HashI]._status = EXIST;
  27. _n++; //插入一个数据
  28. }
  29. //交换新表、旧表(成本不大)
  30. _table.swap(newTable);
  31. }
  32. //插入
  33. //……
  34. }

传统写法思路:创建一个容量足够的新表,将 表中的数据映射至表中,映射完成后,交换表和表,目的是为了更新当前哈希表对象中的表。

关于 平衡因子 的控制
根据别人的试验结果,哈希表中的存储的有效数据量超过哈希表容器的 70% 时,推荐进行扩容。

假设容量为 M,存储的有效数据量为 N,两者比率 N / M == 0.7 时进行扩容。
因为我们这里的容量和数据量都是整数,所以在等式两边*10,可得 N * 10 / M == 7

传统写法比较繁琐,下面就来看看 现代写法

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. // 存在就不允许插入
  4. if (Find(kv.first))
  5. return false;
  6. // 负载因子超过0.7就扩容
  7. //if ((double)_n / (double)_tables.size() >= 0.7)
  8. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
  9. {
  10. //_tables.reserve(_tables.capacity() * 2);
  11. // 错误:1、表为空,扩不上去 2、光扩容量无法访问,size没变
  12. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  13. //_tables.resize(newsize); 单纯扩容的话会导致原有数据的映射关系发生混乱,所以需另新开辟空间
  14. HashTable<K, V> newHT;
  15. newHT._tables.resize(newsize);
  16. // 遍历旧表,重新映射到新表
  17. for (auto& data : _tables)
  18. {
  19. if (data._state == EXIST)
  20. newHT.Insert(data._kv);
  21. }
  22. _tables.swap(newHT._tables);
  23. }
  24. // 插入
  25. // ...
  26. return true;
  27. }

其实 传统写法 中的 插入部分逻辑 与 Insert 中的 插入操作 重复了,因此我们可以借助现代思想(白嫖),创建一个 容量足够的哈希表,将 原表 中的数据遍历插入 新的哈希表 中,插入的过程调用 Insert,代码极其间接,并且不容易出错

细节:需不需将当前对象中的 有效数据量 _n 进行更新?

  • 答案是不需要,往新的哈希表中插入 _n 个数据,意味着无论是 新的哈希表 还是当前对象,它们的有效数据量都是一致的,因此不需要更新

(6)删除

删除 操作就十分简单了,获取待删除数据,将其中的状态改为 删除 DELETE 即可,不必清空数据,只要访问不到就行了,当然,有效数据量-1

  1. bool Erase(const K& key)
  2. {
  3. HashData<K, V>* ret = Find(key);
  4. if (ret)
  5. {
  6. ret->_state = DELETE;
  7. --_n;
  8. return true;
  9. }
  10. return false;
  11. }

像这种线性探测(暴力探测)可以解决 哈希冲突 问题,但会带来新的问题:踩踏

踩踏:元素的存储位置被别人占用了,于是也只能被迫线性探测,引发连锁反应,插入、查找都会越来越慢

哈希冲突 越多,效率 越低

优化方案:二次探测,每次向后探测 i ^ 2 步,尽量减少踩踏

尽管如此,闭散列 的实际效果 不尽人意,因为其本质上就是一个 零和游戏,闭散列 实战价值不大,因此只做简单了解即可,真正重点在于 开散列


2.用开散列解决哈希冲突

所谓 开散列 就在原 存储位置 处带上一个 单链表,如果发生 哈希冲突,就将 冲突的值依次挂载即可,因此也叫做 链地址法、开链法、哈希桶。

开散列 中不需要 负载因子,开散列的每个桶中存放的都是哈希冲突的元素,如果每个位置都被存满了,直接扩容就好了,当然扩容后也需要重新建立映射关系

开散列 中进行查找时,需要先根据 哈希值 找到对应位置,并在 单链表 中进行遍历

一般情况下,单链表的长度不会太长的,因为扩容后,整体长度会降低

如果 单链表 真的过长了(几十个节点),我们还可以将其转为 红黑树,此时效率依旧非常高

(1)哈希仿函数

在闭散列中,已经实现了Hash仿函数,用来获取哈希表中的元素的key,方便后续计算映射位置 

  1. // 仿函数
  2. template<class K>
  3. struct HashFunc
  4. {
  5. size_t operator()(const K& key)
  6. {
  7. return key;
  8. }
  9. };

模板特化:string元素使用频率较高,进行模板特化 

  1. // 特化string
  2. template<>
  3. struct HashFunc<string>
  4. {
  5. // BKDR
  6. size_t operator()(const string& s)
  7. {
  8. size_t hash = 0;
  9. for (auto& ch : s)
  10. {
  11. hash += ch;
  12. hash *= 31; // 大佬定的值
  13. }
  14. return hash;
  15. }
  16. };
(2)存储节点结构的定义

哈希桶 中需要维护一个 单链表,因此需要一个 _next 指针指向下一个节点

  1. template<class K, class V>
  2. struct HashNode
  3. {
  4. HashNode<K, V>* _next;
  5. pair<K, V> _kv;
  6. HashNode(const pair<K, V>& kv)
  7. :_next(nullptr)
  8. ,_kv(kv)
  9. {}
  10. };

因为引入了 单链表,所以 哈希桶 中  的存储类型也变成了 节点指针

  1. template<class K, class V>
  2. class HashTable
  3. {
  4. typedef HashNode<K, V> Node;
  5. //……
  6. private:
  7. vector<Node*> _tables;// 节点指针数组
  8. size_t _n = 0;// 存储有效数据个数
  9. };
(3)析构函数

因为有 单链表,所以在对象析构时,需要手动遍历其中的节点,将其释放,避免 内存泄漏

  1. ~HashTable()
  2. {
  3. for (auto& cur : _tables)
  4. {
  5. while (cur)
  6. {
  7. Node* next = cur->_next;
  8. delete cur;
  9. cur = next;
  10. }
  11. cur = nullptr;
  12. }
  13. }

为什么 哈希表(闭散列) 中不需要写 析构函数

  • 因为在闭散列中,表中存储的数据不涉及自定义类型的动态内存管理,并且 vector 在对象调用默认析构时,会被调用其析构,释放其中的内存
(4)查找

先计算key在哈希表中的位置,然后后再该位置的哈希桶中遍历查找:

  1. Node* Find(const K& key)
  2. {
  3. if (_tables.size() == 0)
  4. return nullptr;
  5. Hash hash;
  6. size_t hashi = hash(key) % _tables.size();// 取余找位置
  7. Node* cur = _tables[hashi];
  8. while (cur)
  9. {
  10. if (cur->_kv.first == key)
  11. return cur;
  12. cur = cur->_next;
  13. }
  14. return nullptr;
  15. }
(5)插入

在进行数据插入时,既可以尾插,也可以头插,因为桶中的存储顺序没有要求

为了操作简单,我们选择 头插

同样的,哈希桶在扩容时,也有传统写法和现代写法,这里采用 传统写法

  1. bool Insert(const pair<K, V>& kv)
  2. {
  3. if (Find(kv.first) != nullptr)
  4. return false; //冗余
  5. Hash hash;
  6. if (_n == _tables.size())
  7. {
  8. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  9. vector<Node*> newtables(newsize, nullptr);
  10. for (auto& cur : _tables)
  11. {
  12. while (cur)
  13. {
  14. Node* next = cur->_next;
  15. size_t hashi = hash(cur->_kv.first) % newtables.size();// 重新计算映射关系
  16. // 头插到新表
  17. cur->_next = newtables[hashi];
  18. newtables[hashi] = cur;
  19. cur = next;
  20. }
  21. }
  22. _tables.swap(newtables);
  23. //现代写法
  24. //size_t newSize = _table.size() == 0 ? 5 : _table.size() * 2;
  25. //HashTable<K, V> newHashTable;
  26. //newHashTable._table.resize(newSize); //直接就扩容了
  27. //for (auto& cur : _table)
  28. //{
  29. // while (cur)
  30. // {
  31. // newHashTable.Insert(cur->_kv);
  32. // cur = cur->_next;
  33. // }
  34. //}
  35. //_table.swap(newHashTable._table);
  36. size_t hashi = hash(kv.first) % _tables.size();// 取余找位置
  37. // 头插
  38. Node* newnode = new Node(kv);
  39. newnode->_next = _tables[hashi];
  40. _tables[hashi] = newnode;
  41. ++_n;
  42. return true;
  43. }

这里的 传统写法 有一个很巧妙的地方:节点回收

既然 旧表 中节点不用了,那我可以直接把其中的节点 转移链接 至 新表 中,这样可以提高效率,且代码十分优雅

(6)删除

①计算key在表中的位置

②要删除的数据是不是该位置的第一个哈希桶,如果是,那就让哈希表的第一个节点变成第二个桶,否则让这个桶的前一个桶指向这个桶的下一个桶

  1. bool Erase(const K& key)
  2. {
  3. Hash hash;
  4. size_t hashi = hash(key) % _tables.size();// 取余找位置
  5. Node* prev = nullptr;
  6. Node* cur = _tables[hashi];
  7. while (cur)
  8. {
  9. if (cur->_kv.first == key)
  10. {
  11. // 没有前节点
  12. if (prev == nullptr)
  13. {
  14. _tables[hashi] = cur->_next;
  15. }
  16. else// 有前节点
  17. {
  18. prev->_next = cur->_next;
  19. }
  20. delete cur;
  21. return true;
  22. }
  23. else
  24. {
  25. prev = cur;
  26. cur = cur->_next;
  27. }
  28. }
  29. return false;
  30. }
(7)桶的长度

哈希桶 中的长度不会太长,因为存在 扩容 机制,并且 每次扩容后,映射关系会进行更新,桶的整体高度会降低

可以插入大量数据,查看 哈希桶 的最大高度

  1. class HashTable
  2. {
  3. //……
  4. size_t MaxBucketSize()
  5. {
  6. size_t max = 0;
  7. for (size_t i = 0; i < _table.size(); i++)
  8. {
  9. Node* cur = _table[i];
  10. size_t size = 0;
  11. while (cur)
  12. {
  13. ++size;
  14. cur = cur->_next;
  15. }
  16. if (size > max)
  17. max = size;
  18. }
  19. return max;
  20. }
  21. //……
  22. };
  23. void TestOpenHash3()
  24. {
  25. //插入大量随机数,查看最长的桶长度
  26. srand((size_t)time(nullptr));
  27. HashTable<int, int> ht;
  28. int n = 1000000;
  29. for (int i = 0; i < n; i++)
  30. {
  31. int val = rand() + i;
  32. ht.Insert(make_pair(val, val));
  33. }
  34. cout << ht.MaxBucketSize() << endl;// 2
  35. }

插入约 100w 数据,哈希桶 的最大高度不过为 2

因此,哈希桶可以做到常数级别的查找速度,并且不存在 踩踏 问题

其实库中的 unordered_set 和 unordered_map 就是使用 哈希桶 封装实现的,就像 红黑树 封装 set 和 map 那样。

三、完整代码

 HashTable.h

  1. #pragma once
  2. #include <iostream>
  3. #include <vector>
  4. using namespace std;
  5. namespace OpenAdress
  6. {
  7. enum State
  8. {
  9. EMPTY,
  10. EXIST,
  11. DELETE
  12. };
  13. template<class K, class V>
  14. struct HashData
  15. {
  16. pair<K, V> _kv;
  17. State _state = EMPTY;
  18. };
  19. template<class K, class V>
  20. class HashTable
  21. {
  22. public:
  23. bool Insert(const pair<K, V>& kv)
  24. {
  25. // 存在就不允许插入
  26. if (Find(kv.first))
  27. return false;
  28. // 负载因子超过0.7就扩容
  29. //if ((double)_n / (double)_tables.size() >= 0.7)
  30. if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
  31. {
  32. //_tables.reserve(_tables.capacity() * 2);
  33. // 错误:1、表为空,扩不上去 2、光扩容量无法访问,size没变
  34. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  35. //_tables.resize(newsize); 单纯扩容的话会导致原有数据的映射关系发生混乱,所以需另新开辟空间
  36. HashTable<K, V> newHT;
  37. newHT._tables.resize(newsize);
  38. // 遍历旧表,重新映射到新表
  39. for (auto& data : _tables)
  40. {
  41. if (data._state == EXIST)
  42. newHT.Insert(data._kv);
  43. }
  44. _tables.swap(newHT._tables);
  45. }
  46. size_t hashi = kv.first % _tables.size();// 取余找位置
  47. // 线性探测
  48. size_t i = 1;
  49. size_t index = hashi;
  50. //状态为空就插入
  51. while (_tables[index]._state == EXIST)
  52. {
  53. index = hashi + i;
  54. index %= _tables.size();// 取余防止越界
  55. ++i;
  56. }
  57. _tables[index]._kv = kv;
  58. _tables[index]._state = EXIST;// 插入后状态置为存在
  59. _n++;
  60. return true;
  61. }
  62. HashData<K, V>* Find(const K& key)
  63. {
  64. if (_tables.size() == 0)
  65. return nullptr;
  66. size_t hashi = key % _tables.size();// 取余找位置
  67. // 线性探测
  68. size_t i = 1;
  69. size_t index = hashi;
  70. while (_tables[index]._state != EMPTY)
  71. {
  72. if (_tables[index]._state == EXIST// 如果不添加这段代码,是删除状态也会被找到,然后返回数据,所以只能是存在状态
  73. && _tables[index]._kv.first == key) {
  74. return &_tables[index];//该位置存在且值为key返回地址方便对该数据进行修改
  75. }
  76. //冲突时,向后查找
  77. index = hashi + i;//线性探测 //index = start + i*i;//二次探测
  78. index %= _tables.size();// 取余防止越界
  79. ++i;
  80. // 如果已经查找一圈,那么说明全是存在+删除
  81. if (index == hashi)
  82. {
  83. break;
  84. }
  85. }
  86. return nullptr;
  87. }
  88. bool Erase(const K& key)
  89. {
  90. HashData<K, V>* ret = Find(key);
  91. if (ret)
  92. {
  93. ret->_state = DELETE;
  94. --_n;
  95. return true;
  96. }
  97. return false;
  98. }
  99. private:
  100. vector<HashData<K, V>> _tables;
  101. size_t _n = 0;// 存储的数据个数
  102. };
  103. void TestHashTable1()
  104. {
  105. int a[] = { 3,33,2,13,5,12,1002 };
  106. HashTable<int, int> ht;
  107. for (auto& e : a)
  108. {
  109. ht.Insert(make_pair(e, e));
  110. }
  111. ht.Insert(make_pair(15, 15)); // 扩容
  112. }
  113. }
  114. namespace HashBucket
  115. {
  116. template<class K, class V>
  117. struct HashNode
  118. {
  119. HashNode<K, V>* _next;
  120. pair<K, V> _kv;
  121. HashNode(const pair<K, V>& kv)
  122. :_next(nullptr)
  123. ,_kv(kv)
  124. {}
  125. };
  126. // 仿函数
  127. template<class K>
  128. struct HashFunc
  129. {
  130. size_t operator()(const K& key)
  131. {
  132. return key;
  133. }
  134. };
  135. // 特化string
  136. template<>
  137. struct HashFunc<string>
  138. {
  139. // BKDR
  140. size_t operator()(const string& s)
  141. {
  142. size_t hash = 0;
  143. for (auto& ch : s)
  144. {
  145. hash += ch;
  146. hash *= 31; // 大佬定的值
  147. }
  148. return hash;
  149. }
  150. };
  151. template<class K, class V, class Hash = HashFunc<K>>
  152. class HashTable
  153. {
  154. typedef HashNode<K, V> Node;
  155. public:
  156. ~HashTable()
  157. {
  158. for (auto& cur : _tables)
  159. {
  160. while (cur)
  161. {
  162. Node* next = cur->_next;
  163. delete cur;
  164. cur = next;
  165. }
  166. cur = nullptr;
  167. }
  168. }
  169. Node* Find(const K& key)
  170. {
  171. if (_tables.size() == 0)
  172. return nullptr;
  173. Hash hash;
  174. size_t hashi = hash(key) % _tables.size();// 取余找位置
  175. Node* cur = _tables[hashi];
  176. while (cur)
  177. {
  178. if (cur->_kv.first == key)
  179. return cur;
  180. cur = cur->_next;
  181. }
  182. return nullptr;
  183. }
  184. bool Erase(const K& key)
  185. {
  186. Hash hash;
  187. size_t hashi = hash(key) % _tables.size();// 取余找位置
  188. Node* prev = nullptr;
  189. Node* cur = _tables[hashi];
  190. while (cur)
  191. {
  192. if (cur->_kv.first == key)
  193. {
  194. // 没有前节点
  195. if (prev == nullptr)
  196. {
  197. _tables[hashi] = cur->_next;
  198. }
  199. else// 有前节点
  200. {
  201. prev->_next = cur->_next;
  202. }
  203. delete cur;
  204. return true;
  205. }
  206. else
  207. {
  208. prev = cur;
  209. cur = cur->_next;
  210. }
  211. }
  212. return false;
  213. }
  214. bool Insert(const pair<K, V>& kv)
  215. {
  216. if (Find(kv.first) != nullptr)
  217. return false; //冗余
  218. Hash hash;
  219. if (_n == _tables.size())
  220. {
  221. size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
  222. vector<Node*> newtables(newsize, nullptr);
  223. for (auto& cur : _tables)
  224. {
  225. while (cur)
  226. {
  227. Node* next = cur->_next;
  228. size_t hashi = hash(cur->_kv.first) % newtables.size();// 重新计算映射关系
  229. // 头插到新表
  230. cur->_next = newtables[hashi];
  231. newtables[hashi] = cur;
  232. cur = next;
  233. }
  234. }
  235. _tables.swap(newtables);
  236. }
  237. size_t hashi = hash(kv.first) % _tables.size();// 取余找位置
  238. // 头插
  239. Node* newnode = new Node(kv);
  240. newnode->_next = _tables[hashi];
  241. _tables[hashi] = newnode;
  242. ++_n;
  243. return true;
  244. }
  245. // size_t newsize = GetNextPrime(_tables.size());
  246. size_t GetNextPrime(size_t prime)
  247. {
  248. // SGI
  249. static const int __stl_num_primes = 28;
  250. static const unsigned long __stl_prime_list[__stl_num_primes] =
  251. {
  252. 53, 97, 193, 389, 769,
  253. 1543, 3079, 6151, 12289, 24593,
  254. 49157, 98317, 196613, 393241, 786433,
  255. 1572869, 3145739, 6291469, 12582917, 25165843,
  256. 50331653, 100663319, 201326611, 402653189, 805306457,
  257. 1610612741, 3221225473, 4294967291
  258. };
  259. size_t i = 0;
  260. for (; i < __stl_num_primes; ++i)
  261. {
  262. if (__stl_prime_list[i] > prime)
  263. return __stl_prime_list[i];
  264. }
  265. return __stl_prime_list[i];
  266. }
  267. // 最长桶长度
  268. size_t MaxBucketSize()
  269. {
  270. size_t max = 0;
  271. for (size_t i = 0; i < _tables.size(); ++i)
  272. {
  273. auto cur = _tables[i];
  274. size_t size = 0;
  275. while (cur)
  276. {
  277. ++size;
  278. cur = cur->_next;
  279. }
  280. // printf("[%d]->%d\n", i, size);
  281. if (size > max)
  282. {
  283. max = size;
  284. }
  285. }
  286. return max;
  287. }
  288. private:
  289. vector<Node*> _tables;// 节点指针数组
  290. size_t _n = 0;// 存储有效数据个数
  291. };
  292. struct HashStr
  293. {
  294. // BKDR
  295. size_t operator()(const string& s)
  296. {
  297. size_t hash = 0;
  298. for (auto& ch : s)
  299. {
  300. hash += ch;
  301. hash *= 31; // 大佬定的值
  302. }
  303. return hash;
  304. }
  305. };
  306. void TestHashTable2()
  307. {
  308. HashTable<string, string> ht; // 走特化
  309. ht.Insert(make_pair("sort", "排序"));
  310. ht.Insert(make_pair("string", "字符串"));
  311. ht.Insert(make_pair("left", "左边"));
  312. ht.Insert(make_pair("right", "右边"));
  313. HashStr hashstr;
  314. cout << hashstr("abcd") << endl;
  315. cout << hashstr("bcda") << endl;
  316. cout << hashstr("aadd") << endl;
  317. cout << hashstr("eat") << endl;
  318. cout << hashstr("ate") << endl;
  319. // 没用BKDR前,虽然顺序不一样,但就单纯的+=字符串的每个字符ascll值,加起来的和是一样的
  320. // BKDR后,值就不一样了
  321. }
  322. void TestHashTable4()
  323. {
  324. size_t N = 900000;
  325. HashTable<int, int> ht;
  326. srand(time(0));
  327. for (size_t i = 0; i < N; ++i)
  328. {
  329. size_t x = rand() + i;
  330. ht.Insert(make_pair(x, x));
  331. }
  332. cout << ht.MaxBucketSize() << endl;
  333. }
  334. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/808345
推荐阅读
相关标签
  

闽ICP备14008679号