「C++」哈希表的实现(unordered系底层)

在这里插入图片描述

💻文章目录

  • 📄前言
  • 哈希表概念
    • 哈希函数
  • 哈希冲突
    • 闭散列
    • 开散列
  • 📓总结


📄前言

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构,使其在查找上的时间复杂度几乎减低到了 O ( 1 ) O(1) O(1)

哈希表概念

顺序结构或者平衡树中,要查找一个元素,必须要经过关键码(查找的数值)的多次比较,顺序表和平衡树最佳的查找时间复杂度都为 O ( l o g 2 N ) O(log2_N) O(log2N)

哈希,是一种关键码与数值所一一映射的结构,如果能通过某种函数(HashFunc)使元素的存储位置和他的关键码创建一种映射关系,那么在查找时可以通过该函数快速的找到元素,而存储关键码和数值的顺序表就是哈希表。

哈希表的样例
在这里插入图片描述

哈希函数

哈希表是通过哈希函数构成的结构,其本质也是数组 。哈希方式中使用的函数也被成为哈希函数,使用哈系函数构成的结构称为哈希表。

常见的哈希函数

  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)作为哈希地址
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

哈希冲突

哈希冲突指的是不同关键码通过哈希函数被分配到了同一个哈希地址,哈希冲突是无法避免的,解决冲突的两种常见办法是:闭散列开散列

闭散列

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

  1. 线性探测
    比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
    因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    • 空间标记
      采用线性探测时,为了识别哈希表的位置是否为空,可以给表中每个空间一个标记。

      // 哈希表每个空间给个标记
      // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
      enum State{EMPTY, EXIST, DELETE};
      
    • 插入

      • 通过哈希函数获取待插入元素在哈希表中的位置
      • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
        在这里插入图片描述
    • 删除
      采用闭散列删除时,采用伪删除法删除一个元素,即把需要删除元素的空间设为DELETE。

线性探测的实现

	template<class K, class V>
	class HashTable
	{
		enum State{ EMPTY, EXIST, DELETE };
		struct Elem	//哈希表存储的元素
		{
            std::pair<K, V> _kv;
			State _state;
		};

	public:
		HashTable(size_t capacity = 3)
			: _ht(capacity), _size(0), _totalSize(0)
		{
			for (size_t i = 0; i < capacity; ++i)
				_ht[i]._state = EMPTY;
		}	//初始化元素

        void print()
        {	//打印元素
            for(int i = 0; i < _ht.size(); ++i)
            {	
                if(_ht[i]._state == EXIST)
                    std::cout << _ht[i]._kv.first << ":" << _ht[i]._kv.second << std::endl;
            }
        }

		// 插入
		bool Insert(const std::pair<K, V>& val)
        {
            if(Find(val.first) != -1)
                return false;
            
            CheckCapacity();	//检查是否需要扩容
                    
            size_t hashi = HashFunc(val.first);
            while(_ht[hashi]._state == EXIST)
            {	//存在冲突的情况,找到下一个非空
                hashi++;

                hashi %= _ht.size();
            }

            _ht[hashi]._kv = val;
            _ht[hashi]._state = EXIST;
            ++_size;

            return true;
        }

		// 查找
		size_t Find(const K& key)
        {
            size_t hashi = HashFunc(key);
            CheckCapacity();
            while(_ht[hashi]._state != EMPTY)
            {
                if(_ht[hashi]._state == EXIST
                && _ht[hashi]._kv.first == key)
                    return hashi;
                
                hashi++;
                hashi %= _ht.size();
            }

            return -1;
        }

		// 删除
		bool Erase(const K& key)
        {
            size_t hashi = Find(key);
            if(hashi == -1) return false;

            _ht[hashi]._state = DELETE;	//直接讲元素设为DELETE
            --_size;
            return true;
        }

		size_t Size()const
		{
			return _size;
		}

		bool Empty() const
		{
			return _size == 0;
		}

		void Swap(HashTable<K, V>& ht)  //交换节点
		{
            std::swap(_size, ht._size);
            std::swap(_totalSize, ht._totalSize);
			_ht.swap(ht._ht);
		}


	private:
		size_t HashFunc(const K& key)
		{
			return key % _ht.capacity();
		}

		void CheckCapacity()
        {
            if(_size * 10 / _ht.size() >= 7)
            {
                HashTable<K, V> newHT;
                newHT._ht.resize(_ht.size() * 2);
                for(size_t i = 0; i < _ht.size(); ++i)
                {
                    if(_ht[i]._state == EXIST)
                        newHT.Insert(_ht[i]._kv);
                }

                _ht.swap(newHT._ht);
            }
        }
	private:
        std::vector<Elem> _ht;
		size_t _size;
	};
}

