This commit is contained in:
krahets
2023-04-07 22:32:13 +08:00
parent c515efea8b
commit 54b72a3ac9
14 changed files with 226 additions and 229 deletions

View File

@@ -1768,15 +1768,11 @@
<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>「数组 Array」是一种线性数据结构,其将相同类型元素存储在连续内存空间中。我们将元素在数组中的位置称为元素的「索引 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>
</div>
<p><strong>数组初始化</strong>。一般会用到无初始值、给定初始值两种写法,可根据需求选取。在不给定初始值的情况下,一般所有元素会被初始化为默认值 <span class="arithmatex">\(0\)</span></p>
<p><strong>数组初始化</strong>。通常有无初始值和给定初始值两种方式,我们可根据需求选择合适的方法。在未给定初始值的情况下,数组的所有元素通常会被初始化为默认值 <span class="arithmatex">\(0\)</span></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">
@@ -1848,15 +1844,19 @@
</div>
</div>
<h2 id="411">4.1.1. &nbsp; 数组优点<a class="headerlink" href="#411" title="Permanent link">&para;</a></h2>
<p><strong>在数组中访问元素非常高效</strong>这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。</p>
<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>
<p><strong>为什么数组元素索引从 0 开始编号?</strong> 根据地址计算公式,<strong>索引本质上表示的是内存地址偏移量</strong>,首个元素的地址偏移量是 <span class="arithmatex">\(0\)</span> ,那么索引是 <span class="arithmatex">\(0\)</span> 也就很自然了。</p>
<p>访问元素的高效性带来了许多便利。例如,我们可以在 <span class="arithmatex">\(O(1)\)</span> 时间内随机获取一个数组中的元素。</p>
<div class="admonition question">
<p class="admonition-title">为什么数组元素的索引要从 <span class="arithmatex">\(0\)</span> 开始编号呢?</p>
<p>观察上图,我们发现数组首个元素的索引为 <span class="arithmatex">\(0\)</span> ,这似乎有些反直觉,因为从 <span class="arithmatex">\(1\)</span> 开始计数会更自然。</p>
<p>然而,从地址计算公式的角度看,<strong>索引本质上表示的是内存地址的偏移量</strong>。首个元素的地址偏移量是 <span class="arithmatex">\(0\)</span> ,因此索引为 <span class="arithmatex">\(0\)</span> 也是合理的。</p>
</div>
<p>访问元素的高效性带来了诸多便利。例如,我们可以在 <span class="arithmatex">\(O(1)\)</span> 时间内随机获取数组中的任意一个元素。</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">
@@ -2321,9 +2321,9 @@
</div>
<p>总结来看,数组的插入与删除操作有以下缺点:</p>
<ul>
<li><strong>时间复杂度高</strong>:数组的插入和删除的平均时间复杂度均为 <span class="arithmatex">\(O(N)\)</span> ,其中 <span class="arithmatex">\(N\)</span> 为数组长度。</li>
<li><strong>丢失元素</strong>:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。</li>
<li><strong>内存浪费</strong>:我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。</li>
<li><strong>时间复杂度高</strong>:数组的插入和删除的平均时间复杂度均为 <span class="arithmatex">\(O(n)\)</span> ,其中 <span class="arithmatex">\(n\)</span> 为数组长度。</li>
<li><strong>丢失元素</strong>:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。</li>
<li><strong>内存浪费</strong>:我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。</li>
</ul>
<h2 id="413">4.1.3. &nbsp; 数组常用操作<a class="headerlink" href="#413" title="Permanent link">&para;</a></h2>
<p><strong>数组遍历</strong>。以下介绍两种常用的遍历方法。</p>

View File

