C++哈希表

news2024/11/16 1:37:38

目录

  • 介绍
    • 哈希概念
    • 哈希冲突
    • 哈希函数
    • 解决哈希冲突
  • 闭散列
    • 介绍
      • 线性探测
      • 二次探测
      • 负载因子
    • 实现
      • 哈希表结构
      • 哈希函数
      • 元素查找
      • 插入元素
      • 删除元素
  • 开散列
    • 介绍
    • 实现
      • 哈希表结构
      • 元素查找
      • 插入元素
      • 删除元素
      • 析构函数

介绍

哈希概念

    了解过搜索二叉树与红黑树后,它们的结构特点主要是为了进行快速查找, O ( l o g 2 N ) O(log_2N) O(log2N)的时间复杂度,通常在几次关键码的比较后就能找到目标元素。但是最最理想的搜索,是能够像数组那样,知道元素的下标,直接就可以访问,时间复杂度达到 O ( 1 ) O(1) O(1)

    其实哈希结构就是与数组类似的,通过元素的关键码计算出下标(哈希映射)。通过这一步计算,可以将元素存储到对于位置,同样也可以在对应位置访问该元素。

示例:将6个数字存储到哈希结构中
在这里插入图片描述

哈希冲突

对于两个数据元素i和j,其中关键码 k i ≠ k j k_i \ne k_j ki=kj,但是hash( k i k_i ki) = hash( k j k_j kj)

即:不同关键字通过相同哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

当插入数字9时,进行哈希计算后下标为1,但是该位置以及存在其他元素。
在这里插入图片描述

哈希函数

引起哈希冲突的一个原因:哈希函数不够合理

哈希函数:能将任意长度的关键码映射成固定长度的数据(下标)

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
    域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该较简单

常见的哈希函数(了解):

  • 直接定址法

取关键字的某个线性函数为散列地址:$Hash(Key)= A*Key + B $

  • 除留余数法

散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数
H a s h ( k e y ) = k e y   m o d   p ( p < = m ) Hash(key) = key \bmod p (p <= m) Hash(key)=keymodp(p<=m)
选择质数作为除数:由于质数本身不存在多余的因子,所以与其他数做取模运算的结果更加分散

  • 平方取中法

例如:关键字为1234,平方为1522756,选取中间3位:277作为哈希地址

  • 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址

  • 随机数法

H a s h ( k e y ) = r a n d o m ( k e y ) Hash(key) = random(key) Hash(key)=random(key)

解决哈希冲突

解决哈希冲突两种常见的方法是:闭散列开散列

闭散列

介绍

闭散列也叫做开放定址法

    当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中下一个空位置中

线性探测

当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止

int index = Hash(key);
while(hashTable[index].state == EXITS)
{
    ++index;//向后遍历
}

对于发生冲突的情况,向后遍历寻找最近的一个空位置—线性探测
在这里插入图片描述

线性探测的缺陷是产生冲突的数据会堆积在一起 ,寻找某个关键码可能需要多次比较,导致效率降低。

二次探测

寻找下一个空位置时,不再挨着逐个去找,选择逐步跳跃式的去找,避免冲突堆积

int index = Hash(key);
int start = index, i = 0;
while(hashTable[index].state == EXITS)
{
    index = start + i * i;
    ++i;
}

插入数字16时,发生冲突,选择二次探测的方式查找空位置
在这里插入图片描述

负载因子

随着插入数据的增多, 插入的数据产生冲突的概率也增加了。冲突的增加,在查找时的效率也会降低。

装载因子(load factor) = 表中有效数据个数 / 空间大小

  • 装载因子越大,数据越多,冲突的可能性越大
  • 装载因子越小,数据越少,冲突的可能性越小

通常来说,当哈希表的装载因子超过0.7时就需要考虑进行扩容,否则可能会导致插入/查找操作的效率大幅下降。

实现

哈希表结构

enum State
{
	EMPTY,	//空状态
	EXITS,	//使用状态
	DELETE,	//删除状态
};

template<class K, class V>
struct HashDate
{
	pair<K, V> _kv; // 存储数据的键值对
	State _state;   // 标记状态
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
private:
	vector<HashDate<K, V>> _table; // 哈希数组
	size_t _n = 0;  // 有效数据个数,来计算装载因子
};

哈希函数

将k类型的关键码转换为整型

template<class K>
    struct HashFunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
        }
    };
// 特化  20:12继续
template<>
struct HashFunc<string>
{
    // BKDR哈希算法
    size_t operator()(const string& key)
    {
        size_t val = 0;
        for (auto ch : key)
        {
            val *= 131;
            val += ch;
        }

        return val;
    }
};

