Polish the contents

1. Array and linked list.
2. Computational complexity.
3. Fix a mistake in counting_sort.md
This commit is contained in:
krahets
2023-04-07 22:31:50 +08:00
parent b7daddf88f
commit de6945f55a
11 changed files with 164 additions and 165 deletions
@@ -1,10 +1,10 @@
# 链表
内存空间是所有程序的公共资源,排除已被占用的内存空间,空闲内存空间往往是散落在内存各处的。上节讲到,**存储数组的内存空间必须是连续的**,当我们需要申请一个非常大的数组时,系统不一定能够分配这么大的连续内存空间。
内存空间是所有程序的公共资源,排除已被占用的内存空间,空闲内存空间通常散落在内存各处。在上一节中,我们提到存储数组的内存空间必须是连续的,当我们需要申请一个非常大的数组时,空闲内存中可能没有这么大的连续空间。
相对地,链表则更加灵活,可以存储非连续的内存空间。「链表 Linked List」是一种线性数据结构,其每个元素都是单独的对象,各个元素(即结点之间通过指针连接。由于结点中记录了连接关系,因此链表的存储方式相比于数组更加灵活,系统可将结点分散在内存各处,而不必保证内存地址的连续性
与数组相比,链表更具灵活性,因为它可以存储非连续的内存空间。「链表 Linked List」是一种线性数据结构,其每个元素都是一个结点对象,各个结点之间通过指针连接,从当前结点通过指针可以访问到下一个结点。由于指针记录了下个结点的内存地址,因此无需保证内存地址的连续性,从而可以将各个结点分散存储在内存各处。
链表「结点 Node」包含两项数据,一是结点「值 Value」,二是指向下一结点的「指针 Pointer」或称「引用 Reference」
链表「结点 Node」包含两项数据,一是结点「值 Value」,二是指向下一结点的「指针 Pointer」或称指向下一结点的「引用 Reference」。
![链表定义与存储方式](linked_list.assets/linkedlist_definition.png)
@@ -154,13 +154,15 @@
}
```
**尾结点指向什么?** 我们一般将链表的最后一个结点称为「尾结点」,其指向的是「空」,在 Java / C++ / Python 中分别记为 `null` / `nullptr` / `None` 。在不引起歧义下,本书都使用 `null` 来表示空。
!!! question "尾结点指向什么?"
**链表初始化方法**。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的首个结点(即头结点)出发,访问其余所有的结点
我们将链表的最后一个结点称为「尾结点」,其指向的是“空”,在 Java, C++, Python 中分别记为 `null`, `nullptr`, `None` 。在不引起歧义的前提下,本书都使用 `null` 来表示空
!!! tip
!!! question "如何称呼链表?"
我们通常将头结点当作链表的代称,例如头结点 `head` 和链表 `head` 实际上是同义的。
在编程语言中,数组整体就是一个变量,例如数组 `nums` ,包含各个元素 `nums[0]` , `nums[1]` 等等。而链表是由多个结点对象组成,我们通常将头结点当作链表的代称,例如头结点 `head` 和链表 `head` 实际上是同义的。
**链表初始化方法**。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的头结点(即首个结点)出发,通过指针 `next` 依次访问所有结点。
=== "Java"
@@ -223,7 +225,6 @@
n2 := NewListNode(2)
n3 := NewListNode(5)
n4 := NewListNode(4)
// 构建引用指向
n0.Next = n1
n1.Next = n2
@@ -335,7 +336,7 @@
## 链表优点
**链表中插入与删除结点的操作效率高**。如,如果我们想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ 相比数组的插入操作高效很多。
**链表中插入与删除结点的操作效率高**。如,如果我们想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ 相比之下,数组的插入操作效率要低得多。
![链表插入结点](linked_list.assets/linkedlist_insert_node.png)
@@ -399,7 +400,7 @@
[class]{}-[func]{insert}
```
在链表中删除结点也方便,只需改变一个结点指针即可。如下图所示,虽然在完成删除后结点 `P` 仍然指向 `n1` ,但实际上 `P` 已经不属于此链表,因为遍历此链表无法访问到 `P`
在链表中删除结点也非常方便,只需改变一个结点指针即可。如下图所示,尽管在删除操作完成后,结点 `P` 仍然指向 `n1`,但实际上 `P` 已经不属于此链表,因为遍历此链表无法访问到 `P`。
![链表删除结点](linked_list.assets/linkedlist_remove_node.png)
@@ -465,7 +466,7 @@
## 链表缺点
**链表访问结点效率低**。上节提到,数组可以在 $O(1)$ 时间下访问任意元素,链表无法直接访问任意结点这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作
**链表访问结点效率低**。上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意结点这是因为系统需要从头结点出发,逐个向后遍历直至找到目标结点。例如,要访问链表索引为 `index`(即第 `index + 1` 个)的结点,则需要向后遍历 `index`
=== "Java"
@@ -527,7 +528,7 @@
[class]{}-[func]{access}
```
**链表的内存占用**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。
**链表的内存占用较大**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多内存空间。
## 链表常用操作
@@ -595,11 +596,11 @@
## 常见链表类型
**单向链表**。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的指针(引用)两项数据。我们将首个结点称为头结点,尾结点指向 `null` 。
**单向链表**。即上述介绍的普通链表。单向链表的结点包含值和指向下一结点的指针(引用)两项数据。我们将首个结点称为头结点,将最后一个结点成为尾结点,尾结点指向 `null` 。
**环形链表**。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,我们可以将任意结点看作是头结点。
**环形链表**。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,任意结点都可以视作头结点。
**双向链表**。单向链表记录了个方向的指针(引用),在双向链表的结点定义中,同时有指向下一结点(后继结点)和上一结点(前驱结点)的指针(引用)」。双向链表相对于单向链表更灵活,可以朝两个方向遍历链表,但也需要占用更多的内存空间。
**双向链表**。单向链表相比,双向链表记录了个方向的指针(引用)双向链表的结点定义同时包含指向后继结点(下一结点)和前驱结点(上一结点)的指针。相较于单向链表,双向链表更灵活,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。
=== "Java"