This commit is contained in:
krahets 2023-02-26 19:53:32 +08:00
parent 62d7f2c85d
commit d5d3a29676
31 changed files with 179 additions and 1 deletions

View file

@ -1602,6 +1602,8 @@
<h1 id="41">4.1. &nbsp; 数组<a class="headerlink" href="#41" title="Permanent link">&para;</a></h1>
<p>「数组 Array」是一种将 <strong>相同类型元素</strong> 存储在 <strong>连续内存空间</strong> 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。</p>
<p><img alt="数组定义与存储方式" src="../array.assets/array_definition.png" /></p>
<p align="center"> Fig. 数组定义与存储方式 </p>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>观察上图,我们发现 <strong>数组首元素的索引为 <span class="arithmatex">\(0\)</span></strong> 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 <span class="arithmatex">\(1\)</span> 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。</p>
@ -1680,6 +1682,8 @@
<h2 id="411">4.1.1. &nbsp; 数组优点<a class="headerlink" href="#411" title="Permanent link">&para;</a></h2>
<p><strong>在数组中访问元素非常高效</strong>。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。</p>
<p><img alt="数组元素的内存地址计算" src="../array.assets/array_memory_location_calculation.png" /></p>
<p align="center"> Fig. 数组元素的内存地址计算 </p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="c1"># 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引</span>
<a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="nv">elementAddr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span>firtstElementAddr<span class="w"> </span>+<span class="w"> </span>elementLength<span class="w"> </span>*<span class="w"> </span>elementIndex
</code></pre></div>
@ -1939,6 +1943,8 @@
</div>
<p><strong>数组中插入或删除元素效率低下</strong>。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。</p>
<p><img alt="数组插入元素" src="../array.assets/array_insert_element.png" /></p>
<p align="center"> Fig. 数组插入元素 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:9"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JavaScript</label><label for="__tabbed_4_6">TypeScript</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -2045,6 +2051,8 @@
</div>
<p>删除元素也是类似,如果我们想要删除索引 <span class="arithmatex">\(i\)</span> 处的元素,则需要把索引 <span class="arithmatex">\(i\)</span> 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。</p>
<p><img alt="数组删除元素" src="../array.assets/array_remove_element.png" /></p>
<p align="center"> Fig. 数组删除元素 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:10"><input checked="checked" id="__tabbed_5_1" name="__tabbed_5" type="radio" /><input id="__tabbed_5_2" name="__tabbed_5" type="radio" /><input id="__tabbed_5_3" name="__tabbed_5" type="radio" /><input id="__tabbed_5_4" name="__tabbed_5" type="radio" /><input id="__tabbed_5_5" name="__tabbed_5" type="radio" /><input id="__tabbed_5_6" name="__tabbed_5" type="radio" /><input id="__tabbed_5_7" name="__tabbed_5" type="radio" /><input id="__tabbed_5_8" name="__tabbed_5" type="radio" /><input id="__tabbed_5_9" name="__tabbed_5" type="radio" /><input id="__tabbed_5_10" name="__tabbed_5" type="radio" /><div class="tabbed-labels"><label for="__tabbed_5_1">Java</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Python</label><label for="__tabbed_5_4">Go</label><label for="__tabbed_5_5">JavaScript</label><label for="__tabbed_5_6">TypeScript</label><label for="__tabbed_5_7">C</label><label for="__tabbed_5_8">C#</label><label for="__tabbed_5_9">Swift</label><label for="__tabbed_5_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">

View file

@ -1607,6 +1607,8 @@
<p>「链表 Linked List」是一种线性数据结构其中每个元素都是单独的对象各个元素一般称为结点之间通过指针连接。由于结点中记录了连接关系因此链表的存储方式相比于数组更加灵活系统不必保证内存地址的连续性。</p>
<p>链表的「结点 Node」包含两项数据一是结点「值 Value」二是指向下一结点的「指针 Pointer」或称「引用 Reference」</p>
<p><img alt="链表定义与存储方式" src="../linked_list.assets/linkedlist_definition.png" /></p>
<p align="center"> Fig. 链表定义与存储方式 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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">JavaScript</label><label for="__tabbed_1_6">TypeScript</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></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -1874,6 +1876,8 @@
<h2 id="421">4.2.1. &nbsp; 链表优点<a class="headerlink" href="#421" title="Permanent link">&para;</a></h2>
<p><strong>在链表中,插入与删除结点的操作效率高</strong>。比如,如果我们想在链表中间的两个结点 <code>A</code> , <code>B</code> 之间插入一个新结点 <code>P</code> ,我们只需要改变两个结点指针即可,时间复杂度为 <span class="arithmatex">\(O(1)\)</span> ,相比数组的插入操作高效很多。</p>
<p><img alt="链表插入结点" src="../linked_list.assets/linkedlist_insert_node.png" /></p>
<p align="center"> Fig. 链表插入结点 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -1965,6 +1969,8 @@
</div>
<p>在链表中删除结点也很方便,只需要改变一个结点指针即可。如下图所示,虽然在完成删除后结点 <code>P</code> 仍然指向 <code>n2</code> ,但实际上 <code>P</code> 已经不属于此链表了,因为遍历此链表是无法访问到 <code>P</code> 的。</p>
<p><img alt="链表删除结点" src="../linked_list.assets/linkedlist_remove_node.png" /></p>
<p align="center"> Fig. 链表删除结点 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:10"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JavaScript</label><label for="__tabbed_4_6">TypeScript</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label><label for="__tabbed_4_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -2483,6 +2489,7 @@
</div>
</div>
<p><img alt="常见链表种类" src="../linked_list.assets/linkedlist_common_types.png" /></p>
<p align="center"> Fig. 常见链表种类 </p>

View file

