| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 模拟实现Hash-Table线性探测/开散列 -> 正文阅读 |
|
[数据结构与算法]模拟实现Hash-Table线性探测/开散列 |
这篇文章都是以如何模拟实现hash(线性探测),关于Hash-Table介绍这里只说大概,方便了解。 概念Hash-Table也叫散列表,哈希表,是根据关键字(key)来直接访问对应存储值的的数据结构。 虽然说是根据关键字,但其本质还是通过 vector 的下标来找到对应值。 构建当我们决定构建一个哈希表的时候,就需要用到一个 vector ,vector 里面存储的值为了方便我们之后使用,会有两个参数: 1、pair ,为了我们之后放数据使用 2、state ,这个值的作用就是为了插入数据的时候判断,这个位置能否插入,因为我们插入数据再删除,我们如何把这个位置变为一个不可使用的值?state 就是为了解决这个问题的。 先将要放的数据类型和一些其他配置写好: enum State { EMPTY, EXITS, DELETE }; ? template <class K,class V> struct HashData { ? pair<K, V> _kv; State _state; }; vector 里面要放的数据就是这个 HashData。 然后我们正式就可以开始写 HashTable 了。 template <class K,class V> class HashTable { typedef HashData<K,V> Data; ? ? ? vector<Data> _table; size_t _size = 0; }; 在插入之前,我们要了解的几点: 1、如何插入 通过将传过来的 K (可能是整数,或者是字符串)来进行处理,得到对应的下标,这个下标有一个特点,不同的值要对应不同的下标,然后在下标对应的值放上要存储的数据。 2、如果插入的位置已经有值了怎么办 那就有两种办法: 线性探测和二次探测,这里分别说一下是什么意思。 线性探测:如果当前位置有值了,就往后面找空位置,然后放上数据,隐患是很容易出现需要找一整个 vector 的情况,但是能用。 二次探测:如果当前位置有值,就通过平方的办法来找下标,如果平方大了,就取余 3、空间不够了怎么办 扩容,然后将原本的数据值交给扩容的空间。 插入思路: 插入之前,先判断这个数据在表内是否存在,如果存在,就不用放进去(不是mul版本),然后判断一下空间大小,如果空间不是很足,就先扩容,之后再通过传进来的第一个值来找到对应下标。 这里有一个问题就是,如何确保传进来的第一个数据是 string 类型还能找到其下标,这里就要通过仿函数了。 就跟之前模拟实现的那些比较大小的值一样,这里我们可以重载 string ,然后找到其对应的值。 template <class K> struct DefaultHash { size_t operator()(const K& key) { return (size_t)key; } }; ? template<> struct DefaultHash < string > { size_t operator()(const string& key) { size_t hash = 0; for (auto ch : key) { hash += hash * 13 + ch; } } }; 通过重载来完成找到下标的办法,这里字符串的计算方法来自《The C Programming Language》,这书很长,但是这个网址找到了常用的几种算法:各种字符串Hash函数 - clq - 博客园 (cnblogs.com) 这里选取的是第一种。 如何判断要不要孔扩容,这里就用存储大小来判断,如果一开始空表肯定是要扩容的,当存储的数据大于 7/10 也可以扩容。 扩容就是新开一个table,然后往里面插入当前数组的有效值(不为空或者删除的值),然后交换啷个vector 即可。 bool Insert(const pair<K, V>& kv) { if (Find(kv.first)){ return false; } //空vector 或者是存储数据超过七成 if (_table.size() == 0 || _size * 10 / _table.size() >= 7) { //判断新开空间大小 size_t newsize = _size == 0 ? 10 : _size * 2; HashTable<K, V> newtable; newtable._table.resize(newsize); //遍历插入 for (auto& e : _table) { if (e._state == EXITS) newtable.Insert(e._kv); } newtable._table.swap(_table); } ? //通过创建对象来使用仿函数 Func f; size_t pos = f(kv.first); pos %= _table.size(); //如果当前位置是已经有值的,就继续往后找,直到为空或者已经删除的位置 while (_table[pos]._state == EXITS) { pos++; pos %= _table.size(); } //将这里的值覆盖即可 _table[pos]._kv = kv; //别忘了打上标记表示这里已经有值 _table[pos]._state = EXITS; _size++; return true; } 查找查找的思路跟插入差不多,找到下标然后判断,如果这个位置不是就往后找,直到为空 Data* Find(const K& key) { //为空就不用找了 if (_table.size() == 0) return false; //找下标 Func func; size_t pos = func(key); pos %= _table.size(); //一直找到为空的位置 while (_table[pos]._state != EMPTY) { if (_table[pos]._state != DELETE && _table[pos]._kv.first == key){ return &_table[pos]; } pos++; pos %= _table.size(); } return nullptr; } 删除这...,直接用Find找,找到了就直接将那个位置的 state 置为删除状态即可。 bool Earse(const K& key) { auto tmp = Find(key); if (tmp){ tmp._state = DELETE; return true; } return false; } 哈希桶之前所使用的 vctor 里面放数据,如果有位置上有数据就往后移,这种办法称之为:闭散列,因为所有的数组都会在 vector 里面不停增长,为了解决这种闭散列可能出现的拥挤问题,这里引出了下面要说的:开散列。 开散列跟闭散列最大的不同在于,闭散列存放的是值,而开散列存放的是指针,这里说一下,指针也分单向双向之类,这里使用的是单向不循环链表。 当我们要存放值的位置上有数据了,我们就不需要再往后找空位置了,只需要用指针将它们连接起来即可。 所以也不需要什么 state 来看当前节点状态,下面就正式来实现一下: 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) {} }; 插入思路其实跟之前那个差不多,都是找下标然后插入,空间不够扩容,这里要说一下的是扩容,当我们把原先位置的 vector 交换给新的 vector 之后,原本的 vector 被释放,但是里面存放的值,也就是我们之前节点的数据可不会被释放,就会出现内存泄漏的情况,为了解决这种问题,我们可以直接在新 vector 里面插入旧 vector 的节点,这样即避免了新开辟节点,也避免了上面的内存泄露。 这里放上代码: bool Insert(const pair<K, V>& kv) { Func Hf; if (Find(kv.first)) return false; //跟之前的 0.7 不同,这里满了就扩容 if (_table.size() == _size) { //算新开的空间大小 size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2; //新开空间 vector<Node*> newtable; newtable.resize(newsize, nullptr); ? //遍历之前 vector 里面的所有节点 bool Insert(const pair<K, V>& kv) { if (Find(kv.first)) return false; ? //跟之前的 0.7 不同,这里满了就扩容 if (_table.size() == _size) { //算新开的空间大小 size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2; //新开空间 vector<Node*> newtable; newtable.resize(newsize, nullptr); ? //遍历之前 vector 里面的所有节点 for (int i = 0; i < _table.size(); i++) { Node* cur = _table[i]; while (cur) { //当我们插入节点之后,这个节点的 next 就指向了空,为了避免出现这个节点下面还有数据的情况,所以要先保留一下 Node* next = cur->_next; ? size_t pos = (Hf(cur->_kv.first)) %= newsize; cur->_next = newtable[pos]; newtable[pos] = cur; ? cur = next; } //将原本位置职位空 _table[i] = nullptr; } //交换空间 newtable.swap(_table); } ? //如果无需扩容/扩容结束,就让新节点的 next 直接指向下标所处的空间,然后让下标所处的空间指向这个即可,相当于链表的头插 size_t pos = Hf(kv.first); pos %= _table.size(); Node* newnode = new Node(kv); newnode->_next = _table[pos]; _table[pos] = newnode; ++_size; return true; } for (int i = 0; i < _table.size(); i++) { Node* cur = _table[i]; while (cur) { ? //当我们插入节点之后,这个节点的 next 就指向了空,为了避免出现这个节点下面还有数据的情况,所以要先保留一下 Node* next = cur->_next; ? size_t pos = (Func(cur->_kv.first)) %= newsize; cur->_next = newtable[pos]; newtable[pos] = cur; ? cur = next; } //将原本位置职位空 _table[i] = nullptr; } //交换空间 newtable.swap(_table); } ? //如果无需扩容/扩容结束,就让新节点的 next 直接指向下标所处的空间,然后让下标所处的空间指向这个即可,相当于链表的头插 size_t pos = Func(kv.first); pos %= _table.size(); Node* newnode = new Node(kv); newnode->_next = _table[pos]; _table[pos] = newnode; ++_size; return true; } 查找bool Find(const K& key) { if (_size == 0) return false; ? Func Hf; size_t pos = (Hf(key)) %= _table.size(); Node* cur = _table[pos]; while (cur) { if (cur->_kv.first == key) return true; ? cur = cur->_next; } return false; } 查找就比较简单,所以这里就不打注释了。 删除bool Erase(const K& key) { if (_size == 0) return false; ? Func Hf; size_t pos = (Hf(key)) %= _table.size(); Node* cur = _table[pos]; Node* prev = nullptr; while (cur) { if (cur->_kv.first == key) { prev == nullptr ? : _table[pos] = nullptr : _table[pos] = cur->_next; delete cur; return true; } prev = cur; cur = cur->_next; } ? return false; } 单链表的知识,这里要用一个 prev 来保存 cur 上一个节点的位置,之后我们要跳过 cur 将链表链接起来。 如果 prev 等于空,就说明这个位置就是我们要找的,所以直接将这个位置置为 cur 的下一个即可。 析构虽然在扩容的时候说不需要写析构,但是我们最后一次使用,它不扩容的时候,那些节点还是需要我们自己释放的,所以析构函数还是有必要的。 ?? ?~HashBacket() |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/28 18:31:13- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |
数据统计 |