Revised the book. (#987)

This commit is contained in:
Yudong Jin 2023-12-14 02:42:53 +08:00 committed by GitHub
parent d67c458caa
commit ed4de4644f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 58 additions and 58 deletions

View file

@ -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/ # https://peps.python.org/pep-0585/
from __future__ import annotations from __future__ import annotations

View file

@ -1,6 +1,6 @@
# 术语表 # 术语表
下表列出了书中出现的重要术语。建议同时记住它们的中英文叫法,以便阅读英文文献。 下表列出了书中出现的重要术语。建议读者同时记住它们的中英文叫法,以便阅读英文文献。
<p align="center"><id> &nbsp; 数据结构与算法的重要名词 </p> <p align="center"><id> &nbsp; 数据结构与算法的重要名词 </p>
@ -18,7 +18,7 @@
| 大 | big- | 优先队列 | priority queue | | 大 | big- | 优先队列 | priority queue |
| 记号 | notation | | | | 记号 | notation | | |
| 渐近上界 | asymptotic upper bound | 堆化 | heapify | | 渐近上界 | asymptotic upper bound | 堆化 | heapify |
| 原码 | signmagnitude | 图 | graph | | 原码 | sign-magnitude | 图 | graph |
| 反码 | 1s complement | 顶点 | vertex | | 反码 | 1s complement | 顶点 | vertex |
| 补码 | 2s complement | 无向图 | undirected graph | | 补码 | 2s complement | 无向图 | undirected graph |
| 数组 | array | 有向图 | directed graph | | 数组 | array | 有向图 | directed graph |

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# 迭代与递归 # 迭代与递归
在算法中,重复执行某个任务是很常见的,其与复杂度分析息息相关。因此,在展开介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。 在算法中,重复执行某个任务是很常见的,它与复杂度分析息息相关。因此,在介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。
## 迭代 ## 迭代
@ -173,12 +173,12 @@
如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。 如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。
那么,迭代和递归具有什么内在联系呢?以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则异曲同工**。 那么,迭代和递归具有什么内在联系呢?以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则异曲同工**。
事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。 事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。
1. **递**:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。 1. **递**:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。
2. **归**:当函数完成执行并返回时,对应的栈帧会从“调用栈”上移除,恢复之前函数的执行环境。 2. **归**:当函数完成执行并返回时,对应的栈帧会从“调用栈”上移除,恢复之前函数的执行环境。
因此,**我们可以使用一个显式的栈来模拟调用栈的行为**,从而将递归转化为迭代形式: 因此,**我们可以使用一个显式的栈来模拟调用栈的行为**,从而将递归转化为迭代形式:
@ -186,9 +186,9 @@
[file]{recursion}-[class]{}-[func]{for_loop_recur} [file]{recursion}-[class]{}-[func]{for_loop_recur}
``` ```
观察以上代码,当递归被转换为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转换,但也不一定值得这样做,有以下两点原因。 观察以上代码,当递归转化为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转化,但不一定值得这样做,有以下两点原因。
- 转化后的代码可能更加难以理解,可读性更差。 - 转化后的代码可能更加难以理解,可读性更差。
- 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。 - 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。
总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法至关重要 总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法至关重要。

View file

@ -760,7 +760,7 @@ $$
### 指数阶 $O(2^n)$ ### 指数阶 $O(2^n)$
指数阶常见于二叉树。观察下图,高度为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间: 指数阶常见于二叉树。观察下图,层数为 $n$ 的“满二叉树”的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间:
```src ```src
[file]{space_complexity}-[class]{}-[func]{build_tree} [file]{space_complexity}-[class]{}-[func]{build_tree}

View file

@ -31,7 +31,7 @@
| 字符 | `char` | 2 bytes | $0$ | $2^{16} - 1$ | $0$ | | 字符 | `char` | 2 bytes | $0$ | $2^{16} - 1$ | $0$ |
| 布尔 | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ | | 布尔 | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ |
请注意,上表针对的是 Java 的基本数据类型的情况。每种编程语言有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。 请注意,上表针对的是 Java 的基本数据类型的情况。每种编程语言有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。
- 在 Python 中,整数类型 `int` 可以是任意大小,只受限于可用内存;浮点数 `float` 是双精度 64 位;没有 `char` 类型,单个字符实际上是长度为 1 的字符串 `str` - 在 Python 中,整数类型 `int` 可以是任意大小,只受限于可用内存;浮点数 `float` 是双精度 64 位;没有 `char` 类型,单个字符实际上是长度为 1 的字符串 `str`
- C 和 C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。 - C 和 C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于包括 Linux 和 macOS 在内的 Unix 64 位操作系统。

View file

@ -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 码点。 - Go 语言的 `string` 类型在内部使用 UTF-8 编码。Go 语言还提供了 `rune` 类型,它用于表示单个 Unicode 码点。
- Rust 语言的 str 和 String 类型在内部使用 UTF-8 编码。Rust 也提供了 `char` 类型,用于表示单个 Unicode 码点。 - Rust 语言的 str 和 String 类型在内部使用 UTF-8 编码。Rust 也提供了 `char` 类型,用于表示单个 Unicode 码点。

View file

@ -21,7 +21,7 @@
## 物理结构:连续与分散 ## 物理结构:连续与分散
**当算法程序运行时,正在处理的数据主要存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。 **当算法程序运行时,正在处理的数据主要存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。
**系统通过内存地址来访问目标位置的数据**。如下图所示,计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。 **系统通过内存地址来访问目标位置的数据**。如下图所示,计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。
@ -29,7 +29,7 @@
!!! tip !!! tip
值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及地址空间、内存管理、缓存机制、虚拟和物理内存等概念。 值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及地址空间、内存管理、缓存机制、虚拟内存和物理内存等概念。
内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。 内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。

View file

@ -18,7 +18,7 @@
![原码、反码与补码之间的相互转换](number_encoding.assets/1s_2s_complement.png) ![原码、反码与补码之间的相互转换](number_encoding.assets/1s_2s_complement.png)
「原码 signmagnitude」虽然最直观但存在一些局限性。一方面**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。 「原码 sign-magnitude」虽然最直观但存在一些局限性。一方面**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。
$$ $$
\begin{aligned} \begin{aligned}

View file

@ -17,17 +17,17 @@
!!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?" !!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?"
哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续哈希表章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续“哈希冲突”章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。
从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或树。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。 从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或一棵树。因此,哈希表可能同时包含线性数据结构(数组、链表)和非线性数据结构(树)
!!! question "`char` 类型的长度是 1 byte 吗?" !!! 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。另外扩容操作也是自动实现的。详见“栈”章节。

View file

@ -68,7 +68,7 @@ $$
不难发现,此问题已不满足无后效性,状态转移方程 $dp[i] = dp[i-1] + dp[i-2]$ 也失效了,因为 $dp[i-1]$ 代表本轮跳 $1$ 阶,但其中包含了许多“上一轮是跳 $1$ 阶上来的”方案,而为了满足约束,我们就不能将 $dp[i-1]$ 直接计入 $dp[i]$ 中。 不难发现,此问题已不满足无后效性,状态转移方程 $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]$ 转移过来。 - 当上一轮跳了 $1$ 阶时,上上一轮只能选择跳 $2$ 阶,即 $dp[i, 1]$ 只能从 $dp[i-1, 2]$ 转移过来。
- 当上一轮跳了 $2$ 阶时,上上一轮可选择跳 $1$ 阶或跳 $2$ 阶,即 $dp[i, 2]$ 可以从 $dp[i-2, 1]$ 或 $dp[i-2, 2]$ 转移过来。 - 当上一轮跳了 $2$ 阶时,上上一轮可选择跳 $1$ 阶或跳 $2$ 阶,即 $dp[i, 2]$ 可以从 $dp[i-2, 1]$ 或 $dp[i-2, 2]$ 转移过来。