@ -1687,6 +1687,8 @@
<li>「指令空间」用于保存编译后的程序指令,<strong>在实际统计中一般忽略不计</strong></li>
</ul>
<p><img alt="算法使用的相关空间" src="../space_complexity.assets/space_types.png" /></p>
<p align="center"> Fig. 算法使用的相关空间 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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">JavaScript</label><label for="__tabbed_1_6">TypeScript</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></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -2171,6 +2173,8 @@ O(1) &lt; O(\log n) &lt; O(n) &lt; O(n^2) &lt; O(2^n) \newline
\end{aligned}
\]</div>
<p><img alt="空间复杂度的常见类型" src="../space_complexity.assets/space_complexity_common_types.png" /></p>
<p align="center"> Fig. 空间复杂度的常见类型 </p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解空间复杂度含义和推算方法上。</p>
@ -2629,6 +2633,8 @@ O(1) &lt; O(\log n) &lt; O(n) &lt; O(n^2) &lt; O(2^n) \newline
</div>
</div>
<p><img alt="递归函数产生的线性阶空间复杂度" src="../space_complexity.assets/space_complexity_recursive_linear.png" /></p>
<p align="center"> Fig. 递归函数产生的线性阶空间复杂度 </p>
<h3 id="on2">平方阶 <span class="arithmatex">\(O(n^2)\)</span><a class="headerlink" href="#on2" title="Permanent link">&para;</a></h3>
<p>平方阶常见于元素数量与 <span class="arithmatex">\(n\)</span> 成平方关系的矩阵、图。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="7:10"><input checked="checked" id="__tabbed_7_1" name="__tabbed_7" type="radio" /><input id="__tabbed_7_2" name="__tabbed_7" type="radio" /><input id="__tabbed_7_3" name="__tabbed_7" type="radio" /><input id="__tabbed_7_4" name="__tabbed_7" type="radio" /><input id="__tabbed_7_5" name="__tabbed_7" type="radio" /><input id="__tabbed_7_6" name="__tabbed_7" type="radio" /><input id="__tabbed_7_7" name="__tabbed_7" type="radio" /><input id="__tabbed_7_8" name="__tabbed_7" type="radio" /><input id="__tabbed_7_9" name="__tabbed_7" type="radio" /><input id="__tabbed_7_10" name="__tabbed_7" type="radio" /><div class="tabbed-labels"><label for="__tabbed_7_1">Java</label><label for="__tabbed_7_2">C++</label><label for="__tabbed_7_3">Python</label><label for="__tabbed_7_4">Go</label><label for="__tabbed_7_5">JavaScript</label><label for="__tabbed_7_6">TypeScript</label><label for="__tabbed_7_7">C</label><label for="__tabbed_7_8">C#</label><label for="__tabbed_7_9">Swift</label><label for="__tabbed_7_10">Zig</label></div>
@ -2877,6 +2883,8 @@ O(1) &lt; O(\log n) &lt; O(n) &lt; O(n^2) &lt; O(2^n) \newline
</div>
</div>
<p><img alt="递归函数产生的平方阶空间复杂度" src="../space_complexity.assets/space_complexity_recursive_quadratic.png" /></p>
<p align="center"> Fig. 递归函数产生的平方阶空间复杂度 </p>
<h3 id="o2n">指数阶 <span class="arithmatex">\(O(2^n)\)</span><a class="headerlink" href="#o2n" title="Permanent link">&para;</a></h3>
<p>指数阶常见于二叉树。高度为 <span class="arithmatex">\(n\)</span> 的「满二叉树」的结点数量为 <span class="arithmatex">\(2^n - 1\)</span> ,使用 <span class="arithmatex">\(O(2^n)\)</span> 空间。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="9:10"><input checked="checked" id="__tabbed_9_1" name="__tabbed_9" type="radio" /><input id="__tabbed_9_2" name="__tabbed_9" type="radio" /><input id="__tabbed_9_3" name="__tabbed_9" type="radio" /><input id="__tabbed_9_4" name="__tabbed_9" type="radio" /><input id="__tabbed_9_5" name="__tabbed_9" type="radio" /><input id="__tabbed_9_6" name="__tabbed_9" type="radio" /><input id="__tabbed_9_7" name="__tabbed_9" type="radio" /><input id="__tabbed_9_8" name="__tabbed_9" type="radio" /><input id="__tabbed_9_9" name="__tabbed_9" type="radio" /><input id="__tabbed_9_10" name="__tabbed_9" type="radio" /><div class="tabbed-labels"><label for="__tabbed_9_1">Java</label><label for="__tabbed_9_2">C++</label><label for="__tabbed_9_3">Python</label><label for="__tabbed_9_4">Go</label><label for="__tabbed_9_5">JavaScript</label><label for="__tabbed_9_6">TypeScript</label><label for="__tabbed_9_7">C</label><label for="__tabbed_9_8">C#</label><label for="__tabbed_9_9">Swift</label><label for="__tabbed_9_10">Zig</label></div>
@ -2992,6 +3000,8 @@ O(1) &lt; O(\log n) &lt; O(n) &lt; O(n^2) &lt; O(2^n) \newline
</div>
</div>
<p><img alt="满二叉树产生的指数阶空间复杂度" src="../space_complexity.assets/space_complexity_exponential.png" /></p>
<p align="center"> Fig. 满二叉树产生的指数阶空间复杂度 </p>
<h3 id="olog-n">对数阶 <span class="arithmatex">\(O(\log n)\)</span><a class="headerlink" href="#olog-n" title="Permanent link">&para;</a></h3>
<p>对数阶常见于分治算法、数据类型转换等。</p>
<p>例如「归并排序」,长度为 <span class="arithmatex">\(n\)</span> 的数组可以形成高度为 <span class="arithmatex">\(\log n\)</span> 的递归树,因此空间复杂度为 <span class="arithmatex">\(O(\log n)\)</span></p>

View file

