diff --git a/chapter_array_and_linkedlist/array/index.html b/chapter_array_and_linkedlist/array/index.html index b0b9c6d79..b0ea76122 100644 --- a/chapter_array_and_linkedlist/array/index.html +++ b/chapter_array_and_linkedlist/array/index.html @@ -1601,9 +1601,7 @@

4.1.   数组

「数组 Array」是一种将 相同类型元素 存储在 连续内存空间 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。

-

array_definition

-

Fig. 数组定义与存储方式

- +

数组定义与存储方式

Note

观察上图,我们发现 数组首元素的索引为 \(0\) 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 \(1\) 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。

@@ -1681,9 +1679,7 @@

4.1.1.   数组优点

在数组中访问元素非常高效。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。

-

array_memory_location_calculation

-

Fig. 数组元素的内存地址计算

- +

数组元素的内存地址计算

# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
 elementAddr = firtstElementAddr + elementLength * elementIndex
 
@@ -1942,7 +1938,7 @@

数组中插入或删除元素效率低下。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。

-

array_insert_element

+

数组插入元素

@@ -2048,7 +2044,7 @@

删除元素也是类似,如果我们想要删除索引 \(i\) 处的元素,则需要把索引 \(i\) 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。

-

array_remove_element

+

数组删除元素

diff --git a/chapter_array_and_linkedlist/linked_list/index.html b/chapter_array_and_linkedlist/linked_list/index.html index 4ac212fa7..49f5fb4f2 100644 --- a/chapter_array_and_linkedlist/linked_list/index.html +++ b/chapter_array_and_linkedlist/linked_list/index.html @@ -1606,9 +1606,7 @@

「链表 Linked List」是一种线性数据结构,其中每个元素都是单独的对象,各个元素(一般称为结点)之间通过指针连接。由于结点中记录了连接关系,因此链表的存储方式相比于数组更加灵活,系统不必保证内存地址的连续性。

链表的「结点 Node」包含两项数据,一是结点「值 Value」,二是指向下一结点的「指针 Pointer」(或称「引用 Reference」)。

-

linkedlist_definition

-

Fig. 链表定义与存储方式

- +

链表定义与存储方式

@@ -1875,7 +1873,7 @@

4.2.1.   链表优点

在链表中,插入与删除结点的操作效率高。比如,如果我们想在链表中间的两个结点 A , B 之间插入一个新结点 P ,我们只需要改变两个结点指针即可,时间复杂度为 \(O(1)\) ,相比数组的插入操作高效很多。

-

linkedlist_insert_node

+

链表插入结点

@@ -1966,7 +1964,7 @@

在链表中删除结点也很方便,只需要改变一个结点指针即可。如下图所示,虽然在完成删除后结点 P 仍然指向 n2 ,但实际上 P 已经不属于此链表了,因为遍历此链表是无法访问到 P 的。

-

linkedlist_remove_node

+

链表删除结点

@@ -2484,7 +2482,7 @@
-

linkedlist_common_types

+

常见链表种类

Fig. 常见链表类型

