【数据结构】哈希表

在这里插入图片描述

上期回顾: 【数据结构】图
个人主页:C_GUIQU
归属专栏:数据结构

在这里插入图片描述

正文

1. 哈希表基础介绍

1.1 哈希表的概念

哈希表(Hash Table),也常被称作散列表,是一种用于存储数据元素的数据结构,它通过一种特定的映射机制——哈希函数(Hash Function),能够快速地实现数据的插入、查找以及删除操作。其核心思想在于将数据元素的关键码(Key)转化为一个在特定范围内(通常对应哈希表的索引范围)的整数值,然后依据这个值将数据元素存放在哈希表相应的位置上,就如同给每个元素都分配了一个专属的“座位”,方便后续快速定位访问。

1.2 哈希表在数据处理中的重要性

在实际的数据处理场景中,我们常常需要处理大量的数据,并且要高效地进行查找、插入和删除等操作。例如在数据库系统里,要快速根据用户的查询条件(如用户名、订单号等关键信息)找到对应的记录;在网络缓存中,要依据网址快速判断缓存中是否已经存在相应资源并获取它。哈希表凭借其平均情况下近乎常数时间复杂度(通常为 O(1))的操作效率,相比其他传统数据结构(如数组查找可能需遍历整个数组,时间复杂度为 O(n);链表查找在最坏情况下效率也较低),在这些场景中展现出了极大的优势,成为了优化数据处理效率的关键工具。

2. 哈希函数

2.1 哈希函数的核心作用与要求

哈希函数是哈希表的“灵魂”所在,它承担着将各式各样的输入关键码(可以是整数、字符串、结构体等各种复杂类型的数据,不过往往需要进行适当预处理使其能参与计算)准确地映射为哈希表索引的重任。一个优质的哈希函数需要满足以下关键要求:

  • 确定性:无论何时对同一个输入关键码调用哈希函数,所得到的输出索引值都必须是固定不变的。这保证了数据存储和查找的一致性,例如,对于学号为“12345”的学生信息,每次通过哈希函数计算其在学生信息哈希表中的存储位置时,结果都应该相同,否则就会导致数据混乱,无法准确查找和操作。
  • 高效性:哈希函数的计算过程应该尽可能简单快捷,避免复杂的计算操作导致花费过多的时间在函数计算上,从而影响整个哈希表操作的效率。毕竟哈希表的优势在于快速操作,如果哈希函数本身计算就很慢,那就失去了使用哈希表的意义。
  • 均匀性:这是衡量哈希函数好坏的重要指标之一。它要求哈希函数能够将不同的输入关键码尽可能均匀地分布在整个哈希表的各个索引位置上。想象一下,如果大量的关键码都集中映射到了哈希表的少数几个槽位中,那么在这些槽位发生冲突(多个关键码映射到同一个位置)的概率就会极高,进而严重影响哈希表的性能,导致查找、插入等操作效率大幅下降。

2.2 常见哈希函数类型及实现原理

  • 取余哈希函数
    这是一种最为直观且常用的哈希函数类型,其基本计算公式为 hash(key) = key % table_size,其中 key 代表输入的关键码,table_size 则是哈希表的大小(也就是哈希表所包含的槽位数量)。例如,假设有一个哈希表大小设定为 10,对于关键码值为 13 的元素,通过 13 % 10 的计算,会得到索引值 3,这意味着该元素将会被存放在哈希表的第 3 个槽位中。不过,这种哈希函数存在一定局限性,当输入的关键码具有某种规律时(比如所有关键码都是 10 的倍数),就容易出现大量元素都映射到索引为 0 的槽位这种分布极不均匀的情况。

  • 乘法哈希函数
    它的计算形式相对复杂一些,一般公式为 hash(key) = floor(table_size * ( ( key * A ) - floor( key * A ) ) ),这里的 A 是一个特定的常数,取值范围通常在 0 到 1 之间,其中比较常用的是黄金分割比例 (sqrt(5) - 1) / 2。乘法哈希函数的原理在于先通过将关键码与常数 A 相乘,然后经过一系列取整等操作,使得生成的哈希值在哈希表范围内尽量均匀分布。这种函数对于关键码范围较大且分布较为复杂的数据情况,相较于取余哈希函数,往往能够取得更好的均匀分布效果,减少冲突发生的可能性。

  • 字符串哈希函数
    当关键码为字符串类型时,就需要专门设计针对字符串的哈希函数了。一种常见的简单实现思路是将字符串看作是由一个个字符组成的数字序列(例如按照字符的 ASCII 码值来处理),然后运用特定的算法来计算哈希值。比如下面这种加权求和取余的方式:hash("abc") = ( (int('a') * p^0 + int('b') * p^1 + int('c') * p^2 ) % table_size),在这个式子中,p 通常选取一个质数(如 31 等),目的是为了让不同字符在计算哈希值时的权重区分更明显,进而减少因字符串相似而导致的冲突情况。通过这样的计算,就可以把字符串形式的关键码转化为适合哈希表存储的索引值了。

