diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index 2ea7a6073..8b719a105 100755 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -371,7 +371,7 @@ comments: true /* 在数组的索引 index 处插入元素 num */ func insert(nums: inout [Int], num: Int, index: Int) { // 把索引 index 以及之后的所有元素向后移动一位 - for i in sequence(first: nums.count - 1, next: { $0 > index + 1 ? $0 - 1 : nil }) { + for i in nums.indices.dropFirst(index).reversed() { nums[i] = nums[i - 1] } // 将 num 赋给 index 处元素 @@ -537,9 +537,8 @@ comments: true ```swift title="array.swift" /* 删除索引 index 处元素 */ func remove(nums: inout [Int], index: Int) { - let count = nums.count // 把索引 index 之后的所有元素向前移动一位 - for i in sequence(first: index, next: { $0 < count - 1 - 1 ? $0 + 1 : nil }) { + for i in nums.indices.dropFirst(index).dropLast() { nums[i] = nums[i + 1] } } diff --git a/docs/chapter_array_and_linkedlist/index.md b/docs/chapter_array_and_linkedlist/index.md index a87fd2af0..ff6081741 100644 --- a/docs/chapter_array_and_linkedlist/index.md +++ b/docs/chapter_array_and_linkedlist/index.md @@ -18,4 +18,5 @@ icon: material/view-list-outline - [4.1 数组](https://www.hello-algo.com/chapter_array_and_linkedlist/array/) - [4.2 链表](https://www.hello-algo.com/chapter_array_and_linkedlist/linked_list/) - [4.3 列表](https://www.hello-algo.com/chapter_array_and_linkedlist/list/) -- [4.4 小结](https://www.hello-algo.com/chapter_array_and_linkedlist/summary/) +- [4.4 内存与缓存 *](https://www.hello-algo.com/chapter_array_and_linkedlist/ram_and_cache/) +- [4.5 小结](https://www.hello-algo.com/chapter_array_and_linkedlist/summary/) diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index 1fba56838..b68473600 100755 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -1104,15 +1104,14 @@ comments: true
表 4-2 计算机的存储设备
+ +图 4-9 计算机存储系统
+ +!!! note + + 计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳的平衡点。 + +总的来说,**硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令**,以提高程序运行效率。这三者共同协作,确保计算机系统的高效运行。 + +如图 4-10 所示,在程序运行时,数据会从硬盘中被读取到内存中,供给 CPU 计算使用。缓存可以看作是 CPU 的一部分,**它通过智能地从内存加载数据**,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。 + +![硬盘、内存和缓存之间的数据流通](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } + +图 4-10 硬盘、内存和缓存之间的数据流通
+ +## 4.4.2 数据结构的内存效率 + +在内存空间利用方面,数组和链表具有各自的优势和局限。 + +一方面,**内存是有限的,且同一块内存不能被多个程序共享**,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存的浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,这种方式提供了更大的灵活性。 + +另一方面,在程序运行时,**随着反复申请与释放内存,空闲内存的碎片化程度会越来越高**,从而导致内存的利用效率降低。数组由于其连续的存储方式,相对不容易导致内存碎片化。相反,链表的元素是分散存储的,在频繁的插入与删除操作中,更容易导致内存碎片化。 + +## 4.4.3 数据结构的缓存效率 + +缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,它只能存储一小部分频繁访问的数据。因此,当 CPU 尝试访问的数据不在缓存中时,就会发生「缓存未命中 cache miss」,此时 CPU 不得不从速度较慢的内存中加载所需数据。 + +显然,**“缓存未命中”越少,CPU 读写数据的效率就越高**,程序性能也就越好。我们将 CPU 从缓存中成功获取数据的比例称为「缓存命中率 cache hit rate」,这个指标通常用来衡量缓存效率。 + +为了尽可能达到更高效率,缓存会采取以下数据加载机制。 + +- **缓存行**:缓存不是单个字节地存储与加载数据,而是以缓存行为单位。相比于单个字节的传输,缓存行的传输形式更加高效。 +- **预取机制**:处理器会尝试预测数据访问模式(例如顺序访问、固定步长跳跃访问等),并根据特定模式将数据加载至缓存之中,从而提升命中率。 +- **空间局部性**:如果一个数据被访问,那么它附近的数据可能也会近期被访问。因此,缓存在加载某一数据时,也会将其附近的数据加载进来,以提高命中率。 +- **时间局部性**:如果一个数据被访问,那么它在不久的将来很可能再次被访问。缓存利用这一原理,通过保留最近访问过的数据来提高命中率。 + +实际上,**数组和链表对缓存的利用效率也是不同的**,主要体现在以下几个方面。 + +- **占用空间**:链表元素比数组元素占用空间更多,导致缓存中容纳的有效数据量更少。 +- **缓存行**:链表数据分散在内存各处,而缓存是“按行加载”的,因此加载到的无效数据的比例更高。 +- **预取机制**:数组比链表的数据访问模式更具“可预测性”,即系统更容易猜出即将被加载的数据。 +- **空间局部性**:数组被存储在集中的内存空间中,因此被加载数据的附近数据更有可能即将被访问。 + +总体而言,**数组具有更高的缓存命中率,因此它在操作效率上通常优于链表**。这使得在解决算法问题时,基于数组实现的数据结构往往更受欢迎。 + +需要注意的是,**高缓存效率并不意味着数组在所有情况下都优于链表**。实际应用中选择哪种数据结构,应根据具体需求来决定。例如,数组和链表都可以实现“栈”数据结构(下一章会详细介绍),但它们适用于不同场景。 + +- 在做算法题时,我们会倾向于选择基于数组实现的栈,因为它提供了更高的操作效率和随机访问的能力,代价仅是需要预先为数组分配一定的内存空间。 +- 如果数据量非常大、动态性很高、栈的预期大小难以估计,那么基于链表实现的栈就更加合适。链表能够将大量数据分散存储于内存的不同部分,并且避免了数组扩容产生的额外开销。 diff --git a/docs/chapter_array_and_linkedlist/summary.md b/docs/chapter_array_and_linkedlist/summary.md index 91a0794db..14ae58d7d 100644 --- a/docs/chapter_array_and_linkedlist/summary.md +++ b/docs/chapter_array_and_linkedlist/summary.md @@ -2,7 +2,7 @@ comments: true --- -# 4.4 小结 +# 4.5 小结 ### 1. 重点回顾 @@ -12,6 +12,9 @@ comments: true - 常见的链表类型包括单向链表、循环链表、双向链表,它们分别具有各自的应用场景。 - 列表是一种支持增删查改的元素有序集合,通常基于动态数组实现,其保留了数组的优势,同时可以灵活调整长度。 - 列表的出现大幅地提高了数组的实用性,但可能导致部分内存空间浪费。 +- 程序运行时,数据主要存储在内存中。数组提供更高的内存空间效率,而链表则在内存使用上更加灵活。 +- 缓存通过缓存行、预取机制以及空间和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。 +- 由于数组具有更高的缓存命中率,因此它通常比链表更高效。在选择数据结构时,应根据具体需求和场景做出恰当选择。 ### 2. Q & A diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index 80d7f167c..7a4f83575 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -190,7 +190,7 @@ comments: true } ``` -根据以上方法,可以得到算法运行时间为 $6n + 12$ ns : +根据以上方法,可以得到算法运行时间为 $(6n + 12)$ ns : $$ 1 + 1 + 10 + (1 + 5) \times n = 6n + 12 diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index c659c1c3b..4ccc38476 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -146,7 +146,7 @@ comments: true ```rust title="" // 使用多种基本数据类型来初始化数组 let numbers: Vec图 3-2 内存条、内存空间、内存地址
+!!! note + + 值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及到地址空间、内存管理、缓存机制、虚拟和物理内存等概念。 + 内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。 -如图 3-3 所示,**物理结构反映了数据在计算机内存中的存储方式**,可分为连续空间存储(数组)和分散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,同时在时间效率和空间效率方面呈现出互补的特点。 +如图 3-3 所示,**物理结构反映了数据在计算机内存中的存储方式**,可分为连续空间存储(数组)和分散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,在时间效率和空间效率方面呈现出互补的特点。 ![连续空间存储与分散空间存储](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index 068dd38f3..b08af9d57 100755 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -1055,7 +1055,7 @@ comments: true ## 11.5.5 尾递归优化 -**在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 +**在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,设递归中的子数组长度为 $m$ ,每轮哨兵划分操作都将产生长度为 $0$ 的左子数组和长度为 $m - 1$ 的右子数组,这意味着每一层递归调用减少的问题规模非常小(只减少一个元素),递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 为了防止栈帧空间的累积,我们可以在每轮哨兵排序完成后,比较两个子数组的长度,**仅对较短的子数组进行递归**。由于较短子数组的长度不会超过 $n / 2$ ,因此这种方法能确保递归深度不超过 $\log n$ ,从而将最差空间复杂度优化至 $O(\log n)$ 。 diff --git a/docs/chapter_tree/array_representation_of_tree.md b/docs/chapter_tree/array_representation_of_tree.md index 12b78337a..776a0ac0d 100644 --- a/docs/chapter_tree/array_representation_of_tree.md +++ b/docs/chapter_tree/array_representation_of_tree.md @@ -371,15 +371,15 @@ comments: true if (val(i) == null) return; // 前序遍历 - if (order == "pre") + if ("pre".equals(order)) res.add(val(i)); dfs(left(i), order, res); // 中序遍历 - if (order == "in") + if ("in".equals(order)) res.add(val(i)); dfs(right(i), order, res); // 后序遍历 - if (order == "post") + if ("post".equals(order)) res.add(val(i)); }