BKDR哈希算法是由徐盛忠教授于1992年提出的一种哈希函数,它是通过将字符串视为整数来计算哈希值的。其基本思想是使用一个较小的质数seed(比如31、131、1313、13131等),将字符串中各个字符的ASCII码转化为整数后,与seed相乘并求和得到一个哈希值

元素查找

HashDate<K, V>* find(const K& key)
{
    if (_table.size() == 0)//没有元素
    {
        return nullptr;
    }


    HashFunc hf;	//通过仿函数将关键码转换为整型
    size_t index = hf(key) % _table.size();//除留余数法

    // 线性探测
    while (_table[index]._state != EMPTY)
    {
        // 找到待查找元素,并返回它的地址
        if (_table[index]._state == EXITS && 
            _table[index]._kv.first == key)
        {
            return &_table[index];
        }

        ++index;
        index %= _table.size();

    }
    return nullptr;// 不存在此元素
}

插入元素

bool insert(const pair<K, V>& kv)
{
    if (find(kv.first))
    {
        return false;	// 元素已存在
    }


    if (_table.size() == 0)
    {
        _table.resize(10); //设置哈希表初始空间大小
    }
    // 装载因子超过阈值0.7就进行扩容
    else if( _n * 1.0 /_table.size() > 0.7)
    {
        //创建一个新的哈希表,大小为原哈希表的2倍
        HashTable<K, V> newHT;
        newHT._table.resize(2 * _table.size());
        //将原数据插入到新哈希表
        for (auto& e : _table)
        {
            if (e._state == EXIST)
            {
                newHT.Insert(e._kv);
            }
        }
        //交换两个哈希表
        _table.swap(newHT._table);
    }

    // 用哈希函数算出哈希表中的映射位置
    HashFunc hf;
    size_t index = hf(kv.first) % _table.size();

    // 线性探测
    while (_table[index]._state == EXITS)
    {
        ++index;
        index %= _table.size(); // 防止下标越界
    }
    //插入元素
    _table[index]._kv = kv;
    _table[index]._state = EXITS;
    ++_n;

    return true;
}

删除元素

bool erase(const K& key)
{
    //查找该元素
    HashDate<K, V>* ret = find(key);
    if (ret == nullptr)
        return false;	//不存在
    else // 找到
    {
        ret->_state = DELETE;//删除
        --_n;
        return true;
    }
}

开散列

介绍

开散列也叫做链地址法

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

在这里插入图片描述
产生哈希冲突的元素会通过链表组织起来,形成一个子集合-----哈希桶

实现

哈希表结构

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)
	{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
private:
	vector<Node*> _tables;//哈希表
	size_t _size = 0; // 存储有效数据个数
};

元素查找

Node* find(const K& key)
{
    if (_table.size() == 0)
        return nullptr;	//哈希表为空

    //哈希映射
    hash hash;
    size_t index = hash(key) % _table.size();	
    Node* cur = _table[index];
    while (cur != nullptr)
    {
        if (cur->_kv.first == key)
            return cur;
        else cur = cur->_next;
    }
    return nullptr;
}

插入元素

bool insert(const pair<K, V>& kv)
{
    // 去重
    if (Find(kv.first)) return false;

    if (_table.size() == 0)
    {
        _table.resize(53); //设置哈希表初始空间大小
    }
    // 装载因子超过阈值0.7就进行扩容
    else if (_n * 1.0 / _table.size() > 0.7)
    {
        //创建一个新的哈希表,大小为原哈希表的2倍
        vector<Node*> newTables;
        newTables.resize(__stl_next_prime(_tables.size()), nullptr);
        // 旧表中节点移动映射新表
        Hash hash;
        for (size_t i = 0; i < _tables.size(); ++i)
        {
            Node* cur = _tables[i];//首节点
            while (cur != nullptr)
            {
                Node* next = cur->_next;

                size_t hashi = hash(cur->_kv.first) % newTables.size();
                //头插
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;

                cur = next;
            }

            _tables[i] = nullptr;
        }
        //交换两个哈希表
        _tables.swap(newTables);
    }

    Hash hash;
    size_t hashi = hash(kv.first) % _tables.size();
    // 头插
    Node* newnode = new Node(kv);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;

    ++_size;

    return true;
}
//返回质数
size_t __stl_next_prime(size_t n)
{
    static const size_t __stl_num_primes = 28;
    static const size_t __stl_prime_list[__stl_num_primes] =
    {
        53, 97, 193, 389, 769,
        1543, 3079, 6151, 12289, 24593,
        49157, 98317, 196613, 393241, 786433,
        1572869, 3145739, 6291469, 12582917, 25165843,
        50331653, 100663319, 201326611, 402653189, 805306457,
        1610612741, 3221225473, 4294967291
    };

    for (size_t i = 0; i < __stl_num_primes; ++i)
    {
        if (__stl_prime_list[i] > n)
        {
            return __stl_prime_list[i];
        }
    }

    return -1;
}