@ -2105,6 +2105,8 @@
</div>
</div>
<p><img alt="算法 A, B, C 的时间增长趋势" src="../time_complexity.assets/time_complexity_simple_example.png" /></p>
<p align="center"> Fig. 算法 A, B, C 的时间增长趋势 </p>
<p>相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?</p>
<p><strong>时间复杂度可以有效评估算法效率</strong>。算法 <code>B</code> 运行时间的增长是线性的,在 <span class="arithmatex">\(n &gt; 1\)</span> 时慢于算法 <code>A</code> ,在 <span class="arithmatex">\(n &gt; 1000000\)</span> 时慢于算法 <code>C</code> 。实质上,只要输入数据大小 <span class="arithmatex">\(n\)</span> 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。</p>
<p><strong>时间复杂度的推算方法更加简便</strong>。在时间复杂度分析中,我们可以将统计「计算操作的运行时间」简化为统计「计算操作的数量」,这是因为,无论是运行平台还是计算操作类型,都与算法运行时间的增长趋势无关。因而,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”,这样的简化做法大大降低了估算难度。</p>
@ -2245,6 +2247,8 @@ T(n) = O(f(n))
$$</p>
</div>
<p><img alt="函数的渐近上界" src="../time_complexity.assets/asymptotic_upper_bound.png" /></p>
<p align="center"> Fig. 函数的渐近上界 </p>
<p>本质上看,计算渐近上界就是在找一个函数 <span class="arithmatex">\(f(n)\)</span> <strong>使得在 <span class="arithmatex">\(n\)</span> 趋向于无穷大时,<span class="arithmatex">\(T(n)\)</span><span class="arithmatex">\(f(n)\)</span> 处于相同的增长级别(仅相差一个常数项 <span class="arithmatex">\(c\)</span> 的倍数)</strong></p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
@ -2473,6 +2477,8 @@ O(1) &lt; O(\log n) &lt; O(n) &lt; O(n \log n) &lt; O(n^2) &lt; O(2^n) &lt; O(n!
\end{aligned}
\]</div>
<p><img alt="时间复杂度的常见类型" src="../time_complexity.assets/time_complexity_common_types.png" /></p>
<p align="center"> Fig. 时间复杂度的常见类型 </p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>部分示例代码需要一些前置知识,包括数组、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解时间复杂度含义和推算方法上。</p>
@ -2952,6 +2958,8 @@ O(1) &lt; O(\log n) &lt; O(n) &lt; O(n \log n) &lt; O(n^2) &lt; O(2^n) &lt; O(n!
</div>
</div>
<p><img alt="常数阶、线性阶、平方阶的时间复杂度" src="../time_complexity.assets/time_complexity_constant_linear_quadratic.png" /></p>
<p align="center"> Fig. 常数阶、线性阶、平方阶的时间复杂度 </p>
<p>以「冒泡排序」为例,外层循环 <span class="arithmatex">\(n - 1\)</span> 次,内层循环 <span class="arithmatex">\(n-1, n-2, \cdots, 2, 1\)</span> 次,平均为 <span class="arithmatex">\(\frac{n}{2}\)</span> 次,因此时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span></p>
<div class="arithmatex">\[
O((n - 1) \frac{n}{2}) = O(n^2)
@ -3320,6 +3328,8 @@ O((n - 1) \frac{n}{2}) = O(n^2)
</div>
</div>
<p><img alt="指数阶的时间复杂度" src="../time_complexity.assets/time_complexity_exponential.png" /></p>
<p align="center"> Fig. 指数阶的时间复杂度 </p>
<p>在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 <span class="arithmatex">\(n\)</span> 次后停止。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="11:10"><input checked="checked" id="__tabbed_11_1" name="__tabbed_11" type="radio" /><input id="__tabbed_11_2" name="__tabbed_11" type="radio" /><input id="__tabbed_11_3" name="__tabbed_11" type="radio" /><input id="__tabbed_11_4" name="__tabbed_11" type="radio" /><input id="__tabbed_11_5" name="__tabbed_11" type="radio" /><input id="__tabbed_11_6" name="__tabbed_11" type="radio" /><input id="__tabbed_11_7" name="__tabbed_11" type="radio" /><input id="__tabbed_11_8" name="__tabbed_11" type="radio" /><input id="__tabbed_11_9" name="__tabbed_11" type="radio" /><input id="__tabbed_11_10" name="__tabbed_11" type="radio" /><div class="tabbed-labels"><label for="__tabbed_11_1">Java</label><label for="__tabbed_11_2">C++</label><label for="__tabbed_11_3">Python</label><label for="__tabbed_11_4">Go</label><label for="__tabbed_11_5">JavaScript</label><label for="__tabbed_11_6">TypeScript</label><label for="__tabbed_11_7">C</label><label for="__tabbed_11_8">C#</label><label for="__tabbed_11_9">Swift</label><label for="__tabbed_11_10">Zig</label></div>
<div class="tabbed-content">
@ -3529,6 +3539,8 @@ O((n - 1) \frac{n}{2}) = O(n^2)
</div>
</div>
<p><img alt="对数阶的时间复杂度" src="../time_complexity.assets/time_complexity_logarithmic.png" /></p>
<p align="center"> Fig. 对数阶的时间复杂度 </p>
<p>与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 <span class="arithmatex">\(\log_2 n\)</span> 的递归树。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="13:10"><input checked="checked" id="__tabbed_13_1" name="__tabbed_13" type="radio" /><input id="__tabbed_13_2" name="__tabbed_13" type="radio" /><input id="__tabbed_13_3" name="__tabbed_13" type="radio" /><input id="__tabbed_13_4" name="__tabbed_13" type="radio" /><input id="__tabbed_13_5" name="__tabbed_13" type="radio" /><input id="__tabbed_13_6" name="__tabbed_13" type="radio" /><input id="__tabbed_13_7" name="__tabbed_13" type="radio" /><input id="__tabbed_13_8" name="__tabbed_13" type="radio" /><input id="__tabbed_13_9" name="__tabbed_13" type="radio" /><input id="__tabbed_13_10" name="__tabbed_13" type="radio" /><div class="tabbed-labels"><label for="__tabbed_13_1">Java</label><label for="__tabbed_13_2">C++</label><label for="__tabbed_13_3">Python</label><label for="__tabbed_13_4">Go</label><label for="__tabbed_13_5">JavaScript</label><label for="__tabbed_13_6">TypeScript</label><label for="__tabbed_13_7">C</label><label for="__tabbed_13_8">C#</label><label for="__tabbed_13_9">Swift</label><label for="__tabbed_13_10">Zig</label></div>
<div class="tabbed-content">
@ -3745,6 +3757,8 @@ O((n - 1) \frac{n}{2}) = O(n^2)
</div>
</div>
<p><img alt="线性对数阶的时间复杂度" src="../time_complexity.assets/time_complexity_logarithmic_linear.png" /></p>
<p align="center"> Fig. 线性对数阶的时间复杂度 </p>
<h3 id="on_1">阶乘阶 <span class="arithmatex">\(O(n!)\)</span><a class="headerlink" href="#on_1" title="Permanent link">&para;</a></h3>
<p>阶乘阶对应数学上的「全排列」。即给定 <span class="arithmatex">\(n\)</span> 个互不重复的元素,求其所有可能的排列方案,则方案数量为</p>
<div class="arithmatex">\[
@ -3882,6 +3896,8 @@ n! = n \times (n - 1) \times (n - 2) \times \cdots \times 2 \times 1
</div>
</div>
<p><img alt="阶乘阶的时间复杂度" src="../time_complexity.assets/time_complexity_factorial.png" /></p>
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
<h2 id="226">2.2.6. &nbsp; 最差、最佳、平均时间复杂度<a class="headerlink" href="#226" title="Permanent link">&para;</a></h2>
<p><strong>某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关</strong>。举一个例子,输入一个长度为 <span class="arithmatex">\(n\)</span> 数组 <code>nums</code> ,其中 <code>nums</code> 由从 <span class="arithmatex">\(1\)</span><span class="arithmatex">\(n\)</span> 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 <span class="arithmatex">\(1\)</span> 的索引。我们可以得出以下结论:</p>
<ul>

View file

@ -1581,6 +1581,8 @@
<li><strong>非线性数据结构</strong>:树、图、堆、哈希表;</li>
</ul>
<p><img alt="线性与非线性数据结构" src="../classification_of_data_structure.assets/classification_logic_structure.png" /></p>
<p align="center"> Fig. 线性与非线性数据结构 </p>
<h2 id="322">3.2.2. &nbsp; 物理结构:连续与离散<a class="headerlink" href="#322" title="Permanent link">&para;</a></h2>
<div class="admonition note">
<p class="admonition-title">Note</p>
@ -1588,6 +1590,8 @@
</div>
<p><strong>「物理结构」反映了数据在计算机内存中的存储方式</strong>。从本质上看,分别是 <strong>数组的连续空间存储</strong><strong>链表的离散空间存储</strong>。物理结构从底层上决定了数据的访问、更新、增删等操作方法,在时间效率和空间效率方面呈现出此消彼长的特性。</p>
<p><img alt="连续空间存储与离散空间存储" src="../classification_of_data_structure.assets/classification_phisical_structure.png" /></p>
<p align="center"> Fig. 连续空间存储与离散空间存储 </p>
<p><strong>所有数据结构都是基于数组、或链表、或两者组合实现的</strong>。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。</p>
<ul>
<li><strong>基于数组可实现</strong>:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 <span class="arithmatex">\(\geq 3\)</span> 的数组)等;</li>

View file

@ -1747,6 +1747,8 @@
\end{aligned}
\]</div>
<p><img alt="IEEE 754 标准下的 float 表示方式" src="../data_and_memory.assets/ieee_754_float.png" /></p>
<p align="center"> Fig. IEEE 754 标准下的 float 表示方式 </p>
<p>以上图为例,<span class="arithmatex">\(\mathrm{S} = 0\)</span> <span class="arithmatex">\(\mathrm{E} = 124\)</span> <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> ,易得</p>
<div class="arithmatex">\[
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
@ -1871,6 +1873,8 @@
<p><strong>算法运行中,相关数据都被存储在内存中</strong>。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储 1 byte 的数据,在算法运行时,所有数据都被存储在这些单元格中。</p>
<p><strong>系统通过「内存地址 Memory Location」来访问目标内存位置的数据</strong>。计算机根据特定规则给表格中每个单元格编号,保证每块内存空间都有独立的内存地址。自此,程序便通过这些地址,访问内存中的数据。</p>
<p><img alt="内存条、内存空间、内存地址" src="../data_and_memory.assets/computer_memory_location.png" /></p>
<p align="center"> Fig. 内存条、内存空间、内存地址 </p>
<p><strong>内存资源是设计数据结构与算法的重要考虑因素</strong>。内存是所有程序的公共资源,当内存被某程序占用时,不能被其它程序同时使用。我们需要根据剩余内存资源的情况来设计算法。例如,若剩余内存空间有限,则要求算法占用的峰值内存不能超过系统剩余内存;若运行的程序很多、缺少大块连续的内存空间,则要求选取的数据结构必须能够存储在离散的内存空间内。</p>

View file

@ -1649,6 +1649,8 @@ G &amp; = \{ V, E \} \newline
\end{aligned}
\]</div>
<p><img alt="链表、树、图之间的关系" src="../graph.assets/linkedlist_tree_graph.png" /></p>
<p align="center"> Fig. 链表、树、图之间的关系 </p>
<p>那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。<strong>相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂</strong></p>
<h2 id="911">9.1.1. &nbsp; 图常见类型<a class="headerlink" href="#911" title="Permanent link">&para;</a></h2>
<p>根据边是否有方向,分为「无向图 Undirected Graph」和「有向图 Directed Graph」。</p>
@ -1657,14 +1659,20 @@ G &amp; = \{ V, E \} \newline
<li>在有向图中,边是有方向的,即 <span class="arithmatex">\(A \rightarrow B\)</span><span class="arithmatex">\(A \leftarrow B\)</span> 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系;</li>
</ul>
<p><img alt="有向图与无向图" src="../graph.assets/directed_graph.png" /></p>
<p align="center"> Fig. 有向图与无向图 </p>
<p>根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。</p>
<ul>
<li>对于连通图,从某个顶点出发,可以到达其余任意顶点;</li>
<li>对于非连通图,从某个顶点出发,至少有一个顶点无法到达;</li>
</ul>
<p><img alt="连通图与非连通图" src="../graph.assets/connected_graph.png" /></p>
<p align="center"> Fig. 连通图与非连通图 </p>
<p>我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如在王者荣耀等游戏中系统会根据共同游戏时间来计算玩家之间的“亲密度”这种亲密度网络就可以使用有权图来表示。</p>
<p><img alt="有权图与无权图" src="../graph.assets/weighted_graph.png" /></p>
<p align="center"> Fig. 有权图与无权图 </p>
<h2 id="912">9.1.2. &nbsp; 图常用术语<a class="headerlink" href="#912" title="Permanent link">&para;</a></h2>
<ul>
<li>「邻接 Adjacency」当两顶点之间有边相连时称此两顶点“邻接”。</li>
@ -1677,6 +1685,8 @@ G &amp; = \{ V, E \} \newline
<p>设图的顶点数量为 <span class="arithmatex">\(n\)</span> ,「邻接矩阵 Adjacency Matrix」使用一个 <span class="arithmatex">\(n \times n\)</span> 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,使用 <span class="arithmatex">\(1\)</span><span class="arithmatex">\(0\)</span> 来表示两个顶点之间有边或无边。</p>
<p>如下图所示,记邻接矩阵为 <span class="arithmatex">\(M\)</span> 、顶点列表为 <span class="arithmatex">\(V\)</span> ,则矩阵元素 <span class="arithmatex">\(M[i][j] = 1\)</span> 代表着顶点 <span class="arithmatex">\(V[i]\)</span> 到顶点 <span class="arithmatex">\(V[j]\)</span> 之间有边,相反地 <span class="arithmatex">\(M[i][j] = 0\)</span> 代表两顶点之间无边。</p>
<p><img alt="图的邻接矩阵表示" src="../graph.assets/adjacency_matrix.png" /></p>
<p align="center"> Fig. 图的邻接矩阵表示 </p>
<p>邻接矩阵具有以下性质:</p>
<ul>
<li>顶点不能与自身相连,因而邻接矩阵主对角线元素没有意义。</li>
@ -1687,6 +1697,8 @@ G &amp; = \{ V, E \} \newline
<h3 id="_2">邻接表<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<p>「邻接表 Adjacency List」使用 <span class="arithmatex">\(n\)</span> 个链表来表示图,链表结点表示顶点。第 <span class="arithmatex">\(i\)</span> 条链表对应顶点 <span class="arithmatex">\(i\)</span> ,其中存储了所有与该顶点相连的顶点。</p>
<p><img alt="图的邻接表表示" src="../graph.assets/adjacency_list.png" /></p>
<p align="center"> Fig. 图的邻接表表示 </p>
<p>邻接表仅存储存在的边,而边的总数往往远小于 <span class="arithmatex">\(n^2\)</span> ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。</p>
<p>观察上图发现,<strong>邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率</strong>。比如当链表较长时可以把链表转化为「AVL 树」,从而将时间效率从 <span class="arithmatex">\(O(n)\)</span> 优化至 <span class="arithmatex">\(O(\log n)\)</span> ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet即哈希表将时间复杂度降低至 <span class="arithmatex">\(O(1)\)</span></p>
<h2 id="914">9.1.4. &nbsp; 图常见应用<a class="headerlink" href="#914" title="Permanent link">&para;</a></h2>

View file

@ -1661,6 +1661,8 @@
<h2 id="931">9.3.1. &nbsp; 广度优先遍历<a class="headerlink" href="#931" title="Permanent link">&para;</a></h2>
<p><strong>广度优先遍历优是一种由近及远的遍历方式,从距离最近的顶点开始访问,并一层层向外扩张</strong>。具体地,从某个顶点出发,先遍历该顶点的所有邻接顶点,随后遍历下个顶点的所有邻接顶点,以此类推……</p>
<p><img alt="图的广度优先遍历" src="../graph_traversal.assets/graph_bfs.png" /></p>
<p align="center"> Fig. 图的广度优先遍历 </p>
<h3 id="_1">算法实现<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>BFS 常借助「队列」来实现。队列具有“先入先出”的性质,这与 BFS “由近及远”的思想是异曲同工的。</p>
<ol>
@ -1878,6 +1880,8 @@
<h2 id="932">9.3.2. &nbsp; 深度优先遍历<a class="headerlink" href="#932" title="Permanent link">&para;</a></h2>
<p><strong>深度优先遍历是一种优先走到底、无路可走再回头的遍历方式</strong>。具体地,从某个顶点出发,不断地访问当前结点的某个邻接顶点,直到走到尽头时回溯,再继续走到底 + 回溯,以此类推……直至所有顶点遍历完成时结束。</p>
<p><img alt="图的深度优先遍历" src="../graph_traversal.assets/graph_dfs.png" /></p>
<p align="center"> Fig. 图的深度优先遍历 </p>
<h3 id="_3">算法实现<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<p>这种“走到头 + 回溯”的算法形式一般基于递归来实现。与 BFS 类似,在 DFS 中我们也需要借助一个哈希表 <code>visited</code> 来记录已被访问的顶点,以避免重复访问顶点。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>

View file

@ -1637,6 +1637,8 @@
<h2 id="622">6.2.2. &nbsp; 链式地址<a class="headerlink" href="#622" title="Permanent link">&para;</a></h2>
<p>在原始哈希表中,桶内的每个地址只能存储一个元素(即键值对)。<strong>考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中</strong></p>
<p><img alt="链式地址" src="../hash_collision.assets/hash_collision_chaining.png" /></p>
<p align="center"> Fig. 链式地址 </p>
<p>链式地址下,哈希表操作方法为:</p>
<ul>
<li><strong>查询元素</strong>:先将 key 输入到哈希函数得到桶内索引,即可访问链表头结点,再通过遍历链表查找对应 value 。</li>
@ -1660,6 +1662,8 @@
<li>若遇到空位,则说明查找键值对不在哈希表中;</li>
</ol>
<p><img alt="线性探测" src="../hash_collision.assets/hash_collision_linear_probing.png" /></p>
<p align="center"> Fig. 线性探测 </p>
<p>线性探测存在以下缺陷:</p>
<ul>
<li><strong>不能直接删除元素</strong>。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 <code>2.</code> 种情况)。因此需要借助一个标志位来标记删除元素。</li>

