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

news2024/12/25 12:49:41
在这里插入图片描述

💻文章目录

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


📄前言

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信扫码登录的两种方式:利用微信开放平台、利用微信公众平台(微信公众号)

微信扫码登录&#xff0c;有两种实现方式&#xff1a; 方式1、微信开放平台是微信为了接入更多第三方应用而开放的接口&#xff0c;依赖公司在【微信开放平台】用【公司营业执照】注册的账号&#xff0c;才能实现扫码登录 方式2、微信公众平台是扫码通过微信公众号授权登录的&a…

2023 如何下载最干净的 win10 win11 微软官方原版系统镜像(详细图文)

前言 不会吧不会吧&#xff0c;不会到现在还有人不会下载原版系统镜像吧 开始 win10官方下载工具下载地址&#xff1a;https://www.microsoft.com/zh-cn/software-download/windows10 win11官方下载工具下载地址&#xff1a;https://www.microsoft.com/zh-cn/software-downl…

Asp.net core WebApi 配置自定义swaggerUI和中文注释,Jwt Bearer配置

1.创建asp.net core webApi项目 默认会引入swagger的Nuget包 <PackageReference Include"Swashbuckle.AspNetCore" Version"6.2.3" />2.配置基本信息和中文注释&#xff08;默认是没有中文注释的&#xff09; 2.1创建一个新的controller using Micr…

基于PLC的采摘机械手系统(论文+源码)

1.系统设计 本次设计围绕基于PLC的采摘机械手系统进行设计&#xff0c; PLC即可编程控制器其是一种常见的微处理器&#xff0c;本次拟采用西门子是S7-200 PLC&#xff0c;一方面对整个设计从器件选型到I/O分配&#xff0c;图纸绘制等进行设计&#xff0c;另一方面还通过组态王…

4/150:寻找两个正序数组的中位数⭐ 5/150最长回文子串

题目&#xff1a;4/150寻找两个正序数组的中位数 给定两个大小分别为 m 和 n 的正序&#xff08;从小到大&#xff09;数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (mn)) 。 题解1&#xff1a;暴力 暴力思路简介&#x…

uniapp运行到安卓基座app/img标签不显示

img是html中的标签&#xff0c;他也是一个单标签 image属于服务器控件&#xff0c;是个双标签 问题&#xff1a;uniapp运行到app安卓基座后图片无法显示 原因&#xff1a;自己使用了img标签&#xff0c;而且输入路径无提示&#xff0c;img标签导致图片不显示 解决&#xff…

J-LINK J-FLASH 下载STM32单片机程序使用教程

J-LINK J-FLASH 下载STM32单片机程序使用教程 Chapter1 J-LINK J-FLASH 下载STM32单片机程序使用教程1.安装提供的 JLINK驱动程序2. 点击打开 J-Flash V7.223.点击 create a new project.&#xff08;使用后可以在软件菜单File保存这个烧写工程&#xff0c;后续直接打开使用即可…

一篇带你串通数据结构

文章目录 导论数据结构的定义数据结构在计算机科学中的重要性为什么学习数据结构很重要 1、基本概念1.1、数据、数据元素和数据项的概念1.2、数据对象与数据结构的关系1.3、逻辑结构与物理结构 2、线性结构2.1、数组2.2、链表2.3、栈2.4、队列 3、非线性结构3.1、树3.2、图 4、…

九、FreeRTOS之FreeRTOS列表和列表项

本节需要掌握以下内容&#xff1a; 1&#xff0c;列表和列表项的简介&#xff08;熟悉&#xff09; 2&#xff0c;列表相关API函数介绍&#xff08;掌握&#xff09; 3&#xff0c;列表项的插入和删除实验&#xff08;掌握&#xff09; 4&#xff0c;课堂总结&#xff08;掌…

Windows启动nacos操作文档

Windows启动nacos操作文档 1、新建数据库nacos_config 2、导入nacos\conf\nacos-mysql.sql文件 /******************************************/ /* 数据库全名 nacos_config */ /* 表名称 config_info */ /******************************************/ CREATE T…

