diff --git a/docs/chapter_appendix/terminology.md b/docs/chapter_appendix/terminology.md index ab4f29cd1..3f9aafcac 100644 --- a/docs/chapter_appendix/terminology.md +++ b/docs/chapter_appendix/terminology.md @@ -5,7 +5,7 @@ status: new # 16.3   术语表 -表 16-1 列出了书中出现的重要术语。建议你同时记住它们的中英文叫法,以便阅读英文文献。 +表 16-1 列出了书中出现的重要术语。建议读者同时记住它们的中英文叫法,以便阅读英文文献。

表 16-1   数据结构与算法的重要名词

@@ -25,7 +25,7 @@ status: new | 大 | big- | 优先队列 | priority queue | | 记号 | notation | | | | 渐近上界 | asymptotic upper bound | 堆化 | heapify | -| 原码 | sign–magnitude | 图 | graph | +| 原码 | sign-magnitude | 图 | graph | | 反码 | 1’s complement | 顶点 | vertex | | 补码 | 2’s complement | 无向图 | undirected graph | | 数组 | array | 有向图 | directed graph | diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index abc0d2599..f79b4ba37 100755 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -6,14 +6,14 @@ comments: true 「列表 list」是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。 -- 链表天然可以被看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。 -- 数组也支持元素增删查改,但由于其长度不可变,因此只能被看作一个具有长度限制的列表。 +- 链表天然可以看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。 +- 数组也支持元素增删查改,但由于其长度不可变,因此只能看作一个具有长度限制的列表。 -当使用数组实现列表时,**长度不可变的性质会导致列表的实用性降低**。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间的浪费。 +当使用数组实现列表时,**长度不可变的性质会导致列表的实用性降低**。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。 为解决此问题,我们可以使用「动态数组 dynamic array」来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。 -实际上,**许多编程语言中的标准库提供的列表都是基于动态数组实现的**,例如 Python 中的 `list` 、Java 中的 `ArrayList` 、C++ 中的 `vector` 和 C# 中的 `List` 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。 +实际上,**许多编程语言中的标准库提供的列表是基于动态数组实现的**,例如 Python 中的 `list` 、Java 中的 `ArrayList` 、C++ 中的 `vector` 和 C# 中的 `List` 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。 ## 4.3.1   列表常用操作 diff --git a/docs/chapter_array_and_linkedlist/ram_and_cache.md b/docs/chapter_array_and_linkedlist/ram_and_cache.md index c4f34c07a..c5ad8930d 100644 --- a/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -5,13 +5,13 @@ status: new # 4.4   内存与缓存 * -在本章的前两节中,我们探讨了数组和链表这两种基础且重要的数据结构,它们分别代表了“连续存储”和“分散存储”这两种不同的物理结构。 +在本章的前两节中,我们探讨了数组和链表这两种基础且重要的数据结构,它们分别代表了“连续存储”和“分散存储”两种物理结构。 实际上,**物理结构在很大程度上决定了程序对内存和缓存的使用效率**,进而影响算法程序的整体性能。 ## 4.4.1   计算机存储设备 -计算机中包括三种不同类型的存储设备:「硬盘 hard disk」、「内存 random-access memory, RAM」、「缓存 cache memory」。表 4-2 展示了它们在计算机系统中的不同角色和性能特点。 +计算机中包括三种类型的存储设备:「硬盘 hard disk」、「内存 random-access memory, RAM」、「缓存 cache memory」。表 4-2 展示了它们在计算机系统中的不同角色和性能特点。

表 4-2   计算机的存储设备