View file

@ -1603,6 +1603,8 @@
<p>哈希表通过建立「键 key」和「值 value」之间的映射实现高效的元素查找。具体地输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 <span class="arithmatex">\(O(1)\)</span></p>
<p>例如,给定一个包含 <span class="arithmatex">\(n\)</span> 个学生的数据库,每个学生有“姓名 <code>name</code> ”和“学号 <code>id</code> ”两项数据,希望实现一个查询功能:<strong>输入一个学号,返回对应的姓名</strong>,则可以使用哈希表实现。</p>
<p><img alt="哈希表的抽象表示" src="../hash_map.assets/hash_map.png" /></p>
<p align="center"> Fig. 哈希表的抽象表示 </p>
<h2 id="611">6.1.1. &nbsp; 哈希表效率<a class="headerlink" href="#611" title="Permanent link">&para;</a></h2>
<p>除了哈希表之外,还可以使用以下数据结构来实现上述查询功能:</p>
<ol>
@ -1988,6 +1990,8 @@
f(x) = x \% 100
\]</div>
<p><img alt="简单哈希函数示例" src="../hash_map.assets/hash_function.png" /></p>
<p align="center"> Fig. 简单哈希函数示例 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -2832,6 +2836,8 @@ f(12836) = f(20336) = 36
\]</div>
<p>两个学号指向了同一个姓名,这明显是不对的,我们将这种现象称为「哈希冲突 Hash Collision」。如何避免哈希冲突的问题将被留在下章讨论。</p>
<p><img alt="哈希冲突示例" src="../hash_map.assets/hash_collision.png" /></p>
<p align="center"> Fig. 哈希冲突示例 </p>
<p>综上所述,一个优秀的「哈希函数」应该具备以下特性:</p>
<ul>
<li>尽量少地发生哈希冲突;</li>

View file

