• C++--哈希表--闭散列方法的模拟实现--1107


    1.哈希

    概念

    可以不经过任何比较,直接从表中得到要搜索的元素。 关键在于通过某种散列函数,使元素的存储位置与它的关键码之间能够建立 一一映射的关系。这样就可以通过o(1)的时间复杂度来寻找到元素。

    例如数据集合{1,7,4,5,9,6},哈希函数hash(key)=key&capacity

     冲突

    hash(7)=7 hash(17)=7,两个不同的数通过哈希函数映射到了一个位置,产生了冲突。哈希函数设计的越精妙,产生冲突的可能性就越低,但无法避免

    解决方法:

    • 闭散列(开放定址法)
    • 开散列(拉链法)

    2.散列

    2.1 散列查找的基本思想

    在记录的存储位置和它的关键码之间建立一个确定的对应关系H,使得每个关键码key和唯一的存储位置H(key)相对应。

    采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表(hash table),将关键码映射为散列表中适当存储位置的函数称为散列函数(hash function),所得到的存储位置成为散列地址(hash address)。

    2.2 常见的散列函数

    直接定址法

    集合为{10,30,50,70,80,90},选取H(key) = key/10

     直接定址法的特点是不会产生冲突,但实际应用中能使用这种散列函数的情况很少。

    它适用于事先知道关键码的分布,关键码集合不是很大且连续性较好的情况。


    除留余数法

    选择某个适当的正整数p,以关键码除以p的余数作为散列地址,H(key) = key mod p


    平方取中法 

    平方取中法是对关键码平方后,按散列表大小,取中间若干位作为散列地址(简称平方后截取),其原理是一个数平方后,中间的几位分布较均匀,从而冲突发生的概率较小。

    对于关键码1234,假设散列地址是2位,由于1234×1234=1522756,选取中间的两位作为散列地址,可以选22也可以选27。

    通常用在事先不知道关键码的分布且关键码的位数不是很大的情况

     2.3 处理冲突的办法

    开放定址法(线性探测/闭散列) 本篇博文后续实现

    链地址法(哈希桶/开散列) 见如下博文C++--哈希表--开散列(哈希桶的模拟实现)--1110_Gosolo!的博客-CSDN博客

    多重散列法

    3. 闭散列

    闭散列,(开放定址法)发生冲突时,如果哈希表没有被填满,则表内一定还有其他空闲位置,可以把冲突值放到下一个没有被占用的空余位置上。

    如何找到下一个没有被占用的空位?答:采用线性探测方法。从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    3.1 线性探测

     线性探测的插入

    如:在上述的哈希表中插入元素44,由于下标为4的位置放入了元素4,于是从该位置往后++,找到第一个不为空的位置,将44放入。

     线性探测的删除

    在寻找要删除的元素时,依然会根据存放在哈希表的下标开始寻找,比如在上述哈希表中寻找4,在4下标位置直接就可以找到该元素。但如果直接将其删除,那后续寻找元素44时,就会因为4下标没有元素,而认为元素44不存在于这张哈希表。所以我们需要设置一个状态来表示删除。

    3.2 哈希表闭散列的模拟实现

    我们写在一个自定义类域 Closehash 里面

    3.2.1 准备工作

    哈希表中元素状态

    1. namespace Closehash
    2. {
    3. //哈希表中元素的状态
    4. enum State
    5. {
    6. EMPTY,
    7. EXIT,
    8. DELETE
    9. };
    10. }

    存储类型用pair即可,但是数据中要包含状态,我们进行一次封装

    1. //由于数据需要一个状态,所以需要将pair封装一层
    2. template<class K,class V>
    3. struct HashDate
    4. {
    5. pair_kv;
    6. State _state;
    7. };

    开始(画饼)构建哈希表的内容

    1. template<class K,class V>
    2. class HashTable
    3. {
    4. public:
    5. bool Insert(const pair& kv);
    6. HashDate* find(const K& key);
    7. bool Erase(const K& key);
    8. private:
    9. vector> _tables;
    10. size_t _size = 0;
    11. };

    3.2.2 闭散列的插入

    1. bool Insert(const pair& kv)
    2. {
    3. //if (Find(kv.first)) return false;
    4. //Find实现了再去掉注释
    5. if (_tables.size() == 0
    6. || 10 * _size / _tables.size() >= 7)//相当于存了70%
    7. {
    8. //开始扩容
    9. size_t newsize = _tables.size()== 0 ? 10 : _tables.size() * 2;
    10. HashTable newHash;
    11. newHash._tables.resize(newsize);
    12. for (auto e: _tables)//注意_tables是HashDate类型 里面有_kv 和_state
    13. {
    14. if (e._state == EXIST)
    15. {
    16. newHash.Insert(e._kv);
    17. }
    18. }
    19. //资本家拷贝方法
    20. _tables.swap(newHash._tables);
    21. }
    22. //走到这里扩容完成 或者空间足够大
    23. size_t hashi = kv.first % _tables.size();//寻找在表中对应的下标是什么
    24. while (_tables[hashi]._state==EXIST)
    25. {
    26. hashi++;
    27. //走到头了得回来
    28. hashi%=_tables.size();
    29. }
    30. _tables[hashi]._kv = kv;
    31. _tables[hashi]._state = EXIST;
    32. _size++;
    33. return true;
    34. }

    测试用例

    1. void TestHT1()
    2. {
    3. int a[] = { 1, 11, 4, 15, 26, 7, 44 };
    4. HashTable<int, int> ht;
    5. for (auto e : a)
    6. {
    7. ht.Insert(make_pair(e, e));
    8. }
    9. ht.Print();
    10. }

     添加个99以验证扩容功能

     3.2.3 闭散列的查找

    1. HashDate* Find(const K& key)
    2. {
    3. if (_tables.size() == 0) return nullptr;
    4. size_t hashi = key % _tables.size();
    5. while (_tables[hashi]._state != EMPTY)
    6. {
    7. if (_tables[hashi]._state != DELETE
    8. && _tables[hashi]._kv.first == key)
    9. {
    10. return &_tables[hashi];
    11. }
    12. hashi++;
    13. hashi% _tables.size();
    14. }
    15. return nullptr;
    16. }

     测试用例

    	cout << ht.Find(4)->_kv.first << endl; 

    3.2.4 闭散列的删除

    1. bool Erase(const K& key)
    2. {
    3. HashDate* ret = Find(key);
    4. if (ret)
    5. {
    6. ret->_state = DELETE;
    7. --_size;
    8. return true;
    9. }
    10. else
    11. {
    12. return false;
    13. }
    14. }

    3.3 模拟实现的闭散列中的问题与改进

    上述测试用例中使用的是pair那我要是用pair呢?我的key还可以直接对数组长度取模吗?或者key是一个更复杂的结构体类型呢?

    文档中对这一问题,给我们预留一个空间,模板中有一个参数可以用来传入仿函数。

    template < class Key,   class T,  class Hash = hash >

    我们这里也按照该方法模拟一个。

    1. template<class K>
    2. struct HashFunc
    3. {
    4. size_t operator()(const K& key)
    5. {
    6. return (size_t)key;
    7. }
    8. };
    9. // 特化
    10. template<>
    11. struct HashFunc
    12. {
    13. // BKDR
    14. size_t operator()(const string& key)
    15. {
    16. size_t val = 0;
    17. for (auto ch : key)
    18. {
    19. val *= 131;
    20. val += ch;
    21. }
    22. return val;
    23. }
    24. };
    25. template<class K,class V,class Hash=HashFunc>
    26. class HashTable
    27. {
    28. public:
    29. bool Insert(const pair& kv);
    30. HashDate* find(const K& key);
    31. bool Erase(const K& key);
    32. private:
    33. vector> _tables;
    34. size_t _size = 0;
    35. };

    在每次求 在哈希表中位置的前面添加

    Hash hash;

    size_t hashi = hash(kv.first) % _tables.size()

    测试用例

    1. void TestHT2()
    2. {
    3. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
    4. //HashTable countHT;
    5. HashTableint> countHT;
    6. for (auto& str : arr)
    7. {
    8. auto ptr = countHT.Find(str);
    9. if (ptr)
    10. {
    11. ptr->_kv.second++;
    12. }
    13. else
    14. {
    15. countHT.Insert(make_pair(str, 1));
    16. }
    17. }
    18. }

    测试用例没加打印...让我来回看了好几遍代码...蠢到无语

    4. 问答

    1. 哈希就是一种用于高效查找的数据结构。

    2. 哈希之所以高效就是采用了哈希函数(散列函数),将元素和其存储位置建立了一一映射的关系,所以哈希必须使用哈希函数。

    3. 哈希冲突是不可能避免的

    4. 已知某个哈希表的n个关键字具有相同的哈希值,如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行()次探测。

      元素1:探测1次

      元素2:探测2次

      元素3:探测3次

      。。。

      元素n:探测n次

      故要将n个元素存入哈希表中,总共需要探测:1+2+3+...+n = n*(n+1)/2

     5. 采用开放定址法处理散列表的冲突时,其平均查找长度高于链接法处理冲突。

    开放定址法一旦产生冲突,冲突容易连在一起,引起一连篇的冲突,链地址法一般不会

    6. 线性探测采用未删除法,当从哈希表中删除某个元素时,并没有将该元素真正的删除掉,而是采用标记的方式处理,但是不能直接将该位置标记为空,否则会影响从该位置产生冲突的元素的查找。

  • 相关阅读:
    springboot基础(79):通过pdf模板生成文件
    取得网络中SQL的服务器列表
    Vue_Bug VUE-ELEMENT-ADMIN默认是英文模式
    SLAM从入门到精通(消息传递)
    Magisk搞机器记录(小米Mix3)
    SpringBoot实践(二十六):Security实现Vue-Element-Admin登录拦截
    c++类与对象
    arr.sort
    多旋翼无人机仿真 rotors_simulator:基于PID控制器的位置控制
    4.4K Star!推荐一款新一代的极简监控系统!轻量高性能!超500个监控指标,颜值高、功能强大!
  • 原文地址:https://blog.csdn.net/qq_68741368/article/details/127752439