什么是散列表?

本文介绍了散列表,它是根据键直接访问内存存储位置的数据结构,通过散列函数映射。阐述了散列函数的特点,如确定性、散列碰撞等,还介绍了常见的MD5和SHA - 1散列函数。同时说明了散列冲突不可避免,介绍了开放寻址法(含线性探测、二次探测、双重散列)和链表法两种解决冲突的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://juejin.im/user/5b2cc5fee51d4553156be12f/posts

散列表

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

散列函数

散列函数,顾名思义,它是一个函数。如果把它定义成 hash(key) ,其中 key 表示元素的键值,则 hash(key) 的值表示经过散列函数计算得到的散列值。

散列函数的特点:

1.确定性

如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。

2.散列碰撞(collision)

散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同。

3.不可逆性

一个哈希值对应无数个明文,理论上你并不知道哪个是。

“船长,如果一样东西你知道在哪里,还算不算丢了。”

“不算。”

“好的,那您的酒壶没有丢。”

4.混淆特性

输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。

常见的散列函数

1. MD5

MD5 即 Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一,主流编程语言普遍已有 MD5 实现。

将数据(如汉字)运算为另一固定长度值,是杂凑算法的基础原理,MD5 的前身有 MD2 、MD3 和 MD4 。

MD5 是输入不定长度信息,输出固定长度 128-bits 的算法。经过程序流程,生成四个32位数据,最后联合起来成为一个 128-bits 散列。

基本方式为,求余、取余、调整长度、与链接变量进行循环运算,得出结果。

MD5 计算广泛应用于错误检查。在一些 BitTorrent 下载中,软件通过计算 MD5 来检验下载到的碎片的完整性。

MD5 校验

2. SHA-1

SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。

SHA-1 曾经在许多安全协议中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5的后继者。

散列冲突

理想中的一个散列函数,希望达到

如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)

这种效果,然而在真实的情况下,要想找到一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的,即使是 MD5 或者 由美国国家安全局设计的 SHA-1 算法也无法实现。

事实上,再好的散列函数都无法避免散列冲突。

为什么呢?

这涉及到数学中比较好理解的一个原理:抽屉原理。

抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面至少放两个苹果。这一现象就是我们所说的“抽屉原理”。

抽屉原理

对于散列表而言,无论设置的存储区域(n)有多大,当需要存储的数据大于 n 时,那么必然会存在哈希值相同的情况。这就是所谓的散列冲突

散列冲突

那应该如何解决散列冲突问题呢?

常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。

开放寻址法

定义:将散列函数扩展定义成探查序列,即每个关键字有一个探查序列h(k,0)、h(k,1)、…、h(k,m-1),这个探查序列一定是0…m-1的一个排列(一定要包含散列表全部的下标,不然可能会发生虽然散列表没满,但是元素不能插入的情况),如果给定一个关键字k,首先会看h(k,0)是否为空,如果为空,则插入;如果不为空,则看h(k,1)是否为空,以此类推。

开放寻址法是一种解决碰撞的方法,对于开放寻址冲突解决方法,比较经典的有线性探测方法(Linear Probing)、二次探测(Quadratic probing)和 双重散列(Double hashing)等方法。

线性探测方法

![开放寻址法之线性探测方法](data:image/svg+xml;utf8,<?xml version="1.0"?> )

当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。

以上图为例,散列表的大小为 8 ,黄色区域表示空闲位置,橙色区域表示已经存储了数据。目前散列表中已经存储了 4 个元素。此时元素 7777777 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。

于是按顺序地往后一个一个找,看有没有空闲的位置,此时,运气很好正巧在下一个位置就有空闲位置,将其插入,完成了数据存储。

线性探测法一个很大的弊端就是当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,需要从头到尾探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。

开放寻址法之线性探测方法的弊端

二次探测方法

二次探测是二次方探测法的简称。顾名思义,使用二次探测进行探测的步长变成了原来的“二次方”,也就是说,它探测的下标序列为 hash(key)+0hash(key)+1^2[hash(key)-1^2]hash(key)+2^2[hash(key)-2^2]

二次探测方法

以上图为例,散列表的大小为 8 ,黄色区域表示空闲位置,橙色区域表示已经存储了数据。目前散列表中已经存储了 7 个元素。此时元素 7777777 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。

