mirror of
https://github.com/krahets/hello-algo.git
synced 2025-07-25 19:22:27 +08:00
Modify 。**
to **。
for better visualization.
This commit is contained in:
@ -14,7 +14,7 @@ comments: true
|
||||
|
||||
观察上图,我们发现 **数组首元素的索引为 $0$** 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 $1$ 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。
|
||||
|
||||
**数组有多种初始化写法。** 根据实际需要,选代码最短的那一种就好。
|
||||
**数组有多种初始化写法**。根据实际需要,选代码最短的那一种就好。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -83,7 +83,7 @@ comments: true
|
||||
|
||||
## 数组优点
|
||||
|
||||
**在数组中访问元素非常高效。** 这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。
|
||||
**在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。
|
||||
|
||||

|
||||
|
||||
@ -195,7 +195,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
|
||||
## 数组缺点
|
||||
|
||||
**数组在初始化后长度不可变。** 由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
|
||||
**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -317,7 +317,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
}
|
||||
```
|
||||
|
||||
**数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点:
|
||||
**数组中插入或删除元素效率低下**。假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点:
|
||||
|
||||
- **时间复杂度高:** 数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
|
||||
- **丢失元素:** 由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
|
||||
@ -488,7 +488,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
|
||||
## 数组常用操作
|
||||
|
||||
**数组遍历。** 以下介绍两种常用的遍历方法。
|
||||
**数组遍历**。以下介绍两种常用的遍历方法。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -611,7 +611,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
}
|
||||
```
|
||||
|
||||
**数组查找。** 通过遍历数组,查找数组内的指定元素,并输出对应索引。
|
||||
**数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -715,8 +715,8 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
|
||||
## 数组典型应用
|
||||
|
||||
**随机访问。** 如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
|
||||
**随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
|
||||
|
||||
**二分查找。** 例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的“翻开中间,排除一半”的方式,来实现一个查电子字典的算法。
|
||||
**二分查找**。例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的“翻开中间,排除一半”的方式,来实现一个查电子字典的算法。
|
||||
|
||||
**深度学习。** 神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。
|
||||
**深度学习**。神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。
|
||||
|
@ -114,7 +114,7 @@ comments: true
|
||||
|
||||
**尾结点指向什么?** 我们一般将链表的最后一个结点称为「尾结点」,其指向的是「空」,在 Java / C++ / Python 中分别记为 `null` / `nullptr` / `None` 。在不引起歧义下,本书都使用 `null` 来表示空。
|
||||
|
||||
**链表初始化方法。** 建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的首个结点(即头结点)出发,访问其余所有的结点。
|
||||
**链表初始化方法**。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的首个结点(即头结点)出发,访问其余所有的结点。
|
||||
|
||||
!!! tip
|
||||
|
||||
@ -248,7 +248,7 @@ comments: true
|
||||
|
||||
## 链表优点
|
||||
|
||||
**在链表中,插入与删除结点的操作效率高。** 例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。
|
||||
**在链表中,插入与删除结点的操作效率高**。例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。
|
||||
|
||||