View file

@ -78,10 +78,10 @@
平方探测主要具有以下优势。 平方探测主要具有以下优势。
- 平方探测通过跳过平方的距离,试图缓解线性探测的聚集效应。 - 平方探测通过跳过探测次数平方的距离,试图缓解线性探测的聚集效应。
- 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。 - 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。
然而,平方探测并不是完美的。 然而,平方探测并不是完美的。
- 仍然存在聚集现象,即某些位置比其他位置更容易被占用。 - 仍然存在聚集现象,即某些位置比其他位置更容易被占用。
- 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。 - 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。
@ -101,7 +101,7 @@
## 编程语言的选择 ## 编程语言的选择
编程语言采取了不同的哈希表实现策略,下举几个例子。 编程语言采取了不同的哈希表实现策略,下举几个例子。
- Python 采用开放寻址。字典 dict 使用伪随机数进行探测。 - Python 采用开放寻址。字典 dict 使用伪随机数进行探测。
- Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会转换为红黑树以提升查找性能。 - Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会转换为红黑树以提升查找性能。

View file

@ -18,7 +18,7 @@
### Q & A ### Q & A
!!! question "哈希表的时间复杂度为什么不是 $O(n)$ " !!! question "哈希表的时间复杂度在什么情况下是 $O(n)$ "
当哈希冲突比较严重时,哈希表的时间复杂度会退化至 $O(n)$ 。当哈希函数设计得比较好、容量设置比较合理、冲突比较平均时,时间复杂度是 $O(1)$ 。我们使用编程语言内置的哈希表时,通常认为时间复杂度是 $O(1)$ 。 当哈希冲突比较严重时,哈希表的时间复杂度会退化至 $O(n)$ 。当哈希函数设计得比较好、容量设置比较合理、冲突比较平均时,时间复杂度是 $O(1)$ 。我们使用编程语言内置的哈希表时,通常认为时间复杂度是 $O(1)$ 。

