diff --git a/codes/python/modules/__init__.py b/codes/python/modules/__init__.py index 0ff746b7d..346cefb0e 100644 --- a/codes/python/modules/__init__.py +++ b/codes/python/modules/__init__.py @@ -1,4 +1,4 @@ -# Follow the PEP 585 – Type Hinting Generics In Standard Collections +# Follow the PEP 585 - Type Hinting Generics In Standard Collections # https://peps.python.org/pep-0585/ from __future__ import annotations diff --git a/docs/chapter_appendix/terminology.md b/docs/chapter_appendix/terminology.md index 0a8c1c794..30940ebac 100644 --- a/docs/chapter_appendix/terminology.md +++ b/docs/chapter_appendix/terminology.md @@ -1,6 +1,6 @@ # 术语表 -下表列出了书中出现的重要术语。建议你同时记住它们的中英文叫法,以便阅读英文文献。 +下表列出了书中出现的重要术语。建议读者同时记住它们的中英文叫法,以便阅读英文文献。

  数据结构与算法的重要名词

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

  计算机的存储设备

@@ -20,52 +20,52 @@ 我们可以将计算机存储系统想象为下图所示的金字塔结构。越靠近金字塔顶端的存储设备的速度越快、容量越小、成本越高。这种多层级的设计并非偶然,而是计算机科学家和工程师们经过深思熟虑的结果。 -- **硬盘难以被内存取代**。首先,内存中的数据在断电后会丢失,因此它不适合长期存储数据;其次,内存的成本大约是硬盘的几十倍,这使得它难以在消费者市场普及。 +- **硬盘难以被内存取代**。首先,内存中的数据在断电后会丢失,因此它不适合长期存储数据;其次,内存的成本是硬盘的几十倍,这使得它难以在消费者市场普及。 - **缓存的大容量和高速度难以兼得**。随着 L1、L2、L3 缓存的容量逐步增大,其物理尺寸会变大,与 CPU 核心之间的物理距离会变远,从而导致数据传输时间增加,元素访问延迟变高。在当前技术下,多层级的缓存结构是容量、速度和成本之间的最佳平衡点。 ![计算机存储系统](ram_and_cache.assets/storage_pyramid.png) !!! note - 计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳的平衡点。 + 计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳平衡点。 -总的来说,**硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令**,以提高程序运行效率。这三者共同协作,确保计算机系统的高效运行。 +总的来说,**硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令**,以提高程序运行效率。三者共同协作,确保计算机系统高效运行。 -如下图所示,在程序运行时,数据会从硬盘中被读取到内存中,供给 CPU 计算使用。缓存可以看作 CPU 的一部分,**它通过智能地从内存加载数据**,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。 +如下图所示,在程序运行时,数据会从硬盘中被读取到内存中,供 CPU 计算使用。缓存可以看作 CPU 的一部分,**它通过智能地从内存加载数据**,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。 ![硬盘、内存和缓存之间的数据流通](ram_and_cache.assets/computer_storage_devices.png) ## 数据结构的内存效率 -在内存空间利用方面,数组和链表具有各自的优势和局限。 +在内存空间利用方面,数组和链表各自具有优势和局限性。 -一方面,**内存是有限的,且同一块内存不能被多个程序共享**,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存的浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,这种方式提供了更大的灵活性。 +一方面,**内存是有限的,且同一块内存不能被多个程序共享**,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,提供了更大的灵活性。 另一方面,在程序运行时,**随着反复申请与释放内存,空闲内存的碎片化程度会越来越高**,从而导致内存的利用效率降低。数组由于其连续的存储方式,相对不容易导致内存碎片化。相反,链表的元素是分散存储的,在频繁的插入与删除操作中,更容易导致内存碎片化。 ## 数据结构的缓存效率 -缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,它只能存储一小部分频繁访问的数据。因此,当 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 7fec1113b..552b70365 100644 --- a/docs/chapter_array_and_linkedlist/summary.md +++ b/docs/chapter_array_and_linkedlist/summary.md @@ -8,15 +8,15 @@ - 常见的链表类型包括单向链表、环形链表、双向链表,它们分别具有各自的应用场景。 - 列表是一种支持增删查改的元素有序集合,通常基于动态数组实现,其保留了数组的优势,同时可以灵活调整长度。 - 列表的出现大幅地提高了数组的实用性,但可能导致部分内存空间浪费。 -- 程序运行时,数据主要存储在内存中。数组提供更高的内存空间效率,而链表则在内存使用上更加灵活。 -- 缓存通过缓存行、预取机制以及空间和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。 +- 程序运行时,数据主要存储在内存中。数组可提供更高的内存空间效率,而链表则在内存使用上更加灵活。 +- 缓存通过缓存行、预取机制以及空间局部性和时间局部性等数据加载机制,为 CPU 提供快速数据访问,显著提升程序的执行效率。 - 由于数组具有更高的缓存命中率,因此它通常比链表更高效。在选择数据结构时,应根据具体需求和场景做出恰当选择。 ### Q & A !!! question "数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?" - 存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率是基本一致的。然而,栈和堆具有各自的特点,从而导致以下不同点。 + 存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。 1. 分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。 2. 大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。 @@ -78,4 +78,4 @@ !!! question "在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?" - 从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它也会影响后继节点的内存回收。 + 从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。 diff --git a/docs/chapter_computational_complexity/iteration_and_recursion.md b/docs/chapter_computational_complexity/iteration_and_recursion.md index 591461d99..b8e3db273 100644 --- a/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -1,6 +1,6 @@ # 迭代与递归 -在算法中,重复执行某个任务是很常见的,其与复杂度分析息息相关。因此,在展开介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。 +在算法中,重复执行某个任务是很常见的,它与复杂度分析息息相关。因此,在介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。 ## 迭代 @@ -173,12 +173,12 @@ 如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。 -那么,迭代和递归具有什么内在联系呢?以上述的递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则是异曲同工的**。 +那么,迭代和递归具有什么内在联系呢?以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则异曲同工**。 事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。 1. **递**:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。 -2. **归**:当函数完成执行并返回时,对应的栈帧会从“调用栈”上被移除,恢复之前函数的执行环境。 +2. **归**:当函数完成执行并返回时,对应的栈帧会被从“调用栈”上移除,恢复之前函数的执行环境。 因此,**我们可以使用一个显式的栈来模拟调用栈的行为**,从而将递归转化为迭代形式: @@ -186,9 +186,9 @@ [file]{recursion}-[class]{}-[func]{for_loop_recur} ``` -观察以上代码,当递归被转换为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转换,但也不一定值得这样做,有以下两点原因。 +观察以上代码,当递归转化为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转化,但不一定值得这样做,有以下两点原因。 - 转化后的代码可能更加难以理解,可读性更差。 - 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。 -总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法是至关重要的。 +总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法至关重要。 diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index c0322af9d..39d2371b0 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -760,7 +760,7 @@ $$ ### 指数阶 $O(2^n)$ -指数阶常见于二叉树。观察下图,高度为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: +指数阶常见于二叉树。观察下图,层数为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: ```src [file]{space_complexity}-[class]{}-[func]{build_tree} diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index ec243f1de..71dca5876 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -31,7 +31,7 @@ | 字符 | `char` | 2 bytes | $0$ | $2^{16} - 1$ | $0$ | | 布尔 | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ | -请注意,上表针对的是 Java 的基本数据类型的情况。每种编程语言有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。 +请注意,上表针对的是 Java 的基本数据类型的情况。每种编程语言都有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。 - 在 Python 中,整数类型 `int` 可以是任意大小,只受限于可用内存;浮点数 `float` 是双精度 64 位;没有 `char` 类型,单个字符实际上是长度为 1 的字符串 `str` 。 - C 和 C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 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 6289d0e54..47f78339f 100644 --- a/docs/chapter_data_structure/character_encoding.md +++ b/docs/chapter_data_structure/character_encoding.md @@ -80,7 +80,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 281bd20f7..b0938d1c2 100644 --- a/docs/chapter_data_structure/classification_of_data_structure.md +++ b/docs/chapter_data_structure/classification_of_data_structure.md @@ -21,7 +21,7 @@ ## 物理结构:连续与分散 -**当算法程序运行时,正在处理的数据主要被存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。 +**当算法程序运行时,正在处理的数据主要存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。 **系统通过内存地址来访问目标位置的数据**。如下图所示,计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。 @@ -29,7 +29,7 @@ !!! tip - 值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及到地址空间、内存管理、缓存机制、虚拟和物理内存等概念。 + 值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及地址空间、内存管理、缓存机制、虚拟内存和物理内存等概念。 内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。 diff --git a/docs/chapter_data_structure/number_encoding.md b/docs/chapter_data_structure/number_encoding.md index ff06063dd..e32f05f45 100644 --- a/docs/chapter_data_structure/number_encoding.md +++ b/docs/chapter_data_structure/number_encoding.md @@ -18,7 +18,7 @@ ![原码、反码与补码之间的相互转换](number_encoding.assets/1s_2s_complement.png) -「原码 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 92638457d..f6f073899 100644 --- a/docs/chapter_data_structure/summary.md +++ b/docs/chapter_data_structure/summary.md @@ -17,17 +17,17 @@ !!! 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 "在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?" - 在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作是由类内部自动完成的。例如,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 130a0ec35..830dfa226 100644 --- a/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/docs/chapter_dynamic_programming/dp_problem_features.md @@ -68,7 +68,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 7d96ded7a..787d2e5b9 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -78,10 +78,10 @@ 平方探测主要具有以下优势。 -- 平方探测通过跳过平方的距离,试图缓解线性探测的聚集效应。 +- 平方探测通过跳过探测次数平方的距离,试图缓解线性探测的聚集效应。 - 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。 -然而,平方探测也并不是完美的。 +然而,平方探测并不是完美的。 - 仍然存在聚集现象,即某些位置比其他位置更容易被占用。 - 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。 @@ -101,7 +101,7 @@ ## 编程语言的选择 -各个编程语言采取了不同的哈希表实现策略,以下举几个例子。 +各种编程语言采取了不同的哈希表实现策略,下面举几个例子。 - Python 采用开放寻址。字典 dict 使用伪随机数进行探测。 - Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会转换为红黑树以提升查找性能。 diff --git a/docs/chapter_hashing/summary.md b/docs/chapter_hashing/summary.md index 4265fb76d..0a7fb60fe 100644 --- a/docs/chapter_hashing/summary.md +++ b/docs/chapter_hashing/summary.md @@ -18,7 +18,7 @@ ### 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 66f3bed10..26a006722 100644 --- a/docs/chapter_heap/build_heap.md +++ b/docs/chapter_heap/build_heap.md @@ -14,14 +14,14 @@ 实际上,我们可以实现一种更为高效的建堆方法,共分为两步。 -1. 将列表所有元素原封不动添加到堆中,此时堆的性质尚未得到满足。 -2. 倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。 +1. 将列表所有元素原封不动地添加到堆中,此时堆的性质尚未得到满足。 +2. 倒序遍历堆(层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。 **每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆**。而由于是倒序遍历,因此堆是“自下而上”构建的。 之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。 -值得说明的是,**叶节点没有子节点,天然就是合法的子堆,因此无须堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化。 +值得说明的是,**由于叶节点没有子节点,因此它们天然就是合法的子堆,无须堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化: ```src [file]{my_heap}-[class]{max_heap}-[func]{__init__} diff --git a/docs/chapter_stack_and_queue/summary.md b/docs/chapter_stack_and_queue/summary.md index 4330e2039..60646a6b2 100644 --- a/docs/chapter_stack_and_queue/summary.md +++ b/docs/chapter_stack_and_queue/summary.md @@ -24,7 +24,7 @@ !!! 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 684162943..ba5c1d8e3 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -294,12 +294,12 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中

  四种旋转情况的选择条件

-| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | -| ------------------- | ---------------- | ---------------- | -| $> 1$ (左偏树) | $\geq 0$ | 右旋 | -| $> 1$ (左偏树) | $<0$ | 先左旋后右旋 | -| $< -1$ (右偏树) | $\leq 0$ | 左旋 | -| $< -1$ (右偏树) | $>0$ | 先右旋后左旋 | +| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | +| ------------------ | ---------------- | ---------------- | +| $> 1$ (左偏树) | $\geq 0$ | 右旋 | +| $> 1$ (左偏树) | $<0$ | 先左旋后右旋 | +| $< -1$ (右偏树) | $\leq 0$ | 左旋 | +| $< -1$ (右偏树) | $>0$ | 先右旋后左旋 | 为了便于使用,我们将旋转操作封装成一个函数。**有了这个函数,我们就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡**。代码如下所示: