This commit is contained in:
krahets
2023-12-14 02:53:40 +08:00
parent 68eab8e7c1
commit a85d95787b
19 changed files with 156 additions and 156 deletions

View File

@@ -3631,12 +3631,12 @@
<h1 id="43">4.3 &nbsp; 列表<a class="headerlink" href="#43" title="Permanent link">&para;</a></h1>
<p>「列表 list」是一个抽象的数据结构概念它表示元素的有序集合支持元素访问、修改、添加、删除和遍历等操作无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。</p>
<ul>
<li>链表天然可以看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。</li>
<li>数组也支持元素增删查改,但由于其长度不可变,因此只能看作一个具有长度限制的列表。</li>
<li>链表天然可以看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。</li>
<li>数组也支持元素增删查改,但由于其长度不可变,因此只能看作一个具有长度限制的列表。</li>
</ul>
<p>当使用数组实现列表时,<strong>长度不可变的性质会导致列表的实用性降低</strong>。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。</p>
<p>当使用数组实现列表时,<strong>长度不可变的性质会导致列表的实用性降低</strong>。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。</p>
<p>为解决此问题,我们可以使用「动态数组 dynamic array」来实现列表。它继承了数组的各项优点并且可以在程序运行过程中进行动态扩容。</p>
<p>实际上,<strong>许多编程语言中的标准库提供的列表是基于动态数组实现的</strong>,例如 Python 中的 <code>list</code> 、Java 中的 <code>ArrayList</code> 、C++ 中的 <code>vector</code> 和 C# 中的 <code>List</code> 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。</p>
<p>实际上,<strong>许多编程语言中的标准库提供的列表是基于动态数组实现的</strong>,例如 Python 中的 <code>list</code> 、Java 中的 <code>ArrayList</code> 、C++ 中的 <code>vector</code> 和 C# 中的 <code>List</code> 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。</p>
<h2 id="431">4.3.1 &nbsp; 列表常用操作<a class="headerlink" href="#431" title="Permanent link">&para;</a></h2>
<h3 id="1">1. &nbsp; 初始化列表<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>我们通常使用“无初始值”和“有初始值”这两种初始化方法:</p>

View File

@@ -3535,10 +3535,10 @@
<!-- Page content -->
<h1 id="44">4.4 &nbsp; 内存与缓存 *<a class="headerlink" href="#44" title="Permanent link">&para;</a></h1>
<p>在本章的前两节中,我们探讨了数组和链表这两种基础且重要的数据结构,它们分别代表了“连续存储”和“分散存储”两种不同的物理结构。</p>
<p>在本章的前两节中,我们探讨了数组和链表这两种基础且重要的数据结构,它们分别代表了“连续存储”和“分散存储”两种物理结构。</p>
<p>实际上,<strong>物理结构在很大程度上决定了程序对内存和缓存的使用效率</strong>,进而影响算法程序的整体性能。</p>
<h2 id="441">4.4.1 &nbsp; 计算机存储设备<a class="headerlink" href="#441" title="Permanent link">&para;</a></h2>
<p>计算机中包括三种不同类型的存储设备:「硬盘 hard disk」、「内存 random-access memory, RAM」、「缓存 cache memory」。表 4-2 展示了它们在计算机系统中的不同角色和性能特点。</p>
<p>计算机中包括三种类型的存储设备:「硬盘 hard disk」、「内存 random-access memory, RAM」、「缓存 cache memory」。表 4-2 展示了它们在计算机系统中的不同角色和性能特点。</p>
<p align="center"> 表 4-2 &nbsp; 计算机的存储设备 </p>
<div class="center-table">
@@ -3587,7 +3587,7 @@
</div>
<p>我们可以将计算机存储系统想象为图 4-9 所示的金字塔结构。越靠近金字塔顶端的存储设备的速度越快、容量越小、成本越高。这种多层级的设计并非偶然,而是计算机科学家和工程师们经过深思熟虑的结果。</p>
<ul>
<li><strong>硬盘难以被内存取代</strong>。首先,内存中的数据在断电后会丢失,因此它不适合长期存储数据;其次,内存的成本大约是硬盘的几十倍,这使得它难以在消费者市场普及。</li>
<li><strong>硬盘难以被内存取代</strong>。首先,内存中的数据在断电后会丢失,因此它不适合长期存储数据;其次,内存的成本是硬盘的几十倍,这使得它难以在消费者市场普及。</li>
<li><strong>缓存的大容量和高速度难以兼得</strong>。随着 L1、L2、L3 缓存的容量逐步增大,其物理尺寸会变大,与 CPU 核心之间的物理距离会变远,从而导致数据传输时间增加,元素访问延迟变高。在当前技术下,多层级的缓存结构是容量、速度和成本之间的最佳平衡点。</li>
</ul>
<p><a class="glightbox" href="../ram_and_cache.assets/storage_pyramid.png" data-type="image" data-width="100%" data-height="auto" data-desc-position="bottom"><img alt="计算机存储系统" class="animation-figure" src="../ram_and_cache.assets/storage_pyramid.png" /></a></p>
@@ -3595,39 +3595,39 @@
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳平衡点。</p>
<p>计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳平衡点。</p>
</div>
<p>总的来说,<strong>硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令</strong>,以提高程序运行效率。三者共同协作,确保计算机系统高效运行。</p>
<p>如图 4-10 所示,在程序运行时,数据会从硬盘中被读取到内存中,供 CPU 计算使用。缓存可以看作 CPU 的一部分,<strong>它通过智能地从内存加载数据</strong>,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。</p>
<p>总的来说,<strong>硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令</strong>,以提高程序运行效率。三者共同协作,确保计算机系统高效运行。</p>
<p>如图 4-10 所示,在程序运行时,数据会从硬盘中被读取到内存中,供 CPU 计算使用。缓存可以看作 CPU 的一部分,<strong>它通过智能地从内存加载数据</strong>,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。</p>
<p><a class="glightbox" href="../ram_and_cache.assets/computer_storage_devices.png" data-type="image" data-width="100%" data-height="auto" data-desc-position="bottom"><img alt="硬盘、内存和缓存之间的数据流通" class="animation-figure" src="../ram_and_cache.assets/computer_storage_devices.png" /></a></p>
<p align="center"> 图 4-10 &nbsp; 硬盘、内存和缓存之间的数据流通 </p>
<h2 id="442">4.4.2 &nbsp; 数据结构的内存效率<a class="headerlink" href="#442" title="Permanent link">&para;</a></h2>
<p>在内存空间利用方面,数组和链表具有各自的优势和局限。</p>
<p>一方面,<strong>内存是有限的,且同一块内存不能被多个程序共享</strong>,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,这种方式提供了更大的灵活性。</p>
<p>在内存空间利用方面,数组和链表各自具有优势和局限</p>
<p>一方面,<strong>内存是有限的,且同一块内存不能被多个程序共享</strong>,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,提供了更大的灵活性。</p>
<p>另一方面,在程序运行时,<strong>随着反复申请与释放内存,空闲内存的碎片化程度会越来越高</strong>,从而导致内存的利用效率降低。数组由于其连续的存储方式,相对不容易导致内存碎片化。相反,链表的元素是分散存储的,在频繁的插入与删除操作中,更容易导致内存碎片化。</p>
<h2 id="443">4.4.3 &nbsp; 数据结构的缓存效率<a class="headerlink" href="#443" title="Permanent link">&para;</a></h2>
<p>缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据因此当 CPU 尝试访问的数据不在缓存中时,就会发生「缓存未命中 cache miss」此时 CPU 不得不从速度较慢的内存中加载所需数据。</p>
<p>缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据因此当 CPU 尝试访问的数据不在缓存中时,就会发生「缓存未命中 cache miss」此时 CPU 不得不从速度较慢的内存中加载所需数据。</p>
<p>显然,<strong>“缓存未命中”越少CPU 读写数据的效率就越高</strong>,程序性能也就越好。我们将 CPU 从缓存中成功获取数据的比例称为「缓存命中率 cache hit rate」这个指标通常用来衡量缓存效率。</p>
<p>为了尽可能达到更高效率,缓存会采取以下数据加载机制。</p>
<p>为了尽可能达到更高效率,缓存会采取以下数据加载机制。</p>
<ul>
<li><strong>缓存行</strong>:缓存不是单个字节地存储与加载数据,而是以缓存行为单位。相比于单个字节的传输,缓存行的传输形式更加高效。</li>
<li><strong>预取机制</strong>:处理器会尝试预测数据访问模式(例如顺序访问、固定步长跳跃访问等),并根据特定模式将数据加载至缓存之中,从而提升命中率。</li>
<li><strong>空间局部性</strong>:如果一个数据被访问,那么它附近的数据可能也会近期被访问。因此,缓存在加载某一数据时,也会其附近的数据加载进来,以提高命中率。</li>
<li><strong>空间局部性</strong>:如果一个数据被访问,那么它附近的数据可能近期也会被访问。因此,缓存在加载某一数据时,也会加载其附近的数据,以提高命中率。</li>
<li><strong>时间局部性</strong>:如果一个数据被访问,那么它在不久的将来很可能再次被访问。缓存利用这一原理,通过保留最近访问过的数据来提高命中率。</li>
</ul>
<p>实际上,<strong>数组和链表对缓存的利用效率是不同的</strong>,主要体现在以下几个方面。</p>
<p>实际上,<strong>数组和链表对缓存的利用效率是不同的</strong>,主要体现在以下几个方面。</p>
<ul>
<li><strong>占用空间</strong>:链表元素比数组元素占用空间更多,导致缓存中容纳的有效数据量更少。</li>
<li><strong>缓存行</strong>:链表数据分散在内存各处,而缓存是“按行加载”的,因此加载到无效数据的比例更高。</li>
<li><strong>缓存行</strong>:链表数据分散在内存各处,而缓存是“按行加载”的,因此加载到无效数据的比例更高。</li>
<li><strong>预取机制</strong>:数组比链表的数据访问模式更具“可预测性”,即系统更容易猜出即将被加载的数据。</li>
<li><strong>空间局部性</strong>:数组被存储在集中的内存空间中,因此被加载数据附近数据更有可能即将被访问。</li>
<li><strong>空间局部性</strong>:数组被存储在集中的内存空间中,因此被加载数据附近数据更有可能即将被访问。</li>
</ul>
<p>总体而言,<strong>数组具有更高的缓存命中率,因此它在操作效率上通常优于链表</strong>。这使得在解决算法问题时,基于数组实现的数据结构往往更受欢迎。</p>
<p>需要注意的是,<strong>高缓存效率并不意味着数组在所有情况下都优于链表</strong>。实际应用中选择哪种数据结构,应根据具体需求来决定。例如,数组和链表都可以实现“栈”数据结构(下一章会详细介绍),但它们适用于不同场景。</p>
<ul>
<li>在做算法题时,我们会倾向于选择基于数组实现的栈,因为它提供了更高的操作效率和随机访问的能力,代价仅是需要预先为数组分配一定的内存空间。</li>
<li>如果数据量非常大、动态性很高、栈的预期大小难以估计,那么基于链表实现的栈更加合适。链表能够将大量数据分散存储于内存的不同部分,并且避免了数组扩容产生的额外开销。</li>
<li>如果数据量非常大、动态性很高、栈的预期大小难以估计,那么基于链表实现的栈更加合适。链表能够将大量数据分散存储于内存的不同部分,并且避免了数组扩容产生的额外开销。</li>
</ul>
<!-- Source file information -->