使用质数作为除数

删除元素

bool erase(const K& key)
{
    //哈希表为空
    if (_tables.size() == 0) return nullptr;

    //find
    Hash hash;
    size_t hashi = hash(key) % _tables.size();
    Node* prev = nullptr;
    Node* cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr) //头删
            {
                _tables[hashi] = cur->_next;
            }
            else	//中间删
            {
                prev->_next = cur->_next;
            }

            delete cur;
            --_size;

            return true;
        }

        prev = cur;
        cur = cur->_next;
    }

    return false;
}

析构函数

~HashTable()
{
    for (size_t i = 0; i < _tables.size(); ++i)
    {
        Node* cur = _tables[i];
        while (cur)
        {
            Node* next = cur->_next;
            delete cur;
            cur = next;
        }
        _tables[i] = nullptr;
    }
}

    🦀🦀观看~~

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

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

相关文章

8.批量消息发送与批量消息消费

highlight: arduino-light 4.4 批量消息 4.4.1 发送限制 生产者进行消息发送时可以一次发送多条消息&#xff0c;批量发送消息能显著提高传递小消息的性能。 不过需要注意以下几点&#xff1a; 批量发送的消息必须具有相同的Topic批量发送的消息必须具有相同的刷盘策略批量发送…

统计数组中非零元素的个数统计数组中每列中非零元素的个数统计数组中每行中非零元素的个数numpy.count_nonzero()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 统计数组中非零元素的个数 统计数组中每列中非零元素的个数 统计数组中每行中非零元素的个数 numpy.count_nonzero() 选择题 以下说法错误的是? import numpy as np anp.array([[0,1,2,3],[3,…

文件加密后怎么打开?打开加密文件的方法

当我们把重要文件加密后&#xff0c;可以有效地保护文件数据安全&#xff0c;那么文件加密后该怎么打开使用呢&#xff1f;下面我们就一起来了解一下吧。 EFS加密 作为Windows系统提供的文件加密方法&#xff0c;EFS加密并不需要密码。在我们使用加密时登录的系统账号时&…

使用HTML制作一个赛龙舟小游戏

在这个信息爆炸的时代&#xff0c;开发者们肩负着前所未有的责任与挑战&#xff0c;以屈原名言 路漫漫其修远兮&#xff0c;吾将上下而求索 为指引&#xff0c;使用HTML制作一个赛龙舟小游戏&#xff0c;以此激励广大开发者在技术征途上不断求索&#xff0c;追求极致。 一、前期…

忘记了谷歌Gmail账号名怎么办?用这种方法轻松找回谷歌邮箱地址

有些朋友以前注册过谷歌邮箱&#xff0c;但很久很久没有再去使用。 现在注册ChatGPT需要谷歌邮箱&#xff0c;于是打算把尘封已久的谷歌邮箱找出来&#xff0c;可是这时候你才发现&#xff0c;谷歌邮箱的账号名早已忘掉了。 今天重点来说说如何找回谷歌账号&#xff0c;希望能够…

ANTLR实战

ANTLR&#xff08;Another Tool for Language Recognition&#xff09;是目前非常活跃的语法生成工具&#xff0c;用Java语言编写&#xff0c;基于LL&#xff08;∗&#xff09;解析方式&#xff0c;使用自上而下的递归下降分析方法。ANTLR可以用来产生词法分析器、语法分析器和…

【运维知识进阶篇】zabbix5.0稳定版详解5(SNMP网络管理协议监控)

简单网络管理协议&#xff08;SNMP&#xff09;是专门设计用于在IP网络管理网络节点&#xff08;服务器、工作站、路由器、交换机及HUBS网络枢纽等&#xff09;的一种标准协议&#xff0c;它是一种应用层协议。 目录 SNMP三种版本 监控SNMP SNMP&#xff1a;OID和MIB介绍可以…

深度学习基于Resnet18的图像多分类--训练自己的数据集(超详细 含源码)

1.ResNet18原理 2.文件存储 一个样本存放的文件夹为dataset 下两个文件夹 train和test文件(训练和预测) 3.训练和测试的文件要相同。下面都分别放了 crane (鹤)、elephant(大象)、leopard(豹子) 4.编写预测的Python文件&#xff1a;code.py 跟dataset是同级路径。 5.code.p…

ROS从入门到精通2-7:Gazebo仿真之动态生成障碍物

目录 0 专栏介绍1 动态生成障碍应用场景2 基于Gazebo动态生成障碍2.1 spawn_model服务2.2 动态构造障碍物URDF2.3 请求服务与动态生成 3 实测演示 0 专栏介绍 本专栏旨在通过对ROS的系统学习&#xff0c;掌握ROS底层基本分布式原理&#xff0c;并具有机器人建模和应用ROS进行实…

CSS | 解决html中img标签图片底部存在空白缝隙的问题

目录 问题描述 原因分析 解决方案 写在最后 问题描述 在学习CSS的过程中&#xff0c;我们经常会遇到图片底侧存在空白缝隙的问题。 代码示例&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" />&l…

SpringCloudAlibaba之Sentinel源码分析--protoc-3.17.3-win64

Sentinel源码分析 文章目录 Sentinel源码分析1.Sentinel的基本概念1.1.ProcessorSlotChain1.2.Node1.3.Entry1.3.1.自定义资源1.3.2.基于注解标记资源 1.4.Context1.4.1.什么是Context1.4.2.Context的初始化1.4.2.1.自动装配1.4.2.2.AbstractSentinelInterceptor1.4.2.3.Contex…

【C++初阶】string类常见题目详解(一)—— 仅仅反转字母、字符串中的第一个唯一字母、字符串最后一个单词的长度、验证回文串、字符串相加

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C初阶 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【C初阶】…

【Python 基础篇】Python 集合及集合常用函数

文章目录 导言一、集合的创建和访问二、集合的常用函数len()add()remove()union()intersection()difference()issubset()issuperset()clear() 总结 导言 在Python中&#xff0c;集合&#xff08;Set&#xff09;是一种无序、不重复的数据类型&#xff0c;用于存储多个唯一的元…

HCIP网络笔记分享——广域网协议及BGP协议

第二部分 HCIA回顾一、广域网技术1、HDLC2、PPP3、PAP4、CHAP5、GRE6、运行路由协议 二、动态路由协议1、OSPF2、重发布3、路由策略3.1 抓流量3.2 具体过程 4、BGP 三、BGP边界网关协议1、BGP的数据包2、BGP的状态机3、BGP的工作过程4、BGP的路由黑洞问题5、BGP的防环问题6、BG…

Studio One6.1.1免费中文版电子音乐、摇滚乐制作软件

Studio One6是一款专业的音乐制作软件&#xff0c;该软件提供了全面的音频编辑和混音功能&#xff0c;包括录制、编曲、合成、采样等多种工具&#xff0c;可用于制作各种类型的音乐&#xff0c;如流行音乐、电子音乐、摇滚乐等。 Studio One6.1的主要特点包括&#xff1a; 1. …

深入理解什么是端口(port)

每当看到有人的简历上写着熟悉 tcp/ip, http 等协议时, 我就忍不住问问他们: 你给我说说, 端口是啥吧! 可惜, 很少有人能说得让人满意... 所以这次就来谈谈端口(port), 这个熟悉的陌生人. 在此过程中, 还会谈谈间接层, naming service 等概念, IoC, 依赖倒置等原则以及 TCP 协议…

JavaEE的学习(Spring +Spring MVC + MyBatis)

一、Spring入门 Spring是一个轻量级的控制反转 (IoC-Inversion of Control)和面向切面 (AOP-Aspect Oriented Programming)的容器&#xff08;框架&#xff09;。它采用分层架构&#xff0c;由大约20个模块组成&#xff0c;这些模块分为Core Container、Data Access/Integrati…

什么是计算机蠕虫?

计算机蠕虫诞生的背景 计算机蠕虫的诞生与计算机网络的发展密切相关。20世纪60年代末和70年代初&#xff0c;互联网还处于早期阶段&#xff0c;存在着相对较少的计算机和网络连接。然而&#xff0c;随着计算机技术的进步和互联网的普及&#xff0c;计算机网络得以迅速扩张&…

TC8:SOMEIPSRV_FORMAT_09-10

SOMEIPSRV_FORMAT_09: Undefined bits in the Flag field 目的 Flag字段中的未定义位应静态设置为0 测试步骤 DUT CONFIGURE:启动具有下列信息的服务Service ID:SERVICE-ID-1Instance数量:1Tester:客户端-1监听在网卡上DUT:发送SOME/IP Notification消息Tester:验证接收…

Flutter应用开发,系统样式改不了?SystemChrome 状态栏、导航栏、屏幕方向……想改就改

文章目录 开发场景SystemChrome 介绍SystemChrome的使用导入 SystemChrome 包隐藏状态栏说明 改变状态栏的样式注意事项其他样式说明 锁定屏幕方向锁定屏幕方向实例注意事项 开发场景 开发APP时&#xff0c;我们经常要客制化状态栏、导航栏栏等的样式和风格&#xff0c;Flutte…