[考研笔记] 数据结构

前言

数据结构作为六七年前甚至小学就有接触过的知识,如今再次与其狭路相逢。不同于之前所有数据结构知识的学习,考研的数据结构会明显偏向于理论知识而非实践应用,故特此另开一篇用以记录学习历程。

【20210924】虽然基本上是按照王道的复习指导来复习的,但实际专业科目并非 408,所以目录和 408 是有出入的,但大体而言的知识点覆盖是一致的。随着 2022 年 408 考纲的修订,会有如下知识点不会提及:串、并查集、平衡树、红黑树、外部排序等。

目录

第一章 绪论

第二章 线性表

第三章 数组

第四章 栈和队列

第七章 树与二叉树

第八章 图

第九章 文件及查找

第十章 内排序

 

数据结构考研笔记

第一章 绪论

1.1 数据结构的基本概念

  1.1.1 基本概念和术语

    数据是信息的载体;

    数据元素是数据的基本单位,由若干个数据项(最小单位)组成;

    数据对象是具有相同性质的数据元素的集合;

    数据类型分为原子类型、结构类型、抽象数据类型

    数据结构是存在某种关系的数据元素的集合,包括逻辑结构、存储结构数据的运算

  1.1.2 数据结构三要素

    ① 逻辑结构

      逻辑结构指数据元素之间存在的逻辑关系,是固有的客观联系;

      逻辑结构分为线性结构非线性结构,比如:线性表、树、图等;

    ② 存储结构

      存储结构又称为物理结构,指数据结构在计算机中的表示(映像),是计算机内部的存储方法;

      存储结构主要有顺序存储、链式存储、索引存储散列存储

      一种逻辑结构通过映像便可以得到它的存储结构;

      诸如顺序表、哈希表、链表这样的表述,它们既体现了逻辑结构(均为线性),又体现了存储结构(顺序、散列、链式);

      而这样的表述我们往往就直接称之为数据结构

      诸如有序表,它只体现了逻辑结构(线性),而存储结构是未知的(可以是顺序、链式……);

      不存在只体现存储结构而不体现逻辑结构的表述;

      所以,我们认为:逻辑结构独立于存储结构。

    ③ 数据的运算(算法)

      算法包括运算的定义(取决于逻辑结构,体现算法功能)与实现(取决于存储结构,体现于操作步骤)。

1.2 算法的基本概念

  算法的 5 个重要特性:有穷性、确定性、有效性(可行性)、输入输出

  一个好的算法的目标:正确性、可读性、鲁棒性、效率与低存储量需求

1.3 算法分析

  时间复杂度指算法所有语句被重复执行次数总和的数量级。

  常见时间复杂度比较:

    O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

    (log 表示以 2 为底的对数)

  空间复杂度指算法耗费存储空间的数量级。

1.4 题目与总结

  本章不在 408 考纲范围内,但时间复杂度的计算却是考试重点。选择题几乎没什么难度,但如果出在应用题上,推导过程的书写却是个问题,总结如下:

  ① 循环条件包含主体变量,将执行次数 t 代入该条件再计算,如:

int i = 1;
while (i <= n) 
    i = i * 2;

  每次 i *= 2,执行次数 t += 1,即 2 ^ t <= n,得 t <= log n,则 T(n) = O(log n)。再如:

int i = 3;
while ((i + 1) * (i + 1) < n) 
    i = i + 1;

  i 的初值不为 1 或 0,则令 t = i - 3,即 i = t + 3,有 (t + 3) * (t + 3) < n,得 t < √n - 3,即 T(n) = O(√n)。

  ② 循环条件与主体变量无关,采用数学归纳法或直接循环计数。如:

int fact(int n) {
    if (n <= 1) return 1;
    return n * fact(n - 1);
}

  对于递归函数,直接得 T(n) = 1 + T(n - 1) = k + T(n - k) = n - 1 + T(1) = n,即 T(n) = O(n)。

 

第二章 线性表

2.1 线性表的基本概念

  2.1.1 线性表的定义

    线性表是具有相同数据类型的 n 个数据元素的有限序列

    线性表的特点:

      ① 表中元素具有逻辑上的顺序性,有先后次序;

      ② 表中元素都是数据元素,每个元素都是单个元素;

      ③ 表中元素的数据类型都相同,占有相同大小的存储空间;

      ④ 表中元素具有抽象性,即仅讨论元素间的逻辑关系,不考虑具体内容。

    线性表是逻辑结构,表示元素一对一的相邻关系;

    顺序表、链表是存储结构,表示在计算机中数据的存储方式。

  2.1.2 线性表的基本操作

    略

2.2 线性表的顺序存储结构

  2.2.1 顺序表的定义

    顺序表指线性表的顺序存储,用一组地址连续的存储单元存储;

    顺序表是一种随机存取的存储结构,存储密度大;

    一般用数组表示顺序表,线性表从 1 开始,数组下标从 0 开始;

     顺序表最主要特点是随机访问,通过首地址与元素序号在 O(1) 找到指定元素。

  2.2.2 顺序表上基本操作的实现

    > 插入结点 O(n)  1 <= i <= Length

    > 删除结点 O(n)  1 <= i <= Length  

    > 按值查找 O(n)  1 <= i <= Length

  *2.2.3 关于静态分配与动态分配

    静态分配代码:

#define MaxSize 50
typedef struct {
    ElemType data[MaxSize];
    int length;
} SeqList;

    动态分配代码:

#define InitSize 20
typedef struct {
    ElemType *data;
    int MaxSize, length;
} SeqList;
L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize); // C
L.data = new ElemType[InitSize]; // C++

    对于动态分配,如果空间占满,则开辟一块更大的存储空间。即如果原有 n 个元素,现申请增加 m 个连续的存储空间,则需要寻找一块 n + m 个连续的存储空间,再将原有的 n 个元素复制到新空间的前 n 个存储空间。

2.3 线性链表及其操作

  2.3.1 单链表的定义

    单链表指线性表的链式存储,用一组任意的存储单元来存储数据元素;

    而为了建立元素之间的线性关系,对每个链表结点,还要存放一个指向后继的指针;

    头指针用以标识单链表,如果其值为 NULL,说明为一个空表;

    在第一个结点前附加一个结点,成为头结点,可以不记录信息,也可以记录表长。

    设置头结点,便于空表与非空表的统一处理。

  2.3.2 单链表上基本操作的实现

    > 建表 O(n)

      ① 头插法:将存有读入数据的新结点插入到当前链表表头;

        使用头插法会导致读入数据与生成链表顺序相反;

      ② 尾插法:增加一个尾指针,以使新结点直接插入到表尾。

    > 查找 O(n)

      ① 按序号查找

      ② 按值查找

    > 插入结点 O(n)

      一般指在某结点的后面插入新结点,即后插操作。

    > 删除结点 O(n)

    > 求表长 O(n)

  2.3.3 双链表

    在单链表基础上增加前驱指针。

  2.3.4 循环链表

    对于循环单链表,尾结点指针不是指向 NULL,而是头结点;

    对于循环双链表,在循环单链表基础上,头结点的前驱指针指向尾结点。

  2.3.5 静态链表

    静态链表借助数组来描述链式存储结构,结点的指针域的值是下一结点的相对地址(数组下标),最后以 -1 或其他值域外的值表示结束。

    静态链表操作起来明显不够方便,其存在的意义在于适用于不支持指针的语言。

  2.3.6 顺序表与链表的比较

    略

2.4 题目与总结

  【王道 2.3 选择第 7 题】给定有 n 个元素的一维数组,建立一个有序单链表的最低时间复杂度是?

    虽然是求最低,但所给一维数组依旧认定为随机数组,不能认为已经有序,故答案为排序所需时间复杂度 O(n log n),而非逐一插入的时间复杂度 O(n)。

  真题的算法题还是很有意思的,给出一个很简单的需求,可以用比较常规的方法做,但往往会有很巧妙的做法,和面试试题的风格接近。比如:

  【王道 2.2 应用第 12 题】【2013 408真题】求长度为 n,值域为 n 的整数序列中出现次数大于 n / 2 的元素的值。

    时间 O(n ^ 2) 空间 O(n):由于值域同样只有 n,可以另开辅助数组记录各元素出现次数,再根据次数是否大于 n / 2 求得结果(得分 10/15);

    时间 O(n log n) 空间 O(1):将数组进行排序,则同值的元素连续出现,记录每个元素连续出现的次数求得结果;其中排序 O(n log n),记录次数 O(n)(得分 11/15);

    时间 O(n) 空间 O(1):将第一个整数假设为结果,计数器初始值为 1,从前向后扫描数组,如果当前元素与当前假设的结果的值一致,计数器 +1,否则 -1;当计数器为 0 时,将当前元素赋值给当前结果,计数器重新记为 1。重复上述过程直到扫描结束,如果最后计数器值大于 n / 2,则答案为当前结果,否则不存在主元素(得分 15/15)。

    奇妙的满分方法。

 

第三章 数组

3.1 数组的定义

  数组是由 n 个相同类型的数据元素构成的有限序列。

  数组是线性表的推广。一维数组可视为一个线性表,二维数组可视为其元素均为定长线性表的线性表,以此类推。

3.2 数组的存储结构

  对于多维数组有两种映射方法:按行优先按列优先。一般默认按行优先。

3.3 特殊矩阵的压缩存储

  压缩存储:多个值相同的元素只分配一个存储空间,0 元素不分配;

  特殊矩阵:如对称矩阵、上三角矩阵、对角矩阵。

3.4 稀疏矩阵的三元组表示

  稀疏矩阵指非零元素个数远小于零个数的矩阵。

  用三元组(行,列,值)来存储,或者十字链表法。

3.5 数组的应用举例

  

 

第四章 堆栈和队列

4.1 堆栈的基本概念

  是一种运算受限的线性表,只允许在一端进行插入或删除操作。

  数学性质:n 个不同元素进栈,出栈元素不同排列的个数为 C(2n, n) / (n + 1),这就是卡特兰数

  栈的基本操作:

    > InitStack(&S) 初始化空栈 S

    > StackEmpty(S) 判断栈是否为空

    > Push(&S, x) 进栈

    > Pop(&S, &x) 出栈,弹出栈顶元素 x 并返回

    > GetTop(S, &x) 读栈顶元素 x 并返回

    > DestroyStack(&S) 销毁栈,释放空间

4.2 堆栈的顺序存储结构

  顺序栈的实现:略

  顺序栈的基本运算:略

  共享栈:两个顺序栈共享一个一维数组,栈底分别设在两端,栈顶向中间延伸。

4.3 堆栈的链式存储结构

  采用链式存储的栈称为链栈

  优点:便于多个栈共享存储空间,提高效率,且不存在栈溢出的情况。

  链栈没有头结点,直接指向栈顶元素。