JOSEF约瑟时间继电器ARTD-DC110V-2H2D 0.25-2.5s导轨安装

ARTD系列断电延时继电器&#xff1a; ARTD-220VDC-1H1D断电延时继电器&#xff1b;ARTD-220VDC-2H断电延时继电器&#xff1b; ARTD-220VDC-2H2D断电延时继电器&#xff1b;ARTD-220VDC-4H断电延时继电器&#xff1b; ARTD-110VDC-1H1D断电延时继电器&#xff1b;ARTD-110VD…

ssm的“魅力”西安宣传网站(有报告)。Javaee项目。

演示视频&#xff1a; ssm的“魅力”西安宣传网站(有报告)。Javaee项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringMvc MybatisVueLayuiElemen…

使用调研工具做好问卷调查的方法与策略

提起问卷调查大家应该都不陌生&#xff0c;学校会使用问卷调查收集学生信息或意见、企业使用问卷调查了解市场、深入用户。和其他的调查方式相比&#xff0c;问卷调查更能贴近被调查者真实想法&#xff0c;反馈真实数据。而互联网的崛起也使得大家纷纷从线下问卷转战到线上问卷…

AD7124-4 实测热电偶数据读取,电压精度到稳定到±1uV, 电压波动260nV, 温度精度到±0.01℃

AD7124-4 实测热电偶数据读取&#xff0c;电压精度到稳定到1uV, 电压波动260nV, 温度精度到0.01℃ AD7124_STM32_ADI官网例程使用stm32 和ad7124做温控调试&#xff0c;发现效果还是不错的&#xff0c;至少比ads1256的效果好多啦&#xff01;Chapter1 AD7124-4 实测热电偶数据读…

电路装修干货下篇|100平米房子需要多少平方电线呢?福州中宅装饰,福州装修

你是否曾经遇到过这样的情况&#xff1a;在装修房子时&#xff0c;对于电线的使用量感到困惑&#xff0c;不知道需要多少平方的电线才能满足100平方的房子需求。别担心&#xff0c;这篇文章将为你解答这个问题。 首先&#xff0c;我们需要了解电线规格。通常&#xff0c;电线规…

DOM 事件的传播机制

前端面试大全DOM 事件的传播机制 &#x1f31f;经典真题 &#x1f31f;事件与事件流 事件流 事件冒泡流 事件捕获流 标准 DOM 事件流 &#x1f31f;事件委托 &#x1f31f;真题解答 &#x1f31f;总结 &#x1f31f;经典真题 谈一谈事件委托以及冒泡原理 &#x1f3…

Android12蓝牙框架

参考&#xff1a; https://evilpan.com/2021/07/11/android-bt/ https://source.android.com/docs/core/connect/bluetooth?hlzh-cn https://developer.android.com/guide/topics/connectivity/bluetooth?hlzh-cn https://developer.android.com/guide/components/intents-fi…

22、DS1302实时时钟

DS1302介绍 DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时&#xff0c;且具有闰年补偿等多种功能 RTC&#xff1a;实时时钟&#xff0c;是一种集成电路&#xff0c;通常称为时钟芯片 引脚定义和应用…

深入计算机系统看性能优化

一&#xff0e;引言 “性能优化”&#xff0c;从计算机诞生之初就一直伴随着计算机技术的发展&#xff0c;直到现在。将来也必定不会消失。这是因为每个人都会追求性价比&#xff0c;花最少的钱&#xff0c;办最多的事。生活中也一样&#xff0c;就比如说泡茶&#xff0c;但凡…

弱网模拟工具

一、背景 一个人晚上在家通过 Wi-Fi 上网&#xff0c;在线电影播放基本流畅&#xff0c;可一旦在晚间用网高峰期打视频电话就画面糊&#xff0c;这时不仅可能带宽受限了&#xff0c;还可能有较高的丢包率。与有线网络通信相比&#xff0c;无线网络通信受环境影响会更大&#x…