|
||||
|
||||
@ -412,7 +412,7 @@ comments: true
|
||||
|
||||
## 链表缺点
|
||||
|
||||
**链表访问结点效率低。** 上节提到,数组可以在 $O(1)$ 时间下访问任意元素,但链表无法直接访问任意结点。这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作。
|
||||
**链表访问结点效率低**。上节提到,数组可以在 $O(1)$ 时间下访问任意元素,但链表无法直接访问任意结点。这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -520,11 +520,11 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
**链表的内存占用多。** 链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。
|
||||
**链表的内存占用多**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。
|
||||
|
||||
## 链表常用操作
|
||||
|
||||
**遍历链表查找。** 遍历链表,查找链表内值为 `target` 的结点,输出结点在链表中的索引。
|
||||
**遍历链表查找**。遍历链表,查找链表内值为 `target` 的结点,输出结点在链表中的索引。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -649,11 +649,11 @@ comments: true
|
||||
|
||||
## 常见链表类型
|
||||
|
||||
**单向链表。** 即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的「指针(引用)」两项数据。我们将首个结点称为头结点,尾结点指向 `null` 。
|
||||
**单向链表**。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的「指针(引用)」两项数据。我们将首个结点称为头结点,尾结点指向 `null` 。
|
||||
|
||||
**环形链表。** 如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,我们可以将任意结点看作是头结点。
|
||||
**环形链表**。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,我们可以将任意结点看作是头结点。
|
||||
|
||||
**双向链表。** 单向链表仅记录了一个方向的指针(引用),在双向链表的结点定义中,同时有指向下一结点(后继结点)和上一结点(前驱结点)的「指针(引用)」。双向链表相对于单向链表更加灵活,即可以朝两个方向遍历链表,但也需要占用更多的内存空间。
|
||||
**双向链表**。单向链表仅记录了一个方向的指针(引用),在双向链表的结点定义中,同时有指向下一结点(后继结点)和上一结点(前驱结点)的「指针(引用)」。双向链表相对于单向链表更加灵活,即可以朝两个方向遍历链表,但也需要占用更多的内存空间。
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
@ -4,13 +4,13 @@ comments: true
|
||||
|
||||
# 列表
|
||||
|
||||
**由于长度不可变,数组的实用性大大降低。** 在很多情况下,我们事先并不知道会输入多少数据,这就为数组长度的选择带来了很大困难。长度选小了,需要在添加数据中频繁地扩容数组;长度选大了,又造成内存空间的浪费。
|
||||
**由于长度不可变,数组的实用性大大降低**。在很多情况下,我们事先并不知道会输入多少数据,这就为数组长度的选择带来了很大困难。长度选小了,需要在添加数据中频繁地扩容数组;长度选大了,又造成内存空间的浪费。
|
||||
|
||||
为了解决此问题,诞生了一种被称为「列表 List」的数据结构。列表可以被理解为长度可变的数组,因此也常被称为「动态数组 Dynamic Array」。列表基于数组实现,继承了数组的优点,同时还可以在程序运行中实时扩容。在列表中,我们可以自由地添加元素,而不用担心超过容量限制。
|
||||
|
||||
## 列表常用操作
|
||||
|
||||
**初始化列表。** 我们通常会使用到“无初始值”和“有初始值”的两种初始化方法。
|
||||
**初始化列表**。我们通常会使用到“无初始值”和“有初始值”的两种初始化方法。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -91,7 +91,7 @@ comments: true
|
||||
List<int> list = numbers.ToList();
|
||||
```
|
||||
|
||||
**访问与更新元素。** 列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问与更新元素,效率很高。
|
||||
**访问与更新元素**。列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问与更新元素,效率很高。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -169,7 +169,7 @@ comments: true
|
||||
list[1] = 0; // 将索引 1 处的元素更新为 0
|
||||
```
|
||||
|
||||
**在列表中添加、插入、删除元素。** 相对于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但是插入与删除元素的效率仍与数组一样低,时间复杂度为 $O(N)$ 。
|
||||
**在列表中添加、插入、删除元素**。相对于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但是插入与删除元素的效率仍与数组一样低,时间复杂度为 $O(N)$ 。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -317,7 +317,7 @@ comments: true
|
||||
list.RemoveAt(3);
|
||||
```
|
||||
|
||||
**遍历列表。** 与数组一样,列表可以使用索引遍历,也可以使用 `for-each` 直接遍历。
|
||||
**遍历列表**。与数组一样,列表可以使用索引遍历,也可以使用 `for-each` 直接遍历。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -437,7 +437,7 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
**拼接两个列表。** 再创建一个新列表 `list1` ,我们可以将其中一个列表拼接到另一个的尾部。
|
||||
**拼接两个列表**。再创建一个新列表 `list1` ,我们可以将其中一个列表拼接到另一个的尾部。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@ -502,7 +502,7 @@ comments: true
|
||||
list.AddRange(list1); // 将列表 list1 拼接到 list 之后
|
||||
```
|
||||
|
||||
**排序列表。** 排序也是常用的方法之一,完成列表排序后,我们就可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法了。
|
||||
**排序列表**。排序也是常用的方法之一,完成列表排序后,我们就可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法了。
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
Reference in New Issue
Block a user