4.4 队列的基本概念

  队列是一种只允许在表的一端插入,另一端删除的线性表。

  队列的操作:

    > InitQueue(&Q) 初始化空队列 Q

    > QueueEmpty(Q) 判断队列是否为空

    > EnQueue(&Q, x) 入队

    > DeQueue(&Q, &x) 出队,删除队头元素 x 并返回

    > GetHead(Q, &x) 读队头元素 x 并返回

4.5 队列的顺序存储结构

  队列的顺序存储:略

  循环队列:普通队列会出现假溢出情况,故引用循环队列,将队列从逻辑上视为一个环,利用取余运算实现。

  由于循环队列在队空与队满的判断条件是等价的,故需要一些处理方式来区分:

    > 牺牲一个单元来区分,约定“队头在队尾下一位置作为队满的标志”;

    > 增设表示元素个数的数据成员。

  循环队列的操作:略

4.6 队列的链式存储结构

  队列的链式存储:采用链式存储的队列称为链队列

  链队列往往设计成带头结点的单链表。

  链式队列的基本操作:略

双端队列

  双端队列指两端都可以入队出队操作的队列,两端称为前端和后端。

栈和队列的应用

  栈在括号匹配中的应用 

    

  栈在表达式求值中的应用

    后缀表达式可以轻松获得运算符关系,且不用处理括号。用栈处理。

  栈在递归中的应用

    可以用栈来模拟递归过程,以消除递归。

    对于同一个问题,非递归算法效率通常比递归算法更高

  队列在层次遍历中的应用

    比如 BFS。

  队列在计算机系统中的应用

    比如页面替换算法。

