【C++】哈希——unordered系列容器&哈希概念&哈希冲突(下)

简介: 【C++】哈希——unordered系列容器&哈希概念&哈希冲突(下)

闭散列二次探测的哈希表代码实现插入删除查找

实际上,线性探测也是具有一定的缺陷的,线性探测的缺陷就是会将产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为image.png

其中:i=1,2,3...,H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

那么,对于2.1中如果要插入44,产生冲突,使用解决后的情况为

5739f252583296fa362baa490091fbc8.png

由于闭散列不管使用什么探测方式,都是治标不治本,所以这里就不再继续代码实现二次探测了,下面我们看一看开散列的实现方式


5.2 开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

54071a3ab2a0c28089ba4eacfb18f70e.png

6502dabbf0a37003b17a1123088b7d88.png

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

开散列哈希表代码实现插入删除查找


1、首先由于结构的原因,要把数据链接起来需要一个节点类:

template<class K, class V>
struct HashNode
{
    std::pair<K, V> _kv;
    HashNode* _next;
    HashNode(const std::pair<K, V>& kv)
        :_kv(kv)
            ,_next(nullptr)
        {}
};


2、同样的,由于KV结构中的key是一个泛型,当我们在进行哈希映射的时候,需要先让其映射成为整型,然后才能够映射到哈希地址,所以这里提供一个仿函数,用于将key映射到整型家族

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};
template<>//模板的特化
struct HashFunc<std::string>
{
    size_t operator()(const std::string& key)
    {
        size_t hash = 0;
        for (auto ch : key)
        {
            hash *= 131;
            hash += ch;
        }
        return hash;
    }
};


3、开散列的哈希表结构

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    HashTable()
        :_n(0)
        {
            _tables.resize(10);
        }
private:
    std::vector<Node*> _tables;//本质上是一个指针数组,存放节点指针。
    size_t _n = 0;
};


4、插入

bool Insert(const std::pair<K,V>& kv)
{
    if(Find(kv.first))
        return false;
    //扩容
    //负载因子为1的时候就扩容
    if(_n == _tables.size())
    {
        //扩容方式有两种,一种是遍历,然后创建新节点挂在新表上
        //由于方案一造成的消耗较大,所以这里就不实现了
        //                HashTable<K, V, Hash> newTable;
        //                newTable._tables.resize(2 * _tables.size());
        //另一种是直接更改节点的指向
        std::vector<Node*> newTables;
        newTables.resize(2* _tables.size(), nullptr);//这里暂时使用2倍的方式扩容
        for(size_t i = 0; i < _tables.size(); ++i)//遍历旧表,依次拿到每个桶的头节点
        {
            Node* cur = _tables[i];
            while(cur)
            {
                Node* next = cur->_next;//先使用一个指针保存next,以免更改cur指向之后找不到其他节点
                size_t hashi = Hash()(cur->_kv.first) % newTables.size();//计算哈希位置
                //头插到新表中
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;
                cur = next;//迭代到next
            }
            _tables[i] = nullptr;//将旧表的内容置空,以免出现自动析构旧表的时候释放节点
        }
        _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;
}


删除

bool Erase(const K& key)
{
    size_t hashi = Hash()(key) % _tables.size();
    Node* prev = nullptr;//用prev存放当前节点的上一个节点,从而链接cur的前后节点
    Node* cur = _tables[hashi];
    while(cur)
    {
        if(cur->_kv.first == key)//找到了,准备删除
        {
            if(_tables[hashi] == cur)//删除桶的头节点
            {
                _tables[hashi] = cur->_next;
            }
            else//删除非头节点
            {
                prev->_next = cur->_next;
            }
            delete cur;
            --_n;
            return true;
        }
        else//没找到
        {
            prev = cur;
            cur = cur->_next;
        }
    }
    return false;
}


查找

Node* Find(const K& key)
{
    size_t hashi = Hash()(key) % _tables.size();//找到key对应的哈希地址
    Node* cur = _tables[hashi];//遍历该地址对应的哈希桶
    while(cur)
    {
        if(cur->_kv.first == key)//找到了
        {
            return cur;
        }
        else//没找到
        {
            cur = cur->_next;
        }
    }
    return nullptr;
}


本节完……

相关文章
|
27天前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
41 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
27天前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
44 5
|
27天前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
44 2
|
27天前
|
存储 算法 C++
【算法】哈希映射(C/C++)
【算法】哈希映射(C/C++)
|
2月前
|
运维 Cloud Native Devops
云原生架构的崛起与实践云原生架构是一种通过容器化、微服务和DevOps等技术手段,帮助应用系统实现敏捷部署、弹性扩展和高效运维的技术理念。本文将探讨云原生的概念、核心技术以及其在企业中的应用实践,揭示云原生如何成为现代软件开发和运营的主流方式。##
云原生架构是现代IT领域的一场革命,它依托于容器化、微服务和DevOps等核心技术,旨在解决传统架构在应对复杂业务需求时的不足。通过采用云原生方法,企业可以实现敏捷部署、弹性扩展和高效运维,从而大幅提升开发效率和系统可靠性。本文详细阐述了云原生的核心概念、主要技术和实际应用案例,并探讨了企业在实施云原生过程中的挑战与解决方案。无论是正在转型的传统企业,还是寻求创新的互联网企业,云原生都提供了一条实现高效能、高灵活性和高可靠性的技术路径。 ##
151 3
|
1月前
|
程序员 C++ 开发者
C++入门教程:掌握函数重载、引用与内联函数的概念
通过上述介绍和实例,我们可以看到,函数重载提供了多态性;引用提高了函数调用的效率和便捷性;内联函数则在保证代码清晰的同时,提高了程序的运行效率。掌握这些概念,对于初学者来说是非常重要的,它们是提升C++编程技能的基石。
21 0
|
3月前
|
Linux 持续交付 虚拟化
在Linux中,Docker和容器虚拟概念是什么?
在Linux中,Docker和容器虚拟概念是什么?
|
3月前
|
消息中间件 Kubernetes 数据库
在k8S中,初始化容器(init container)概念原理是什么?
在k8S中,初始化容器(init container)概念原理是什么?
|
3月前
|
安全 编译器 容器
C++STL容器和智能指针
C++STL容器和智能指针
|
3月前
|
C++ 容器
C++中自定义结构体或类作为关联容器的键
C++中自定义结构体或类作为关联容器的键
39 0