@@ -29,7 +29,7 @@ status: new 我们可以将计算机存储系统想象为图 4-9 所示的金字塔结构。越靠近金字塔顶端的存储设备的速度越快、容量越小、成本越高。这种多层级的设计并非偶然,而是计算机科学家和工程师们经过深思熟虑的结果。 -- **硬盘难以被内存取代**。首先,内存中的数据在断电后会丢失,因此它不适合长期存储数据;其次,内存的成本大约是硬盘的几十倍,这使得它难以在消费者市场普及。 +- **硬盘难以被内存取代**。首先,内存中的数据在断电后会丢失,因此它不适合长期存储数据;其次,内存的成本是硬盘的几十倍,这使得它难以在消费者市场普及。 - **缓存的大容量和高速度难以兼得**。随着 L1、L2、L3 缓存的容量逐步增大,其物理尺寸会变大,与 CPU 核心之间的物理距离会变远,从而导致数据传输时间增加,元素访问延迟变高。在当前技术下,多层级的缓存结构是容量、速度和成本之间的最佳平衡点。 ![计算机存储系统](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } @@ -38,11 +38,11 @@ status: new !!! note - 计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳的平衡点。 + 计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳平衡点。 -总的来说,**硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令**,以提高程序运行效率。这三者共同协作,确保计算机系统的高效运行。 +总的来说,**硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令**,以提高程序运行效率。三者共同协作,确保计算机系统高效运行。 -如图 4-10 所示,在程序运行时,数据会从硬盘中被读取到内存中,供给 CPU 计算使用。缓存可以看作 CPU 的一部分,**它通过智能地从内存加载数据**,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。 +如图 4-10 所示,在程序运行时,数据会从硬盘中被读取到内存中,供 CPU 计算使用。缓存可以看作 CPU 的一部分,**它通过智能地从内存加载数据**,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。 ![硬盘、内存和缓存之间的数据流通](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } @@ -50,35 +50,35 @@ status: new ## 4.4.2   数据结构的内存效率 -在内存空间利用方面,数组和链表具有各自的优势和局限。 +在内存空间利用方面,数组和链表各自具有优势和局限性。 -一方面,**内存是有限的,且同一块内存不能被多个程序共享**,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存的浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,这种方式提供了更大的灵活性。 +一方面,**内存是有限的,且同一块内存不能被多个程序共享**,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,提供了更大的灵活性。 另一方面,在程序运行时,**随着反复申请与释放内存,空闲内存的碎片化程度会越来越高**,从而导致内存的利用效率降低。数组由于其连续的存储方式,相对不容易导致内存碎片化。相反,链表的元素是分散存储的,在频繁的插入与删除操作中,更容易导致内存碎片化。 ## 4.4.3   数据结构的缓存效率 -缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,它只能存储一小部分频繁访问的数据。因此,当 CPU 尝试访问的数据不在缓存中时,就会发生「缓存未命中 cache miss」,此时 CPU 不得不从速度较慢的内存中加载所需数据。 +缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据,因此当 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 452cb9ae7..bd28c1234 100644 --- a/docs/chapter_array_and_linkedlist/summary.md +++ b/docs/chapter_array_and_linkedlist/summary.md @@ -12,15 +12,15 @@ comments: true - 常见的链表类型包括单向链表、环形链表、双向链表,它们分别具有各自的应用场景。 - 列表是一种支持增删查改的元素有序集合,通常基于动态数组实现,其保留了数组的优势,同时可以灵活调整长度。 - 列表的出现大幅地提高了数组的实用性,但可能导致部分内存空间浪费。 -- 程序运行时,数据主要存储在内存中。数组提供更高的内存空间效率,而链表则在内存使用上更加灵活。 -- 缓存通过缓存行、预取机制以及空间和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。 +- 程序运行时,数据主要存储在内存中。数组可提供更高的内存空间效率,而链表则在内存使用上更加灵活。 +- 缓存通过缓存行、预取机制以及空间局部性和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。 - 由于数组具有更高的缓存命中率,因此它通常比链表更高效。在选择数据结构时,应根据具体需求和场景做出恰当选择。 ### 2.   Q & A !!! question "数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?" - 存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率是基本一致的。然而,栈和堆具有各自的特点,从而导致以下不同点。 + 存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。 1. 分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。 2. 大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。 @@ -82,4 +82,4 @@ comments: true !!! question "在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?" - 从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它也会影响后继节点的内存回收。 + 从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。 diff --git a/docs/chapter_computational_complexity/iteration_and_recursion.md b/docs/chapter_computational_complexity/iteration_and_recursion.md index fb859d5c4..99ca5c63a 100644 --- a/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -4,7 +4,7 @@ comments: true # 2.2   迭代与递归 -在算法中,重复执行某个任务是很常见的,其与复杂度分析息息相关。因此,在展开介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。 +在算法中,重复执行某个任务是很常见的,它与复杂度分析息息相关。因此,在介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。 ## 2.2.1   迭代 @@ -1462,12 +1462,12 @@ comments: true 如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。 -那么,迭代和递归具有什么内在联系呢?以上述的递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则是异曲同工的**。 +那么,迭代和递归具有什么内在联系呢?以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则异曲同工**。 事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。 1. **递**:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。 -2. **归**:当函数完成执行并返回时,对应的栈帧会从“调用栈”上被移除,恢复之前函数的执行环境。 +2. **归**:当函数完成执行并返回时,对应的栈帧会被从“调用栈”上移除,恢复之前函数的执行环境。 因此,**我们可以使用一个显式的栈来模拟调用栈的行为**,从而将递归转化为迭代形式: @@ -1748,9 +1748,9 @@ comments: true } ``` -观察以上代码,当递归被转换为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转换,但也不一定值得这样做,有以下两点原因。 +观察以上代码,当递归转化为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转化,但不一定值得这样做,有以下两点原因。 - 转化后的代码可能更加难以理解,可读性更差。 - 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。 -总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法是至关重要的。 +总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法至关重要。 diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index 40e78675b..9c5e4a139 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -1851,7 +1851,7 @@ $$ ### 4.   指数阶 $O(2^n)$ -指数阶常见于二叉树。观察图 2-19 ,高度为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: +指数阶常见于二叉树。观察图 2-19 ,层数为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: === "Python" diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index fff30a55b..79360a2ab 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -39,7 +39,7 @@ comments: true -请注意,表 3-1 针对的是 Java 的基本数据类型的情况。每种编程语言有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。 +请注意,表 3-1 针对的是 Java 的基本数据类型的情况。每种编程语言都有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。 - 在 Python 中,整数类型 `int` 可以是任意大小,只受限于可用内存;浮点数 `float` 是双精度 64 位;没有 `char` 类型,单个字符实际上是长度为 1 的字符串 `str` 。 - C 和 C++ 未明确规定基本数据类型大小,而因实现和平台各异。表 3-1 遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。 diff --git a/docs/chapter_data_structure/character_encoding.md b/docs/chapter_data_structure/character_encoding.md index e144f0491..8fff382e0 100644 --- a/docs/chapter_data_structure/character_encoding.md +++ b/docs/chapter_data_structure/character_encoding.md @@ -90,7 +90,7 @@ UTF-8 的编码规则并不复杂,分为以下两种情况。 出于以上原因,部分编程语言提出了一些不同的编码方案。 -- Python 中的 `str` 使用 Unicode 编码,并采用一种灵活的字符串表示,存储的字符长度取决于字符串中最大的 Unicode 码点。若字符串中全部是 ASCII 字符,则每个字符占用 1 个字节;如果有字符超出了 ASCII 范围,但全部在基本多语言平面(BMP)内,则每个字符占用 2 个字节;如果有超出 BMP 的字符,则每个字符占用 4 个字节。 +- Python 中的 `str` 使用 Unicode 编码,并采用一种灵活的字符串表示,存储的字符长度取决于字符串中最大的 Unicode 码点。若字符串中全部是 ASCII 字符,则每个字符占用 1 字节;如果有字符超出了 ASCII 范围,但全部在基本多语言平面(BMP)内,则每个字符占用 2 字节;如果有超出 BMP 的字符,则每个字符占用 4 字节。 - Go 语言的 `string` 类型在内部使用 UTF-8 编码。Go 语言还提供了 `rune` 类型,它用于表示单个 Unicode 码点。 - Rust 语言的 str 和 String 类型在内部使用 UTF-8 编码。Rust 也提供了 `char` 类型,用于表示单个 Unicode 码点。 diff --git a/docs/chapter_data_structure/classification_of_data_structure.md b/docs/chapter_data_structure/classification_of_data_structure.md index 9d8f9599b..7fefd4f61 100644 --- a/docs/chapter_data_structure/classification_of_data_structure.md +++ b/docs/chapter_data_structure/classification_of_data_structure.md @@ -27,7 +27,7 @@ comments: true ## 3.1.2   物理结构:连续与分散 -**当算法程序运行时,正在处理的数据主要被存储在内存中**。图 3-2 展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。 +**当算法程序运行时,正在处理的数据主要存储在内存中**。图 3-2 展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。 **系统通过内存地址来访问目标位置的数据**。如图 3-2 所示,计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。 @@ -37,7 +37,7 @@ comments: true !!! tip - 值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及到地址空间、内存管理、缓存机制、虚拟和物理内存等概念。 + 值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及地址空间、内存管理、缓存机制、虚拟内存和物理内存等概念。 内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。 diff --git a/docs/chapter_data_structure/number_encoding.md b/docs/chapter_data_structure/number_encoding.md index 03f5488b0..104521031 100644 --- a/docs/chapter_data_structure/number_encoding.md +++ b/docs/chapter_data_structure/number_encoding.md @@ -24,7 +24,7 @@ comments: true

