mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-13 23:56:07 +00:00
deploy
This commit is contained in:
@@ -3481,25 +3481,25 @@
|
||||
<p>观察以上公式,当哈希表容量 <code>capacity</code> 固定时,<strong>哈希算法 <code>hash()</code> 决定了输出值</strong>,进而决定了键值对在哈希表中的分布情况。</p>
|
||||
<p>这意味着,为了减小哈希冲突的发生概率,我们应当将注意力集中在哈希算法 <code>hash()</code> 的设计上。</p>
|
||||
<h2 id="631">6.3.1 哈希算法的目标<a class="headerlink" href="#631" title="Permanent link">¶</a></h2>
|
||||
<p>为了实现“既快又稳”的哈希表数据结构,哈希算法应包含以下特点:</p>
|
||||
<p>为了实现“既快又稳”的哈希表数据结构,哈希算法应包含以下特点。</p>
|
||||
<ul>
|
||||
<li><strong>确定性</strong>:对于相同的输入,哈希算法应始终产生相同的输出。这样才能确保哈希表是可靠的。</li>
|
||||
<li><strong>效率高</strong>:计算哈希值的过程应该足够快。计算开销越小,哈希表的实用性越高。</li>
|
||||
<li><strong>均匀分布</strong>:哈希算法应使得键值对平均分布在哈希表中。分布越平均,哈希冲突的概率就越低。</li>
|
||||
</ul>
|
||||
<p>实际上,哈希算法除了可以用于实现哈希表,还广泛应用于其他领域中。举两个例子:</p>
|
||||
<p>实际上,哈希算法除了可以用于实现哈希表,还广泛应用于其他领域中。</p>
|
||||
<ul>
|
||||
<li><strong>密码存储</strong>:为了保护用户密码的安全,系统通常不会直接存储用户的明文密码,而是存储密码的哈希值。当用户输入密码时,系统会对输入的密码计算哈希值,然后与存储的哈希值进行比较。如果两者匹配,那么密码就被视为正确。</li>
|
||||
<li><strong>数据完整性检查</strong>:数据发送方可以计算数据的哈希值并将其一同发送;接收方可以重新计算接收到的数据的哈希值,并与接收到的哈希值进行比较。如果两者匹配,那么数据就被视为完整的。</li>
|
||||
</ul>
|
||||
<p>对于密码学的相关应用,哈希算法需要满足更高的安全标准,以防止从哈希值推导出原始密码等逆向工程,包括:</p>
|
||||
<p>对于密码学的相关应用,为了防止从哈希值推导出原始密码等逆向工程,哈希算法需要具备更高等级的安全特性。</p>
|
||||
<ul>
|
||||
<li><strong>抗碰撞性</strong>:应当极其困难找到两个不同的输入,使得它们的哈希值相同。</li>
|
||||
<li><strong>雪崩效应</strong>:输入的微小变化应当导致输出的显著且不可预测的变化。</li>
|
||||
</ul>
|
||||
<p>请注意,<strong>“均匀分布”与“抗碰撞性”是两个独立的概念</strong>,满足均匀分布不一定满足抗碰撞性。例如,在随机输入 <code>key</code> 下,哈希函数 <code>key % 100</code> 可以产生均匀分布的输出。然而该哈希算法过于简单,所有后两位相等的 <code>key</code> 的输出都相同,因此我们可以很容易地从哈希值反推出可用的 <code>key</code> ,从而破解密码。</p>
|
||||
<h2 id="632">6.3.2 哈希算法的设计<a class="headerlink" href="#632" title="Permanent link">¶</a></h2>
|
||||
<p>哈希算法的设计是一个复杂且需要考虑许多因素的问题。然而对于简单场景,我们也能设计一些简单的哈希算法。以字符串哈希为例:</p>
|
||||
<p>哈希算法的设计是一个需要考虑许多因素的复杂问题。然而对于某些要求不高的场景,我们也能设计一些简单的哈希算法。</p>
|
||||
<ul>
|
||||
<li><strong>加法哈希</strong>:对输入的每个字符的 ASCII 码进行相加,将得到的总和作为哈希值。</li>
|
||||
<li><strong>乘法哈希</strong>:利用了乘法的不相关性,每轮乘以一个常数,将各个字符的 ASCII 码累积到哈希值中。</li>
|
||||
@@ -3929,7 +3929,7 @@
|
||||
</div>
|
||||
<p>观察发现,每种哈希算法的最后一步都是对大质数 <span class="arithmatex">\(1000000007\)</span> 取模,以确保哈希值在合适的范围内。值得思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。</p>
|
||||
<p>先抛出结论:<strong>当我们使用大质数作为模数时,可以最大化地保证哈希值的均匀分布</strong>。因为质数不会与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。</p>
|
||||
<p>举个例子,假设我们选择合数 <span class="arithmatex">\(9\)</span> 作为模数,它可以被 <span class="arithmatex">\(3\)</span> 整除。那么所有可以被 <span class="arithmatex">\(3\)</span> 整除的 <code>key</code> 都会被映射到 <span class="arithmatex">\(0\)</span> , <span class="arithmatex">\(3\)</span> , <span class="arithmatex">\(6\)</span> 这三个哈希值。</p>
|
||||
<p>举个例子,假设我们选择合数 <span class="arithmatex">\(9\)</span> 作为模数,它可以被 <span class="arithmatex">\(3\)</span> 整除。那么所有可以被 <span class="arithmatex">\(3\)</span> 整除的 <code>key</code> 都会被映射到 <span class="arithmatex">\(0\)</span>、<span class="arithmatex">\(3\)</span>、<span class="arithmatex">\(6\)</span> 这三个哈希值。</p>
|
||||
<div class="arithmatex">\[
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 9 \newline
|
||||
@@ -3949,8 +3949,8 @@
|
||||
<p>总而言之,我们通常选取质数作为模数,并且这个质数最好足够大,以尽可能消除周期性模式,提升哈希算法的稳健性。</p>
|
||||
<h2 id="633">6.3.3 常见哈希算法<a class="headerlink" href="#633" title="Permanent link">¶</a></h2>
|
||||
<p>不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。</p>
|
||||
<p>在实际中,我们通常会用一些标准哈希算法,例如 MD5 , SHA-1 , SHA-2 , SHA3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。</p>
|
||||
<p>近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。直至目前:</p>
|
||||
<p>在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2、SHA3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。</p>
|
||||
<p>近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。</p>
|
||||
<ul>
|
||||
<li>MD5 和 SHA-1 已多次被成功攻击,因此它们被各类安全应用弃用。</li>
|
||||
<li>SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常被用在各类安全应用与协议中。</li>
|
||||
@@ -4007,7 +4007,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<h2 id="634">6.3.4 数据结构的哈希值<a class="headerlink" href="#634" title="Permanent link">¶</a></h2>
|
||||
<p>我们知道,哈希表的 <code>key</code> 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。以 Python 为例,我们可以调用 <code>hash()</code> 函数来计算各种数据类型的哈希值,包括:</p>
|
||||
<p>我们知道,哈希表的 <code>key</code> 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。以 Python 为例,我们可以调用 <code>hash()</code> 函数来计算各种数据类型的哈希值。</p>
|
||||
<ul>
|
||||
<li>整数和布尔量的哈希值就是其本身。</li>
|
||||
<li>浮点数和字符串的哈希值计算较为复杂,有兴趣的同学请自行学习。</li>
|
||||
|
||||
@@ -3497,32 +3497,32 @@
|
||||
|
||||
<h1 id="62">6.2 哈希冲突<a class="headerlink" href="#62" title="Permanent link">¶</a></h1>
|
||||
<p>上节提到,<strong>通常情况下哈希函数的输入空间远大于输出空间</strong>,因此理论上哈希冲突是不可避免的。比如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一数组索引。</p>
|
||||
<p>哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为解决该问题,我们可以每当遇到哈希冲突时就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们切换一下思路:</p>
|
||||
<p>哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为解决该问题,我们可以每当遇到哈希冲突时就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以采用以下思路。</p>
|
||||
<ol>
|
||||
<li>改良哈希表数据结构,<strong>使得哈希表可以在存在哈希冲突时正常工作</strong>。</li>
|
||||
<li>仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。</li>
|
||||
</ol>
|
||||
<p>哈希表的结构改良方法主要包括链式地址和开放寻址。</p>
|
||||
<p>哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。</p>
|
||||
<h2 id="621">6.2.1 链式地址<a class="headerlink" href="#621" title="Permanent link">¶</a></h2>
|
||||
<p>在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图 6-5 展示了一个链式地址哈希表的例子。</p>
|
||||
<p><img alt="链式地址哈希表" src="../hash_collision.assets/hash_table_chaining.png" /></p>
|
||||
<p align="center"> 图 6-5 链式地址哈希表 </p>
|
||||
|
||||
<p>链式地址下,哈希表的操作方法包括:</p>
|
||||
<p>哈希表在链式地址下的操作方法发生了一些变化。</p>
|
||||
<ul>
|
||||
<li><strong>查询元素</strong>:输入 <code>key</code> ,经过哈希函数得到数组索引,即可访问链表头节点,然后遍历链表并对比 <code>key</code> 以查找目标键值对。</li>
|
||||
<li><strong>添加元素</strong>:先通过哈希函数访问链表头节点,然后将节点(即键值对)添加到链表中。</li>
|
||||
<li><strong>删除元素</strong>:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标节点,并将其删除。</li>
|
||||
</ul>
|
||||
<p>该方法存在一些局限性,包括:</p>
|
||||
<p>链式地址存在以下局限性。</p>
|
||||
<ul>
|
||||
<li><strong>占用空间增大</strong>,链表包含节点指针,它相比数组更加耗费内存空间。</li>
|
||||
<li><strong>查询效率降低</strong>,因为需要线性遍历链表来查找对应元素。</li>
|
||||
</ul>
|
||||
<p>以下给出了链式地址哈希表的简单实现,需要注意:</p>
|
||||
<p>以下代码给出了链式地址哈希表的简单实现,需要注意两点。</p>
|
||||
<ul>
|
||||
<li>为了使得代码尽量简短,我们使用列表(动态数组)代替链表。在这种设定下,哈希表(数组)包含多个桶,每个桶都是一个列表。</li>
|
||||
<li>以下代码实现了哈希表扩容方法。具体来看,当负载因子超过 <span class="arithmatex">\(0.75\)</span> 时,我们将哈希表扩容至 <span class="arithmatex">\(2\)</span> 倍。</li>
|
||||
<li>使用列表(动态数组)代替链表,从而简化代码。在这种设定下,哈希表(数组)包含多个桶,每个桶都是一个列表。</li>
|
||||
<li>以下实现包含哈希表扩容方法。当负载因子超过 <span class="arithmatex">\(0.75\)</span> 时,我们将哈希表扩容至 <span class="arithmatex">\(2\)</span> 倍。</li>
|
||||
</ul>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JS</label><label for="__tabbed_1_6">TS</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label><label for="__tabbed_1_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -4622,7 +4622,7 @@
|
||||
<h2 id="622">6.2.2 开放寻址<a class="headerlink" href="#622" title="Permanent link">¶</a></h2>
|
||||
<p>「开放寻址 open addressing」不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测、多次哈希等。</p>
|
||||
<h3 id="1">1. 线性探测<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p>线性探测采用固定步长的线性查找来进行探测,对应的哈希表操作方法为:</p>
|
||||
<p>线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。</p>
|
||||
<ul>
|
||||
<li><strong>插入元素</strong>:通过哈希函数计算数组索引,若发现桶内已有元素,则从冲突位置向后线性遍历(步长通常为 <span class="arithmatex">\(1\)</span> ),直至找到空位,将元素插入其中。</li>
|
||||
<li><strong>查找元素</strong>:若发现哈希冲突,则使用相同步长向后线性遍历,直到找到对应元素,返回 <code>value</code> 即可;如果遇到空位,说明目标键值对不在哈希表中,返回 <span class="arithmatex">\(\text{None}\)</span> 。</li>
|
||||
@@ -4631,12 +4631,12 @@
|
||||
<p><img alt="开放寻址和线性探测" src="../hash_collision.assets/hash_table_linear_probing.png" /></p>
|
||||
<p align="center"> 图 6-6 开放寻址和线性探测 </p>
|
||||
|
||||
<p>然而,线性探测存在以下缺陷:</p>
|
||||
<p>然而,线性探测存在以下缺陷。</p>
|
||||
<ul>
|
||||
<li><strong>不能直接删除元素</strong>。删除元素会在数组内产生一个空位,当查找该空位之后的元素时,该空位可能导致程序误判元素不存在。为此,通常需要借助一个标志位来标记已删除元素。</li>
|
||||
<li><strong>容易产生聚集</strong>。数组内连续被占用位置越长,这些连续位置发生哈希冲突的可能性越大,进一步促使这一位置的聚堆生长,形成恶性循环,最终导致增删查改操作效率劣化。</li>
|
||||
</ul>
|
||||
<p>以下代码实现了一个简单的开放寻址(线性探测)哈希表。值得注意两点:</p>
|
||||
<p>以下代码实现了一个简单的开放寻址(线性探测)哈希表。</p>
|
||||
<ul>
|
||||
<li>我们使用一个固定的键值对实例 <code>removed</code> 来标记已删除元素。也就是说,当一个桶内的元素为 <span class="arithmatex">\(\text{None}\)</span> 或 <code>removed</code> 时,说明这个桶是空的,可用于放置键值对。</li>
|
||||
<li>在线性探测时,我们从当前索引 <code>index</code> 向后遍历;而当越过数组尾部时,需要回到头部继续遍历。</li>
|
||||
@@ -5884,7 +5884,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="2">2. 多次哈希<a class="headerlink" href="#2" title="Permanent link">¶</a></h3>
|
||||
<p>顾名思义,多次哈希方法是使用多个哈希函数 <span class="arithmatex">\(f_1(x)\)</span> , <span class="arithmatex">\(f_2(x)\)</span> , <span class="arithmatex">\(f_3(x)\)</span> , <span class="arithmatex">\(\dots\)</span> 进行探测。</p>
|
||||
<p>顾名思义,多次哈希方法是使用多个哈希函数 <span class="arithmatex">\(f_1(x)\)</span>、<span class="arithmatex">\(f_2(x)\)</span>、<span class="arithmatex">\(f_3(x)\)</span>、<span class="arithmatex">\(\dots\)</span> 进行探测。</p>
|
||||
<ul>
|
||||
<li><strong>插入元素</strong>:若哈希函数 <span class="arithmatex">\(f_1(x)\)</span> 出现冲突,则尝试 <span class="arithmatex">\(f_2(x)\)</span> ,以此类推,直到找到空位后插入元素。</li>
|
||||
<li><strong>查找元素</strong>:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;或遇到空位或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 <span class="arithmatex">\(\text{None}\)</span> 。</li>
|
||||
|
||||
@@ -3875,7 +3875,7 @@
|
||||
<h2 id="612">6.1.2 哈希表简单实现<a class="headerlink" href="#612" title="Permanent link">¶</a></h2>
|
||||
<p>我们先考虑最简单的情况,<strong>仅用一个数组来实现哈希表</strong>。在哈希表中,我们将数组中的每个空位称为「桶 bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 <code>key</code> 对应的桶,并在桶中获取 <code>value</code> 。</p>
|
||||
<p>那么,如何基于 <code>key</code> 来定位对应的桶呢?这是通过「哈希函数 hash function」实现的。哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中,输入空间是所有 <code>key</code> ,输出空间是所有桶(数组索引)。换句话说,输入一个 <code>key</code> ,<strong>我们可以通过哈希函数得到该 <code>key</code> 对应的键值对在数组中的存储位置</strong>。</p>
|
||||
<p>输入一个 <code>key</code> ,哈希函数的计算过程分为两步:</p>
|
||||
<p>输入一个 <code>key</code> ,哈希函数的计算过程分为以下两步。</p>
|
||||
<ol>
|
||||
<li>通过某种哈希算法 <code>hash()</code> 计算得到哈希值。</li>
|
||||
<li>将哈希值对桶数量(数组长度)<code>capacity</code> 取模,从而获取该 <code>key</code> 对应的数组索引 <code>index</code> 。</li>
|
||||
@@ -3883,7 +3883,7 @@
|
||||
<div class="highlight"><pre><span></span><code><a id="__codelineno-24-1" name="__codelineno-24-1" href="#__codelineno-24-1"></a><span class="nv">index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span>hash<span class="o">(</span>key<span class="o">)</span><span class="w"> </span>%<span class="w"> </span>capacity
|
||||
</code></pre></div>
|
||||
<p>随后,我们就可以利用 <code>index</code> 在哈希表中访问对应的桶,从而获取 <code>value</code> 。</p>
|
||||
<p>设数组长度 <code>capacity = 100</code> 、哈希算法 <code>hash(key) = key</code> ,易得哈希函数为 <code>key % 100</code> 。图 6-2 以 <code>key</code> 学号和 <code>value</code> 姓名为例,展示了哈希函数的工作原理。</p>
|
||||
<p>设数组长度 <code>capacity = 100</code>、哈希算法 <code>hash(key) = key</code> ,易得哈希函数为 <code>key % 100</code> 。图 6-2 以 <code>key</code> 学号和 <code>value</code> 姓名为例,展示了哈希函数的工作原理。</p>
|
||||
<p><img alt="哈希函数工作原理" src="../hash_map.assets/hash_function.png" /></p>
|
||||
<p align="center"> 图 6-2 哈希函数工作原理 </p>
|
||||
|
||||
|
||||
@@ -3440,7 +3440,7 @@
|
||||
<li>不同编程语言采取了不同的哈希表实现。例如,Java 的 <code>HashMap</code> 使用链式地址,而 Python 的 <code>Dict</code> 采用开放寻址。</li>
|
||||
<li>在哈希表中,我们希望哈希算法具有确定性、高效率和均匀分布的特点。在密码学中,哈希算法还应该具备抗碰撞性和雪崩效应。</li>
|
||||
<li>哈希算法通常采用大质数作为模数,以最大化地保证哈希值的均匀分布,减少哈希冲突。</li>
|
||||
<li>常见的哈希算法包括 MD5, SHA-1, SHA-2, SHA3 等。MD5 常用于校验文件完整性,SHA-2 常用于安全应用与协议。</li>
|
||||
<li>常见的哈希算法包括 MD5、SHA-1、SHA-2 和 SHA3 等。MD5 常用于校验文件完整性,SHA-2 常用于安全应用与协议。</li>
|
||||
<li>编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。</li>
|
||||
</ul>
|
||||
<h2 id="641-q-a">6.4.1 Q & A<a class="headerlink" href="#641-q-a" title="Permanent link">¶</a></h2>
|
||||
|
||||
Reference in New Issue
Block a user