开散列

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

在这里插入图片描述

开散列哈希表及其迭代器的声明

template <class T>
struct HashBucketNode
{
    HashBucketNode<T>* _next;	//下一个节点
    T _data;		//节点的数据
    HashBucketNode(const T& data)
    :_data(data)
    ,_next(nullptr)
    {}
};

// 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
// 因为迭代器中用到了哈希桶,所以得先声明。
template<class K, class T, class KeyOfValue, class HF>
class HashBucket;

// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template <class K, class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator 
{
	typedef HashBucket<K, V, KeyOfValue, HF> HBK;	
	typedef HashBucketNode<V> Node;		
	typedef HBIterator<K, V, Ref, Ptr, KeyOfValue, HF> Self;	
    typedef HBIterator<K, V, V&, V*, KeyOfValue, HF> iterator;	//这个是为了实现unordered_set所准备的

	Node* _node;             // 当前迭代器关联的节点
	const HBK* _pHt;         // 哈希桶--主要是为了找下一个空桶时候方便
    size_t _hashi;	//当前的未知

    HBIterator(const iterator& it)
    :_node(it._node)
    ,_pHt(it._pHt)
    ,_hashi(it._hashi)
    {}

	HBIterator(Node* pNode = nullptr, const HBK* pHt = nullptr, size_t hashi = -1)
    :_node(pNode)
    ,_pHt(pHt)
    ,_hashi(hashi)
    {}

    Self& operator++();	//C++ unordereded系不支持--操作
    Ref operator*();
	Ptr operator->();
}

template <class K>
struct HashFuc	//将数据转换成int,方便哈希函数取余数
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};

template <class K, class T, class KeyOfValue, class HF = HashFuc<K>>
class HashBucket	//哈系桶
{
    template<class Key, class Value, class Ref, class Ptr, class KeyOfT, class Hash>  //clang error
	friend struct HBIterator;	//迭代器需要使用到类的私有成员,所以将其设为友元类

public:
    typedef HashBucketNode<T> Node;
    typedef HBIterator<K, T, T&, T*, KeyOfValue, HF> iterator;
    typedef HBIterator<K, T, const T&, const T*, KeyOfValue, HF> const_iterator;
    