@@ -1768,9 +1768,9 @@
<h1 id="42">4.2. &nbsp; 链表<a class="headerlink" href="#42" title="Permanent link">&para;</a></h1>
<p>内存空间是所有程序的公共资源,排除已被占用的内存空间,空闲内存空间往往是散落在内存各处的。上节讲到,<strong>存储数组的内存空间必须是连续的</strong>当我们需要申请一个非常大的数组时,系统不一定能够分配这么大的连续内存空间。</p>
<p>相对地,链表则更加灵活,可以存储非连续的内存空间。「链表 Linked List」是一种线性数据结构每个元素都是单独的对象,各个元素(即结点之间通过指针连接。由于结点中记录了连接关系,因此链表的存储方式相比于数组更加灵活,系统可将结点分散在内存各处,而不必保证内存地址的连续性</p>
<p>链表「结点 Node」包含两项数据一是结点「值 Value」二是指向下一结点的「指针 Pointer」或称「引用 Reference」</p>
<p>内存空间是所有程序的公共资源,排除已被占用的内存空间,空闲内存空间通常散落在内存各处。在上一节中,我们提到存储数组的内存空间必须是连续的,而当我们需要申请一个非常大的数组时,空闲内存中可能没有这么大的连续空间。</p>
<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>
@@ -1904,12 +1904,15 @@
</div>
</div>
</div>
<p><strong>尾结点指向什么?</strong> 我们一般将链表的最后一个结点称为「尾结点」,其指向的是「空」,在 Java / C++ / Python 中分别记为 <code>null</code> / <code>nullptr</code> / <code>None</code> 。在不引起歧义下,本书都使用 <code>null</code> 来表示空。</p>
<p><strong>链表初始化方法</strong>。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的首个结点(即头结点)出发,访问其余所有的结点。</p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>我们通常将头结点当作链表的代称,例如头结点 <code>head</code> 和链表 <code>head</code> 实际上是同义的。</p>
<div class="admonition question">
<p class="admonition-title">尾结点指向什么?</p>
<p>我们将链表的最后一个结点称为「尾结点」,其指向的是“空”,在 Java, C++, Python 中分别记为 <code>null</code>, <code>nullptr</code>, <code>None</code> 。在不引起歧义的前提下,本书都使用 <code>null</code> 来表示空。</p>
</div>
<div class="admonition question">
<p class="admonition-title">如何称呼链表?</p>
<p>在编程语言中,数组整体就是一个变量,例如数组 <code>nums</code> ,包含各个元素 <code>nums[0]</code> , <code>nums[1]</code> 等等。而链表是由多个结点对象组成,我们通常将头结点当作链表的代称,例如头结点 <code>head</code> 和链表 <code>head</code> 实际上是同义的。</p>
</div>
<p><strong>链表初始化方法</strong>。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的头结点(即首个结点)出发,通过指针 <code>next</code> 依次访问所有结点。</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">
@@ -1965,12 +1968,11 @@
<a id="__codelineno-13-5" name="__codelineno-13-5" href="#__codelineno-13-5"></a><span class="nx">n2</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">NewListNode</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<a id="__codelineno-13-6" name="__codelineno-13-6" href="#__codelineno-13-6"></a><span class="nx">n3</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">NewListNode</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
<a id="__codelineno-13-7" name="__codelineno-13-7" href="#__codelineno-13-7"></a><span class="nx">n4</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">NewListNode</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span>
<a id="__codelineno-13-8" name="__codelineno-13-8" href="#__codelineno-13-8"></a>
<a id="__codelineno-13-9" name="__codelineno-13-9" href="#__codelineno-13-9"></a><span class="c1">// 构建引用指向</span>
<a id="__codelineno-13-10" name="__codelineno-13-10" href="#__codelineno-13-10"></a><span class="nx">n0</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n1</span>
<a id="__codelineno-13-11" name="__codelineno-13-11" href="#__codelineno-13-11"></a><span class="nx">n1</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n2</span>
<a id="__codelineno-13-12" name="__codelineno-13-12" href="#__codelineno-13-12"></a><span class="nx">n2</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n3</span>
<a id="__codelineno-13-13" name="__codelineno-13-13" href="#__codelineno-13-13"></a><span class="nx">n3</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n4</span>
<a id="__codelineno-13-8" name="__codelineno-13-8" href="#__codelineno-13-8"></a><span class="c1">// 构建引用指向</span>
<a id="__codelineno-13-9" name="__codelineno-13-9" href="#__codelineno-13-9"></a><span class="nx">n0</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n1</span>
<a id="__codelineno-13-10" name="__codelineno-13-10" href="#__codelineno-13-10"></a><span class="nx">n1</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n2</span>
<a id="__codelineno-13-11" name="__codelineno-13-11" href="#__codelineno-13-11"></a><span class="nx">n2</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n3</span>
<a id="__codelineno-13-12" name="__codelineno-13-12" href="#__codelineno-13-12"></a><span class="nx">n3</span><span class="p">.</span><span class="nx">Next</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n4</span>
</code></pre></div>
</div>
<div class="tabbed-block">
@@ -2066,7 +2068,7 @@
</div>
</div>
<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><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>
@@ -2159,7 +2161,7 @@
</div>
</div>
</div>
<p>在链表中删除结点也方便,只需改变一个结点指针即可。如下图所示,虽然在完成删除后结点 <code>P</code> 仍然指向 <code>n1</code> ,但实际上 <code>P</code> 已经不属于此链表,因为遍历此链表无法访问到 <code>P</code></p>
<p>在链表中删除结点也非常方便,只需改变一个结点指针即可。如下图所示,尽管在删除操作完成后,结点 <code>P</code> 仍然指向 <code>n1</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>
@@ -2285,7 +2287,7 @@
</div>
</div>
<h2 id="422">4.2.2. &nbsp; 链表缺点<a class="headerlink" href="#422" title="Permanent link">&para;</a></h2>
<p><strong>链表访问结点效率低</strong>。上节提到,数组可以在 <span class="arithmatex">\(O(1)\)</span> 时间下访问任意元素,链表无法直接访问任意结点这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 <code>index</code> (即第 <code>index + 1</code> 个)的结点,那么需要 <code>index</code> 次访问操作</p>
<p><strong>链表访问结点效率</strong>上节所述,数组可以在 <span class="arithmatex">\(O(1)\)</span> 时间下访问任意元素。然而,链表无法直接访问任意结点这是因为系统需要从头结点出发,逐个向后遍历直至找到目标结点。例如,要访问链表索引为 <code>index</code>(即第 <code>index + 1</code> 个)的结点,则需要向后遍历 <code>index</code> </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">
@@ -2408,7 +2410,7 @@
</div>
</div>
</div>
<p><strong>链表的内存占用</strong>。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。</p>
<p><strong>链表的内存占用较大</strong>。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多内存空间。</p>
<h2 id="423">4.2.3. &nbsp; 链表常用操作<a class="headerlink" href="#423" title="Permanent link">&para;</a></h2>
<p><strong>遍历链表查找</strong>。遍历链表,查找链表内值为 <code>target</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>
@@ -2551,9 +2553,9 @@
</div>
</div>
<h2 id="424">4.2.4. &nbsp; 常见链表类型<a class="headerlink" href="#424" title="Permanent link">&para;</a></h2>
<p><strong>单向链表</strong>。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的指针(引用)两项数据。我们将首个结点称为头结点,尾结点指向 <code>null</code></p>
<p><strong>环形链表</strong>。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,我们可以将任意结点看作是头结点。</p>
<p><strong>双向链表</strong>。单向链表记录了个方向的指针(引用),在双向链表的结点定义中,同时有指向下一结点(后继结点)和上一结点(前驱结点)的指针(引用)」。双向链表相对于单向链表更灵活,可以朝两个方向遍历链表,但也需要占用更多的内存空间。</p>
<p><strong>单向链表</strong>。即上述介绍的普通链表。单向链表的结点包含值和指向下一结点的指针(引用)两项数据。我们将首个结点称为头结点,将最后一个结点成为尾结点,尾结点指向 <code>null</code></p>
<p><strong>环形链表</strong>。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,任意结点都可以视作头结点。</p>
<p><strong>双向链表</strong>单向链表相比,双向链表记录了个方向的指针(引用)双向链表的结点定义同时包含指向后继结点(下一结点)和前驱结点(上一结点)的指针。相较于单向链表,双向链表更灵活,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。</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>
<div class="tabbed-content">
<div class="tabbed-block">

View File

@@ -739,7 +739,7 @@
<li class="md-nav__item">
<a href="#432" class="md-nav__link">
4.3.2. &nbsp; 列表简易实现 *
4.3.2. &nbsp; 列表实现 *
</a>
</li>
@@ -1711,7 +1711,7 @@
<li class="md-nav__item">
<a href="#432" class="md-nav__link">
4.3.2. &nbsp; 列表简易实现 *
4.3.2. &nbsp; 列表实现 *
</a>
</li>
@@ -1740,10 +1740,10 @@
<h1 id="43">4.3. &nbsp; 列表<a class="headerlink" href="#43" title="Permanent link">&para;</a></h1>
<p><strong>由于长度不可变,数组的实用性大大降低</strong>。在多情况下,我们事先并不知道会输入多少数据,这就为数组长度的选择带来了很大困难。长度选小了,需要在添加数据频繁扩容数组;长度选大了,又造成内存空间的浪费。</p>
<p>解决此问题,诞生了一种被称为「列表 List」的数据结构。列表可以被理解为长度可变的数组,因此也常被称为「动态数组 Dynamic Array」。列表基于数组实现,继承了数组的优点,同时还可以在程序运行中实时扩容。在列表中,我们可以自由添加元素,而不用担心超过容量限制。</p>
<p><strong>数组长度不可变导致实用性降低</strong>。在多情况下,我们事先无法确定需要存储多少数据,这使数组长度的选择变得困难。长度过小,需要在持续添加数据频繁扩容数组;长度过大,则会造成内存空间的浪费。</p>
<p>为解决此问题,出现了一种被称为「动态数组 Dynamic Array」的数据结构长度可变的数组,也常被称为「列表 List」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。在列表中,我们可以自由添加元素,而无需担心超过容量限制。</p>
<h2 id="431">4.3.1. &nbsp; 列表常用操作<a class="headerlink" href="#431" title="Permanent link">&para;</a></h2>
<p><strong>初始化列表</strong>我们通常会使用“无初始值”和“有初始值”的两种初始化方法。</p>
<p><strong>初始化列表</strong>。通常我们会使用“无初始值”和“有初始值”的两种初始化方法。</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">
@@ -1826,7 +1826,7 @@
</div>
</div>
</div>
<p><strong>访问与更新元素</strong>。列表的底层数据结构是数组,因此可以在 <span class="arithmatex">\(O(1)\)</span> 时间内访问更新元素,效率很高。</p>
<p><strong>访问与更新元素</strong>由于列表的底层数据结构是数组,因此可以在 <span class="arithmatex">\(O(1)\)</span> 时间内访问更新元素,效率很高。</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">
@@ -1907,7 +1907,7 @@
</div>
</div>
</div>
<p><strong>在列表中添加、插入、删除元素</strong>。相于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 <span class="arithmatex">\(O(1)\)</span> ,但插入删除元素的效率仍与数组一样低,时间复杂度为 <span class="arithmatex">\(O(N)\)</span></p>
<p><strong>在列表中添加、插入、删除元素</strong>。相于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 <span class="arithmatex">\(O(1)\)</span> ,但插入删除元素的效率仍与数组相同,时间复杂度为 <span class="arithmatex">\(O(N)\)</span></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">
@@ -2078,7 +2078,7 @@
</div>
</div>
</div>
<p><strong>遍历列表</strong>。与数组一样,列表可以使用索引遍历,也可以使用 <code>for-each</code> 直接遍历。</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">
<div class="tabbed-block">
@@ -2214,7 +2214,7 @@
</div>
</div>
</div>
<p><strong>拼接两个列表</strong>再创建一个新列表 <code>list1</code> ,我们可以将其中一个列表拼接到另一个的尾部。</p>
<p><strong>拼接两个列表</strong>给定一个新列表 <code>list1</code>,我们可以将列表拼接到原列表的尾部。</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">
@@ -2280,7 +2280,7 @@
</div>
</div>
</div>
<p><strong>排序列表</strong>。排序也是常用的方法之一完成列表排序后,我们可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法</p>
<p><strong>排序列表</strong>。排序也是常用的方法之一完成列表排序后,我们便可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法。</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">
<div class="tabbed-block">
@@ -2334,14 +2334,14 @@
</div>
</div>
</div>
<h2 id="432">4.3.2. &nbsp; 列表简易实现 *<a class="headerlink" href="#432" title="Permanent link">&para;</a></h2>
<p>为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点:</p>
<h2 id="432">4.3.2. &nbsp; 列表实现 *<a class="headerlink" href="#432" title="Permanent link">&para;</a></h2>
<p>为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点:</p>
<ul>
<li><strong>初始容量</strong>:选取一个合理的数组初始容量 <code>initialCapacity</code> 。在本示例中,我们选择 10 作为初始容量。</li>
<li><strong>数量记录</strong>需要声明一个变量 <code>size</code> ,用记录列表当前有多少个元素,并随着元素插入删除实时更新。根据此变量,可以定位列表尾部,以及判断是否需要扩容。</li>
<li><strong>扩容机制</strong>:插入元素可能导致超出列表容量,此时需要扩容列表,方法是建立一个更大的数组来替换当前数组。需要给定一个扩容倍数 <code>extendRatio</code> 在本示例中,我们规定每次将数组扩容至之前的 2 倍。</li>
<li><strong>初始容量</strong>:选取一个合理的数组初始容量。在本示例中,我们选择 10 作为初始容量。</li>
<li><strong>数量记录</strong>:声明一个变量 size,用记录列表当前元素数量,并随着元素插入删除实时更新。根据此变量,我们可以定位列表尾部,以及判断是否需要扩容。</li>
<li><strong>扩容机制</strong>:插入元素可能超出列表容量,此时需要扩容列表。扩容方法是根据扩容倍数创建一个更大的数组,并将当前数组的所有元素依次移动至新数组。在本示例中,我们规定每次将数组扩容至之前的 2 倍。</li>
</ul>
<p>本示例是为了帮助读者对如何实现列表产生直观的认识。实际编程语言中,列表实现远比以下代码复杂且标准,感兴趣的读者可以查阅源码学习。</p>
<p>本示例旨在帮助读者直观理解列表的工作机制。实际编程语言中,列表实现更加标准和复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。</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>
<div class="tabbed-content">
<div class="tabbed-block">

View File

@@ -1681,11 +1681,11 @@
<h1 id="44">4.4. &nbsp; 小结<a class="headerlink" href="#44" title="Permanent link">&para;</a></h1>
<ul>
<li>数组和链表是两种基本数据结构,代表数据在计算机内存中的两种存储方式,即连续空间存储和离散空间存储。两者的优点与缺点呈现出此消彼长的关系</li>
<li>数组支持随机访问、内存空间占用小;但插入删除元素效率低,且初始化后长度不可变。</li>
<li>链表通过更改指针实现高效的结点插入与删除,且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型单向链表、循环链表、双向链表。</li>
<li>列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,可以灵活改变长度。列表的出现大大提升了数组的用性,但副作用是会造成部分内存空间浪费。</li>
<li>下表总结对比了数组与链表的各项特性。</li>
<li>数组和链表是两种基本数据结构,分别代表数据在计算机内存中的连续空间存储和离散空间存储方式。两者的优缺点呈现出互补的特性</li>
<li>数组支持随机访问、占用内存较少;但插入删除元素效率低,且初始化后长度不可变。</li>
<li>链表通过更改指针实现高效的结点插入与删除,且可以灵活调整长度;但结点访问效率低、占用内存多。常见的链表类型包括单向链表、循环链表、双向链表。</li>
<li>动态数组,又称列表,是基于数组实现的一种数据结构。它保留了数组的优势,同时可以灵活调整长度。列表的出现极大地提高了数组的用性,但可能导致部分内存空间浪费。</li>
<li>下表总结对比了数组与链表的各项特性。</li>
</ul>
<div class="center-table">
<table>
@@ -1720,12 +1720,12 @@
</tbody>
</table>
</div>
<div class="admonition question">
<p class="admonition-title">缓存局部性的简单解释</p>
<div class="admonition note">
<p class="admonition-title">缓存局部性</p>
<p>在计算机中,数据读写速度排序是“硬盘 &lt; 内存 &lt; CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其它数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个结点,这样的多次“搬运”降低了整体效率。</p>
</div>
<ul>
<li>下表对比了数组与链表各种操作效率。</li>
<li>下表对比了数组与链表各种操作上的效率。</li>
</ul>
<div class="center-table">
<table>