数据结构是存储和组织数据的方式,通过提高存取效率、节省内存和支持复杂操作来提高算法性能和程序性能。 常见的数据结构包括数组、链表、栈、队列、哈希表、二叉树等。

  • 操作速度按步数计算,即时间复杂度(衡量操作步数随输入规模的变化)。核心操作:读取、查找、插入、删除。

  • 算法运行时所需的额外内存,即空间复杂度(衡量所需的额外内存空间随输入规模变化)。

大 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)则意味着一个算法无论处理多少数据,其消耗的内存恒定。

◆ 空间复杂度是根据额外需要的内存空间(也叫辅助空间)来算的,也就是说原本的数据不纳入计算。

– 来自微信读书