    iterator find(const K& key);	//查找
    std::pair<iterator, bool> insert(const T& val);    //插入
    bool erase(const K& key);       //删除
private:
	void CheckCapacity();	//检查是否需要扩容
private:
    HF _hf;
    KeyOfValue _kot;
    std::vector<Node*> _ht;
    size_t _size;

迭代器的实现

Self& operator++()
{
    _node = _node->_next;	
    if(!_node)	//如果节点为空,在哈希表探索
    {
        while(!_node && _hashi < _pHt->_ht.size())
        {	//寻找下一个非空节点
            _node = _pHt->_ht[++_hashi];
        }
    }
    if(_hashi >= _pHt->_ht.size())
        _node = nullptr;

    return *this;
}

Ref operator*()
{
    return _node->_data;
}

Ptr operator->()
{
    return &operator*();
}

插入实现

iterator find(const K& key)
{
    size_t hashi = _hf(key) % _ht.size();
    
    Node* cur = _ht[hashi];
    while(cur)
    {
        if(_kot(cur->_data) == key)
            return iterator(cur, this, hashi);

        cur = cur->_next;
    }

    return iterator(cur);
}

std::pair<iterator, bool> insert(const T& val)    //插入
{
    iterator it = find(_kot(val));	//寻找位置
    if (it != end())
    {	//节点存在的情况
        return std::make_pair(it, false);
    }
        
    CheckCapacity();	//检查扩容

    size_t hashi = _hf(_kot(val)) % _ht.size();
    Node* node = new Node(val);

    node->_next = _ht[hashi];		//头插到所在位置
    _ht[hashi] = node;
    _size++;
    
    return std::make_pair(iterator(node, this, hashi), true);
}

void CheckCapacity()
{
    if(_size == _ht.size())
    {
        std::vector<Node*> newHT;
        newHT.resize(_ht.size() * 2, nullptr);
        for(size_t i = 0; i < _ht.size(); ++i)
        {
            Node* node = _ht[i];
            while(node)
            {
                Node* next = node->_next;
                size_t hashi = _hf(_kot(node->_data)) % newHT.size();
                node->_next = newHT[hashi];
                newHT[hashi] = node;

                node = next;
            }
            _ht[i] = nullptr;
        }

        _ht.swap(newHT);
    }
}

删除

bool erase(const K& key)       //删除
{
    size_t hashi = _hf(key) % _ht.size();
    Node* cur = _ht[hashi];
    Node* prev = nullptr;
    while(cur) 
    {
        if(_kot(cur->_data) == key)
        {
            if(!prev)
            {
                _ht[hashi] = cur->_next;
            }
            else 
            {   
                prev->_next = cur->_next;
            }
            
            --_size;
            delete cur;
            return true;
        }
        prev = cur;
        cur = cur->_next;
    }
    return false;
}

📓总结

优点缺点
闭散列实现简单容易导致数据堆积
开散列存储开销减少如果数据过于集中,会导致查找性能上的损耗

📜博客主页:主页
📫我的专栏:C++
📱我的github:github

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/211250.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一文了解工业互联网是什么,和传统互联网的区别有哪些

几个问题 工业互联网和传统互联网有什么区别 1 业务方面&#xff0c;传统的互联网企业更多是toC的业务&#xff0c;直接面对的是一个个的个体&#xff0c;而工业互联网离消费者更远一点&#xff0c;往往是toB或者toG的&#xff1b; 个人认为这也是最根本的区别&#xff0c;由…

Innodb数据空间占用探索

了解数据存储空间占用&#xff0c;可以更方便我们再企业中对于数据库相关优化做评估。 一、查看当前数据表空间占用信息 首先这里准备一张数据库表约2.3w数据量&#xff1a; CREATE TABLE project (tenantsid bigint(20) NOT NULL DEFAULT 0 COMMENT 租户ID,project_id bigi…

09-命令者模式-C语言实现

命令者模式是一个高内聚的模式&#xff0c; 其定义为&#xff1a; Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.&#xff08;将一个请求封装成一个对象&…

almalinux centos8系统zlmediakit编译安装

脚本 # 安装依赖 gcc-c.x86_64 这个不加的话会有问题&#xff0c; cmake需要在线安装 sudo yum -y install gcc gcc-c libssl-dev libsdl-dev libavcodec-dev libavutil-dev ffmpeg git openssl-devel gcc-c.x86_64 cmake mkdir -p /home/zenglg cd /home/zenglg git clon…

【开箱即用】前后端同时开源!周末和AI用Go语言共同研发了一款笔记留言小程序!

大家好&#xff0c;我是豆小匠。 真的是当你在怀疑AI会不会取代人类的时候&#xff0c;别人已经用AI工具加速几倍的生产速度了… 周末体验了和AI共同开发的感受&#xff0c;小项目真的可以一人全干了… 本次实验使用的AI工具有两个&#xff1a;1. GitHub Copilot&#xff08;…

JVM运行时数据区域

文章目录 内存结构程序计数器&#xff08;寄存器&#xff09;虚拟机栈局部变量表两类异常状况 线程运行诊断 本地方法栈堆方法区运行时常量池串池&#xff08;StringTable&#xff09;字符串的拼接串池的位置StringTable垃圾回收StringTable性能调优 直接内存 内存结构 程序计…

blue beacon rssi 指纹室内定位数据集

数据集是开展实验的基础&#xff0c;搜集并分享。如果你有关于室内定位的问题&#xff0c;请联系博主。 namedatesetpapercommentBLEBeacon: A Real-Subject Trial Dataset from Mobile Bluetooth Low Energy Beaconshttps://github.com/dimisik/BLEBeacon-Datasethttps://arxi…

【云备份】业务处理

文章目录 1. 业务处理作用功能 2. 代码框架编写构造函数UpLoad ——文件上传请求ListShow —— 展示页面请求处理实现Download —— 下载请求的处理实现断点续传实现 1. 业务处理 作用 业务处理模块是对客户端的业务请求进行处理 功能 1.文件上传请求&#xff1a;备份客户端…

RK3568平台开发系列讲解(Linux系统篇)netlink 监听广播信息

** 🚀返回专栏总目录 文章目录 一、什么是netlink 机制二、netlink 的使用2.1、创建 socket2.2、绑定套接字2.3、接收数据沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍如何通过 netlink 监听广播信息。 一、什么是netlink 机制 Netlink 是 Linux 内核中…

反序列化漏洞详解(一)

目录 一、php面向对象 二、类 2.1 类的定义 2.2 类的修饰符介绍 三、序列化 3.1 序列化的作用 3.2 序列化之后的表达方式/格式 ① 简单序列化 ② 数组序列化 ③ 对象序列化 ④ 私有修饰符序列化 ⑤ 保护修饰符序列化 ⑥ 成员属性调用对象 序列化 四、反序列化 …

Stream

什么是Stream&#xff1f; 也叫Stream流&#xff0c;是jdk8开始新增的一套API&#xff0c;可以用来操作集合或者数组的数据 优势&#xff1a;Stream流大量的结合了Lambda的语法风格来编程&#xff0c;提供了一种更加强大&#xff0c;更加简单的方式操作集合或数组中的数据&am…

CTF-虚拟机-QEMU-前置知识-操作流程与源码阅读

文章目录 总览内存PCI设备PCI配置空间前64个字节对应源码Memorry空间的BARIO空间的BAR MMIOPMIOIspci访问PCI设备配置空间中的Memory空间和IO空间MMIOPMIO QQM&#xff08;qemu object model&#xff09;简洁概要将 TypeInfo 注册 TypeImpl&#xff1a;ObjectClass的初始化&…

新款任务悬赏拉新地推本地任务同城地区定位游戏试玩任务联盟众人帮威客兼职任务墙

新款任务悬赏拉新地推本地任务同城地区定位游戏试玩任务联盟众人帮威客兼职任务墙 源码开源无任何加密及授权 后端采用PHPTinkCMF 前端采用UniappVUE 网页端双端APP可封装小程序可对接公众号登录 采用原生混合框架&#xff0c;拒绝卡顿。 https://download.csdn.net/downl…

文件操作--IO

目录 ♫什么是文件 ♫文件路径 ♫文件类型 ♫文件的管理 ♪File的构造方法 ♪File的常用方法 ♫文件的内容操作 ♪InputStream ♪OutputStream ♪字符流读写文件 ♫Scanner与流对象 ♫什么是文件 文件在计算机里可以指“狭义”的文件&#xff08;指硬盘上的文件和目录&…

第一百八十七回 DropdownButton组件

文章目录 1. 概念介绍2. 使用方法2.1 DropdownButton2.2 DropdownMenuItem 3. 示例代码4. 内容总结5. 经验分享 我们在 上一章回中介绍了"DropdownMenu组件"相关的内容&#xff0c;本章回中将介绍 DropdownButton组件.闲话休提&#xff0c;让我们一起Talk Flutter吧…

基于SpringBoot学生宿舍管理系统的设计与开发

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对学生宿舍信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差…

javaee实验:MVC 框架技术应用——URL 映射及方法参数的使用

目录 urlmvc框架mvc框架的设计mvc流程 实验目的实验内容实验过程创建项目创建项目结构编写代码简单测试一下 url 和 Hypertext 以及 HTTP 一样&#xff0c;URL 是 Web 中的一个核心概念。它是浏览器用来检索 web 上公布的任何资源的机制 URL 代表着是统一资源定位符&#xff…

OpenCV技术应用(6)— 暖色滤镜和冷色滤镜

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。本节课就手把手教大家如何将一幅图像转化成暖色滤镜和冷色滤镜&#xff0c;希望大家学习之后能够有所收获~&#xff01;&#x1f308; 目录 &#x1f680;1.技术介绍 &#x1f680;2.暖色滤镜 &#x1f680;3.冷色滤…

每日一练:阿姆斯特朗数

1. 概述 阿姆斯特朗数&#xff08;Armstrong number&#xff09;&#xff0c;也称为自恋数、自幂数&#xff08;narcissistic number&#xff09;、水仙花数&#xff0c;是指一个n位数&#xff08;n≥3&#xff09;&#xff0c;它的每个位上的数字的n次幂之和等于它本身。换句话…

AVFormatContext协议层:理论与实战

文章目录 前言一、协议操作对象结构二、初始化 AVIOContext 函数调用关系三、avio 实战 1&#xff1a;打开本地文件或网络直播流1、示例源码2、运行结果①、解决方法 1②、解决方法 2 四、avio 实战 2&#xff1a;自定义 AVIO1、示例源码2、运行结果 五、avio 实战 3&#xff1…