@ -1688,6 +1688,8 @@
<li>「小顶堆 Min Heap」任意结点的值 <span class="arithmatex">\(\leq\)</span> 其子结点的值;</li>
</ul>
<p><img alt="小顶堆与大顶堆" src="../heap.assets/min_heap_and_max_heap.png" /></p>
<p align="center"> Fig. 小顶堆与大顶堆 </p>
<h2 id="811">8.1.1. &nbsp; 堆术语与性质<a class="headerlink" href="#811" title="Permanent link">&para;</a></h2>
<ul>
<li>由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。</li>
@ -1993,6 +1995,8 @@
<p><strong>二叉树指针</strong>。使用数组表示二叉树时,元素代表结点值,索引代表结点在二叉树中的位置,<strong>而结点指针通过索引映射公式来实现</strong></p>
<p>具体地,给定索引 <span class="arithmatex">\(i\)</span> ,那么其左子结点索引为 <span class="arithmatex">\(2i + 1\)</span> 、右子结点索引为 <span class="arithmatex">\(2i + 2\)</span> 、父结点索引为 <span class="arithmatex">\((i - 1) / 2\)</span> (向下整除)。当索引越界时,代表空结点或结点不存在。</p>
<p><img alt="堆的表示与存储" src="../heap.assets/representation_of_heap.png" /></p>
<p align="center"> Fig. 堆的表示与存储 </p>
<p>我们将索引映射公式封装成函数,以便后续使用。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:10"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JavaScript</label><label for="__tabbed_2_6">TypeScript</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label></div>
<div class="tabbed-content">
@ -3013,6 +3017,8 @@
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1
\]</div>
<p><img alt="完美二叉树的各层结点数量" src="../heap.assets/heapify_operations_count.png" /></p>
<p align="center"> Fig. 完美二叉树的各层结点数量 </p>
<p>化简上式需要借助中学的数列知识,先对 <span class="arithmatex">\(T(h)\)</span> 乘以 <span class="arithmatex">\(2\)</span> ,易得</p>
<div class="arithmatex">\[
\begin{aligned}

View file

@ -1610,6 +1610,8 @@
<li>算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。</li>
</ul>
<p><img alt="数据结构与算法的关系" src="../what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png" /></p>
<p align="center"> Fig. 数据结构与算法的关系 </p>
<p>如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。</p>
<div class="center-table">
<table>

View file

@ -1694,6 +1694,8 @@
<h2 id="012">0.1.2. &nbsp; 内容结构<a class="headerlink" href="#012" title="Permanent link">&para;</a></h2>
<p>本书主要内容分为复杂度分析、数据结构、算法三个部分。</p>
<p><img alt="Hello 算法内容结构" src="../about_the_book.assets/hello_algo_mindmap.png" /></p>
<p align="center"> Fig. Hello 算法内容结构 </p>
<h3 id="_1">复杂度分析<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>首先介绍数据结构与算法的评价维度、算法效率的评估方法,引出了计算复杂度概念。</p>
<p>接下来,从 <strong>函数渐近上界</strong> 入手,分别介绍了 <strong>时间复杂度</strong><strong>空间复杂度</strong>,包括推算方法、常见类型、示例等。同时,剖析了 <strong>最差、最佳、平均</strong> 时间复杂度的联系与区别。</p>
@ -1727,6 +1729,8 @@
</ul>
<p>根据观察,很多同学都是从“第二阶段”开始学习算法的。而作为入门教程,<strong>本书内容主要对应“第一阶段”</strong>,致力于帮助读者更高效地开展第二、三阶段的学习。</p>
<p><img alt="算法学习路线" src="../suggestions.assets/learning_route.png" /></p>
<p align="center"> Fig. 算法学习路线 </p>
<h2 id="014">0.1.4. &nbsp; 本书特点<a class="headerlink" href="#014" title="Permanent link">&para;</a></h2>
<p><strong>以实践为主</strong>。我们知道,学习英语期间光啃书本是远远不够的,需要多听、多说、多写,在实践中培养语感、积累经验。编程语言也是一门语言,因此学习方法也应是类似的,需要多看优秀代码、多敲键盘、多思考代码逻辑。</p>
<p>本书的理论部分占少量篇幅,主要分为两类:一是基础且必要的概念知识,以培养读者对于算法的感性认识;二是重要的分类、对比或总结,这是为了帮助你站在更高视角俯瞰各个知识点,形成连点成面的效果。</p>

View file

@ -1599,6 +1599,8 @@
<li>在页面底部填写更改说明然后单击“Propose file change”按钮页面跳转后点击“Create pull request”按钮发起拉取请求即可。</li>
</ol>
<p><img alt="页面编辑按键" src="../contribution.assets/edit_markdown.png" /></p>
<p align="center"> Fig. 页面编辑按键 </p>
<p>图片无法直接修改,需要通过新建 <a href="https://github.com/krahets/hello-algo/issues">Issue</a> 或评论留言来描述图片问题,我会第一时间重新画图并替换图片。</p>
<h2 id="042">0.4.2. &nbsp; 内容创作<a class="headerlink" href="#042" title="Permanent link">&para;</a></h2>
<p>如果您想要参与本开源项目,包括翻译代码至其他编程语言、拓展文章内容等,那么需要实施 Pull Request 工作流程:</p>

View file

@ -1777,6 +1777,8 @@
<p>视频和图片相比于文字的信息密度和结构化程度更高,更容易理解。在本书中,<strong>知识重难点会主要以动画、图解的形式呈现</strong>,而文字的作用则是作为动画和图的解释与补充。</p>
<p>阅读本书时,若发现某段内容提供了动画或图解,<strong>建议你以图为主线</strong>,将文字内容(一般在图的上方)对齐到图中内容,综合来理解。</p>
<p><img alt="动画图解示例" src="../suggestions.assets/animation.gif" /></p>
<p align="center"> Fig. 动画图解示例 </p>
<h2 id="023">0.2.3. &nbsp; 在代码实践中加深理解<a class="headerlink" href="#023" title="Permanent link">&para;</a></h2>
<p>本书的配套代码托管在<a href="https://github.com/krahets/hello-algo">GitHub 仓库</a><strong>源代码包含详细注释,配有测试样例,可以直接运行</strong></p>
<ul>
@ -1791,15 +1793,22 @@
</code></pre></div>
<p>当然你也可以点击“Download ZIP”直接下载代码压缩包解压即可。</p>
<p><img alt="克隆仓库与下载代码" src="../suggestions.assets/download_code.png" /></p>
<p align="center"> Fig. 克隆仓库与下载代码 </p>
<h3 id="3">3) 运行源代码<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>若代码块的顶部标有文件名称,则可在仓库 <code>codes</code> 文件夹中找到对应的 <strong>源代码文件</strong></p>
<p><img alt="代码块与对应的源代码文件" src="../suggestions.assets/code_md_to_repo.png" /></p>
<p align="center"> Fig. 代码块与对应的源代码文件 </p>
<p>源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上。</p>
<p><img alt="运行代码示例" src="../suggestions.assets/running_code.gif" /></p>
<p align="center"> Fig. 运行代码示例 </p>
<h2 id="024">0.2.4. &nbsp; 在提问讨论中共同成长<a class="headerlink" href="#024" title="Permanent link">&para;</a></h2>
<p>阅读本书时,请不要“惯着”那些弄不明白的知识点。<strong>欢迎在评论区留下你的问题</strong>,小伙伴们和我都会给予解答,您一般 2 日内会得到回复。</p>
<p>同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步!</p>
<p><img alt="评论区示例" src="../suggestions.assets/comment.gif" /></p>
<p align="center"> Fig. 评论区示例 </p>

View file

@ -1594,6 +1594,8 @@
<h2 id="1031">10.3.1. &nbsp; 算法实现<a class="headerlink" href="#1031" title="Permanent link">&para;</a></h2>
<p>如果我们想要给定数组中的一个目标元素 <code>target</code> ,获取该元素的索引,那么可以借助一个哈希表实现查找。</p>
<p><img alt="哈希查找数组索引" src="../hashing_search.assets/hash_search_index.png" /></p>
<p align="center"> Fig. 哈希查找数组索引 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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">JavaScript</label><label for="__tabbed_1_6">TypeScript</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></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -1692,6 +1694,8 @@
</div>
<p>再比如,如果我们想要给定一个目标结点值 <code>target</code> ,获取对应的链表结点对象,那么也可以使用哈希查找实现。</p>
<p><img alt="哈希查找链表结点" src="../hashing_search.assets/hash_search_listnode.png" /></p>
<p align="center"> Fig. 哈希查找链表结点 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:10"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JavaScript</label><label for="__tabbed_2_6">TypeScript</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">