View file

@ -14,14 +14,14 @@
实际上,我们可以实现一种更为高效的建堆方法,共分为两步。 实际上,我们可以实现一种更为高效的建堆方法,共分为两步。
1. 将列表所有元素原封不动添加到堆中,此时堆的性质尚未得到满足。 1. 将列表所有元素原封不动添加到堆中,此时堆的性质尚未得到满足。
2. 倒序遍历堆(层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。 2. 倒序遍历堆(层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。
**每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆**。而由于是倒序遍历,因此堆是“自下而上”构建的。 **每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆**。而由于是倒序遍历,因此堆是“自下而上”构建的。
之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。 之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。
值得说明的是,**叶节点没有子节点,天然就是合法的子堆,因此无须堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化 值得说明的是,**由于叶节点没有子节点,因此它们天然就是合法的子堆,无须堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化
```src ```src
[file]{my_heap}-[class]{max_heap}-[func]{__init__} [file]{my_heap}-[class]{max_heap}-[func]{__init__}

View file

@ -24,7 +24,7 @@
!!! question "撤销undo和反撤销redo具体是如何实现的" !!! question "撤销undo和反撤销redo具体是如何实现的"
使用两个栈,栈 `A` 用于撤销,栈 `B` 用于反撤销。 使用两个栈,栈 `A` 用于撤销,栈 `B` 用于反撤销。
1. 每当用户执行一个操作,将这个操作压入栈 `A` ,并清空栈 `B` 1. 每当用户执行一个操作,将这个操作压入栈 `A` ,并清空栈 `B`
2. 当用户执行“撤销”时,从栈 `A` 中弹出最近的操作,并将其压入栈 `B` 2. 当用户执行“撤销”时,从栈 `A` 中弹出最近的操作,并将其压入栈 `B`

View file

@ -295,7 +295,7 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中
<p align="center"><id> &nbsp; 四种旋转情况的选择条件 </p> <p align="center"><id> &nbsp; 四种旋转情况的选择条件 </p>
| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | | 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 |
| ------------------- | ---------------- | ---------------- | | ------------------ | ---------------- | ---------------- |
| $> 1$ (左偏树) | $\geq 0$ | 右旋 | | $> 1$ (左偏树) | $\geq 0$ | 右旋 |
| $> 1$ (左偏树) | $<0$ | 先左旋后右旋 | | $> 1$ (左偏树) | $<0$ | 先左旋后右旋 |
| $< -1$ 右偏树 | $\leq 0$ | 左旋 | | $< -1$ 右偏树 | $\leq 0$ | 左旋 |