3. 冲突解决方法

3.1 冲突产生的原因及影响

由于哈希函数的映射范围是由哈希表的大小所限定的,而在实际应用中,需要处理的输入关键码数量往往是非常庞大甚至近乎无限的,所以几乎不可避免地会出现不同的关键码经过哈希函数计算后,得到相同索引值(也就是映射到同一个槽位)的情况,这就是所谓的冲突(Collision)。冲突一旦产生,如果不能妥善处理,将会导致哈希表在该槽位及其后续操作上变得混乱,严重影响查找、插入和删除等操作的效率,使得原本期望的快速操作无法实现,甚至可能退化为类似线性查找的低效率情况。

3.2 开放定址法

  • 线性探测
    这是开放定址法中最为简单直接的一种探测方式。其基本原理是,当某个关键码通过哈希函数计算出的初始槽位 hash(key) 已经被其他元素占用(即发生冲突)时,就按照顺序依次检查 hash(key) + 1hash(key) + 2hash(key) + 3……这些后续的槽位,直到找到一个空闲的槽位为止,然后将元素存放在该空闲槽位中。例如,假设有一个大小为 10 的哈希表,哈希函数算出关键码 key1 对应的索引为 3,但槽位 3 此时已被别的元素占据了,那么就接着查看槽位 4、5 等,依此类推,直到找到空闲的槽位来存放 key1 对应的元素。然而,线性探测存在一个明显的问题,那就是容易引发“聚集”现象。具体来说,当连续的槽位都被占用后,后续再有冲突发生时,需要花费更多的时间去寻找空闲位置,因为要不断地顺序往后探测,这无疑会使哈希表的操作效率大打折扣。

  • 二次探测
    二次探测在解决冲突时采取了一种与线性探测不同的步长策略,它不是按照固定的步长 1 去依次查找空闲位置,而是依据一个二次函数形式的步长来进行探测,即依次探测 hash(key) + 1^2hash(key) + 2^2hash(key) + 3^2…… 例如,若初始槽位发生冲突,第一次探测会查看 hash(key) + 1^2 对应的槽位是否空闲,若仍冲突则查看 hash(key) + 2^2 对应的槽位,以此类推。这种探测方式能够在一定程度上缓解线性探测所导致的聚集问题,不过它也并非完美无缺,在某些特殊的数据分布情况下,依然可能存在效率不高的问题,而且在探测过程中需要特别留意不能超出哈希表的边界范围,通常会采用对步长进行取余等操作来确保探测始终在哈希表内部进行。

  • 双重哈希
    双重哈希使用了两个不同的哈希函数 hash1(key)hash2(key) 来协同处理冲突。当通过 hash1(key) 计算出的槽位出现冲突时,利用 hash2(key) 来确定探测的步长,按照 (hash1(key) + i * hash2(key)) % table_size(这里的 i 从 0 开始逐步递增)这样的规则去寻找空闲位置。双重哈希的优势在于能够更加分散地安排元素,进一步减少聚集现象的发生,使元素在哈希表中的分布更加均匀,从而提升整体的操作效率。但要注意的是,两个哈希函数的设计需要精心考量、合理搭配,只有这样才能发挥出双重哈希的良好效果,否则可能会因为函数之间的不协调而达不到预期的冲突解决效果。

