数据结构是存储和组织数据的方式,通过提高存取效率、节省内存和支持复杂操作来提高算法性能和程序性能。 常见的数据结构包括数组、链表、栈、队列、哈希表、二叉树等。
-
操作速度按步数计算,即时间复杂度(衡量操作步数随输入规模的变化)。核心操作:读取、查找、插入、删除。
-
算法运行时所需的额外内存,即空间复杂度(衡量所需的额外内存空间随输入规模变化)。
大 O 记法
| O ( 1 ) | 常数时间,不随数据规模变化。 |
| O ( N ) | 线性时间,每增加一个元素,多一步。 |
| O ( log N ) | 对数时间,数据翻倍,步数 +1。 |
| O ( N² ) | 常见于嵌套循环(冒泡、选择、插入排序)。 - 大 O 只保留最高阶项,忽略常数。 |
-
大 O 默认 最坏情况。
- 平均情况:更贴近实际。
- 插入排序:适合基本有序的数据。
- 选择排序:适合逆序数据。
- 有序数组:插入需要查找位置;查找可更快。
- 二分查找 vs 线性查找
- 100 个元素:线性查找需 100 步,二分查找需 7 步。
- 规律:数组长度 ×2,二分查找最多步数 +1。
各数据结构对比
| 数据结构 | 查找 | 插入 | 删除 | 是否保持顺序 | 典型应用 |
| 数组 | O(1)(按索引)/ O(N)(按值) | O(N) | O(N) | ✅ | 顺序存储、随机访问 |
| 有序数组 | O(log N)(二分) | O(N) | O(N) | ✅ | 需要快速查找、少插入 |
| 链表 | O(N) | O(1)(表头)/ O(N)(一般) | O(1)(头/尾)/ O(N)(一般) | ✅ | 动态数据、频繁增删 |
| 双向链表 | O(N) | O(1)(尾插) | O(1)(头尾删) | ✅ | 队列实现 |
| 栈 | O(N) | O(1) (push) | O(1)(pop) | ❌ | 调用栈、撤销操作 |
| 队列 | O(N) | O(1)(尾入) | O(1)(头出) | ❌ | 调度、任务排队 |
| 散列表 | O(1) | O(1) | O(1) | ❌ | 快速查找、集合 |
| 二叉搜索树 (BST) | O(log N) | O(log N) | O(log N) | ✅ | 有序数据、频繁改动 |
| 图 | O(V+E) | O(1)–O(E) | O(1)–O(E) | ✅ | 关系型数据、网络建模 |
散列表(哈希表)
- 核心思想:键 → 散列函数 → 索引格子。
- 插入、查找、删除:平均 O(1)。
- 冲突处理:分离链接(链表)等。
- 负载因子 ≈ 0.7 最佳。
- 缺点:不保持顺序。
栈与队列
- 栈 (Stack):LIFO(后进先出)。操作限制在栈顶。
- 队列 (Queue):FIFO(先进先出)。插入在尾,删除在头。
- 应用:函数调用栈、任务调度。
递归与快速排序
- 递归:用调用栈实现,必须有基准情形。
- 快速排序:分区 → 递归子数组 → O(N log N) 平均效率。
链表
- 单链表:结点 = 数据 + 指针。
- 插入表头:O(1)。
- 查找/遍历:O(N)。
- 双向链表:结点有前后指针。
- 队列实现可做到插入、删除都是 O(1)。
二叉树
- 二叉搜索树 (BST):
- 左子树 < 根 < 右子树。
- 查找、插入、删除:平均 O(log N)。
- 缺点:若输入有序 → 树退化为链表 → O(N)。
- 删除规则:无子结点 / 单子结点 / 两子结点(用后继替代)。
图
- 图:处理关系型数据。
- 遍历方式:
- BFS(广度优先搜索,用队列)。
- DFS(深度优先搜索,用栈或递归)。
- 加权图:边带有权重。
C版实现
数组
#include <stdio.h>
int main()
{
// 初始化数组和元素个数
int arr[100] = {10, 20, 30, 40, 50}; // 数组初始值
int n = 5; // 当前数组元素个数
int i;
// 1️⃣ 读取(遍历数组)
printf("数组元素:");
for (i = 0; i < n; i++)
{
printf("%d ", arr[i]); // 依次打印每个元素
}
printf("\n");
// 2️⃣ 查找(找到元素的位置)
int target = 30; // 要查找的元素
int pos = -1; // 用于记录找到的位置,-1表示未找到
for (i = 0; i < n; i++)
{
if (arr[i] == target)
{ // 如果当前元素等于目标
pos = i; // 记录位置
break; // 找到后退出循环
}
}
if (pos != -1)
printf("元素 %d 在位置 %d\n", target, pos);
else
printf("未找到元素 %d\n", target);
// 3️⃣ 插入(在位置 2 插入 25)
int insertPos = 2; // 插入位置
int value = 25; // 要插入的值
// 将插入位置及之后的元素右移一位
for (i = n; i > insertPos; i--)
{
arr[i] = arr[i - 1];
}
arr[insertPos] = value; // 插入新元素
n++; // 元素数量增加
printf("插入后数组:");
for (i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
// 4️⃣ 删除(删除位置 3 的元素)
int deletePos = 3; // 要删除的元素位置
// 将删除位置之后的元素左移一位覆盖
for (i = deletePos; i < n - 1; i++)
{
arr[i] = arr[i + 1];
}
n--; // 元素数量减少
printf("删除后数组:");
for (i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
};
================================================================
《数据结构与算法图解 (杰伊•温格罗, 袁志鹏译) (Z-Library).pdf》
第1章 数据结构为何重要
◆ 本章接下来将会分析两种数据结构:数组和集合
◆ 一般数据结构都有以下 4 种操作(或者说用法)。 读取:查看数据结构中某一位置上的数据。对于数组来说,这意味着查看某个索引所指的数据值。例如,查看索引 2 上有什么食品,就是一种读取。 查找:从数据结构中找出某个数据值的所在。对于数组来说,这意味着检查其是否包含某个值,如果包含,那么还得给出其索引。例如,检查”dates”是否存在于食品清单之中,给出其对应的索引,就是一种查找。 插入:给数据结构增加一个数据值。对于数组来说,这意味着多加一个格子并填入一个值。例如,往购物清单中多加一项”figs”,就是一种插入。 删除:从数据结构中移走一个数据值。对于数组来说,这意味着把数组中的某个数据项移走。例如,把购物清单中的”bananas”移走,就是一种删除。
◆ 本书的第一个重要理论:操作的速度,并不按时间计算,而是按步数计算。
1.1.1 读取
◆ 因此,衡量步数是分析速度的关键。 此外,操作的速度,也常被称为时间复杂度。在本书中,我们会提到速度、时间复杂度、效率、性能,但它们其实指的都是步数。
◆ 首先看看读取,即查看数组中某个索引所指的数据值。这只要一步就够了,因为计算机本身就有跳到任一索引位置的能力
1.1.2 查找
◆ 计算机之所以在读取数组中某个索引所指的值时,能直接跳到那个位置上,是因为它具备以下条件。(1) 计算机可以一步就跳到任意一个内存地址上。(就好比,要是你知道大街 123 号在哪儿,那么就可以直奔过去。)(2) 数组本身会记有第一个格子的内存地址,因此,计算机知道这个数组的开头在哪里。(3) 数组的索引从 0 算起。
◆ 这种逐个格子去检查的做法,就是最基本的查找方法—线性查找。
1.1.4 删除
◆ 一个含有 N 个元素的数组,其插入数据的最坏情况会花费 N + 1 步。即插入在数组开头,导致 N 次移动,加上一次插入。
◆ 可以推出,对于含有 N个元素的数组,删除操作最多需要 N 步。
1.2 集合:一条规则决定性能
◆ 集合。它是一种不允许元素重复的数据结构。
1.3 总结
◆ 在 N 个元素的集合中进行插入的最好情况需要 N + 1 步—N 步去确认被插入的值不在集合中,加上最后插入的 1 步。
◆ 最坏的情况则是在集合的开头插入,这时计算机得检查 N 个格子以保证集合不包含那个值,然后用 N 步来把所有值右移,最后再用 1步来插入新值。总共 2N + 1 步。
第2章 算法为何重要
◆ 在计算机的世界里,算法则是指某项操作的过程。上一章我们研究了 4 种主要操作,包括读取、查找、插入和删除。这一章我们还是会经常提到它们,而且一种操作可能会有不止一种做法。也就是说,一种操作会有多种算法的实现。
◆ 有序数组跟上一章讨论的数组几乎一样,唯一区别就是有序数组要求其值总是保持有序(你猜对了)。即每次插入新值时,它会被插入到适当的位置,使整个数组的值仍然按顺序排列。常规的数组则并不考虑是否有序,直接把值加到末尾也没问题。
2.2 查找有序数组
◆ 可以看到,往有序数组中插入新值,需要先做一次查找以确定插入的位置。这是它跟常规数组的关键区别(在性能方面)之一。 虽然插入的性能比不上常规数组,但在查找方面,有序数组却有着特殊优势。
2.4 二分查找与线性查找
◆ 对于长度太小的有序数组,二分查找并不比线性查找好多少。但我们来看看更大的数组。
◆ 2025/04/30发表想法
2^7=128>100
原文:对于拥有 100 个值的数组来说,两种查找需要的最多步数如下所示。
线性查找:100 步
二分查找:7 步
◆ 对于拥有 100 个值的数组来说,两种查找需要的最多步数如下所示。 线性查找:100 步 二分查找:7 步
2.5 总结
◆ 规律就是,每次有序数组长度乘以 2,二分查找所需的最多步数只会加 1。
◆ 如之前所述,比较算法的方式就是比较各自的步数。
第3章 大O记法
◆ 量化线性查找效率的更准确的方式应该是:对于具有 N 个元素的数组,线性查找最多需要 N步
3.2 常数时间与线性时间
◆ 大 O 解答的是这样的问题:当数据增长时,步数如何变化?
◆ 从图中可以看出,O(N)呈现为一条对角线。当数据增加一个单位时,算法也随之增加一步。也就是说,数据越多,算法所需的步数就越多。O(N)也被称为线性时间。 相比之下,O(1)则为一条水平线,因为不管数据量是多少,算法的步数都恒定。所以,O(1)也被称为常数时间。
◆ 因为大 O 主要关注的是数据量变动时算法的性能变化
◆ 简单来说,O(1)就是用来表示所有数据增长但步数不变的算法。
3.3 同一算法,不同场景
◆ 但若无特别说明,大 O 记法一般都是指最坏情况。
3.5 对数
◆ O(log N) 我将其读作“O log N”。归于此类的算法,它们的时间复杂度都叫作对数时间。
◆ 简单来说,O(log N)意味着该算法当数据量翻倍时,步数加 1。
3.6 解释O(log N)
◆ 现在回到大 O 记法。当我们说 O(log N)时,其实指的是 O(log2 N),不过为了方便就省略了 2而已。
第4章 运用大O来给代码提速
◆ 排序算法是计算机科学中被广泛研究的一个课题。历时多年,它发展出了数十种算法,这些算法都着眼于一个问题:如何将一个无序的数字数组整理成升序?
4.5 二次问题
◆ O(N 2)也被叫作二次时间。
◆ 大 O 测量的是步数与数据量的关系。
◆ 毫无疑问,嵌套循环算法的效率就是 O(N2)。一旦看到嵌套循环,你就应该马上想到 O(N 2)。
5.5 忽略常数
◆ 还记得我们说过,大 O 记法用来表示步数与数据量的关系。所以你可能会以为步数约为 N 2 [插图]但事实上,选择排序的大 O 记法为 O(N2),跟冒泡排序一样。这是因为大 O 记法的一条重要规则我们至今还没提到: 大 O 记法忽略常数。
5.7 一个实例
◆ 这就是大 O 记法忽略常数的原因。大 O 记法只表明,对于不同分类,存在一临界点,在这一点之后,一类算法会快于另一类,并永远保持下去。至于这个点在哪里,大 O 并不关心。 因此,不需要写成 O(100N),归类到 O(N)就好了。
◆ 因此,大 O 记法非常适合用于不同大 O 分类下的算法的对比,对于大 O 同类的算法,我们还需要进一步的解析才能分辨出具体差异。
5.8 总结
◆ 不过在对比算法时,还需要考虑一个重要因素。至今我们关注的都是最坏情况下算法会跑得多慢,但其实最坏情况并不总会发生。没错,我们遇到的大都是平均情况。下一章,我们会学习怎样顾及所有情况。
第6章 乐观地调优
◆ 顾及最坏情况以外的场景将是多么有用。
6.4 插入排序的效率
◆ 插入排序包含 4 种步骤:移除、比较、平移和插入。
◆ 对于有 N 个元素的数组,大约需要 N2/ 2 次比较(102/ 2 是 50,202/ 2是 200)。
◆ 大 O 的另一条重要规则: 大 O 只保留最高阶的 N。
6.5 平均情况
◆ 确实,在最坏情况里,选择排序比插入排序快。但是我们还应该考虑平均情况。
◆ 最坏情况是所有数据都要比较和平移;最好情况是每轮一次比较、零次平移;对于平均情况,总的来看,是比较和平移一半的数据。
◆ 选择排序是无论何种情况,最坏、平均、最好,都要 N2 / 2 步。 因为这个算法没有提早结束某一轮的机制,不管遇到什么,每一轮都得比较所选索引右边的所有值。
◆ 那么哪种算法更好?选择排序还是插入排序?答案是:看情况。对于平均情况(数组里的值随机分布),它们性能相近。如果你确信数组是大致有序的,那么插入排序比较好。如果是大致逆序,则选择排序更快。如果你无法确定数据是什么样,那就算是平均情况了,两种都可以。
6.7 总结
◆ break 可以中断内部循环,节省步数和时间。
◆ 一种跟数组类似的数据结构,它的一些特点使其在某些场景中的性能优于数组。
第7章 查找迅速的散列表
◆ 查找迅速的散列表
◆ 大多数编程语言都自带散列表这种能够快速读取的数据结构。但在不同的语言中,它有不同的名字,除了散列表,还有散列、映射、散列映射、字典、关联数组。
◆ 散列表由一对对的数据组成。一对数据里,一个叫作键,另一个叫作值。键和值应该具有某种意义上的关系。
7.2 用散列函数来做散列
◆ 将字符串转为数字串的过程就是散列,其中用于对照的密码,就是散列函数。
◆ 当然散列函数不只是这一种,例如对各字母匹配的数字求和的过程,也可以作为散列函数。
◆ 散列函数也可以是对各字母匹配的数字求积的过程。这样的话,BAD 就会得出 8。
◆ 本章剩余部分将会采用最后一种散列函数。虽然现实世界中的散列函数比这复杂得多,但以简单的乘法函数为例会比较易懂。
◆ 一个散列函数需满足以下条件才有效:每次对同一字符串调用该散列函数,返回的都应是同一数字串。如果每次都返回不一样的结果,那就无效。
7.4 处理冲突
◆ 这下你应该明白为什么从散列表里读取数据只需要 O(1)了吧,因为其过程所花的时间是恒定的。它总是先计算出键的散列值,然后根据散列值跳到对应的格子去。
◆ 不过,散列表也会带来一些麻烦。
◆ 往已被占用的格子里放东西,会造成冲突。幸好,我们有解决办法。 一种经典的做法就是分离链接。当冲突发生时,我们不是将值放到格子里,而是放到该格子所关联的数组里。
◆ 该数组又以子数组构成,每个子数组含两个元素,第一个是被检索的词,后一个是其相应的同义词。
◆ 线性地
7.5 找到平衡
◆ 如果数据都刚好存在同一个格子里,那么查找就相当于在数组上进行。因此散列表的最坏情况就是 O(N)。
◆ 归根到底,散列表的效率取决于以下因素。 要存多少数据。 有多少可用的格子。 用什么样的散列函数。
7.6 一个实例
◆ 所以,一个好的散列函数,应当能将数据分散到所有可用的格子里去。
◆ 使用散列表时所需要权衡的:既要避免冲突,又要节约空间。 要想解决这个问题,可参考计算机科学家研究出的黄金法则:每增加 7 个元素,就增加 10个格子。 如果要保存 14 个元素,那就得准备 20 个格子,以此类推。 数据量与格子数的比值称为负载因子。把这个术语代入刚才的理论,就是:理想的负载因子是 0.7(7 个元素 / 10 个格子)。
◆ 很多时候,我们都可以把散列表当成集合来用。 把数组作为集合的话,数据是直接放到格子里的。用散列表时,则是将数据作为键,值可以为任何形式,例如数字 1,或者布尔值 true 也行。
◆ 这样每次插入新值,都只需花 O(1)的时间,而不是线性查找的 O(N)。即使数据已存在时也是这个速度。 set[“banana”] = 1; 再次插入”banana”时,我们并不需要检查它存在与否,因为即使存在,也只是将其对应的值重写成 1。
◆ 散列表确实非常适用于检查数据的存在性。
7.7 总结
◆ 因为其 O(1)的读取和插入带来了无与伦比的性能优势。
◆ 下一章就研究两种能帮助改善代码可读性和可维护性的数据结构
第8章 用栈和队列来构造灵巧的代码
◆ 本章你将会学习两种新的数据结构:栈和队列。事实上它们并不是全新的东西,只不过是多加了一些约束条件的数组而已
◆ 具体一点说,栈和队列都是处理临时数据的灵活工具。在操作系统、打印任务、数据遍历等各种需要临时容器才能构造出美妙算法的场景,它们都大有作为。
◆ 栈存储数据的方式跟数组一样,都是将元素排成一行。只不过它还有以下 3 条约束。 只能在末尾插入数据。 只能读取末尾的数据。 只能移除末尾的数据。
◆ 绝大部分计算机科学家都把栈的末尾称为栈顶,把栈的开头称为栈底。
8.2 栈实战
◆ 压栈和出栈可被形容为 LIFO(last in,first out)后进先出。解释起来就是最后入栈的元素,会最先出栈。
8.3 队列
◆ 我们会类似地用栈去跟踪函数的调用,那也是递归的核心思想。
◆ 套用到队列上,就是首先加入队列的,将会首先从队列移出。因此计算机科学家都用缩写“FIFO”(first in, first out)先进先出,来形容它。
◆ 与栈类似,队列也有 3 个限制(但内容不同)。 只能在末尾插入数据(这跟栈一样)。 只能读取开头的数据(这跟栈相反)。 只能移除开头的数据(这也跟栈相反)。
9.3 阅读递归代码
◆ (1) 找出基准情形。 (2) 看该函数在基准情形下会做什么。 (3) 看该函数在到达基准情形的前一步会做什么。 (4) 就这样往前推,看每一步都在做什么。
9.4 计算机眼中的递归
◆ 计算机是用栈来记录每个调用中的函数。这个栈就叫作调用栈。
9.5 递归实战
◆ 有趣的是,无限递归(如本章开头的例子)的程序会一直将同一方法加到调用栈上,直到计算机的内存空间不足,最终导致栈溢出的错误。
◆ 此脚本遍历给定目录下的所有文件
◆ 打印了当前目录的直属子目录的名字
9.6 总结
◆ 正如文件系统的例子所示,递归十分适用于那些无法预估计算深度的问题。
第10章 飞快的递归算法
◆ 快速排序依赖于一个名为分区的概念
10.2 快速排序
◆ 快速排序严重依赖于分区
◆ (1) 把数组分区。使轴到正确的位置上去。(2) 对轴左右的两个子数组递归地重复第 1、2 步,也就是说,两个子数组都各自分区,并形成各自的轴以及由轴分隔的更小的子数组。然后也对这些子数组分区,以此类推。(3) 当分出的子数组长度为 0 或 1 时,即达到基准情形,无须进一步操作。
10.6 总结
◆ 其实能递归的不只有算法,还有数据结构。后面几章将要接触的链表、二叉树以及图,就利用了自身递归的特性,给我们提供了迅速的数据操作方式。
11.2 实现一个链表
◆ 与数组不同的是,组成链表的格子不是连续的。它们可以分布在内存的各个地方。这种不相邻的格子,就叫作结点。
◆ 这就是链表的关键了:每个结点除了保存数据,它还保存着链表里的下一结点的内存地址。
◆ 因为每个结点都需要 2 个格子,头一格用作数据存储,后一格用作指向下一结点的链(最后一个结点的链是 null,因为它是终点),所
11.3 读取
◆ 既然知道了链表是什么,那么接下来做个它跟数组的性能对比,观察它们在读取、查找、插入和删除上有何优劣。
11.4 查找
◆ 所谓查找就是从列表中找出某个特定值所在的索引
11.5 插入
◆ 在某些情况下,链表的插入跟数组相比,有着明显的优势。回想插入数组的最坏情况:当插入位置为索引 0 时,因为需要先将插入位置右侧的数据都右移一格,所以会导致 O(N)的时间复杂度。然而,若是往链表的表头进行插入,则只需一步,即 O(1)。
◆ 链表的插入效率为 O(N),与数组一样。
11.6 删除
◆ 删除链表的最后一个结点,其实际的删除动作只需 1 步—令倒数第二的结点的链指向 null。然而,要找出倒数第二的结点,得花 N 步,因为我们依然只能从第一个结点顺着链往下一个个地找。
11.7 链表实战
◆ 高效地遍历单个列表并删除其中多个元素,是链表的亮点之一。
11.8 双向链表
◆ 链表的另一个引人注目的应用,就是作为队列的底层数据结构。
◆ 那么用链表来代替数组有什么好处呢?
◆ 再强调一次,队列插入数据只能在末尾。如上文所述,在数组的末尾插入是极快的,时间复杂度为O(1)。链表则要 O(N)。所以在插入方面,选择数组比链表更好。
◆ 但到了删除的话,就是链表更快了,因为它只要 O(1),而数组是 O(N)。基于以上分析,似乎用数组还是链表都无所谓。因为它们总有一种操作是 O(1),另一种是 O(N):数组的插入是 O(1),删除是 O(N);链表则反过来,分别是 O(N)和 O(1)。然而,要是采用双向链表这一链表的变种,就能使队列的插入和删除都为 O(1)。双向链表跟链表差不多,只是它每个结点都含有两个链—一个指向下一结点,另一个指向前一结点。此外,它还能直接访问第一个和最后一个结点。
第12章 让一切操作都更快的二叉树
◆ 有序数组存在着另一个问题。
◆ 有序数组的插入和删除是缓慢的。
◆ 散列表能以 O(1)的效率进行查找、插入和删除,但它又有另一明显的不足:不保持顺序。
◆ 既要保持顺序,又要快速查找、插入和删除,看来有序数组和散列表都不行。那还有什么数据结构可以选择?看看二叉树吧。
◆ 树也是基于结点的数据结构,但树里面的每个结点,可以含有多个链分别指向其他多个结点。
◆ 最上面的那一结点(此例中的“j”)被称为根。是的,图中的根位于树的顶端
◆ 二叉树是一种遵守以下规则的树。 每个结点的子结点数量可为 0、1、2。 如果有两个子结点,则其中一个子结点的值必须小于父结点,另一个子结点的值必须大于父结点。
12.2 查找
◆ 因为二叉树具有这样独特的结构,所以我们能在其中非常快速地进行查找操作
◆ 二叉树的查找算法先从根结点开始。(1) 检视该结点的值。(2) 如果正是所要找的值,太好了!(3) 如果要找的值小于当前结点的值,则在该结点的左子树查找。(4) 如果要找的值大于当前结点的值,则在该结点的右子树查找。
◆ 二叉树查找的时间复杂度是 O(log N)
◆ 因为每行进一步,我们就把剩余的结点排除了一半(不过很快就能看到,只在最好情况下,即理想的平衡二叉树才有这样的效率)。
◆ 再与二分查找比较,它也是每次尝试会排除一半可能性的 O(log N)算法,可见二叉树查找跟有序数组的二分查找拥有同样的效率。要说二叉树哪里比有序数组更亮眼,那应该是插入操作。
12.3 插入
◆ 插入这 1 步总是发生在查找之后,所以总共 log N + 1 步。按照忽略常数的大 O 来说,就是 O(log N)步。
◆ 有序数组的插入则是 O(N),因为该过程中除了查找,还得移动大量的元素来给新元素腾出空间。
◆ 这就是二叉树的高效之处。有序数组查找需要 O(log N),插入需要 O(N),而二叉树都是只要 O(log N)。当你估计应用会发生许多数据改动时,这一比较将有助你做出正确选择。
◆ 注意,只有用随意打乱的数据创建出来的树才有可能是比较平衡的。要是插入的都是已排序的数据,那么这棵树就失衡了,它用起来也会比较低效。
12.4 删除
◆ 至此,删除操作遵循以下规则。 如果要删除的结点没有子结点,那直接删掉它就好。 如果要删除的结点有一个子结点,那删掉它之后,还要将子结点填到被删除结点的位置上。
◆ 如果要删除的结点有两个子结点,则将该结点替换成其后继结点。一个结点的后继结点,就是所有比被删除结点大的子结点中,最小的那个。
◆ 以下为二叉树的删除算法的所有规则。 如果要删除的结点没有子结点,那直接删掉它就好。 如果要删除的结点有一个子结点,那删掉它之后,还要将子结点填到被删除结点的位置上。 如果要删除的结点有两个子结点,则将该结点替换成其后继结点。一个结点的后继结点,就是所有比被删除结点大的子结点中,最小的那个。 如果后继结点带有右子结点,则在后继结点填补被删除结点以后,用此右子结点替代后继结点的父节点的左子结点。
◆ 跟查找和插入一样,平均情况下二叉树的删除效率也是 O(log N)。因为删除包括一次查找,以及少量额外的步骤去处理悬空的子结点。有序数组的删除则由于需要左移元素去填补被删除元素产生的空隙,最终导致 O(N)的时间复杂度。
12.5 二叉树实战
◆ 二叉树在查找、插入和删除上引以为傲的 O(log N)效率,使其成为了存储和修改有序数据的一大利器。它尤其适用于需要经常改动的数据,虽然在查找上它跟有序数组不相伯仲,但在插入和删除方面,它迅速得多。
◆ 访问数据结构中所有元素的过程,叫作遍历数据结构。
12.6 总结
◆ 二叉树是一种强大的基于结点的数据结构,它既能维持元素的顺序,又能快速地查找、插入和删除。尽管比它的近亲链表更为复杂,但它更有用。值得一提的是,树形的数据结构除了二叉树以外还有很多种,包括堆、B 树、红黑树、2-3-4树等。它们也各有自己适用的场景。
◆ 图是社交网络和地图软件等复杂应用的核心组成部分,强大且灵活。
第13章 连接万物的图
◆ 图是一种善于处理关系型数据的数据结构,使用它可以很轻松地表示数据之间是如何关联的。
13.2 广度优先搜索
◆ 广度优先搜索
◆ 图有两种经典的遍历方式:广度优先搜索和深度优先搜索。
◆ 广度优先搜索算法需要用队列(参见第 8 章)来记录后续要处理哪些顶点。该队列最初只含有起步的顶点(对本例来说,就是 Alice)。于
◆ 将算法的步骤分为两类之后,我们可以看出图的广度优先搜索的效率。 让顶点出队,将其设为当前顶点。 访问每个顶点的邻接点。
13.4 加权图
◆ 还有一种图叫作加权图
◆ 它跟普通的图类似,但边上带有信息。
13.6 总结
◆ 我们知道了图是处理关系型数据的强大工具,它除了能让代码跑得更快,还能帮忙解决一些复杂的问题。学习至今,我们关注的主要是代码运行的速度。我们以时间和算法的步数来衡量代码的性能。然而,性能的衡量方法不止这些。在某些情况下,还有比速度更重要的东西,比如我们可能更关心一种数据结构或算法会消耗多少内存。下一章,我们就来学习如何分析一段代码在空间上的效率。
第14章 对付空间限制
◆ 在分析各种算法的效率时,我们只关注了它们的时间复杂度。换句话说,就是它们运行得有多快。但有些时候,我们还得以另一种名为空间复杂度的度量方式,去估计它们会消耗多少内存
◆ 当内存有限时,空间复杂度便会成为选择算法的一个重要的参考因素。
◆ 还是用描述时间复杂度的大 O 记法来描述空间复杂度。
◆ 至今我们一直这样用大 O 记法来描述一个算法的速度:当所处理的数据有 N 个元素时,该算法所需的步数相对于元素数量是多少。例如,O(N)算法就是处理 N 个元素需要 N 步的算法。 O(N 2)算法就是处理 N 个元素需要 N2步的算法。类似地,我们也可以用大 O 来描述一个算法需要多少空间:当所处理的数据有 N 个元素时,该算法还需额外消耗多少元素大小的内存空间。
◆ 记住,时间复杂度的O(1)意味着一个算法无论处理多少数据,其速度恒定。相似地,空间复杂度的 O(1)则意味着一个算法无论处理多少数据,其消耗的内存恒定。
◆ 空间复杂度是根据额外需要的内存空间(也叫辅助空间)来算的,也就是说原本的数据不纳入计算。
– 来自微信读书