View file

@ -1590,6 +1590,8 @@
<h2 id="1011">10.1.1. &nbsp; 算法实现<a class="headerlink" href="#1011" title="Permanent link">&para;</a></h2>
<p>线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组 <code>nums</code> 中查找目标元素 <code>target</code> 的对应索引,那么可以在数组中进行线性查找。</p>
<p><img alt="在数组中线性查找元素" src="../linear_search.assets/linear_search.png" /></p>
<p align="center"> Fig. 在数组中线性查找元素 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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">JavaScript</label><label for="__tabbed_1_6">TypeScript</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></div>
<div class="tabbed-content">
<div class="tabbed-block">

View file

@ -1625,6 +1625,8 @@
<li>以此类推…… <strong>循环 <span class="arithmatex">\(n - 1\)</span> 轮「冒泡」,即可完成整个数组的排序</strong></li>
</ol>
<p><img alt="冒泡排序流程" src="../bubble_sort.assets/bubble_sort_overview.png" /></p>
<p align="center"> Fig. 冒泡排序流程 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:10"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JavaScript</label><label for="__tabbed_2_6">TypeScript</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">

View file

@ -1590,6 +1590,8 @@
<p>「插入操作」原理:选定某个待排序元素为基准数 <code>base</code>,将 <code>base</code> 与其左侧已排序区间元素依次对比大小,并插入到正确位置。</p>
<p>回忆数组插入操作,我们需要将从目标索引到 <code>base</code> 之间的所有元素向右移动一位,然后再将 <code>base</code> 赋值给目标索引。</p>
<p><img alt="单次插入操作" src="../insertion_sort.assets/insertion_operation.png" /></p>
<p align="center"> Fig. 单次插入操作 </p>
<h2 id="1131">11.3.1. &nbsp; 算法流程<a class="headerlink" href="#1131" title="Permanent link">&para;</a></h2>
<ol>
<li>第 1 轮先选取数组的 <strong>第 2 个元素</strong><code>base</code> ,执行「插入操作」后,<strong>数组前 2 个元素已完成排序</strong></li>
@ -1597,6 +1599,8 @@
<li>以此类推……最后一轮选取 <strong>数组尾元素</strong><code>base</code> ,执行「插入操作」后,<strong>所有元素已完成排序</strong></li>
</ol>
<p><img alt="插入排序流程" src="../insertion_sort.assets/insertion_sort_overview.png" /></p>
<p align="center"> Fig. 插入排序流程 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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">JavaScript</label><label for="__tabbed_1_6">TypeScript</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></div>
<div class="tabbed-content">
<div class="tabbed-block">

View file

@ -1646,6 +1646,8 @@
<li>排序算法可以根据需要设定 <strong>判断规则</strong>,例如数字大小、字符 ASCII 码顺序、自定义规则;</li>
</ul>
<p><img alt="排序中不同的元素类型和判断规则" src="../intro_to_sort.assets/sorting_examples.png" /></p>
<p align="center"> Fig. 排序中不同的元素类型和判断规则 </p>
<h2 id="1111">11.1.1. &nbsp; 评价维度<a class="headerlink" href="#1111" title="Permanent link">&para;</a></h2>
<p>排序算法主要可根据 <strong>稳定性 、就地性 、自适应性 、比较类</strong> 来分类。</p>
<h3 id="_1">稳定性<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>

View file

@ -1592,6 +1592,8 @@
<li><strong>合并阶段</strong>:划分到子数组长度为 1 时,开始向上合并,不断将 <strong>左、右两个短排序数组</strong> 合并为 <strong>一个长排序数组</strong>,直至合并至原数组时完成排序;</li>
</ol>
<p><img alt="归并排序的划分与合并阶段" src="../merge_sort.assets/merge_sort_overview.png" /></p>
<p align="center"> Fig. 归并排序的划分与合并阶段 </p>
<h2 id="1151">11.5.1. &nbsp; 算法流程<a class="headerlink" href="#1151" title="Permanent link">&para;</a></h2>
<p><strong>「递归划分」</strong> 从顶至底递归地 <strong>将数组从中点切为两个子数组</strong>,直至长度为 1 </p>
<ol>

View file

@ -1886,6 +1886,8 @@
</ol>
<p>观察发现,快速排序和「二分查找」的原理类似,都是以对数阶的时间复杂度来缩小处理区间。</p>
<p><img alt="快速排序流程" src="../quick_sort.assets/quick_sort_overview.png" /></p>
<p align="center"> Fig. 快速排序流程 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:10"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JavaScript</label><label for="__tabbed_2_6">TypeScript</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">

View file

@ -1614,6 +1614,8 @@
<h1 id="53">5.3. &nbsp; 双向队列<a class="headerlink" href="#53" title="Permanent link">&para;</a></h1>
<p>对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活在其头部和尾部都能执行元素添加或删除操作。</p>
<p><img alt="双向队列的操作" src="../deque.assets/deque_operations.png" /></p>
<p align="center"> Fig. 双向队列的操作 </p>
<h2 id="531">5.3.1. &nbsp; 双向队列常用操作<a class="headerlink" href="#531" title="Permanent link">&para;</a></h2>
<p>双向队列的常用操作见下表,方法名需根据特定语言来确定。</p>
<div class="center-table">

View file

@ -1643,6 +1643,8 @@
<p>「队列 Queue」是一种遵循「先入先出 first in, first out」数据操作规则的线性数据结构。顾名思义队列模拟的是排队现象即外面的人不断加入队列尾部而处于队列头部的人不断地离开。</p>
<p>我们将队列头部称为「队首」,队列尾部称为「队尾」,将把元素加入队尾的操作称为「入队」,删除队首元素的操作称为「出队」。</p>
<p><img alt="队列的先入先出规则" src="../queue.assets/queue_operations.png" /></p>
<p align="center"> Fig. 队列的先入先出规则 </p>
<h2 id="521">5.2.1. &nbsp; 队列常用操作<a class="headerlink" href="#521" title="Permanent link">&para;</a></h2>
<p>队列的常用操作见下表,方法名需根据特定语言来确定。</p>
<div class="center-table">

View file

@ -1698,6 +1698,8 @@
<p>“盘子”是一种形象比喻,我们将盘子替换为任意一种元素(例如整数、字符、对象等),就得到了栈数据结构。</p>
<p>我们将这一摞元素的顶部称为「栈顶」,将底部称为「栈底」,将把元素添加到栈顶的操作称为「入栈」,将删除栈顶元素的操作称为「出栈」。</p>
<p><img alt="栈的先入后出规则" src="../stack.assets/stack_operations.png" /></p>
<p align="center"> Fig. 栈的先入后出规则 </p>
<h2 id="511">5.1.1. &nbsp; 栈常用操作<a class="headerlink" href="#511" title="Permanent link">&para;</a></h2>
<p>栈的常用操作见下表(方法命名以 Java 为例)。</p>
<div class="center-table">

View file