3.3 链地址法

  • 基本原理与数据结构设计
    链地址法采用了一种将冲突元素组织成链表的方式来解决冲突问题。具体而言,哈希表的每个槽位实际上是一个链表的头指针,当多个不同的关键码通过哈希函数计算后都映射到同一个槽位时,就把这些关键码对应的元素依次插入到该槽位对应的链表中。例如,假设有三个关键码 key1key2key3 都通过哈希函数映射到了索引为 5 的槽位,那么在槽位 5 处就会构建形成一个链表,key1 对应的元素节点作为链表头(如果它是第一个插入的),随后 key2key3 对应的元素节点则按照插入顺序依次链接到链表中。这样,在查找、插入或删除某个元素时,只需要针对对应的链表进行常规的链表操作即可,比如在查找元素时,从链表头开始遍历链表,对比每个节点的关键码是否与目标关键码一致;插入元素时,在链表合适的位置插入新节点;删除元素时,找到链表中对应的节点并将其移除。

  • 链地址法的优势与适应性
    链地址法相较于开放定址法具有诸多优点。首先,它处理冲突的方式相对简单直观,代码实现上也更容易理解和维护。其次,在进行插入和删除操作时,仅仅涉及到对相应链表的操作,不会像开放定址法那样可能会影响到其他原本不冲突的元素位置,对哈希表整体结构的影响较小。而且,面对数据量动态变化较大以及元素分布不均匀的复杂情况时,链地址法表现出了很强的适应性,即使某个槽位对应的链表长度较长(意味着冲突较多),也不会导致整个哈希表的操作性能急剧下降,依然能够保持相对稳定的操作效率。

4. 哈希表的详细实现(以链地址法为例)

4.1 定义节点结构体

template<typename Key, typename Value>
struct HashNode {
   
    Key key;
    Value value;
    HashNode* next;

    HashNode(Key k, Value v) : key(k), value(v), next(NULL) {
   }
};

在这里,我们定义了一个模板结构体 HashNode,它用于存储键值对以及指向下一个节点的指针。其中,KeyValue 是模板参数,这意味着可以根据实际的使用场景,灵活传入不同类型的数据,比如可以让 Keyint 类型,用于存储学生的学号,Valuestring 类型,用来存放学生的姓名,以此来构建一个存储学生信息的哈希表。构造函数 HashNode(Key k, Value v) 的作用是初始化节点的键、值以及将 next 指针初始化为 NULL,表示当前节点暂时没有后续节点,即处于链表的末尾或者是单独的一个节点状态。

4.2 哈希表类定义

template<typename Key, typename Value>
class HashTable 
为了在Windows安装ADB工具,你可以按照以下步骤进行操作: 1. 首先,下载ADB工具包并解压缩到你自定义的安装目录。你可以选择将其解压缩到任何你喜欢的位置。 2. 打开运行窗口,可以通过按下Win+R键来快速打开。在运行窗口中输入"sysdm.cpl"并按下回车键。 3. 在系统属性窗口中,选择"高级"选项卡,然后点击"环境变量"按钮。 4. 在环境变量窗口中,选择"系统变量"部分,并找到名为"Path"的变量。点击"编辑"按钮。 5. 在编辑环境变量窗口中,点击"新建"按钮,并将ADB工具的安装路径添加到新建的路径中。确保路径正确无误后,点击"确定"按钮。 6. 返回到桌面,打开命令提示符窗口。你可以通过按下Win+R键,然后输入"cmd"并按下回车键来快速打开命令提示符窗口。 7. 在命令提示符窗口中,输入"adb version"命令来验证ADB工具是否成功安装。如果显示版本信息,则表示安装成功。 这样,你就成功在Windows安装ADB工具。你可以使用ADB工具来执行各种操作,如枚举设备、进入/退出ADB终端、文件传输、运行命令、查看系统日志等。具体的操作方法可以参考ADB工具的官方文档或其他相关教程。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [windows环境安装adb驱动](https://blog.youkuaiyun.com/zx54633089/article/details/128533343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Windows安装使用ADB简单易懂教程](https://blog.youkuaiyun.com/m0_37777700/article/details/129836351)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guiat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值