diff --git a/chapter_computational_complexity/space_complexity/index.html b/chapter_computational_complexity/space_complexity/index.html index 641f8de2b..03edf26c1 100644 --- a/chapter_computational_complexity/space_complexity/index.html +++ b/chapter_computational_complexity/space_complexity/index.html @@ -1686,9 +1686,7 @@
  • 「栈帧空间」用于保存调用函数的上下文数据。系统每次调用函数都会在栈的顶部创建一个栈帧,函数返回时,栈帧空间会被释放。
  • 「指令空间」用于保存编译后的程序指令,在实际统计中一般忽略不计
  • -

    space_types

    -

    Fig. 算法使用的相关空间

    - +

    算法使用的相关空间

    @@ -2172,9 +2170,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline \text{常数阶} < \text{对数阶} < \text{线性阶} < \text{平方阶} < \text{指数阶} \end{aligned} \]
    -

    space_complexity_common_types

    -

    Fig. 空间复杂度的常见类型

    - +

    空间复杂度的常见类型

    Tip

    部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解空间复杂度含义和推算方法上。

    @@ -2632,9 +2628,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
    -

    space_complexity_recursive_linear

    -

    Fig. 递归函数产生的线性阶空间复杂度

    - +

    递归函数产生的线性阶空间复杂度

    平方阶 \(O(n^2)\)

    平方阶常见于元素数量与 \(n\) 成平方关系的矩阵、图。

    @@ -2882,9 +2876,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
    -

    space_complexity_recursive_quadratic

    -

    Fig. 递归函数产生的平方阶空间复杂度

    - +

    递归函数产生的平方阶空间复杂度

    指数阶 \(O(2^n)\)

    指数阶常见于二叉树。高度为 \(n\) 的「满二叉树」的结点数量为 \(2^n - 1\) ,使用 \(O(2^n)\) 空间。

    @@ -2999,9 +2991,7 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
    -

    space_complexity_exponential

    -

    Fig. 满二叉树下的指数阶空间复杂度

    - +

    满二叉树产生的指数阶空间复杂度

    对数阶 \(O(\log n)\)

    对数阶常见于分治算法、数据类型转换等。

    例如「归并排序」,长度为 \(n\) 的数组可以形成高度为 \(\log n\) 的递归树,因此空间复杂度为 \(O(\log n)\)

    diff --git a/chapter_computational_complexity/time_complexity/index.html b/chapter_computational_complexity/time_complexity/index.html index a7816bf4a..14e783e9c 100644 --- a/chapter_computational_complexity/time_complexity/index.html +++ b/chapter_computational_complexity/time_complexity/index.html @@ -2104,9 +2104,7 @@
    -

    time_complexity_simple_example

    -

    Fig. 算法 A, B, C 的时间增长趋势

    - +

    算法 A, B, C 的时间增长趋势

    相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?

    时间复杂度可以有效评估算法效率。算法 B 运行时间的增长是线性的,在 \(n > 1\) 时慢于算法 A ,在 \(n > 1000000\) 时慢于算法 C 。实质上,只要输入数据大小 \(n\) 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。

    时间复杂度的推算方法更加简便。在时间复杂度分析中,我们可以将统计「计算操作的运行时间」简化为统计「计算操作的数量」,这是因为,无论是运行平台还是计算操作类型,都与算法运行时间的增长趋势无关。因而,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”,这样的简化做法大大降低了估算难度。

    @@ -2246,9 +2244,7 @@ $$ T(n) = O(f(n)) $$

    -

    asymptotic_upper_bound

    -

    Fig. 函数的渐近上界

    - +

    函数的渐近上界

    本质上看,计算渐近上界就是在找一个函数 \(f(n)\)使得在 \(n\) 趋向于无穷大时,\(T(n)\)\(f(n)\) 处于相同的增长级别(仅相差一个常数项 \(c\) 的倍数)

    Tip

    @@ -2476,9 +2472,7 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n! \text{常数阶} < \text{对数阶} < \text{线性阶} < \text{线性对数阶} < \text{平方阶} < \text{指数阶} < \text{阶乘阶} \end{aligned} \]
    -

    time_complexity_common_types

    -

    Fig. 时间复杂度的常见类型

    - +

    时间复杂度的常见类型

    Tip

    部分示例代码需要一些前置知识,包括数组、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解时间复杂度含义和推算方法上。

    @@ -2957,9 +2951,7 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!
    -

    time_complexity_constant_linear_quadratic

    -

    Fig. 常数阶、线性阶、平方阶的时间复杂度

    - +

    常数阶、线性阶、平方阶的时间复杂度

    以「冒泡排序」为例,外层循环 \(n - 1\) 次,内层循环 \(n-1, n-2, \cdots, 2, 1\) 次,平均为 \(\frac{n}{2}\) 次,因此时间复杂度为 \(O(n^2)\)

    \[ O((n - 1) \frac{n}{2}) = O(n^2) @@ -3327,9 +3319,7 @@ O((n - 1) \frac{n}{2}) = O(n^2)
    -

    time_complexity_exponential

    -

    Fig. 指数阶的时间复杂度

    - +

    指数阶的时间复杂度

    在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 \(n\) 次后停止。

    @@ -3538,9 +3528,7 @@ O((n - 1) \frac{n}{2}) = O(n^2)
    -

    time_complexity_logarithmic

    -

    Fig. 对数阶的时间复杂度

    - +

    对数阶的时间复杂度

    与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 \(\log_2 n\) 的递归树。

    @@ -3756,9 +3744,7 @@ O((n - 1) \frac{n}{2}) = O(n^2)
    -

    time_complexity_logarithmic_linear

    -

    Fig. 线性对数阶的时间复杂度

    - +

    线性对数阶的时间复杂度

    阶乘阶 \(O(n!)\)

    阶乘阶对应数学上的「全排列」。即给定 \(n\) 个互不重复的元素,求其所有可能的排列方案,则方案数量为

    \[ @@ -3895,9 +3881,7 @@ n! = n \times (n - 1) \times (n - 2) \times \cdots \times 2 \times 1
    -

    time_complexity_factorial

    -

    Fig. 阶乘阶的时间复杂度

    - +

    阶乘阶的时间复杂度

    2.2.6.   最差、最佳、平均时间复杂度

    某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关。举一个例子,输入一个长度为 \(n\) 数组 nums ,其中 nums 由从 \(1\)\(n\) 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 \(1\) 的索引。我们可以得出以下结论:

    -

    classification_logic_structure

    -

    Fig. 线性与非线性数据结构

    - +

    线性与非线性数据结构

    3.2.2.   物理结构:连续与离散

    Note

    若感到阅读困难,建议先看完下个章节「数组与链表」,再回过头来理解物理结构的含义。

    「物理结构」反映了数据在计算机内存中的存储方式。从本质上看,分别是 数组的连续空间存储链表的离散空间存储。物理结构从底层上决定了数据的访问、更新、增删等操作方法,在时间效率和空间效率方面呈现出此消彼长的特性。

    -

    classification_phisical_structure

    -

    Fig. 连续空间存储与离散空间存储

    - +

    连续空间存储与离散空间存储

    所有数据结构都是基于数组、或链表、或两者组合实现的。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。

    -

    directed_graph

    +

    有向图与无向图

    根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。

    -

    connected_graph

    +

    连通图与非连通图

    我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如,在王者荣耀等游戏中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以使用有权图来表示。

    -

    weighted_graph

    +

    有权图与无权图

    9.1.2.   图常用术语