
1. 项目概述为什么期末复习必须死磕哈希表又到期末了数据结构这门课哈希表绝对是老师最爱考、学生最容易懵的章节之一。我当年复习的时候就发现教材上关于哈希表的构造和冲突解决往往就是干巴巴地列几个公式、画几个图看完感觉懂了合上书啥也记不住。直到后来自己刷题、做项目才真正把这块骨头啃下来。今天我就以一个过来人的身份把哈希表这块的复习要点掰开揉碎了讲尤其是那6种构造方法和4种解决冲突方法这不仅是应付考试的选择题、简答题、算法设计题的核心更是你未来面试、写代码时绕不开的实用技能。哈希表Hash Table的本质就是一个“超级索引”。它通过一个函数哈希函数把任意长度的输入比如一个字符串“John Doe”映射到一个固定范围的索引值比如数组下标5然后直接在这个位置存取数据。理想情况下这个操作的时间复杂度是O(1)快得飞起。但现实很骨感不同的输入可能会被映射到同一个索引这就是“冲突”。所以整个哈希表技术的核心就两块怎么设计这个映射函数构造方法以及映射撞车了怎么办解决冲突方法。期末考试翻来覆去就是考你对这两块的理解深度和灵活应用。2. 哈希表核心构造方法与冲突解决的逻辑框架在深入细节之前我们必须建立一个顶层的认知框架。你可以把哈希表想象成一个有固定数量停车位哈希地址的停车场。现在有两件关键事要做分配车位构造方法给定一辆车关键字Key用一套规则决定它应该停进哪个编号的车位。这个规则就是哈希函数。好的规则应该让车辆尽可能均匀地分散在所有车位上避免某些车位挤爆另一些却空着。处理占位解决冲突方法当你按照规则找到车位时发现已经有一辆车另一个Key停在那里了。这时候怎么办是让后到的车另寻他处开放定址还是在原车位上搭建一个立体车库停放多辆车链地址法期末考题无论是让你计算某个关键字的哈希地址还是分析平均查找长度ASL或是比较不同方法的优劣都是基于这个框架展开的。接下来我们就逐一拆解这6种构造方法和4种解决冲突方法我会结合具体例子和计算过程让你不仅记住“是什么”更明白“为什么”和“怎么算”。3. 六种哈希函数构造方法详解与实战对比哈希函数的设计目标很明确计算简单、散列均匀、冲突概率低。下面这六种方法是教材里的常客也是考试重点。3.1 直接定址法这是最简单粗暴的一种。取关键字本身或者关键字的某个线性函数值作为哈希地址。公式一般是Hash(key) a * key b其中a、b为常数。实战场景与计算 假设我们要存储某公司员工信息以员工工号从2024001开始连续编号为关键字。我们可以直接定义Hash(工号) 工号 - 2024000。那么工号2024001就存到地址12024002存到地址2以此类推。为什么用它优点绝对没有冲突是理想的“一对一”映射查找速度是严格的O(1)。缺点适用范围极窄。它要求关键字的分布必须连续或者是非常密集。如果工号是2024001, 2024010, 2024300这样跳跃的那么哈希表空间数组的绝大部分都会是空的造成巨大的空间浪费。注意直接定址法在考试中常作为“特例”出现用于对比其他会产生冲突的方法。你需要迅速识别出关键字是否具备“连续或近乎连续”的特性。3.2 数字分析法这种方法适用于关键字是位数较多的数字如手机号、身份证号并且已知这些数字的某些位上分布不均匀某些位上的数字重复多而某些位分布均匀。我们抽取其中分布均匀的若干位组合起来作为哈希地址。实战场景与计算 假设有一批关键字是某地的8位电话号码如 88234567, 88235123, 88239876... 我们发现前三位“882”都是区号所有号码都相同肯定不能用来做哈希否则全冲突。中间两位“34”、“35”、“39”分布比较随机后三位“567”、“123”、“876”分布也比较均匀。我们可以抽取中间两位后两位组成一个4位的哈希地址。例如Hash(88234567) 3456Hash(88235123) 3512为什么用它优点基于已知的数据集特征进行设计如果位选取恰当能有效减少冲突。缺点严重依赖数据集你必须事先分析所有关键字的构成。如果来了一个新的、不符合原分布规律的关键字冲突率可能会急剧上升。因此它适用于关键字集合已知且静态或变化不大的情况。3.3 平方取中法先求出关键字的平方值然后取平方值的中间几位作为哈希地址。具体取多少位取决于哈希表的大小表长。实战场景与计算 假设关键字是1234哈希表长度为1000即地址范围0~999需要3位数。计算平方1234² 1522756。取中间3位从中间开始取1522756我们可以取第3到第5位227或者第2到第4位522。通常约定一种规则即可比如取右起第4位开始的3位从右往左数个位是第1位。这里1522756右起第4、3、2位是7, 5, 2等等这样取容易乱。更通用的方法是将平方数视为固定位数的字符串不足补零然后取中间部分。假设我们总取中间3位1522756是7位数中间3位就是第3、4、5位2, 2, 7即227。 所以Hash(1234) 227。为什么用它优点平方操作能使关键字的每一位都参与到最终的地址计算中特别是中间几位受到了关键字所有位的影响。因此即使关键字只有少量变化如1234和1235平方后的中间几位通常也会有很大不同有助于分散冲突。缺点计算量相对稍大需要做乘法。对于非常庞大的数据集性能开销需要考虑。3.4 折叠法将关键字分割成位数相等的几部分最后一部分位数可以略少然后将这几部分叠加求和根据哈希表大小取后几位作为哈希地址。实战场景与计算 关键字 123456789哈希表长度1000地址3位数。分割每3位一段分成123 456 789。叠加求和123 456 789 1368。取后三位Hash(123456789) 368。还有一种“移位折叠”在叠加前将偶数段或奇数段反转后再加。例如反转偶数段456-654那么求和为123 654 789 1566取后三位566。这种方法能更好地打乱模式。为什么用它优点适用于关键字位数很多且每一位上的数字分布可能不均匀的情况。通过折叠和求和将长关键字压缩成短地址同时混合了所有部分的信息。缺点可能存在一定的“信息损失”求和后高位被截断。但哈希函数本身不要求可逆所以问题不大。3.5 除留余数法最常用这是实践中最常用、最核心的方法。取关键字被某个数p除后的余数作为哈希地址。公式Hash(key) key MOD p。 这里的p的选择是成败关键。实战场景与计算 关键字集合为 {12, 25, 36, 48, 60}哈希表长度表长m10。 如果我们随意取p10则 Hash(12)2, Hash(25)5, Hash(36)6, Hash(48)8, Hash(60)0。分布均匀无冲突。 但如果关键字集合是 {12, 22, 32, 42, 52}p仍取10则所有关键字的哈希地址都是2冲突极其严重。为什么p的选择至关重要理论证明p应选取一个不大于表长m但最接近或等于m的质数。为什么减少模运算的规律性如果p是一个合数比如p10它的因子有2和5。那么所有关键字中能被2整除的数偶数将只会映射到偶数地址奇数关键字映射到奇数地址分布不均。质数只有1和它本身两个因子能最大程度地打破关键字可能存在的周期性规律使得余数分布更均匀。考试必考计算给你一个表长m让你选p。例如m15那么不大于15的质数有13, 11, 7, 5, 3, 2。应选择最接近15的质数即p13。实操心得在期末试卷和面试中除留余数法是出现概率最高的。你必须熟练掌握给定一组关键字和表长m计算哈希地址并填入哈希表的全过程同时能分析冲突情况。这是大题的基础。3.6 随机数法取关键字的随机函数值作为哈希地址Hash(key) random(key)。这里random是一个伪随机数生成器当种子key相同时生成的随机数序列是固定的。为什么用它优点当关键字长度不等且分布不明时随机数法通常能得到较好的散列效果冲突概率取决于随机数生成器的质量。缺点“随机”意味着每次计算结果相同这很重要哈希函数必须是确定的Deterministic同一个key必须每次都能得到同一个地址否则就找不到存进去的数据了。所以这里的random是伪随机函数。其次随机数生成本身有一定计算开销。方法对比总结表方法适用场景优点缺点考试关注点直接定址关键字分布连续无冲突O(1)查找空间浪费严重识别适用条件数字分析关键字位数多且位分布已知针对性强冲突少依赖静态数据集不通用给定位数要求抽取平方取中关键字位数中等分布不详散列均匀关键字符号均参与计算量稍大计算平方并取指定位折叠法关键字位数很多混合所有部分信息有一定信息损失分割、叠加、取模计算除留余数最通用计算简单效果好p的选择至关重要必考计算地址、填表、分析ASL随机数法关键字不规则散列效果好计算慢需确定函数了解原理较少直接计算4. 四种冲突解决策略全解析与性能评估冲突不可避免所以必须有预案。这四种方法是解决冲突的经典策略各有其应用场景和性能特征。4.1 开放定址法Open Addressing核心思想一旦发生冲突就按照某种探测序列在哈希表中寻找下一个“开放”的即空的地址直到找到为止。所有的数据都存放在表本身这个数组中。 通用探测公式Hi (H(key) di) MOD m。其中H(key)是初始哈希地址m是表长di是第i次探测的增量序列。i从0开始。根据增量序列di的不同分为以下三种主要方法4.1.1 线性探测法Linear Probing增量序列di 1, 2, 3, ... , m-1。即每次冲突后顺序查看下一个单元是否为空。实战与计算 表长m10哈希函数H(key)key MOD 7注意p7是质数。 依次插入关键字序列{8, 14, 19, 23, 30}。H(8)1地址1空插入。H(14)0地址0空插入。H(19)5地址5空插入。H(23)2地址2空插入。H(30)2冲突开始线性探测d11: (21) MOD 10 3地址3空插入。此时哈希表为[14, 8, 23, 30, -, 19, -, -, -, -] (地址0到9)为什么要注意“堆积”线性探测的缺点是容易产生“一次聚集”Primary Clustering或“堆积”。即连续被占用的地址单元形成一些区块。这会导致后续的关键字在探测时需要跳过很长的已占用序列大大增加查找时间。例如如果接下来要插入关键字9H(9)2会发现地址2、3都被占了需要探测到地址4才空。4.1.2 平方探测法Quadratic Probing增量序列di 1², -1², 2², -2², 3², -3², ...。即探测的步长是平方数并且正负交替。实战与计算 接上例插入30时冲突H(30)2。i1: d11²1, (21) MOD 10 3地址3空插入成功。 如果地址3也冲突则继续i2: d2-1²-1, (2-1) MOD 10 1检查地址1。i3: d32²4, (24) MOD 10 6检查地址6。...为什么它能缓解堆积平方探测的步长是变化的避免了线性探测那种“扎堆”的情况能有效缓解“一次聚集”称为“二次聚集”但影响比一次聚集小。一个重要考点平方探测法要求表长m必须是形如4k3的质数才能保证探测序列能够遍历所有表项。例如m7, 11, 19, 23等。4.1.3 再哈希法Double Hashing使用第二个哈希函数来计算增量di i * H2(key)。即每次探测的步长由另一个哈希函数决定。实战与计算 设H1(key)key MOD 7, H2(key)5 - (key MOD 5)。注意H2不能为0 插入30时冲突H1(30)2。i1: d11 * H2(30) 1 * (5 - (30 MOD 5)) 1 * (5-0)5, (25) MOD 10 7检查地址7。i2: d22 * 5 10, (210) MOD 10 2回到原点但通常i会继续增加实际探测序列为2,7,(2),7...这里出现了循环说明H2和表长m选择不当。好的H2应与m互质。为什么它更优再哈希法通过第二个哈希函数为不同的关键字生成不同的探测序列极大地减少了“聚集”现象是开放定址法中较好的方法。但计算开销稍大。注意事项开放定址法通病删除操作不能直接删如果直接删除某个单元会截断探测路径导致后续查找失败因为查找时遇到空就认为不存在。通常采用“标记删除”法即给删除的单元打一个特殊标记如DELETED查找时视其为非空继续探测插入时视其为空可复用。装载因子装载因子α 表中已填入记录数 / 哈希表长度。开放定址法要求α必须小于1通常建议α 0.7~0.8否则插入失败概率激增查找效率也急剧下降。4.2 链地址法Chaining又称拉链法这是实践中应用最广泛、最直观的方法。它的思想是把哈希到同一地址的所有关键字都放在一个链表中。哈希表的每个单元不再存储数据本身而是存储一个链表头指针或引用。实战与计算 表长m5H(key)key MOD 5。 插入序列{12, 22, 35, 8, 24}。H(12)2地址2链表12 - NULLH(22)2冲突链地址法处理将22插入地址2的链表头部或尾部。链表变为22 - 12 - NULLH(35)0地址0链表35 - NULLH(8)3地址3链表8 - NULLH(24)4地址4链表24 - NULL最终哈希表结构是一个数组链表的结构。为什么它如此受欢迎处理简单冲突解决直观就是链表插入。无堆积问题同义词哈希地址相同的关键字只在各自的链表上不会影响其他地址的探测。易于删除直接在链表上删除节点即可。可容纳更多数据装载因子α可以大于1因为链表可以动态增长。当然α过大会导致单个链表过长退化为顺序查找。平均查找长度ASL计算 这是考试大题对于链地址法查找成功的ASL是查找每个关键字需要遍历链表节点数的平均值。 上例中查找12需要遍历2个节点22-12查找22需要1个查找35需要1个查找8需要1个查找24需要1个。 成功ASL (11112)/5 1.2。 查找失败的ASL是指查找一个不存在的关键字时需要遍历的链表节点数的平均值。通常假设要查找的关键字等概率地映射到每个地址。对于上例地址0链表长度为1查找失败需遍历1个节点发现不是地址1链表长度为0遍历0个地址2链表长度为2需遍历完2个地址3长度1遍历1个地址4长度1遍历1个。 失败ASL (1 0 2 1 1) / 5 1.0。注意分母是表长m5代表可能映射到的地址数4.3 再哈希法Rehashing这里的“再哈希法”与开放定址法中的“双哈希”同名但概念不同。它是指准备一组哈希函数{H1, H2, H3, ...}。当使用H1发生冲突时换用H2计算地址如果再冲突换H3以此类推。为什么用得少这种方法要求预先设计多个好的哈希函数且每个函数计算不能太复杂。在实践中设计难度较大不如链地址法或双哈希法开放定址通用教材和考试中多作为概念了解。4.4 建立公共溢出区法思路很简单将哈希表分为两部分基本表和溢出表。所有冲突的记录不再在基本表内解决而是统一存放到另一个独立的存储区域——溢出表中。工作流程根据哈希函数计算地址若基本表该地址为空则存入。若冲突则将该记录顺序存入溢出表。查找时先在基本表对应地址查找若找不到则到溢出表中进行顺序查找。为什么它是一种选择优点实现非常简单逻辑清晰。基本表的插入和查找无冲突时速度极快。缺点溢出表成为了性能瓶颈。当冲突较多时溢出表会变得很长在溢出表上的查找退化为O(n)的顺序查找整体性能下降。它适用于冲突发生较少的情况。冲突解决方法对比总结表方法核心思想优点缺点关键考点与ASL计算特点开放定址法在表内找下一个空位所有数据存于一处空间利用率高缓存友好删除麻烦需标记易聚集α1线性探测会堆积。计算ASL时成功查找需考虑探测次数。平方探测m需为4k3质数。链地址法同义词组成链表处理简单无堆积删除易允许α1需要额外指针空间缓存不友好最常考ASL成功ASL求各关键字查找长度均值。失败ASL求各地址链表长度均值分母为m。再哈希法换一个哈希函数聚集少需设计多个哈希函数了解概念较少直接计算。公共溢出区冲突记录全放另一个表实现简单基本表操作快溢出表成为性能瓶颈理解原理能描述查找过程。5. 期末实战综合题型拆解与计算演练光说不练假把式。下面我们用一个典型的期末/考研大题把构造方法和冲突解决方法串起来。题目设哈希表表长m13哈希函数为 H(key) key MOD 11。采用链地址法解决冲突。请画出依次插入关键字序列 { 16, 74, 60, 43, 54, 90, 46, 31, 29, 88, 77 } 后的哈希表并计算查找成功和查找失败的平均查找长度ASL。解题步骤确定参数表长m13但哈希函数模数是p11。这里要注意表长和模数可以不同。地址范围是0到12因为m13但哈希函数计算结果要对11取模所以初始哈希地址范围是0到10。由于采用链地址法即使H(key)算出来是0-10我们依然有13个表项0-12来存放链表头多出的位置地址11,12可能永远为空但这不影响。计算哈希地址并插入H(16) 16 MOD 11 5H(74) 74 MOD 11 8H(60) 60 MOD 11 5冲突链入地址5链表H(43) 43 MOD 11 10H(54) 54 MOD 11 10冲突链入地址10链表H(90) 90 MOD 11 2H(46) 46 MOD 11 2冲突链入地址2链表H(31) 31 MOD 11 9H(29) 29 MOD 11 7H(88) 88 MOD 11 0H(77) 77 MOD 11 0冲突链入地址0链表画出哈希表用数组链表表示地址0: 88 - 77 - NULL 地址1: NULL 地址2: 90 - 46 - NULL (通常按插入顺序新插入的放表头这里假设按题目顺序插入90先46后) 地址3: NULL 地址4: NULL 地址5: 16 - 60 - NULL 地址6: NULL 地址7: 29 - NULL 地址8: 74 - NULL 地址9: 31 - NULL 地址10: 43 - 54 - NULL 地址11: NULL (表长13多出的地址) 地址12: NULL计算查找成功的平均查找长度ASL成功 需要统计查找每个关键字需要遍历的链表节点数比较次数。查找时从链表头开始一次比较算一次。查找16地址5链表 16-60第1个节点就是比较1次。查找74地址8链表只有74比较1次。查找60地址5链表 16-60需先比较161次再比较60第2次共2次。查找43地址10链表 43-54第1个节点就是比较1次。查找54地址10链表 43-54先比较431次再比较542次共2次。查找90地址2链表 90-46第1个节点就是比较1次。查找46地址2链表 90-46先比较901次再比较462次共2次。查找31地址9比较1次。查找29地址7比较1次。查找88地址0链表 88-77比较1次。查找77地址0链表 88-77先比较881次再比较772次共2次。总比较次数 1(16)1(74)2(60)1(43)2(54)1(90)2(46)1(31)1(29)1(88)2(77) 15次。 关键字总数n11。ASL成功 15 / 11 ≈ 1.364。计算查找失败的平均查找长度ASL失败 查找失败时假设待查关键字等概率地映射到哈希函数所能计算出的每一个地址上即H(key)的值域。本题H(key)key MOD 11值域是0,1,2,...,10 共11个地址。注意表长m13但地址11和12是哈希函数永远算不出来的所以不考虑它们。 对于每个可能的哈希地址0到10我们计算“在这个地址对应的链表上查找失败需要比较多少次”。查找失败意味着一直比较到链表末尾的NULL。地址0链表长度为2 (88-77-NULL)需要比较3次8877NULL不对。查找时先与88比1次不匹配再与77比2次不匹配遇到NULL停止。所以比较次数为2次即链表长度。地址1链表长度0直接发现为空比较0次。地址2链表长度2 (90-46-NULL)比较2次。地址3长度0比较0次。地址4长度0比较0次。地址5长度2 (16-60-NULL)比较2次。地址6长度0比较0次。地址7长度1 (29-NULL)比较1次。地址8长度1 (74-NULL)比较1次。地址9长度1 (31-NULL)比较1次。地址10长度2 (43-54-NULL)比较2次。总失败比较次数 20200201112 11次。 可能的地址数 11。ASL失败 11 / 11 1.0。踩坑提醒计算ASL失败时分母是哈希函数的值域大小即模数p本题是11而不是表长m本题是13这是很多同学容易出错的地方一定要理解查找失败是基于“这个关键字如果存在它应该落在哪个地址”来考虑的它只可能落在H(key)能算出的那些地址上。6. 高频考点与独家避坑技巧根据多年刷题和教学经验哈希表这章除了上述计算还有一些容易混淆和出错的点。6.1 装载因子α的理解与运用装载因子α 表中记录数n / 哈希表长度m。它是衡量哈希表满的程度也是决定其性能的关键参数。对于开放定址法α必须小于1。通常α 0.7时性能尚可α 0.8后查找性能会非线性下降。考试中可能会让你根据预计存储的记录数n和设定的α反推需要的表长mm ≥ n / α。对于链地址法α可以大于1它表示每个链表的平均长度。α越大平均链表越长查找效率越低趋向O(n)。通常希望α控制在1~2以内。考题变形“若采用线性探测法要求装载因子不超过0.7现有100个记录则哈希表长度至少应为多少” 答案m ≥ 100 / 0.7 ≈ 142.86取至少143。同时为了使用除留余数法最好选择大于143的质数例如149。6.2 不同冲突解决方法下的删除操作这是简答题和算法设计题的常客。链地址法删除最简单找到节点后在链表中删除即可。时间复杂度O(1)~O(α)。开放定址法不能直接物理删除必须采用“标记删除”。在删除位置做一个特殊标记如设为DELETED。查找时遇到DELETED标记应继续探测因为后面可能还有同义词。插入时遇到DELETED标记可以将其覆盖复用空间。你需要能说清楚为什么不能直接置空。6.3 平均查找长度ASL的深度辨析ASL是衡量哈希表效率的核心指标一定要会算更要理解。ASL成功查找表中已有记录的平均比较次数。计算方法是对每个关键字统计找到它需要比较的次数求和后除以关键字总数n。ASL失败查找表中不存在的记录的平均比较次数。计算方法是对哈希函数值域内的每一个地址统计在该地址对应的链表或探测序列上一直查到“终止条件”链表NULL或开放定址的空位所需的比较次数求和后除以哈希函数值域的大小通常是模数p或表长m需根据方法判断。关键区别失败ASL的分母不是n也不是表长m在链地址法中而是可能映射到的地址总数。对于除留余数法H(key)key MOD p就是p对于直接定址法就是关键字取值范围的大小。这个概念务必厘清。6.4 从考题到应用如何选择构造与冲突解决方法考试中可能会出简答题“比较开放定址法和链地址法的优缺点并说明各自适用场景。” 你可以这样组织答案开放定址法优点所有数据存储在连续数组中存储效率高缓存局部性好CPU缓存命中率高序列化方便。缺点有聚集现象删除操作复杂需标记装载因子必须小于1扩容成本高需要rehash所有元素。适用场景数据量相对固定或可预估对缓存性能要求高内存空间紧凑的场景。链地址法优点无聚集现象处理冲突简单删除操作容易装载因子可以大于1扩容相对灵活可以单独对长链表进行拆分。缺点需要额外的指针存储空间节点内存不连续缓存局部性差小对象存储时空间开销比例大。适用场景数据量动态变化频繁插入删除内存相对充裕的场景。这也是大多数编程语言如Java的HashMapPython的dict内置哈希表实现采用的方法通常结合数组链表/红黑树。最后我个人在复习和实际编码中的体会是哈希表这一章理解远比死记硬背重要。一定要动手画图把插入过程一步步画出来计算ASL时把比较次数一个个数出来。考试时无论题目怎么变核心就是那6种构造方法和4种冲突解决方法的不同组合。把本文的例子和习题吃透哈希表这部分的分你基本就稳拿了。如果时间允许最好能用你熟悉的编程语言C、Java、Python都行亲手实现一个简单的链地址法哈希表从put、get到remove实现一遍理解会深刻十倍。