View File

@@ -3517,14 +3517,14 @@
<li>常见的链表类型包括单向链表、环形链表、双向链表,它们分别具有各自的应用场景。</li>
<li>列表是一种支持增删查改的元素有序集合,通常基于动态数组实现,其保留了数组的优势,同时可以灵活调整长度。</li>
<li>列表的出现大幅地提高了数组的实用性,但可能导致部分内存空间浪费。</li>
<li>程序运行时,数据主要存储在内存中。数组提供更高的内存空间效率,而链表则在内存使用上更加灵活。</li>
<li>缓存通过缓存行、预取机制以及空间和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。</li>
<li>程序运行时,数据主要存储在内存中。数组提供更高的内存空间效率,而链表则在内存使用上更加灵活。</li>
<li>缓存通过缓存行、预取机制以及空间局部性和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。</li>
<li>由于数组具有更高的缓存命中率,因此它通常比链表更高效。在选择数据结构时,应根据具体需求和场景做出恰当选择。</li>
</ul>
<h3 id="2-q-a">2. &nbsp; Q &amp; A<a class="headerlink" href="#2-q-a" title="Permanent link">&para;</a></h3>
<div class="admonition question">
<p class="admonition-title">数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?</p>
<p>存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。</p>
<p>存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。</p>
<ol>
<li>分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。</li>
<li>大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。</li>
@@ -3583,7 +3583,7 @@
</div>
<div class="admonition question">
<p class="admonition-title">在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?</p>
<p>从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。</p>
<p>从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。</p>
</div>
<!-- Source file information -->