@ -1779,8 +1779,12 @@
<p>在「二叉搜索树」章节中提到,在进行多次插入与删除操作后,二叉搜索树可能会退化为链表。此时所有操作的时间复杂度都会由 <span class="arithmatex">\(O(\log n)\)</span> 劣化至 <span class="arithmatex">\(O(n)\)</span></p>
<p>如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。</p>
<p><img alt="AVL 树在删除结点后发生退化" src="../avl_tree.assets/avltree_degradation_from_removing_node.png" /></p>
<p align="center"> Fig. AVL 树在删除结点后发生退化 </p>
<p>再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。</p>
<p><img alt="AVL 树在插入结点后发生退化" src="../avl_tree.assets/avltree_degradation_from_inserting_node.png" /></p>
<p align="center"> Fig. AVL 树在插入结点后发生退化 </p>
<p>G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。<strong>论文中描述了一系列操作使得在不断添加与删除结点后AVL 树仍然不会发生退化</strong>,进而使得各种操作的时间复杂度均能保持在 <span class="arithmatex">\(O(\log n)\)</span> 级别。</p>
<p>换言之在频繁增删查改的使用场景中AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。</p>
<h2 id="741-avl">7.4.1. &nbsp; AVL 树常见术语<a class="headerlink" href="#741-avl" title="Permanent link">&para;</a></h2>
@ -2177,6 +2181,8 @@
</div>
<p>进而,如果结点 <code>child</code> 本身有右子结点(记为 <code>grandChild</code> ),则需要在「右旋」中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的左子结点。</p>
<p><img alt="有 grandChild 的右旋操作" src="../avl_tree.assets/avltree_right_rotate_with_grandchild.png" /></p>
<p align="center"> Fig. 有 grandChild 的右旋操作 </p>
<p>“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:10"><input checked="checked" id="__tabbed_5_1" name="__tabbed_5" type="radio" /><input id="__tabbed_5_2" name="__tabbed_5" type="radio" /><input id="__tabbed_5_3" name="__tabbed_5" type="radio" /><input id="__tabbed_5_4" name="__tabbed_5" type="radio" /><input id="__tabbed_5_5" name="__tabbed_5" type="radio" /><input id="__tabbed_5_6" name="__tabbed_5" type="radio" /><input id="__tabbed_5_7" name="__tabbed_5" type="radio" /><input id="__tabbed_5_8" name="__tabbed_5" type="radio" /><input id="__tabbed_5_9" name="__tabbed_5" type="radio" /><input id="__tabbed_5_10" name="__tabbed_5" type="radio" /><div class="tabbed-labels"><label for="__tabbed_5_1">Java</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Python</label><label for="__tabbed_5_4">Go</label><label for="__tabbed_5_5">JavaScript</label><label for="__tabbed_5_6">TypeScript</label><label for="__tabbed_5_7">C</label><label for="__tabbed_5_8">C#</label><label for="__tabbed_5_9">Swift</label><label for="__tabbed_5_10">Zig</label></div>
<div class="tabbed-content">
@ -2333,8 +2339,12 @@
<h3 id="case-2-">Case 2 - 左旋<a class="headerlink" href="#case-2-" title="Permanent link">&para;</a></h3>
<p>类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。</p>
<p><img alt="左旋操作" src="../avl_tree.assets/avltree_left_rotate.png" /></p>
<p align="center"> Fig. 左旋操作 </p>
<p>同理,若结点 <code>child</code> 本身有左子结点(记为 <code>grandChild</code> ),则需要在「左旋」中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的右子结点。</p>
<p><img alt="有 grandChild 的左旋操作" src="../avl_tree.assets/avltree_left_rotate_with_grandchild.png" /></p>
<p align="center"> Fig. 有 grandChild 的左旋操作 </p>
<p>观察发现,<strong>「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的</strong>。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 <code>left</code> 替换为 <code>right</code> 、所有的 <code>right</code> 替换为 <code>left</code> ,即可得到「左旋」代码。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="6:10"><input checked="checked" id="__tabbed_6_1" name="__tabbed_6" type="radio" /><input id="__tabbed_6_2" name="__tabbed_6" type="radio" /><input id="__tabbed_6_3" name="__tabbed_6" type="radio" /><input id="__tabbed_6_4" name="__tabbed_6" type="radio" /><input id="__tabbed_6_5" name="__tabbed_6" type="radio" /><input id="__tabbed_6_6" name="__tabbed_6" type="radio" /><input id="__tabbed_6_7" name="__tabbed_6" type="radio" /><input id="__tabbed_6_8" name="__tabbed_6" type="radio" /><input id="__tabbed_6_9" name="__tabbed_6" type="radio" /><input id="__tabbed_6_10" name="__tabbed_6" type="radio" /><div class="tabbed-labels"><label for="__tabbed_6_1">Java</label><label for="__tabbed_6_2">C++</label><label for="__tabbed_6_3">Python</label><label for="__tabbed_6_4">Go</label><label for="__tabbed_6_5">JavaScript</label><label for="__tabbed_6_6">TypeScript</label><label for="__tabbed_6_7">C</label><label for="__tabbed_6_8">C#</label><label for="__tabbed_6_9">Swift</label><label for="__tabbed_6_10">Zig</label></div>
<div class="tabbed-content">
@ -2491,12 +2501,18 @@
<h3 id="case-3-">Case 3 - 先左后右<a class="headerlink" href="#case-3-" title="Permanent link">&para;</a></h3>
<p>对于下图的失衡结点 3 <strong>单一使用左旋或右旋都无法使子树恢复平衡</strong>,此时需要「先左旋后右旋」,即先对 <code>child</code> 执行「左旋」,再对 <code>node</code> 执行「右旋」。</p>
<p><img alt="先左旋后右旋" src="../avl_tree.assets/avltree_left_right_rotate.png" /></p>
<p align="center"> Fig. 先左旋后右旋 </p>
<h3 id="case-4-">Case 4 - 先右后左<a class="headerlink" href="#case-4-" title="Permanent link">&para;</a></h3>
<p>同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 <code>child</code> 执行「右旋」,然后对 <code>node</code> 执行「左旋」。</p>
<p><img alt="先右旋后左旋" src="../avl_tree.assets/avltree_right_left_rotate.png" /></p>
<p align="center"> Fig. 先右旋后左旋 </p>
<h3 id="_3">旋转的选择<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<p>下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 <strong>右旋、左旋、先右后左、先左后右</strong> 的旋转操作。</p>
<p><img alt="AVL 树的四种旋转情况" src="../avl_tree.assets/avltree_rotation_cases.png" /></p>
<p align="center"> Fig. AVL 树的四种旋转情况 </p>
<p>具体地,在代码中使用 <strong>失衡结点的平衡因子、较高一侧子结点的平衡因子</strong> 来确定失衡结点属于上图中的哪种情况。</p>
<div class="center-table">
<table>

View file

@ -1674,6 +1674,8 @@
<li>任意结点的左子树和右子树也是二叉搜索树,即也满足条件 <code>1.</code> </li>
</ol>
<p><img alt="二叉搜索树" src="../binary_search_tree.assets/binary_search_tree.png" /></p>
<p align="center"> Fig. 二叉搜索树 </p>
<h2 id="731">7.3.1. &nbsp; 二叉搜索树的操作<a class="headerlink" href="#731" title="Permanent link">&para;</a></h2>
<h3 id="_1">查找结点<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>给定目标结点值 <code>num</code> ,可以根据二叉搜索树的性质来查找。我们声明一个结点 <code>cur</code> ,从二叉树的根结点 <code>root</code> 出发,循环比较结点值 <code>cur.val</code><code>num</code> 之间的大小关系</p>
@ -1894,6 +1896,8 @@
</ol>
<p>二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。</p>
<p><img alt="在二叉搜索树中插入结点" src="../binary_search_tree.assets/bst_insert.png" /></p>
<p align="center"> Fig. 在二叉搜索树中插入结点 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -2174,8 +2178,12 @@
<p>与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 &lt; 根结点 &lt; 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况:</p>
<p><strong>当待删除结点的子结点数量 <span class="arithmatex">\(= 0\)</span></strong>,表明待删除结点是叶结点,直接删除即可。</p>
<p><img alt="在二叉搜索树中删除结点(度为 0" src="../binary_search_tree.assets/bst_remove_case1.png" /></p>
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 0 </p>
<p><strong>当待删除结点的子结点数量 <span class="arithmatex">\(= 1\)</span></strong>,将待删除结点替换为其子结点即可。</p>
<p><img alt="在二叉搜索树中删除结点(度为 1" src="../binary_search_tree.assets/bst_remove_case2.png" /></p>
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 1 </p>
<p><strong>当待删除结点的子结点数量 <span class="arithmatex">\(= 2\)</span></strong>,删除操作分为三步:</p>
<ol>
<li>找到待删除结点在 <strong>中序遍历序列</strong> 中的下一个结点,记为 <code>nex</code> </li>
@ -2741,6 +2749,8 @@
<p>我们知道,「中序遍历」遵循“左 <span class="arithmatex">\(\rightarrow\)</span><span class="arithmatex">\(\rightarrow\)</span> 右”的遍历优先级,而二叉搜索树遵循“左子结点 <span class="arithmatex">\(&lt;\)</span> 根结点 <span class="arithmatex">\(&lt;\)</span> 右子结点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小结点,从而得出一条重要性质:<strong>二叉搜索树的中序遍历序列是升序的</strong></p>
<p>借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 <span class="arithmatex">\(O(n)\)</span> 时间,而无需额外排序,非常高效。</p>
<p><img alt="二叉搜索树的中序遍历序列" src="../binary_search_tree.assets/bst_inorder_traversal.png" /></p>
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
<h2 id="732">7.3.2. &nbsp; 二叉搜索树的效率<a class="headerlink" href="#732" title="Permanent link">&para;</a></h2>
<p>假设给定 <span class="arithmatex">\(n\)</span> 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:</p>
<ul>
@ -2803,6 +2813,8 @@
<p>在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。</p>
</div>
<p><img alt="二叉搜索树的平衡与退化" src="../binary_search_tree.assets/bst_degradation.png" /></p>
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
<h2 id="734">7.3.4. &nbsp; 二叉搜索树常见应用<a class="headerlink" href="#734" title="Permanent link">&para;</a></h2>
<ul>
<li>系统中的多级索引,高效查找、插入、删除操作。</li>

