|
|
|
@ -25,7 +25,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<title>6.2. 哈希冲突 - Hello 算法</title>
|
|
|
|
|
<title>6.2. 哈希冲突(New) - Hello 算法</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -113,7 +113,7 @@
|
|
|
|
|
<div class="md-header__topic" data-md-component="header-topic">
|
|
|
|
|
<span class="md-ellipsis">
|
|
|
|
|
|
|
|
|
|
6.2. 哈希冲突
|
|
|
|
|
6.2. 哈希冲突(New)
|
|
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
@ -1003,12 +1003,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label class="md-nav__link md-nav__link--active" for="__toc">
|
|
|
|
|
6.2. 哈希冲突
|
|
|
|
|
6.2. 哈希冲突(New)
|
|
|
|
|
<span class="md-nav__icon md-icon"></span>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<a href="./" class="md-nav__link md-nav__link--active">
|
|
|
|
|
6.2. 哈希冲突
|
|
|
|
|
6.2. 哈希冲突(New)
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -2139,16 +2139,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<h1 id="62">6.2. 哈希冲突<a class="headerlink" href="#62" title="Permanent link">¶</a></h1>
|
|
|
|
|
<p>在理想情况下,哈希函数应为每个输入生成唯一的输出,实现 key 和 value 的一一对应。然而实际上,向哈希函数输入不同的 key 却产生相同输出的情况是存在的,这种现象被称为「哈希冲突 Hash Collision」。哈希冲突可能导致查询结果错误,从而严重影响哈希表的可用性。</p>
|
|
|
|
|
<p>那么,为何会出现哈希冲突呢?从本质上看,由于哈希函数的输入空间通常远大于输出空间,因此多个输入产生相同输出的情况是不可避免的。例如,若输入空间为全体整数,而输出空间为固定大小的数组,则必然有多个整数映射至同一数组索引。</p>
|
|
|
|
|
<p>为了减轻哈希冲突,一方面,<strong>可以通过扩大哈希表容量来降低冲突概率</strong>。极端情况下,当输入空间和输出空间大小相等时,哈希表等同于数组,每个 key 都对应唯一的数组索引,可谓“大力出奇迹”。</p>
|
|
|
|
|
<p>另一方面,<strong>可以考虑优化哈希表的表示以缓解哈希冲突</strong>,常用方法包括「链式地址 Separate Chaining」和「开放寻址 Open Addressing」。</p>
|
|
|
|
|
<p>在理想情况下,哈希函数为每个输入生成唯一的输出,实现 key 和数组索引的一一对应。但实际上,<strong>哈希函数的输入空间通常远大于输出空间</strong>,因此多个输入产生相同输出的情况是不可避免的。例如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一数组索引。</p>
|
|
|
|
|
<p>这种多个输入对应同一输出索引的现象被称为「哈希冲突 Hash Collision」。哈希冲突会导致查询结果错误,严重影响哈希表的可用性。哈希冲突的解决方法主要有两种:</p>
|
|
|
|
|
<ul>
|
|
|
|
|
<li><strong>扩大哈希表容量</strong>:哈希表容量越大,键值对聚集的概率就越低。极端情况下,当输入空间和输出空间大小相等时,哈希表等同于数组,每个 key 都对应唯一的数组索引。</li>
|
|
|
|
|
<li><strong>优化哈希表结构</strong>:常用方法包括链式地址和开放寻址。</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<h2 id="621">6.2.1. 哈希表扩容<a class="headerlink" href="#621" title="Permanent link">¶</a></h2>
|
|
|
|
|
<p>哈希函数的最后一步通常是对桶数量 <span class="arithmatex">\(n\)</span> 取余,作用是将哈希值映射到桶索引范围,从而将 key 放入对应的桶中。当哈希表容量越大(即 <span class="arithmatex">\(n\)</span> 越大)时,多个 key 被分配到同一个桶中的概率就越低,冲突就越少。</p>
|
|
|
|
|
<p>因此,<strong>当哈希表内的冲突总体较为严重时,编程语言通常通过扩容哈希表来缓解冲突</strong>。类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,开销较大。</p>
|
|
|
|
|
<p>编程语言通常使用「负载因子 Load Factor」来衡量哈希冲突的严重程度,<strong>定义为哈希表中元素数量除以桶数量</strong>,常作为哈希表扩容的触发条件。在 Java 中,当负载因子超过 $ 0.75$ 时,系统会将 HashMap 容量扩展为原先的 <span class="arithmatex">\(2\)</span> 倍。</p>
|
|
|
|
|
<p>编程语言通常使用「负载因子 Load Factor」来衡量哈希冲突的严重程度,<strong>定义为哈希表中元素数量除以桶数量</strong>,常作为哈希表扩容的触发条件。在 Java 中,当负载因子超过 <span class="arithmatex">\(0.75\)</span> 时,系统会将 HashMap 容量扩展为原先的 <span class="arithmatex">\(2\)</span> 倍。</p>
|
|
|
|
|
<h2 id="622">6.2.2. 链式地址<a class="headerlink" href="#622" title="Permanent link">¶</a></h2>
|
|
|
|
|
<p>在原始哈希表中,每个桶仅能存储一个键值对。<strong>链式地址将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中</strong>。</p>
|
|
|
|
|
<p>在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 Separate Chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。</p>
|
|
|
|
|
<p><img alt="链式地址哈希表" src="../hash_collision.assets/hash_collision_chaining.png" /></p>
|
|
|
|
|
<p align="center"> Fig. 链式地址哈希表 </p>
|
|
|
|
|
|
|
|
|
@ -2549,7 +2551,7 @@
|
|
|
|
|
<p>为了提高效率,<strong>我们可以将链表转换为「AVL 树」或「红黑树」</strong>,从而将查询操作的时间复杂度优化至 <span class="arithmatex">\(O(\log n)\)</span> 。</p>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 id="623">6.2.3. 开放寻址<a class="headerlink" href="#623" title="Permanent link">¶</a></h2>
|
|
|
|
|
<p>开放寻址法不引入额外的数据结构,而是通过“多次探测”来解决哈希冲突,<strong>探测方式主要包括线性探测、平方探测、多次哈希</strong>。</p>
|
|
|
|
|
<p>「开放寻址 Open Addressing」不引入额外的数据结构,而是通过“多次探测”来解决哈希冲突,探测方式主要包括线性探测、平方探测、多次哈希。</p>
|
|
|
|
|
<h3 id="_1">线性探测<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
|
|
|
|
<p>线性探测采用固定步长的线性查找来解决哈希冲突。</p>
|
|
|
|
|
<ul>
|
|
|
|
@ -2995,9 +2997,9 @@
|
|
|
|
|
<p>与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会增加额外的计算量。</p>
|
|
|
|
|
<div class="admonition note">
|
|
|
|
|
<p class="admonition-title">编程语言的选择</p>
|
|
|
|
|
<p>Java 采用「链式地址」。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会被转换为红黑树以提升查找性能。</p>
|
|
|
|
|
<p>Python 采用「开放寻址」。字典 dict 使用伪随机数进行探测。</p>
|
|
|
|
|
<p>Golang 采用「链式地址」。Go 规定每个桶最多存储 8 个键值对,超出容量则连接一个溢出桶;当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。</p>
|
|
|
|
|
<p>Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会被转换为红黑树以提升查找性能。</p>
|
|
|
|
|
<p>Python 采用开放寻址。字典 dict 使用伪随机数进行探测。</p>
|
|
|
|
|
<p>Golang 采用链式地址。Go 规定每个桶最多存储 8 个键值对,超出容量则连接一个溢出桶;当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|