4.7 题目与总结

  【王道 3.2 选择第 8 题】【2011 408真题】循环队列存储在数组 A[0..n-1] 中,队列非空时 front 和 rear 分别指向队头与队尾。初始时队列为空,且第一个进入队列的元素存储在 A[0],则 front 和 rear 的初值为?

    反向思考。首先插入后的队首与队尾指向的位置均为 0,且插入元素只会更改 rear 的值,则 front 在插入前仍为 0,而 rear 需要 -1,对于循环队列,0 的前一个位置为 n - 1。故答案分别为 0, n - 1。

  【王道 3.3 选择第 11 题】【2012 408真题】将中缀表达式 a+b-a*((c+d)/e-f)+g 转换为等价的后缀表达式 ab+acd+e/f-*-g+ 时,用栈存放暂时不能确定运算次序的操作符,转换过程中栈的操作符个数最大为?

    看了下解析,实在是太麻烦了点吧。不过自己做的时候没想太多,前后两道题也做对了。

    无括号时正常乘除在先加减在后。有括号时,遇到右括号,完成对应的左括号之后范围内所有运算。

    比如本题,+ 直接运算,可弹出;- 不动,因为后面是个 *;加上第一个 (;遇到第一个 ) 时,第二个 ( 及里面的 + 弹出;遇到第二个 ) 时,第一个 ( 及里面的 / 与 - 弹出;* 运算完成,- * 先后弹出;+ 直接运算,弹出。

    整个过程中,操作符最大的时刻在:- * ( / -,即遇到第二个 ) 之前。 

 

串的定义和实现

  串的定义

    是由零个或多个字符组成的有限序列,为字符串的简称。

  串的存储结构

    串有三种存储方式:

      > 定长顺序存储表示:分配一个固定长度

      > 堆分配存储表示:按串长动态分配,使用指针指向串的起始地址

      > 块链存储表示:类似于线性表链式存储结构,具体实现时可以使每个结点存放一个或多个字符

  串的基本操作

    > StrAssign(&T, chars) 将串 T 赋值为 chars

    > StrCopy(&T, S) 将串 S 复制得到串 T

    > StrEmpty(S) 判断串 S 是否为空

    > StrCompare(S, T) 比较串 S 与 T 的大小(S > T 返回值 > 0;= / =;< / <)

    > StrLength(S) 返回串 S 的长度

    > SubString(&Sub, S, pos, len) 返回串 S 的第 pos 个字符起长度为 len 的子串 Sub

    > Concat(&T, S1, S2) 返回串 S1 和串 S2 连接而成的串 T

    > Index(S, T) 返回主串 S 中与串 T 相同的子串第一次出现的位置(不存在则为 0 )

    > ClearString(&S) 清空串 S

    > DestroyString(&S) 销毁串 S,释放空间

串的模式匹配

  简单的模式匹配算法

    子串的定位操作通常称为串的模式匹配,求的是子串(模式串)在主串中的位置。

    具体算法略。

  改进的模式匹配算法 —— KMP 算法

    部分匹配值:字符串的最长公共前后缀长度;

    比如字符串 'ababa',其部分匹配值为 3,对应的前缀与后缀为 'aba';

    在算法竞赛中,通常我们会将模式串的所有前缀的部分匹配值后移一位再依次存入一个数组,一般称为 fail 数组或 next 数组;第一位填充 -1;

    还是 'ababa',其 fail 数组为:[-1, 0, 0, 1, 2];

    对于算法竞赛上 KMP 算法使用的更详细介绍,可以移步 [知识点]KMP算法,尽管在知识点大全的填坑计划中被遗弃,但其实内容在去年刚刚更新,时效性与正确性应该不会有太大欠缺。

    而对于考研或者其他相关的笔试,更强调的是算法思想而非实现过程,所以在学习时可以用不同的思路。

    求得 fail 数组后,每次匹配失败,则将模式串当前指针前移到 fail[匹配失败的位置],主串指针不动,同时再次对齐两个指针。这样思考更适用于笔试考场。

    注意题目可能给出的 fail 数组所有值 +1,以适应首位序号不为 0 而为 1 的标号方式。

  KMP 算法的进一步优化

    优化构建 fail 数组的过程。假设模式串为 a,且匹配失败恰好发生在第 i 位,则下次匹配必然是与第 fail[i] 位,但如果 a[i] = a[fail[i]],则必然再次匹配失败,以此类推。所以为了避免多余的匹配,我们将这种情况下的 fail[i] = j 优化为 fail[i] = fail[j],相当于再递归一次。

    优化后的数组一般命名为 nextval 数组。

 

第七章 树与二叉树

7.1 树的基本概念

  7.1.1 树的定义

    树是 n 个结点的有限集。对于任意一棵空树,满足:

      ① 有且仅有一个特定的根结点

      ② n > 1 时,除去根结点外的其他结点又可分为若干个互不相交的子树

  7.1.2 树逻辑上的特点

    

  7.1.3 树的逻辑表示方法

    文氏图表示法;凹入表示法;嵌套括号(广义表)表示法;树形表示法。

    【可能需要补充】

  7.1.4 基本名词术语

    > 祖先 / 子孙 / 双亲(父) / 孩子(子) / 兄弟

    > 度:结点的子结点个数为结点的度;最大结点的度为树的度

    > 分支结点 / 叶子结点

    > 深度(自顶向下) / 高度(自底向上) / 层次

    > 有序树 / 无序树

    > 路径 / 路径长度

    > 森林:m 棵互不相交的树集合,加上一个共同根结点后即可认为是一棵树

  7.1.5 树的性质

    ① 树的结点数 = 所有结点度数之和 + 1;

    ② 度为 m 的树第 i 层至多 m^(i-1) 个结点;

    ③ 高度为 h 的 m 叉树至多 (m^h - 1) / (m - 1) 个结点;

    ④ 具有 n 个结点的 m 叉树最小高度为 log_m[n(m-1) + 1] 向上取整。

7.2 树的存储结构

  7.2.1 多重链表结构

    将每个结点的子结点用单链表链接起来形成线性结构,即孩子表示法

    找子结点时操作简单,而找父结点则相当麻烦,同时很浪费空间。

  7.2.2 三重链表结构

    每个结点包含结点值、指向父结点的指针、指向结点第一个子结点的指针、指向结点下一个兄弟结点的指针,即孩子兄弟表示法(二叉树表示法)双亲表示法的结合。

    由于存储内容多,对于各类操作都容易实现,又不浪费空间。

  【注:这一节的说法区别较大】

7.3 二叉树

  7.3.1 二叉树的定义

    二叉树是度不大于 2 的有序树,即每个结点至多 2 棵子树,且有左右之分;

    空树与只有根结点的情况都是二叉树;

    即使某个结点只有一棵子树,也需要明确其是左子树还是右子树。

  7.3.2 两种特殊形态的二叉树

    ① 满二叉树:高度为 h 且含有 2^h - 1 个结点的二叉树;

      每层都含有最多的结点,故可以按层序编号;

      对于编号为 i 的结点,左孩子为 2i,后孩子为 2i + 1。

    ② 完全二叉树:最后一层可以不含有最多结点的满二叉树;

      特征很多,都容易推出,略。

    还有二叉排序树与平衡二叉树也是特殊的二叉树。

  7.3.3 二叉树的性质

    > 非空二叉树叶子结点数 = 度为 2 的结点数 + 1(常用结论)

    其他略。

  7.3.4 二叉树的基本操作

    

  7.3.5 二叉树与树、树林之间的转换

    遵循左孩子右兄弟原则;

    树的前根序列、森林的先序遍历、二叉树的先序遍历相对应;

    树的后根序列、森林的中序遍历、二叉树的中序遍历相对应。

7.4 二叉树的存储结构

  7.4.1 二叉树的顺序存储结构

    一般只用于满二叉树与完全二叉树,否则太浪费空间;

    数组下标从 1 开始更恰当,以满足父子结点之间的编号关系。

  7.4.2 二叉树的链式存储结构

    每个结点包含结点值、指向左孩子结点的指针、指向右孩子结点的指针。

7.5 二叉树的遍历

  7.5.1 什么是二叉树的遍历

    二叉树的遍历指按某条搜索路径访问每个结点有且仅有一次。

  7.5.2 前序遍历

    先根结点,再左子树,再右子树。

  7.5.3 中序遍历

    先左子树,再根结点,再右子树。

  7.5.4 后序遍历

    先左子树,再右子树,再根结点。

  7.5.5 按层次遍历

    以满二叉树/完全二叉树按层次编号的顺序进行遍历。

  7.5.6 递归问题的非递归算法的设计

    即用栈来模拟递归的过程;

    效率更高,但编写起来更麻烦。

  7.5.7 由遍历序列恢复二叉树

    由前序/后序遍历加上中序遍历,可唯一确定一棵二叉树。

  7.5.8 树和树林的遍历

    暂略

7.6 线索二叉树

  7.6.1 基本概念

    线索二叉树将结点的前驱或后继的指针存放到叶子结点的空指针中,以更为方便地遍历二叉树;

    为此我们需要额外增加两个标志域以表示其左右指针是指向子结点还是前驱/后继。

  7.6.2 什么是线索二叉树

    

  7.6.3 线索二叉树的构造

    为方便,可以在二叉树的线索链表中添加一个头结点;

    头结点的左指针指向二叉树根结点,右指针指向最后一个结点。

  7.6.4 线索二叉树的利用

    

  7.6.5 线索二叉树的建立

    唯有后序线索树的遍历需要利用栈。

7.7 二叉排序树

  7.7.1 二叉排序树的定义

    二叉排序树的左子树所有结点小于根结点,右子树所有结点大于根结点;

    二叉排序树的中序遍历必然严格单调递增。

  7.7.2 二叉排序树的建立

    

  7.7.3 二叉排序树的删除

    删除结点如果:

      > 右子树空,则用左儿子结点填补;

      > 左子树空,则用右儿子结点填补;

      > 左右子树均非空,则用右子树的中序序列的第一个结点填补。

  7.7.4 二叉排序树的查找

    查找效率取决于树的高度:

      > 如果左右子树高度差不超过 1(即平衡二叉树),则平均查找长度为 O(log2n)

      > 如果是一棵单支树(即类似于单链表),则为 O(n)

7.8 哈夫曼树及其应用

  7.8.1 哈夫曼树的基本概念

    WPL:树中所有叶结点带权路径长度(路径长度 * 结点权值)之和;

    对于 n 个带权叶结点构成的所有二叉树中,WPL 值最小的为哈夫曼树

  7.8.2 哈夫曼树的构造

    每次选取两棵根结点权值最小的树作为新结点的左右子树,以此反复;

    哈夫曼树没有度为 1 的结点。

  7.8.3 哈夫曼编码

    

  关于哈夫曼树及哈夫曼编码更详细的介绍,可以参考 [知识点] 7.5 Huffman 哈夫曼树 

7.9 题目与总结

  【王道 5.1 选择第 7 题】【2010 408 真题】在一棵度为 4 的树中,若有 20 个度为 4 的结点,10 个度为 3 的结点,1 个度为 2 的结点,10 个度为 1 的结点,则树的叶结点个数是?

    根据树的性质,树的结点数 = 树的分支数 + 1 = 树的度数 + 1,即:

    叶结点个数 + 20 + 10 + 1 + 10 = 20 * 4 + 10 * 3 + 1 * 2 + 10 * 1 + 1

    可得:叶结点个数 = 82

   【王道 5.3 选择第 29 题】二叉树在线索化后,仍不能有效求解的问题是?A. 前序线索二叉树求前序后继 B. 中序线索二叉树求中序后继 C. 中序线索二叉树求中序前驱 D. 后续线索二叉树求后序后继

    线索二叉树不能有效求解,意味着在遍历过程中线索无法指向实际存在的前驱/后继。

    对于前序/中序的遍历过程,它们的共同点在于一定会在叶子结点遍历到与该结点不直接相连的结点上,那么线索也就必定存在;而后序遍历是可能有非叶子结点遍历过去的,而非叶子结点不能建立线索,故无法继续遍历到它的实际后继。

    故只能选 D。

  【王道 5.4 选择第 27 题】若度为 m 的哈夫曼树中,叶子结点个数为 n,则非叶子结点的个数为?

    初看这道题觉得很古怪,以往我们都将哈夫曼树既定地认为是二叉树,而这里突然问一个度未知的哈夫曼树,所以也只能将计就计地类比一下了。

    度为 m 的哈夫曼树,根据其建立过程,每次选择根结点度数最小的 m 棵树进行合并,意味着其所有结点的度数只有 0 和 m 两种可能。

    已知总结点个数 = 分支数 - 1 = 非叶子结点个数 y * m - 1 = n + y,解得:y = (n - 1) / (m - 1),最后向上取整。

 

第八章 图

8.1 图的基本概念

  8.1.1 图的定义

    图不可以为空图,点集不得为空,但边集可以。

  8.1.2 图的分类

    

  8.1.3 名词术语

    ① 有向图:边集由有向边(弧)构成;

    ② 无向图:边集由无向边(边)构成;

      弧一般用 <> 表示,边一般用 () 表示;

    ③ 简单图:没有重边,没有自环;否则为多重图;

    ④ 完全图:任意两点之间都存在边(或者两条方向相反的弧);

    ⑤ 子图:边集与点集均为另一个图的子集;

      当边集等价时,则称为生成子图

    ⑥ 无向图的连通、连通图、连通分量:

      极大连通子图:又称为连通分量,对于连通图,极大连通子图即其自身;对于非连通图,有多个极大连通子图;

      极小连通子图:即生成树,对于非连通图没有意义;

    ⑦ 有向图的强连通、强连通图、强连通分量:

      极大强连通子图:又称为强连通分量,类比于连通分量;

      不存在极小强连通子图;

    ⑧ 生成树:包含全部顶点的极小连通子图;

    ⑨ 度、入度、出度:略;

    ⑩ 网:带权图的别称;

    ⑪ 稠密图、稀疏图:均为模糊而相对的概念;

    ⑫ 路径、路径长度、回路:略;

    ⑬ 简单路径、简单回路:结点不重复出现;

    ⑭ 距离:最短路径;

    ⑮ 有向树:形如树的有向图。

8.2 图的存储方法

  8.2.1 邻接矩阵存储方法

    设邻接矩阵为 A,A^n 的元素 A^n[i][j] 表示点 i 到点 j 长度为 n 的路径数目

  8.2.2 邻接表存储方法

    邻接表顶点数 n 决定顶点表个数,边数 e 决定边表个数;

    两种存储方法对于不同操作各有优势。

8.3 图的遍历

  8.3.1 深度优先遍历

    

  8.3.2 广度优先遍历

    BFS 树的高度总会小于等于 DFS 树。

8.4 最小生成树

  8.4.1 什么是最小生成树

    

  8.4.2 求最小生成树

    Prim 算法:每次取当前点集合距离最近的点,时间复杂度 O(n ^ 2);

    ② Kruskal 算法:将边按边权从小到大排,每次取两端在不同连通分量的边,时间复杂度 O(m log m);要使用到并查集

  关于更多最小生成树问题的介绍,可以参考 [知识点] 8.3 最小生成树

8.5 最短路径问题

  8.5.1 路径长度的定义

    

  8.5.2 问题的提出

    

  8.5.3 解决问题所需要确定的数据结构

    

  8.5.4 算法(用自然语言表达)

    ① Dijkstra 算法:单源最短路径算法,时间复杂度 O(n ^ 2);

    ② Floyd 算法:多源最短路径算法,时间复杂度 O(n ^ 3)。

  关于更多最短路径问题的介绍,可以参考 [知识点] 8.5 最短路

8.6 AOV 网与拓扑排序

  8.6.1 AOV 网的定义

    AOV 网,Activity On Vertex Network,建立在 DAG(有向无环图)上的用点表示活动、用弧表示活动优先级的网。弧是无权的。

  8.6.2 拓扑排序

    

  8.6.3 拓扑排序的方法

    时间复杂度 O(n + m)。

8.7 AOE 网与关键路径

  8.7.1 AOE 网的定义

    AOE 网,Activity On Edge Network,建立在 DAG(有向无环图)上的用点表示事件、用弧表示活动(开销)的网。弧是带权的。

  8.7.2 AOE 网的存储方法

    

  8.7.3 关键路径

    具有最大路径长度(路径各个活动所需时间之和)的路径为关键路径,上面的活动为关键活动;

    这个最大路径长度即完成整个工程所需的最短工期,只有加快关键活动进度才能缩短工期。

  8.7.4 求关键路径

    拓扑序求出各事件最早发生时间 ve(最长路径长度);

    逆拓扑序求出各事件最晚发生时间 vl;

    各活动最早发生时间 e 等于起点事件的最早发生时间;

    各活动的最晚发生时间 l 等于终点事件的最晚发生时间减去活动时间 w;

    所有 e = l 的活动为关键活动,构成关键路径。

 

第九章 文件与查找

9.1 文件的基本概念

  9.1.1 名次术语

    属性:记录:文件:关键字: 

  9.1.2 文件的逻辑结构

    

  9.1.3 文件的物理结构

    顺序(连续) 链接 索引 散列(随机)

  9.1.4 文件的基本操作

    查找 插入 删除 修改 排序

9.2 顺序文件

  9.2.1 顺序文件的基本概念

    顺序文件:物理结构与逻辑结构记录排列先后次序一致的文件;

    从逻辑结构上划分,按关键字有序的顺序文件成为排序顺序文件,否则为一般顺序文件

    从存储结构上划分,在存储介质上采用连续组织方式的顺序文件称为连续顺序文件,采用链接组织方式的顺序文件称为链接顺序文件

    两种划分方式的名称可叠加使用,比如排序连续顺序文件。

  9.2.2 连续顺序文件的查找

    顺序查找法;二分查找法(前提是有序,根据教材默认为向下取整,边界不取中间值);

    关于二分查找的判定树:

      ①

      

      ② 判断是否可以作为二分查找判定树,将序列按中序遍历填入树中,观察取整方式是否一致。

  9.2.3 链接顺序文件的查找

    

9.3 索引文件

  9.3.1 索引文件的基本概念

    索引:记录关键字值与记录的存储位置之间的对应关系;

    索引文件:由基本数据与索引表两部分组成的数据文件。

  9.3.2 稠密索引文件

    每一个记录在索引表中都占有一项。

  9.3.3 非稠密索引分块文件

    将记录分成若干块,索引表只记录每一块的首地址;

    块内数据不用有序,块间则必须有序。

  9.3.4 多级索引文件

    

9.4 B- 树和 B+ 树

【这一部分应该是之前搞竞赛时唯一完全没接触过的部分】

  9.4.1 B- 树的定义

    B- 树(Balance-Tree),即多路平衡查找树,又称为 B 树。它是在二叉排序树上的优化,带有一定的分块思想。

    B- 树每一个(内部)结点包含如下有序信息:

      关键字个数,子结点指针 0,关键字值 1,子结点指针 1,关键字值 2,子结点指针 2,...

    对于子结点指针 x 指向的子结点,其关键字值的值域为:(关键字值 x - 1,关键字值 x)

    对于 m 阶的 B- 树,满足:

      ① 是一棵 m 叉树,即任意结点最多有 m 个子结点,即最多有 m - 1 个关键字;

      ② 根结点最少有 2 个子结点,其他非叶子结点最少有 m / 2 个子结点;

      ③ 叶子结点都位于同一层,不包含任何关键字信息,所以一般又称其为外部结点

    随便丢个图理解下:

    

    B- 树的高度:对于有 n 个关键字的 m 阶 B- 树,其高度范围为 log_m (n+1) <= h <= (log_(m/2) ((n+1)/2)) + 1。

  9.4.2 B- 树的查找

    

  9.4.3 B- 树的插入

    插入位置一定是最底层中的某个非叶子结点;

    插入过程可能会导致溢出,使得关键字个数超过 m - 1,此时需要进行结点分裂

    从溢出结点开始将其关键字从中间位置分成两部分,一部分放在原结点,一部分放在新结点,并使其依旧满足 B- 树的性质,依次递归直到不出现溢出或者到根结点为止,具体操作举例如下:

    

    特别提及 B- 树的删除。相对应地,在删除时涉及到结点合并

    如果被删结点非叶子结点,则用被删关键字的前驱或后继替代,并对应地在原位置删除即可;

    如果被删结点为叶子结点:

      > 所在结点关键字个数 >= m / 2,则可直接删除;

      > 否则,如果该结点的兄弟结点(左或右)关键字个数 >= m / 2,则可轮替其父结点、兄弟结点中的关键字,具体操作举例如下:

        

      > 否则,将其父结点中关键字下放到兄弟结点中,依次递归直到所有结点中关键字个数满足 B- 树性质或者到根结点为止,具体操作举例如下:

        

  9.4.4 B+ 树的定义

    B+ 树是应数据库所需而出现的一种 B- 树的变形树。和 B- 树相比,B+ 树:

      > 分支结点不记录关键字值个数,即有 n 个关键字的结点只有 n 个子结点;

      > 非叶子结点只起到索引作用,也就是说其包含的值在叶子结点部分均会出现,即叶子结点数 = 记录数;

      > 叶结点被链接成一个不等长链表,故多出一个指向最左边的叶结点的入口,即有两种查找方式;不论哪种方式,必然需要查找到叶子结点所在层。

    

9.5 散列文件

  9.5.1 散列文件的基本概念

    散列函数:把关键字映射成该关键字对应的地址的函数,记为 H(k) = Addr,Addr 可以使数组下标、索引或内存地址;

    同义词:被映射到同一地址的不同关键字互为同义词,这种现象为冲突

    ASL 平均查找长度只依赖于装填因子(记录数 n / 表长 m);

    查找效率取决于散列函数的定义冲突的处理方法装填因子大小

  9.5.2 散列函数的构造

    > 直接定址法:H(k) = ak + b;

    > 除留余数法:H(k) = k MOD p,p 为小于等于地址范围的素数;

    > 其他:数字分析法;平方取中法;叠加法;基数转换法;随机数法。

  9.5.3 冲突的处理方法

    > 开放定址法:H'(key) = (H(key) + d_i) / m;

      ① 线性探测再散列:d_i = i,即顺序查找下一空闲单元;

        这种方法容易造成元素的聚集(堆积),从而降低查找效率。

      ② 二次探测再散列:d_i = i ^ 2,表长必须为 4k + 3 形式的素数;

      ③ 伪随机探测再散列;

    > 再散列法:多个散列函数;

    > 链地址法:将所有同义词存储到同一条链表中。

  关于散列(哈希)表更多的介绍,可以参考 [知识点] 7.2 哈希表

9.6 题目与总结

  【王道 7.3 选择第 9 题】【2013 408 真题】在一棵高度为 2 的 5 阶 B 树中,所含关键字的个数至少是?

    首先对于根结点,至少有 2 个子结点,即至少有 1 个关键字;

    然后对于深度为 2 的一层非叶子结点,至少有 5 / 2 = 3(向上取整)个子结点,即至少有 2 个关键字;

    故关键字至少有 1 * 1 + 2 * 2 = 5 个。

    这类型的问题的分析方式全部统一如此,切记,m 阶 B 树是 m 叉树,但 m 叉树不代表一定存在结点有 m 个子结点!

    比如这道题,就不要去考虑哪一个结点必须得安排上 4 个子结点。

   【王道 7.4 选择第 18 题】【2019 408 真题】现有长度为 11 且初始为空的散列表 HT,散列函数是 H(key) = key % 7,采用线性探查法解决冲突,将关键字序列 87, 40, 30, 6, 11, 22, 98, 20 依次插入 HT 后,HT 查找失败的平均查找长度是?

     哈希值范围为 0 ~ 6,故查找失败对应的地址有 7 个。关键字序列哈希后对应的散列地址分别为 0 ~ 7;故对于需要查找的对应地址为 0 的关键字,需要从 0 到 8 依次查找一次,共计 9 次;地址为 1 的关键字,需要从 1 到 8 查找 8 次,以此类推。

    故 ASL = (9 + 8 + ... + 3) / 7 = 6。

 

第十章 内排序

10.1 排序的基本概念

  10.1.1 排序的定义

    

  10.1.2 排序的功能

    对任意 n 个关键字排序的比较次数至少为 log(n!) 向上取整

  10.1.3 排序的分类

    稳定性:对于相等的两个元素,如果排序后先后顺序依旧不变,则该排序算法是稳定的。

    

  关于更多排序算法的介绍,可以参考 [知识点] 2.4 排序十讲

10.2 插入排序法

  10.2.1 核心思想

    从第 i 位开始,每次循环将第 1 到 i - 1 位视作已排序部分,第 i 到 n 位为未排序部分,在已排序部分中将第 i 位的元素插入到合适的位置,以此类推,共需要循环 n - 1 次;

  10.2.2 算法

    

10.3 选择排序法

  10.3.1 核心思想

    每次从第 i 个数到第 n 个数中找到最小的元素,将这个元素和第 i 个位置上的元素交换。

  10.3.2 算法

    

10.4 冒泡排序法

  10.4.1 核心思想

    从第 1 位开始扫描,检查相邻的两个元素,如果前一个元素大于后一个元素,则交换两个元素的位置,一直扫描到第 n 位;

    扫描完后,能且仅能确定第 n 个元素为整个序列的最大值,则下一轮扫描为从第 1 位到第 n - 1 位,并确定第 n - 1 位为倒数第二大元素,以此类推,扫描 n - 1 次后,完成排序。

  10.4.2 算法

    

10.5 Shell 排序法

  10.5.1 核心思想

    希尔(Shell)排序是插入排序的一种,相当于分治进行多次直接插入排序,每次只对间距为固定长度的几个元素排序;

    目前如何获得最优时间复杂度仍是未解决的问题;

    目前希尔提出的第一段排序间距为 d_1 = n / 2,递推公式 d_i+1 = d_i / 2,最后一段等于 1;

    仅适用于顺序存储。

  10.5.2 算法

    

10.6 快速排序法

  10.6.1 核心思想

    快速排序是交换排序的一种。上述《排序十讲》已经有详细介绍,不过似乎不仅划分方式没有明确,如何移动也是有不同的做法的;

    从结果而言,这些做法是无异的,并且也都属于 O(n log n) 的范畴内,但效率上存在一定差异,更重要的是目前涉及到各种理论操作的题目,比如求问使用快速排序第一趟结果如何,或者反而求之,就会因不同做法而不同。目前看到的三种做法在下面提到。

  10.6.2 算法步骤

    对于如何移动元素,有三种做法:

      ① 来自之前的做法是,直接平移元素,即保留所有小于或大于基准数的元素的相对顺序;这样常数肯定是较高的,以为需要进行多次循环;

      ② 来自某校课件的做法是,从左到右找大于基准数的数,从右到左找小于基准数的数,并交换,直到两个指针重叠,再继续向左找下一个小于基准数的数,将其与基准数交换。

      ③ 来自王道教材(严蔚敏《数据结构》)的做法和第二种差不多,只是它会把基准数一开始就单独列出并空缺出一个位置,然后左右指针依次找大于/小于基准数的数并将其移动到空缺位,直到两个指针重叠,将基准数填入即可(我觉得我讲得都比王道清楚,不太理解王道为什么一没文字描述二图也画得一般)。

    在已经基本有序的数据中,快速排序算法难以发挥长处。

10.7 堆积排序法

  关于堆与堆排序的内容,可以参考 [知识点] 7.4 堆与优先队列

  10.7.1 堆积的定义

    

  10.7.2 排序的核心思想

    

  10.7.3 排序步骤

    略

  10.7.4 调整子算法

    略

  10.7.5 建初始堆积

    略

  10.7.6 堆积排序算法

    略

10.8 二路归并排序法

  10.8.1 什么是二路归并

    

  10.8.2 核心思想

    将数列平均划分为两个子序列,分别递归,再次划分为两个子序列,并进行排序,最后逐层回溯将划分的两个子序列合并。

*10.9 排序算法总结

  在 m 趟排序后,

    > 冒泡排序、选择排序至少最后(最前) m 个数是最终位置;

    > 快速排序、堆排序至少有 m 个数是最终位置;

    > 插入排序前 m 个数有序;

  插入排序、归并排序在最后一趟排序前,可以所有数都不在最终位置;

  在数据基本有序时,快速排序效率最低;

  插入排序、冒泡排序最优可以达到线性时间复杂度(即 O(n));

  插入排序、选择排序、基数排序的排序趟数与序列初始状态无关;

  希尔排序、堆排序在采用链式存储后效率会下降;

  冒泡排序是唯一可以提前终止(当本轮排序已经有序时)的排序。

10.10 题目

  【王道 8.3 选择第 17 题】【2019 408 真题】下列序列中,不可能是快速排序第二趟结果的是?

      A. 5, 2, 16, 12, 28, 60, 32, 72

      B. 2, 16, 5, 28, 12, 60, 32, 72

      C. 2, 12, 16, 5, 28, 32, 72, 60

      D. 5, 2, 12, 28, 16, 32, 72, 60

    根据快速排序特性,完成第一趟后必有至少 1 个数满足其左侧数均小于而右侧数均大于的情况;完成第二趟后必有至少 2 个数,以此类推。但如果这两个数均不在序列最左/右侧,那么则必须要有至少 3 个数(这一点在这道题前面 2014 年 408 真题里的解析却不是这么写的,可以认为是存在漏洞的解释)。

    以此,A 选项有 28, 72 满足,其中 72 在最右侧,故满足;

    B 选项有 2, 72 满足,分别在最左、右侧,故满足;

    C 选项有 2, 32, 28 满足,故满足;

    D 选项有 12, 32 满足,而均在序列中间,故不满足。

    所以选 D。

  【王道 8.4 选择第 3 题】设线性表中每个元素有两个数据项 k1, k2,先按照 k1 从小到大排序,k1 相等时按 k2 从小到大排序,满足这种要求的排序方法是?

      A. 先按 k1 进行直接插入排序,再按 k2 进行简单选择排序

      B. 先按 k2 进行直接插入排序,再按 k1 进行简单选择排序

      C. 先按 k1 进行简单选择排序,再按 k2 进行直接插入排序

      D. 先按 k2 进行简单选择排序,再按 k1 进行直接插入排序

    不论是第一轮还是第二轮排序,都是以整体为范围来排序。所以后进行的排序会成为最终关键字,也就是第一关键字,故 k1 后排序,排除 A, C;

    直接插入排序是稳定排序,简单选择排序是不稳定排序。第一轮排序是无所谓的,但如果第二轮排序使用不稳定排序,会导致第一轮确定好的 k2 的大小关系被破坏,故只能选择 D。

posted @ 2021-09-08 23:26  jinkun113  阅读(2957)  评论(0编辑  收藏  举报