图 3-4   原码、反码与补码之间的相互转换

-「原码 sign–magnitude」虽然最直观,但存在一些局限性。一方面,**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。 +「原码 sign-magnitude」虽然最直观,但存在一些局限性。一方面,**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。 $$ \begin{aligned} diff --git a/docs/chapter_data_structure/summary.md b/docs/chapter_data_structure/summary.md index abaeb8261..2d9733edd 100644 --- a/docs/chapter_data_structure/summary.md +++ b/docs/chapter_data_structure/summary.md @@ -21,17 +21,17 @@ comments: true !!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?" - 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续哈希表章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。 - 从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或树。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。 + 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续“哈希冲突”章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。 + 从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或一棵树。因此,哈希表可能同时包含线性数据结构(数组、链表)和非线性数据结构(树)。 !!! question "`char` 类型的长度是 1 byte 吗?" - `char` 类型的长度由编程语言采用的编码方法决定。例如,Java、JavaScript、TypeScript、C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。 + `char` 类型的长度由编程语言采用的编码方法决定。例如,Java、JavaScript、TypeScript、C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes。 -!!! question "基于数组实现的数据结构也被称为“静态数据结构” 是否有歧义?因为栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。" +!!! question "基于数组实现的数据结构也称“静态数据结构” 是否有歧义?因为栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。" - 栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将老数组的内容复制到新数组中。 + 栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将旧数组的内容复制到新数组中。 !!! question "在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?" - 在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作是由类内部自动完成的。例如,Java 的 ArrayList 的初始容量通常为 10 。另外,扩容操作也是自动实现的。详见本书的“列表”章节。 + 在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作由类内部自动完成。例如,Java 的 ArrayList 的初始容量通常为 10。另外,扩容操作也是自动实现的。详见后续的“列表”章节。 diff --git a/docs/chapter_dynamic_programming/dp_problem_features.md b/docs/chapter_dynamic_programming/dp_problem_features.md index de0617d46..b42e0b07a 100644 --- a/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/docs/chapter_dynamic_programming/dp_problem_features.md @@ -556,7 +556,7 @@ $$ 不难发现,此问题已不满足无后效性,状态转移方程 $dp[i] = dp[i-1] + dp[i-2]$ 也失效了,因为 $dp[i-1]$ 代表本轮跳 $1$ 阶,但其中包含了许多“上一轮是跳 $1$ 阶上来的”方案,而为了满足约束,我们就不能将 $dp[i-1]$ 直接计入 $dp[i]$ 中。 -为此,我们需要扩展状态定义:**状态 $[i, j]$ 表示处在第 $i$ 阶并且上一轮跳了 $j$ 阶**,其中 $j \in \{1, 2\}$ 。此状态定义有效地区分了上一轮跳了 $1$ 阶还是 $2$ 阶,我们可以据此来判断当前状态是从何而来的。 +为此,我们需要扩展状态定义:**状态 $[i, j]$ 表示处在第 $i$ 阶并且上一轮跳了 $j$ 阶**,其中 $j \in \{1, 2\}$ 。此状态定义有效地区分了上一轮跳了 $1$ 阶还是 $2$ 阶,我们可以据此判断当前状态是从何而来的。 - 当上一轮跳了 $1$ 阶时,上上一轮只能选择跳 $2$ 阶,即 $dp[i, 1]$ 只能从 $dp[i-1, 2]$ 转移过来。 - 当上一轮跳了 $2$ 阶时,上上一轮可选择跳 $1$ 阶或跳 $2$ 阶,即 $dp[i, 2]$ 可以从 $dp[i-2, 1]$ 或 $dp[i-2, 2]$ 转移过来。 diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index 51f1ea4d2..a227bb496 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -2846,10 +2846,10 @@ comments: true 平方探测主要具有以下优势。 -- 平方探测通过跳过平方的距离,试图缓解线性探测的聚集效应。 +- 平方探测通过跳过探测次数平方的距离,试图缓解线性探测的聚集效应。 - 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。 -然而,平方探测也并不是完美的。 +然而,平方探测并不是完美的。 - 仍然存在聚集现象,即某些位置比其他位置更容易被占用。 - 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。 @@ -2869,7 +2869,7 @@ comments: true ## 6.2.3   编程语言的选择 -各个编程语言采取了不同的哈希表实现策略,以下举几个例子。 +各种编程语言采取了不同的哈希表实现策略,下面举几个例子。 - Python 采用开放寻址。字典 dict 使用伪随机数进行探测。 - Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会转换为红黑树以提升查找性能。 diff --git a/docs/chapter_hashing/summary.md b/docs/chapter_hashing/summary.md index 39e4b3cd7..5b852864b 100644 --- a/docs/chapter_hashing/summary.md +++ b/docs/chapter_hashing/summary.md @@ -22,7 +22,7 @@ comments: true ### 2.   Q & A -!!! question "哈希表的时间复杂度为什么不是 $O(n)$ ?" +!!! question "哈希表的时间复杂度在什么情况下是 $O(n)$ ?" 当哈希冲突比较严重时,哈希表的时间复杂度会退化至 $O(n)$ 。当哈希函数设计得比较好、容量设置比较合理、冲突比较平均时,时间复杂度是 $O(1)$ 。我们使用编程语言内置的哈希表时,通常认为时间复杂度是 $O(1)$ 。 diff --git a/docs/chapter_heap/build_heap.md b/docs/chapter_heap/build_heap.md index 470735180..1ec536df9 100644 --- a/docs/chapter_heap/build_heap.md +++ b/docs/chapter_heap/build_heap.md @@ -18,14 +18,14 @@ comments: true 实际上,我们可以实现一种更为高效的建堆方法,共分为两步。 -1. 将列表所有元素原封不动添加到堆中,此时堆的性质尚未得到满足。 -2. 倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。 +1. 将列表所有元素原封不动地添加到堆中,此时堆的性质尚未得到满足。 +2. 倒序遍历堆(层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。 **每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆**。而由于是倒序遍历,因此堆是“自下而上”构建的。 之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。 -值得说明的是,**叶节点没有子节点,天然就是合法的子堆,因此无须堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化。 +值得说明的是,**由于叶节点没有子节点,因此它们天然就是合法的子堆,无须堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化: === "Python" diff --git a/docs/chapter_stack_and_queue/summary.md b/docs/chapter_stack_and_queue/summary.md index 5fda37738..58c7cf4a9 100644 --- a/docs/chapter_stack_and_queue/summary.md +++ b/docs/chapter_stack_and_queue/summary.md @@ -28,7 +28,7 @@ comments: true !!! question "撤销(undo)和反撤销(redo)具体是如何实现的?" - 使用两个堆栈,栈 `A` 用于撤销,栈 `B` 用于反撤销。 + 使用两个栈,栈 `A` 用于撤销,栈 `B` 用于反撤销。 1. 每当用户执行一个操作,将这个操作压入栈 `A` ,并清空栈 `B` 。 2. 当用户执行“撤销”时,从栈 `A` 中弹出最近的操作,并将其压入栈 `B` 。 diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index ecac354ca..afd9d1a75 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -1118,12 +1118,12 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中
-| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | -| ------------------- | ---------------- | ---------------- | -| $> 1$ (左偏树) | $\geq 0$ | 右旋 | -| $> 1$ (左偏树) | $<0$ | 先左旋后右旋 | -| $< -1$ (右偏树) | $\leq 0$ | 左旋 | -| $< -1$ (右偏树) | $>0$ | 先右旋后左旋 | +| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | +| ------------------ | ---------------- | ---------------- | +| $> 1$ (左偏树) | $\geq 0$ | 右旋 | +| $> 1$ (左偏树) | $<0$ | 先左旋后右旋 | +| $< -1$ (右偏树) | $\leq 0$ | 左旋 | +| $< -1$ (右偏树) | $>0$ | 先右旋后左旋 |