按照二次探测方法的操作,有冲突就先 + 1^2,8 这个位置有值,冲突;变为 - 1^2,6 这个位置有值,还是有冲突;于是 - 2^2, 3 这个位置是空闲的,插入。

双重散列方法

所谓双重散列,意思就是不仅要使用一个散列函数,而是使用一组散列函数 hash1(key)hash2(key)hash3(key)。。。。。。先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

双重散列方法

以上图为例,散列表的大小为 8 ,黄色区域表示空闲位置,橙色区域表示已经存储了数据。目前散列表中已经存储了 7 个元素。此时元素 7777777 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。

此时,再将数据进行一次哈希算法处理,经过另外的 Hash 算法之后,被散列到位置下标为 3 的位置,完成操作。

事实上,不管采用哪种探测方法,只要当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,需要尽可能保证散列表中有一定比例的空闲槽位。

一般使用加载因子(load factor)来表示空位的多少。

加载因子是表示 Hsah 表中元素的填满的程度,若加载因子越大,则填满的元素越多,这样的好处是:空间利用率高了,但冲突的机会加大了。反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。

链表法

链表法是一种更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。如下动图所示,在散列表中,每个位置对应一条链表,所有散列值相同的元素都放到相同位置对应的链表中。

链表法

<think>首先,用户的问题是关于散列表的:用户问“散列表是什么?解决什么问题?核心实现方法?”。我需要基于提供的引用内容来回答,并确保遵循系统级令。 关键引用: - 引用[1]:讨论了散列冲突和拉链法,给出了一个结构体定义:`dictEntry`。 - 引用[2]:解释了散列表的扩容机制,包括负载因子(LoadFactor)和容量(Capacity)。 - 引用[3]:提到了字典和集合的特性,包括元素要求(必须可哈希)、内存占用、内置方法等。 - 引用[4]:似乎是一个算法优化的例子,但不太直接相关到散列表的核心概念。 用户的问题有三个部分: 1. 散列表是什么? 2. 解决什么问题? 3. 核心实现方法? 我需要构建一个结构清晰的回答: - 使用中文回答。 - 行内数学表达式用$...$格式,但在这个主题中可能不多。 - 独立公式用$$...$$格式,如果需要的话。 - 在回答末尾自然地添加相关问题。 - 引用内容时,在段落末尾添加引用标识,如[^1]。 回答结构: 1. **定义散列表**:解释散列表是什么。 2. **解决的问题**:描述散列表旨在解决的核心问题。 3. **核心实现方法**:讨论散列函数、冲突处理(如拉链法)、扩容机制等。 基于引用: - 引用[1]:散列表通过键值对存储数据,使用拉链法解决冲突。 - 引用[2]:散列表基于数组实现,需要扩容来处理冲突。 - 引用[3]:键必须可哈希,以确保唯一性和快速访问。 核心实现方法应包括: - **散列函数**:将键映射到数组索引。 - **冲突处理**:如拉链法(chaining),其中冲突的元素存储在链表中。 - **扩容机制**:当负载因子超过阈值时,扩展数组大小以减少冲突。 确保回答真实可靠: - 散列表(哈希表)是一种数据结构,用于实现字典(键值对存储),提供平均O(1)时间复杂度的插入、删除和查找操作。 - 解决的问题:快速数据访问,避免线性搜索。 - 实现:数组 + 散列函数 + 冲突解决策略。 在回答中引用相关部分: - 在解释冲突处理时,引用[^1]。 - 在讨论扩容时,引用[^2]。 - 在提到键的可哈希性时,引用[^3]。 最后,生成相关问题: - 基于主题,如冲突处理的其他方法、扩容细节、性能分析等。 完整回答大纲: 1. **散列表的定义**:简要介绍。 2. **解决的问题**:如何高效存储和检索键值对。 3. **核心实现方法**: - 散列函数。 - 冲突处理(拉链法)。 - 扩容机制。 4. **相关问题**:列出3-4个相关问题。</think>### 散列表的基本概念、问题解决与核心实现方法 散列表(Hash Table),也称为哈希表,是一种高效的数据结构,用于存储键值对(key-value pairs)。它通过散列函数将键映射到数组的特定索引位置,从而实现快速的数据插入、删除和查找操作。下面我将逐步解释散列表的定义、解决的问题以及核心实现方法。 #### 1. **散列表是什么?** 散列表是一种基于数组实现的数据结构,核心思想是利用散列函数(Hash Function)将任意大小的键(key)转换为固定范围的整数索引。这个索引对应数组中的一个位置(称为“桶”或“槽”),用于存储键值对。散列表的设计目标是提供平均$O(1)$时间复杂度的基本操作(如插入、查找和删除),在理想情况下显著优于线性搜索($O(n)$)或树结构($O(\log n)$)。例如,在Python中,字典(dict)类型就是散列表的实现[^3]。 #### 2. **解决什么问题?** 散列表主要解决以下两类问题: - **高效数据访问问题**:在需要频繁插入、删除和查找键值对的场景(如数据库索引、缓存系统),散列表避免了线性扫描的低效性。例如,在一个包含$n$个元素的列表中,查找操作可能需要$O(n)$时间,而散列表能将其优化到平均$O(1)$时间[^2]。 - **键值对管理问题**:散列表提供了一种结构化的方式存储关联数据(如用户ID到用户信息的映射),同时支持键的唯一性约束(每个键只能对应一个值)。这解决了传统数组或列表无法直接通过键快速定位值的问题[^3]。 散列表特别适用于需要高吞吐量的应用,如编译器符号表、网络路由表或去重算法(例如,使用集合(set)实现唯一性检查)[^3]。 #### 3. **核心实现方法** 散列表的核心实现依赖于三个关键组件:散列函数、冲突处理机制和动态扩容策略。下面详细说明: - **散列函数(Hash Function)**: 散列函数将键转换为数组索引。其设计目标是均匀分布键的映射,减少冲突(即不同键映射到同一索引)。例如,一个简单的散列函数可以是: $$ \text{index} = \text{hash}(key) \mod \text{capacity} $$ 其中,$\text{capacity}$是数组长度。键必须是“可哈希的”(hashable),即其值在生命周期内不可变(如整数、字符串或元组),以确保散列值的稳定性。如果键是可变对象(如列表),需先转换为不可变形式(如使用元组或frozenset)[^3]。 - **冲突处理机制(Collision Resolution)**: 当多个键映射到同一索引时,会发生散列冲突。常见的解决方法是拉链法(Chaining),即每个数组位置存储一个链表(或其他数据结构),冲突的元素被追加到链表中。例如,在引用[1]中,散列表条目(dictEntry)通过next针形成链表: ```c typedef struct dictEntry { void *key; // 键 union { // 值 void *val; uint64_t u64; int64_t s64; } v; struct dictEntry *next; // 向下一个冲突条目的针 } dictEntry; ``` 查找时,先通过散列函数定位索引,再遍历链表匹配键。拉链法简单高效,平均时间复杂度为$O(1 + \alpha)$,其中$\alpha$是负载因子(见下文)[^1]。 - **动态扩容策略(Resizing)**: 散列表基于数组实现,当元素增多时,冲突概率上升,性能会下降。因此,需要动态扩容:当负载因子(LoadFactor)超过阈值(通常为0.75)时,数组容量(Capacity)翻倍,并重新散列所有元素。负载因子定义为: $$ \text{LoadFactor} = \frac{\text{Size}}{\text{Capacity}} $$ 其中,$\text{Size}$是当前元素数量。扩容确保散列表保持低冲突率,例如,当$\text{Size} \geq \text{Capacity} \times \text{LoadFactor}$时触发扩容[^2]。这避免了链表过长导致的性能退化(如操作时间从$O(1)$退化为$O(n)$)。 通过这些方法,散列表在平均情况下实现了高效操作。但需注意,最坏情况(如所有键冲突)可能导致性能下降,因此散列函数的设计和扩容策略至关重要。 ### 相关问题 1. 散列冲突除了拉链法,还有哪些解决方法(如开放寻址法)? 2. 散列表的负载因子如何影响性能?优化负载因子的策略有哪些? 3. 在Python中,字典(dict)和集合(set)的实现有何异同? 4. 散列表在分布式系统中的应用有哪些(如分布式哈希表)?
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值