View file

@ -1791,6 +1791,8 @@
<p>结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点将左子结点以下的树称为该结点的「左子树 Left Subtree」右子树同理。</p>
<p>除了叶结点外,每个结点都有子结点和子树。例如,若将下图的「结点 2」看作父结点那么其左子结点和右子结点分别为「结点 4」和「结点 5」左子树和右子树分别为「结点 4 及其以下结点形成的树」和「结点 5 及其以下结点形成的树」。</p>
<p><img alt="父结点、子结点、子树" src="../binary_tree.assets/binary_tree_definition.png" /></p>
<p align="center"> Fig. 父结点、子结点、子树 </p>
<h2 id="711">7.1.1. &nbsp; 二叉树常见术语<a class="headerlink" href="#711" title="Permanent link">&para;</a></h2>
<p>二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。</p>
<ul>
@ -1804,6 +1806,8 @@
<li>结点「高度 Height」最远叶结点到该结点走过边的数量</li>
</ul>
<p><img alt="二叉树的常用术语" src="../binary_tree.assets/binary_tree_terminology.png" /></p>
<p align="center"> Fig. 二叉树的常用术语 </p>
<div class="admonition tip">
<p class="admonition-title">高度与深度的定义</p>
<p>值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。</p>
@ -1942,6 +1946,8 @@
</div>
<p><strong>插入与删除结点</strong>。与链表类似,插入与删除结点都可以通过修改指针实现。</p>
<p><img alt="在二叉树中插入与删除结点" src="../binary_tree.assets/binary_tree_add_remove.png" /></p>
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@ -2044,16 +2050,24 @@
<p>在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。</p>
</div>
<p><img alt="完美二叉树" src="../binary_tree.assets/perfect_binary_tree.png" /></p>
<p align="center"> Fig. 完美二叉树 </p>
<h3 id="_2">完全二叉树<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<p>「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满且最底层结点尽量靠左填充。</p>
<p><strong>完全二叉树非常适合用数组来表示</strong>。如果按照层序遍历序列的顺序来存储,那么空结点 <code>null</code> 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。</p>
<p><img alt="完全二叉树" src="../binary_tree.assets/complete_binary_tree.png" /></p>
<p align="center"> Fig. 完全二叉树 </p>
<h3 id="_3">完满二叉树<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<p>「完满二叉树 Full Binary Tree」除了叶结点之外其余所有结点都有两个子结点。</p>
<p><img alt="完满二叉树" src="../binary_tree.assets/full_binary_tree.png" /></p>
<p align="center"> Fig. 完满二叉树 </p>
<h3 id="_4">平衡二叉树<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<p>「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 <span class="arithmatex">\(\leq 1\)</span></p>
<p><img alt="平衡二叉树" src="../binary_tree.assets/balanced_binary_tree.png" /></p>
<p align="center"> Fig. 平衡二叉树 </p>
<h2 id="714">7.1.4. &nbsp; 二叉树的退化<a class="headerlink" href="#714" title="Permanent link">&para;</a></h2>
<p>当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。</p>
<ul>
@ -2061,6 +2075,8 @@
<li>链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 <span class="arithmatex">\(O(n)\)</span> </li>
</ul>
<p><img alt="二叉树的最佳与最二叉树的最佳和最差结构差情况" src="../binary_tree.assets/binary_tree_corner_cases.png" /></p>
<p align="center"> Fig. 二叉树的最佳与最二叉树的最佳和最差结构差情况 </p>
<p>如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。</p>
<div class="center-table">
<table>
@ -2100,8 +2116,12 @@
<p>那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将结点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父结点索引与子结点索引之间的「映射公式」:<strong>设结点的索引为 <span class="arithmatex">\(i\)</span> ,则该结点的左子结点索引为 <span class="arithmatex">\(2i + 1\)</span> 、右子结点索引为 <span class="arithmatex">\(2i + 2\)</span></strong></p>
<p><strong>本质上,映射公式的作用就是链表中的指针</strong>。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。</p>
<p><img alt="完美二叉树的数组表示" src="../binary_tree.assets/array_representation_mapping.png" /></p>
<p align="center"> Fig. 完美二叉树的数组表示 </p>
<p>然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 <code>null</code> ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,<strong>即理论上存在许多种二叉树都符合该层序遍历序列</strong>。显然,这种情况无法使用数组来存储二叉树。</p>
<p><img alt="给定数组对应多种二叉树可能性" src="../binary_tree.assets/array_representation_without_empty.png" /></p>
<p align="center"> Fig. 给定数组对应多种二叉树可能性 </p>
<p>为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,<strong>即在序列中使用特殊符号来显式地表示“空位”</strong>。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:10"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JavaScript</label><label for="__tabbed_4_6">TypeScript</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label><label for="__tabbed_4_10">Zig</label></div>
<div class="tabbed-content">
@ -2163,8 +2183,12 @@
</div>
</div>
<p><img alt="任意类型二叉树的数组表示" src="../binary_tree.assets/array_representation_with_empty.png" /></p>
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
<p>回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。<strong>因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”</strong>。因此,完全二叉树非常适合使用数组来表示。</p>
<p><img alt="完全二叉树的数组表示" src="../binary_tree.assets/array_representation_complete_binary_tree.png" /></p>
<p align="center"> Fig. 完全二叉树的数组表示 </p>
<p>数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。</p>

View file

@ -1658,6 +1658,8 @@
<p>「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树并在每层中按照从左到右的顺序访问结点。</p>
<p>层序遍历本质上是「广度优先搜索 Breadth-First Traversal」其体现着一种“一圈一圈向外”的层进遍历方式。</p>
<p><img alt="二叉树的层序遍历" src="../binary_tree_traversal.assets/binary_tree_bfs.png" /></p>
<p align="center"> Fig. 二叉树的层序遍历 </p>
<h3 id="_1">算法实现<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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">JavaScript</label><label for="__tabbed_1_6">TypeScript</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></div>
@ -1873,6 +1875,8 @@
<p>相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」其体现着一种“先走到尽头再回头继续”的回溯遍历方式。</p>
<p>如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。</p>
<p><img alt="二叉搜索树的前、中、后序遍历" src="../binary_tree_traversal.assets/binary_tree_dfs.png" /></p>
<p align="center"> Fig. 二叉搜索树的前、中、后序遍历 </p>
<div class="center-table">
<table>
<thead>

File diff suppressed because one or more lines are too long

Binary file not shown.