跳转至

数据结构打卡表参考文档

Ch1 数组的应用

在应用题中,“数组”常结合“矩阵压缩存储”考察,此类题目需要注意以下条件:

✧ 行优先存储 or 列优先存储?

✧ 矩阵下标从 1 or 0 开始?——若题目未特别说明,矩阵下标默认从1开始

✧ 数组下标从 0 or 1 开始?——若题目未特别说明,数组下标默认从0开始

1.1 对称矩阵的压缩存储

若考察“对称矩阵的压缩存储”,除了关注 行优先 or 列优先,还需注意题目要求压缩存储的是 下三角区域 or 上三角区域:

1.1.1 自己动手创造,画一个5行5列的对称矩阵

1720596802820-4b24b011-f94b-4fc7-b518-4c868928e028.png

1.1.2 + 1.1.4 按“行优先/列优先”压缩存储上述矩阵,画出一维数组的样子

1720596837220-7536f937-7ded-40c8-907f-7eaccd615d0b.png

1.1.3 + 1.1.5 写出元素 i,j 与 数组下标之间的对应关系

设n行n列矩阵下标从1开始,数组下标从0开始。矩阵元素 i,j 和数组元素 k 的对应关系为:

①压缩存储下三角区域+主对角线,行优先存储

1720596881745-6f903a02-e9ac-4171-8657-49417e4a5b3d.svg

②压缩存储下三角区域+主对角线,列优先存储

1720596898599-a9e81608-5aaf-4252-8908-97d9de69e931.svg

③压缩存储上三角区域+主对角线,行优先存储

1720596898624-a11e58b2-c829-4ffe-8794-8ab9f6749a17.svg

④压缩存储上三角区域+主对角线,列优先存储

1720596898712-eb33c686-5cb2-4ea1-bf2a-400789fa4ec1.svg

Info

注:基于对称矩阵的特性可知,只需将式①的 i、j 互换即可得到④;只需将式②的 i、j 互换即可得到③;

1.1.6 假设你的对称矩阵表示一个无向图,画出无向图的样子

1720596942647-eb443f7e-1c12-428a-b5ea-9b7e0cc1a527.png

注:“无向图的邻接矩阵”刚好是一个“对称矩阵”,因此这两个考点很可能会结合考察

1.2 上/下三角矩阵的压缩存储

1.2.1 自己动手创造,画一个5行5列的下三角矩阵

1720597002058-38996306-6deb-4f63-8a74-b906b76c9823.png

1.2.2 + 1.2.4 按“行优先/列优先”压缩存储上述矩阵,画出一维数组的样子

1720597023452-b7312434-e86c-48a7-a3bc-6cc5d6e96b83.png

1.2.3 + 1.2.5 写出元素 i,j 与 数组下标之间的对应关系

设n行n列矩阵下标从1开始,数组下标从0开始。矩阵元素 i,j 和数组元素 k 的对应关系为:

①压缩存储下三角区域+主对角线,行优先存储

1720597058510-8761427c-2e52-4995-81f8-9df14497a875.svg

②压缩存储下三角区域+主对角线,列优先存储

1720597058551-7cdb1944-b377-4b6c-98c5-d5c800d13dc0.svg

1.2.6 假设你的对称矩阵表示一个有向图,画出有向图的样子

1720597086168-5d685cfb-1609-4abe-aa56-95321d0ebe19.png

Info

注:若一个有向图的邻接矩阵仅有下三角区域非空,主对角线和上三角区都为空(如下所示),则该有向图一定是有向无环图。因为所有的“有向边”都是大编号顶点指向小编号顶点,所以对于任意两个顶点 i、j (假设 i > j),则有可能存在 i 到 j 的路径,但一定不存在 j 到 i 的路径,因此不可能形成“环”。

1720597116971-0f21abc7-bfb5-454b-92a7-990f6570a4b6.png

1.3 三对角矩阵的压缩存储

Warning

注:任务 1.3.1~1.3.5 请参考王道书(或考点精讲课件)

“三对角矩阵(带状矩阵)”更有可能考选择题,不太可能在应用题考察。从出题人的视角猜测,三对角矩阵不太好和其他考点综合考察。

相比之下,“对称矩阵”可能和“无向图”综合考察;“三角矩阵”可能和“有向无环图”综合考察

Ch2 栈、队列的应用

2.1 栈的定义和基本操作实现

2.1.1 + 2.1.2 定义顺序栈,并实现基本操作

顺序栈的定义和实现,参考王道书 3.1.2

2.1.3 + 2.1.4 定义链栈,并实现基本操作(要求单链表实现,栈顶在链头)

//定义栈结点
typedef struct SNode{           //定义单链表结点类型
    int data;                       //每个节点存放一个数据元素
    struct SNode *next;         //指针指向下一个节点
}SNode, *LiStack;

//初始化一个链栈(单链表实现,栈顶在链头)
bool InitStack(LiStack &S) {
    S = (SNode *) malloc(sizeof(SNode)); //分配一个头结点
    S->next = NULL;         //头结点之后暂时还没有节点
    return true;
}

//判断栈是否为空
bool StackEmpty(LiStack S){
    if(S->next==NULL)   //头结点后面没有结点
        return true;    //返回true,表示栈为空
    else
        return false;
}

//入栈(本质上是单链表的“头插法”)
bool Push (LiStack &S, int x){
    SNode * p = (SNode *) malloc(sizeof(SNode));    //新分配一个结点
    p->data = x;     //存入新元素
    p->next = S->next;
    S->next = p;     //新结点插入到头结点后面
    return true;
}

//出栈(本质上是单链表的“头删法”)
bool Pop (LiStack &S, int &x){
    if (StackEmpty(S))      //栈空,出栈操作失败
        return false;

    SNode * p = S->next;     //栈不空,链头结点出栈
    x = p->data;             //x返回栈顶元素
    S->next = p->next;       //头删法,栈顶元素"断链"
    free(p);                 //释放结点
    return true;
}

2.1.5 + 2.1.6 定义链栈,并实现基本操作(要求双链表实现,栈顶在链尾)

//定义栈结点
typedef struct DbSNode{             //定义双链表结点类型
    int data;                       //每个节点存放一个数据元素
    struct DbSNode *last,*next;     //指向前后两个结点
}DbSNode;

typedef struct DbLiStack{           //双链表实现的栈(栈顶在链尾)
    struct DbSNode *head, *rear;    //两个指针,分别指向链头、链尾
}DbLiStack, *DbStack;

//初始化一个链栈(单链表实现,栈顶在链头)
bool InitDbStack(DbStack &S) {
    S = (DbLiStack *) malloc(sizeof(DbLiStack));    //初始化一个链栈(双链表实现,栈顶在链尾)
    DbSNode * p = (DbSNode *) malloc(sizeof(DbSNode)); //新建一个头结点
    p->next = NULL;         //头结点之后暂时还没有节点
    p->last = NULL;         //头结点之前没有节点

    S->head = p;
    S->rear = p;

    return true;
}

//判断栈是否为空
bool DbStackEmpty(DbStack S){
    if(S->head == S->rear)   //头指针和尾指针指向同一个结点
        return true;    //返回true,表示栈为空
    else
        return false;
}

//入栈(在双链表链尾插入)
bool DbPush (DbStack &S, int x){
    DbSNode * p = (DbSNode *) malloc(sizeof(DbSNode));     //新分配一个结点
    p->data = x;     //存入新元素
    p->next = NULL;
    p->last = S->rear;  //新结点插入链尾
    S->rear->next = p;
    S->rear = p;
    return true;
}

//出栈(删除双链表链尾元素)
bool DbPop (DbStack &S, int &x){
    if (DbStackEmpty(S))     //栈空,出栈操作失败
        return false;

    DbSNode * p = S->rear;    //栈不空,链尾结点出栈
    x = p->data;             //x返回栈顶元素
    S->rear = p->last;       //更新链尾指针
    S->rear->next = NULL;
    free(p);                 //释放结点
    return true;
}

2.2 队列的定义和基本操作实现

2.2.1 + 2.2.2 写代码:定义顺序存储的队列,并实现基本操作

参考王道书3.2.2

2.2.3 + 2.2.4 定义链式存储的队列,并实现基本操作

参考王道书 3.2.3

Ch3 树的应用

3.1 二叉树的性质、定义、画图

3.1.1 总结二叉树的度、树高、结点数等属性之间的关系

略

3.1.2 定义顺序存储的二叉树(从数组下标1开始存储)

Warning

二叉树顺序存储的数据结构定义,需要注意以下两点:

  1. 二叉树的结点用数组存储,每个结点需要标记“是否为空”

  2. 各结点的数组下标隐含结点关系,要能根据一个结点的数组下标 i,计算出其父节点、左孩子、右孩子的数组下标

typedef struct TreeNode {
    int data;       //结点中的数据元素
    bool isEmpty;   //结点是否为空
} TreeNode;

//初始化顺序存储的二叉树,所有结点标记为"空"
void InitSqBiTree (TreeNode t[], int length) {
    for (int i=0; i<length; i++){
        t[i].isEmpty=true;
    }
}

int main(){
    TreeNode t[100];        //定义一棵顺序存储的二叉树
    InitSqBiTree(t, 100);   //初始化为空树
    //...
}

3.1.3~3.1.5 实现函数,找到结点 i 的父结点、左孩子、右孩子

Warning

若顺序二叉树从数组下标1开始存储结点,则:

● 结点 i 的父结点编号为 i/2

● 结点 i 的左孩子编号为 i*2

● 结点 i 的右孩子编号为 i*2+1

若顺序二叉树从数组下标0开始存储结点,则:

● 结点 i 的父结点编号为 [(i+1)/2] - 1

● 结点 i 的左孩子编号为 [(i+1)2] - 1 = 2i + 1

● 结点 i 的右孩子编号为 [(i+1)2+1] - 1 = 2i + 2

//判断下标为 index 的结点是否为空
bool isEmpty(TreeNode t[], int length, int index){
    if (index >= length || index < 1) return true;   //下标超出合法范围
    return t[index].isEmpty;
}

//找到下标为 index 的结点的左孩子,并返回左孩子的下标,如果没有左孩子,则返回 -1
int getLchild(TreeNode t[], int length, int index){
    int lChild = index * 2;   //如果左孩子存在,则左孩子的下标一定是 index * 2
    if (isEmpty(t, length, lChild)) return -1;  //左孩子为空
    return lChild;
}

//找到下标为 index 的结点的右孩子,并返回右孩子的下标,如果没有右孩子,则返回 -1
int getRchild(TreeNode t[], int length, int index){
    int rChild = index * 2 + 1;                 //如果右孩子存在,则右孩子的下标一定是 index * 2 + 1
    if (isEmpty(t, length, rChild)) return -1;  //右孩子为空
    return rChild;
}

//找到下标为 index 的结点的父节点,并返回父节点的下标,如果没有父节点,则返回 -1
int getFather(TreeNode t[], int length, int index){
    if (index == 1) return -1;          //根节点没有父节点
    int father = index / 2;             //如果父节点存在,则父节点的下标一定是 index/2,整数除法会自动向下取整
    if (isEmpty(t, length, father)) return -1;
    return father;
}

3.1.6 利用上述三个函数,实现先/中/后序遍历

Warning

对于链式存储的二叉树,先序、中序、后序遍历的递归遍历代码实现很简单,大家都会

对于顺序存储的二叉树,利用上面👆封装的函数,也可以实现递归遍历

//从下标为 index 的结点开始先序遍历
void PreOrderSqTree (TreeNode *t, int length, int index){
    if (isEmpty(t, length, index))    //当前为空节点
        return;
    visitNode(t[index]);     //访问结点
    PreOrderSqTree(t, length, getLchild(t, length, index));       //先序遍历左子树
    PreOrderSqTree(t, length, getRchild(t, length, index));       //先序遍历右子树
}

//从下标为 index 的结点开始中序遍历
void InOrderSqTree (TreeNode *t, int length, int index){
    if (isEmpty(t, length, index))    //当前为空节点
        return;
    InOrderSqTree(t, length, getLchild(t, length, index));       //中序遍历左子树
    visitNode(t[index]);     //访问结点
    InOrderSqTree(t, length, getRchild(t, length, index));       //中序遍历右子树
}

//从下标为 index 的结点开始后序遍历
void PostOrderSqTree (TreeNode *t, int length, int index){
    if (isEmpty(t, length, index))    //当前为空节点
        return;
    PostOrderSqTree(t, length, getLchild(t, length, index));       //后序遍历左子树
    PostOrderSqTree(t, length, getRchild(t, length, index));       //后序遍历右子树
    visitNode(t[index]);     //访问结点
}

int main(){
    TreeNode t[100];        //定义一棵顺序存储的二叉树
    InitSqBiTree(t, 100);   //初始化为空树
    //...在空二叉树中插入数据
    //...略
    InOrderSqTree (t, 100, 1);    //从根节点1出发,进行中序遍历
}

3.1.7 定义顺序存储的二叉树(从数组下标0开始存储)

与 3.1.2 完全相同

3.1.8~3.1.10 实现函数,找到结点 i 的父结点、左孩子、右孩子(结点从下标0开始存储)

Warning

与 3.1.3~3.1.5 思路相同,只不过结点编号的计算有些区别而已

若顺序二叉树从数组下标0开始存储结点,则:

● 结点 i 的父结点编号为 [(i+1)/2] - 1

● 结点 i 的左孩子编号为 [(i+1)2] - 1 = 2i + 1

● 结点 i 的右孩子编号为 [(i+1)2+1] - 1 = 2i + 2

//判断下标为 index 的结点是否为空
bool isEmpty(TreeNode t[], int length, int index){
    if (index >= length || index < 0) return true;   //下标超出合法范围
    return t[index].isEmpty;
}

//找到下标为 index 的结点的左孩子,并返回左孩子的下标,如果没有左孩子,则返回 -1
int getLchild(TreeNode t[], int length, int index){
    int lChild = index * 2 + 1;   //如果左孩子存在,则左孩子的下标一定是 index * 2
    if (isEmpty(t, length, lChild)) return -1;  //左孩子为空
    return lChild;
}

//找到下标为 index 的结点的右孩子,并返回右孩子的下标,如果没有右孩子,则返回 -1
int getRchild(TreeNode t[], int length, int index){
    int rChild = index * 2 + 2;                 //如果右孩子存在,则右孩子的下标一定是 index * 2 + 1
    if (isEmpty(t, length, rChild)) return -1;  //右孩子为空
    return rChild;
}

//找到下标为 index 的结点的父节点,并返回父节点的下标,如果没有父节点,则返回 -1
int getFather(TreeNode t[], int length, int index){
    if (index == 1) return -1;          //根节点没有父节点
    int father = (index + 1) / 2 - 1;             //如果父节点存在,则父节点的下标一定是 index/2,整数除法会自动向下取整
    if (isEmpty(t, length, father)) return -1;
    return father;
}

3.1.11 利用上述三个函数,实现先/中/后序遍历

与 3.1.6 一模一样,不再赘述

3.2 树的性质

3.2.1 总结树的度、树高、结点数等属性之间的关系

注:通过“树、二叉树的性质”相关小题、大题积累解题思路

3.3 树的定义、画图

3.3.1 使用“双亲表示法”定义顺序存储的树(以及森林)

参考王道书 5.4.1 双亲表示法的代码描述

1720597960782-4a8f2afe-3042-4d3f-bbbc-c8d22474f67c.png

Warning

双亲表示法可以表示“多叉树”,如下所示:

1720607881771-43b3d4e5-bebe-4ee4-aeaa-e0bba67bc4e8.png

显然,双亲表示法也可以表示“森林”

只需令森林中的每棵树根结点的 parent 值为 -1 即可,如下所示:

1720607901846-bbd6114a-ad58-4e86-b30a-9521123df15a.png

注意上面👆这个例子,如果未来考“森林”的应用题,很可能考双亲表示法的数据结构定义+画图

3.3.2 使用“孩子表示法”,定义链式存储的树(以及森林)

//Child表示下一个孩子的信息
typedef struct Child{
    int index;             //孩子编号
    struct Child * next;   //下一个孩子
} Child;

//TreeNode用于保存结点信息
typedef struct TreeNode {
    char data;             //结点信息
    Child * firstChild;    //指向第一个孩子
} TreeNode;

TreeNode tree[10];   //定义一棵拥有10个结点的树(孩子表示法)

Warning

孩子表示法可表示“多叉树”,如下所示:

1720607977584-baea6567-452c-4966-91c1-0355e751d6ee.png

显然,孩子表示法也可以表示“森林”,如下所示:

1720607998383-543e9590-7af1-4e36-89f7-93da010ce622.png

3.3.3 对比:树的孩子表示法存储 v.s. 图的邻接表存储 v.s. 散列表的拉链法 v.s. 基数排序

①树的孩子表示法,请参考上面👆

②图的邻接表存储,请参考王道书6.2.1,数据结构示意图如下所示:

1720608031930-189d1e27-4a66-4ea4-af02-5a084bcde6a2.png

如果一个图拥有10个顶点,则可以这么定义其数据结构(请与本文档 3.3.2 “孩子表示法” 的数据结构定义对比):

typedef struct ArcNode{         //边表结点
    int adjvex;                 //该弧所指向的顶点的位置
    struct ArcNode *next;       //指向下一条弧的指针
    //InfoType info;            //网的边权值
}ArcNode; 

typedef struct VNode{           //顶点表结点
    char data;                  //顶点信息
    ArcNode *first;             //指向第一条依附该顶点的弧的指针
}VNode;

VNode graph[10];               //定义一个拥有10个顶点的图(邻接表法)

③散列表的拉链法,请参考王道书 7.5.3,数据结构示意图如下所示:1720608079485-28be128a-acac-4a66-93ba-273ad2b03fcc.png、

④基数排序,请参考王道书 8.5.2,算法示意图如下所示:

1720608105038-dbb3b2d5-bc37-4e03-93a1-ee66d6296241.png

可见,上述四种数据结构/算法的示意图结构是类似的。相应地,对四种数据结构的定义也是类似的,可放在一起学习和记忆。

408重点掌握 ①树的孩子表示法、②图的邻接表 的数据结构定义代码,剩下的 ③散列表拉链法、④基数排序 可参考本文档 5.3.8、6.4.3。

3.3.4 使用“孩子兄弟表示法”,定义链式存储的树(以及森林)

可参考王道书 5.4.1

typedef struct CSNode{
    ElemType data;                              //数据域
    struct CSNode *firstchild,*nextsibling;    //第一个孩子和右兄弟指针
}CSNode,*CSTree;

Warning

孩子表示法可表示“多叉树”,如下所示:

1720608154824-afbc89f1-8583-4a32-ae0a-d8df99c9fccc.png

显然,孩子表示法可表示“森林”,如下所示:1720608180466-f64c4936-b16c-4987-870e-233d0f2ce4c8.png

3.3.5 + 3.3.6 画出“树”和“森林”的数据结构示意图

请参考上面 👆 3.3.1、3.3.2、3.3.4

特别注意:应用题中很可能考双亲表示法、孩子表示法、孩子兄弟表示法的数据结构定义+画图

3.4 哈夫曼树的应用

该部分请参考王道书、考点精讲(基础课)课件自行完成

另外,请认真完成 2020年真题的应用题,即王道书 5.5.3 大题第3题

3.5 并查集的应用

3.5.1~3.5.3 实现并查集的数据结构定义,并实现 Union、Find 两个基本操作

#define SIZE 13
int UFSets[SIZE];       //用一个数组表示并查集

//初始化并查集
void Initial(int S[]){
    for(int i=0;i<SIZE;i++)         
        S[i]=-1;
}

//Find “查”操作,找x所属集合(返回x所属根结点)
int Find(int S[],int x){
    while(S[x]>=0)          //循环寻找x的根
        x=S[x];
    return x;                       //根的S[]小于0
}

//Union “并”操作,将两个集合合并为一个
void Union(int S[],int Root1,int Root2){
    //要求Root1与Root2是不同的集合
    if(Root1==Root2)    return;     
    //将根Root2连接到另一根Root1下面
    S[Root2]=Root1;
}

可采取“小树合并到大树”的策略优化 Union操作

//Union “并”操作,小树合并到大树
void Union(int S[],int Root1,int Root2){
    if(Root1==Root2)    return;     
    if(S[Root2]>S[Root1]) { //Root2结点数更少
        S[Root1] += S[Root2];   //累加结点总数
        S[Root2]=Root1; //小树合并到大树
    } else {
        S[Root2] += S[Root1];   //累加结点总数
        S[Root1]=Root2; //小树合并到大树            
    }
}

可采取“路径压缩”的策略优化 Find 操作

//Find “查”操作优化,先找到根节点,再进行“压缩路径”
int Find(int S[],int x){
    int root = x;
    while(S[root]>=0)  root=S[root]; //循环找到根
    while(x!=root){ //压缩路径
      int t=S[x];       //t指向x的父节点
      S[x]=root;        //x直接挂到根节点下
      x=t;
    }
    return root;                        //返回根节点编号
}

3.5.4 设计一个例子,对10个元素 Union

假设初始长度为 10 ( 0 ~ 9 )的并查集,按 1-2、3-4、5-6、7-8、 0-5 、1-9、9-3、8-0、4-6 的顺序进行Union操作

1720608266301-c7ad0e0b-521b-41ad-95b4-1df2673135c0.png

1720608287824-4ba74213-cb65-481e-9be1-dd25e88eaa1b.png

1720608300244-429df947-aed5-41a7-976a-2b0a22ef027d.png

1720608318243-519f125c-8f3c-4308-b9d3-24e162afd9ea.png

1720608333997-df67bec8-6c7f-421e-bc39-5d50e850d639.png

3.5.5 基于上述例子,进行若干次 Find,并完成“压缩路径”

依次对 6、9、8 执行 Find 操作,并完成压缩路径

1720608365156-8d4ddd8e-f690-46db-b759-a43e6ae1fea6.png

1720608385622-33493ac5-6dc1-427d-85ab-169828155bf7.png

1720608413071-940b58ff-79fb-46de-8c7c-e7ccd7b17626.png

3.6 二叉排序树、平衡二叉树的应用题潜在考法

3.6.1 自己设计一个例子,给出不少于10个关键字序列,按顺序插入一棵初始为空的二叉排序树,画出每一次插入后的样子

注:自己设计插入序列,并在“408快乐小网站”模拟执行

Binary Search Trees

例:从一棵初始为空的BST开始,依次插入:50、26、10、3、5、60、90、40、55、100、59,最终结果如下

1720608456946-0d199c3e-cb51-45e8-aa76-89afb1d85e1d.png

插入过程自己用网站模拟,可使用网站的“Pause、Step Forward”两个按钮单步执行。

3.6.2 基于你设计的例子,计算二叉排序树在查找成功和查找失败时的 ASL

计算“树形查找”的ASL时,需要补充“失败结点”,再进行计算

1720608480123-682ff219-fe1e-462a-9793-60094d8cc822.png

1720608492547-8ed41840-e4ff-48aa-8351-76de7307278a.svg

1720608492580-edd9a4e9-c809-49f6-a040-061baadfa4b9.svg

3.6.3 基于你设计的例子,依次删除不少于4个元素,画出每一次删除之后的样子(需要包含四种删除情况——删一个叶子结点、删一个只有左子树的结点、删一个只有右子树的结点、删一个既有左子树又有右子树的结点)

例:基于 3.6.1 的例子,依次删除:59(叶子)、10(仅有左子树)、90(仅有右子树)、50(拥有左右子树)。4次删除后,二叉排序树的状态分别如下:

1720608519105-6c3a9e6a-4d93-4581-9efd-9905647ed354.png1720608538633-4ac6e822-da98-4271-ad97-2c752f3f1625.png

1720608554445-507af1f3-281f-4abc-86c5-cced9019bcd2.png1720608574381-f619b562-20e3-4fa6-8c4c-b721b2b220ea.png

3.6.4 自己设计一个例子,给出不少于10个关键字序列,按顺序插入一棵初始为空的平衡二叉树,画出每一次插入后的样子(你设计的例子要涵盖LL、RR、LR、RL四种调整平衡的情况)

注:自己设计插入序列,并在“408快乐小网站”模拟执行

AVL Trees (Balanced binary search trees)

例:从一棵初始为空的AVL Trees 开始,依次插入:50、26、10(LL)、3、5(LR)、60、90(RR)、40、55、100、59(RL),最终结果如下

1720608630399-91b2598e-130d-4d65-8f1e-3a8f02d2cfd7.png

插入过程自己用网站模拟,可使用网站的“Pause、Step Forward”两个按钮单步执行。上述例子中,插入元素 10、5、90、59 时分别发生了 LL、LR、RR、RL 四种“调整平衡”的情况。

3.6.5 基于你设计的例子,计算平衡二叉树在查找成功和查找失败时的 ASL

计算“树形查找”的ASL时,需要补充“失败结点”,再进行计算

1720608691007-887ce203-1d36-4fd1-b6fa-6d8aa36b33fe.png

1720608701165-5d46b146-cbe7-4352-8151-5c5e88bc52a8.svg

1720608701264-239bfb7f-befd-41b0-9b97-8b6d4102d1e4.svg

Ch4 图的应用

4.1 图的性质

注:通过“图的性质”相关小题、大题积累解题思路

4.2 图的数据结构定义

4.2.1 写代码:定义一个顺序存储的图(邻接矩阵实现)

#define N 8         //顶点数目的最大值,根据题目要求自己定义
typedef struct{
    char vex[N];    //N个顶点信息,每个顶点存储一个char字符,可根据题目要求将char改为其他类型
    int weight[N][N];   //N*N邻接矩阵,每条边的权值用int变量表示
    int vexnum,arcnum;  //图的当前顶点数和弧数
}MGraph;

4.2.2 写代码:定义一个链式存储的图(邻接表实现)

#define N 8     //图中顶点数目的最大值,根据题目要求自己定义
typedef struct ArcNode{         //弧结点
    int vexIndex;               //该弧所指向的顶点编号
    int weight;                //该弧的权值
    struct ArcNode *next;       //指向下一个弧结点
}ArcNode; 

typedef struct VNode{   //顶点结点
    char data;          //顶点信息,每个顶点存储一个char字符,可根据题目要求将char改为其他类型
    ArcNode *first;     //指向第一条依附该顶点的弧的指针
}VNode;

typedef struct{
    VNode vex[N];           //N个顶点
    int vexnum,arcnum;      //图的顶点数和弧数
} ALGraph;                  //ALGraph是以邻接表存储的图类型

4.2.3 自己设计一个不少于6个结点的 带权无向图 ,并画出其邻接矩阵、邻接表的样子

1720608764474-5d52e0e6-7650-4959-a45a-00842d6b339a.png

4.2.4 自己设计一个不少于6个结点的 带权有向图 ,并画出其邻接矩阵、邻接表的样子

1720608807870-8113e1a4-b92d-4ecd-9391-10bd04ab2d9b.png

4.3 图的应用:最小生成树

4.3.1 自己设计一个不少于6个结点的 带权无向连通图 ,并画出其邻接矩阵、邻接表的样子

1720608835497-ef9203d2-f711-42f0-bdc0-3167f07e6fd0.png

4.3.2 基于上述无向连通图,使用Prim算法生成MST,画出算法执行过程的示意图,并计算MST的总代价

注:普利姆算法是选代价最小的“点”,克鲁斯卡尔算法是选代价最小的“边”,别记混了。

可以这么联想记忆,“Prim”是字母 P 开头的,“点”的英文是Point,也是 P 开头的,因此Prim算法是选“点”。

Warning

从顶点0开始,运行 Prim 算法的过程如下:

1720608884964-463a6384-fa46-4cbe-bebd-d73c248758c8.png

Warning

从顶点7开始,运行 Prim 算法的过程如下。这里想强调的是:当同时有两个代价相同的顶点时,优先将编号小的顶点加入MST。如:第 1 轮从顶点7出发,与其相连的顶点3、顶点5代价都是最小的,优先选择编号更小的顶点3。

1720608917185-c059a80c-2249-4cc1-a9d6-d4ca80c9918a.png

4.3.3 基于上述无向连通图,使用Kruskal算法生成MST,画出算法执行过程的示意图,并计算MST的总代价

1720608943893-d0357bea-2df4-4b64-bd42-d5fe2bf84e60.png

Warning

Kruskal 算法的执行过程是:先将各条边按照权值递减的排序,再按顺序依次检查是否将这些边加入MST。各条边排序结果如下:

( 2 ,4 ) ——权值 1

( 2, 5 ) ——权值 3

( 3, 7 ) ——权值 3

( 5, 7 ) ——权值 3

( 0, 3 ) ——权值 5

( 6, 7 ) ——权值 5

( 5, 6 ) ——权值 6

( 1, 2 ) ——权值 9

可见,当多条边的权值相同时,“起点”编号更小的边排在前面。因此,在Kruskal算法的第2轮中,虽然有3条边 (2,5) (3,7) (5,7) 的权值都是最小的,但算法会优先选择将排序靠前的 (2,5) 加入MST。

4.4 图的应用:最短路径

4.4.1 基于你设计的 带权有向图 ,从某一结点出发,执行Dijkstra算法求单源最短路径。用文字描述每一轮执行的过程

下面将从顶点1出发,执行 Dijkstra 算法

1720609160323-433271c7-77c8-4630-98da-655611afbed7.png

Warning

注1:如果题目让你描述迪杰斯特拉算法的执行过程,可以试着模仿王道书画一张表。如下所示👇。你会发现这个表画起来很崩溃,复杂的飞起。相信我,老师改卷也不愿意改这么复杂的答案。因此,考场上不太可能让你画这么复杂的表。

顶点(这一列不包含起点) 第 1 轮 第 2 轮 第 3 轮 第 4 轮 第 5 轮 第 6 轮 第 7 轮
0 ∞ ∞ ∞ ∞ ∞ ∞ ∞
2 4
1→2
已完成 已完成 已完成 已完成 已完成 已完成
3 ∞ ∞ 7
1→5→3
7
1→5→3
已完成 已完成 已完成
4 ∞ 13
1→2→4
13
1→2→4
13
1→2→4
13
1→2→4
已完成 已完成
5 5
1→5
5
1→5
已完成 已完成 已完成 已完成 已完成
6 5
1→6
5
1→6
5
1→6
已完成 已完成 已完成 已完成
7 ∞ ∞ ∞ ∞ ∞ 14
1→2→4→7
已完成
集合S {1, 2} {1,2,5} {1,2,5,6} {1,2,5,6,3} {1,2,5,6,3,4} {1,2,5,6,3,4,7} {1,2,5,6,3,4,7,0}

注2:以下是一种更适合考试的“简洁答题方法”。自己打草稿,快速确定每一轮Dijkstra算法的运行结果,然后按下面这种方式答题到试卷上。

答:

从顶点1出发,运行迪杰斯特拉算法的过程如下:

第一轮:顶点1到2的最短路径为 1→2,距离为4

第二轮:顶点1到5的最短路径为 1→5,距离为5

第三轮:顶点1到6的最短路径为 1→6,距离为5

第四轮:顶点1到3的最短路径为 1→5→3,距离为7

第五轮:顶点1到4的最短路径为 1→2→4,距离为13

第六轮:顶点1到7的最短路径为 1→2→4→7,距离为14

第七轮:不存在顶点1到0的路径,距离为∞

4.4.2 文字描述:用BFS算法求单源最短路径的过程

注意:BFS算法只能对无权图求最短路径

4.5 图的应用:拓扑排序

4.5.1 自己设计一个不少于6个结点的 带权有向无环图 ,并画出其邻接矩阵的样子

1720622193190-73eefabd-05ac-463f-8366-52db121db49a.png

4.5.2 用一维数组将你设计的有向无环图的邻接矩阵进行 压缩存储

注:有向无环图,一定可以转化为一个上三角或下三角矩阵。但是需要调整顶点的编号。

如果要用上三角矩阵表示有向无环图的邻接矩阵,可以对图进行拓扑排序,按照拓扑排序序列,重新调整各个顶点的编号。这样可以确保,所有的弧都是从小编号顶点指向大编号顶点,从而也就保证了邻接矩阵可以转化为“上三角矩阵”

1720622215275-0070dda6-fde3-4531-9e2c-0da8f6c166ec.png

1720622232788-3b4e288b-3d2d-4e3c-95f9-849b5c6e2fbd.png

“上三角矩阵”可以按行优先压缩存储,如下所示:

1720622248212-af5d31a1-20cd-4a54-bd26-da20a4c257dc.png

4.5.3 文字描述:基于你压缩存储的数组,如何判断结点 i、j 之间是否有边?

行优先压缩存储上三角矩阵,元素 (i, j) 和一维数组间的对应关系为:

1720622283062-4e918e26-f7dd-4fcf-810f-9d38b916e060.svg

只需用上述公式将 i、j 转化为 k,再检查一维数组中下标为 k 的元素即可。

4.5.4 基于你设计的带权有向无环图,写出所有合法的拓扑排序序列

1720622304269-69a27218-8cc8-47d6-a09b-427332494db4.png

拓扑排序序列1:0,1,2,3,4,5,6,7

拓扑排序序列2:0,1,2,3,5,4,6,7

拓扑排序序列3:0,1,2,4,3,5,6,7

拓扑排序序列4:0,1,4,2,3,5,6,7

可参考下面这张图分析所有可能出现的拓扑排序序列:

1720622338299-74a7df77-9b1e-468d-8f09-8ecb4bbc478e.png

4.5.5 文字描述:拓扑排序的过程

1720622377006-1899a485-279d-4888-95fd-c3fa8aface8f.png

4.6 图的应用:关键路径

4.6.1 基于你设计的 带权有向无环图 ,写出所有合法的关键路径,并算出关键路径总长度

1720622403487-ae297875-8eec-4bd9-9a8b-9e54e902a4e9.png

Warning

用手算的方式求关键路径,就是要找到从起点到终点的所有路径中最长的那条。

在上面这个图中,起点是0,终点是7,从0到7最长的路径是 0→1→2→3→5→6→7,关键路径总长度为 25

用手算的方式求关键路径,就是要找到从起点到终点的所有路径中最长的那条。

在上面这个图中,起点是0,终点是7,从0到7最长的路径是 0→1→2→3→5→6→7,关键路径总长度为 25

1720622439106-f67152aa-1f76-4f1a-b89f-b74cacc3578c.png

Ch5 查找算法的分析和应用

5.1 分块查找

5.1.1 自己设计一个分块查找的例子,不少于15个数据元素,并建立分块查找的索引

1720622470230-3f23fcfe-bc4a-4f7c-b36b-41425faf24ae.png

5.1.2 基于上述例子,计算查找成功的ASL、查找失败的ASL

注:对于分块查找算法,可能会让你分析查找成功的ASL。查找失败的ASL没有统一的计算方法,不可能考。

上图中,共有15个元素,计算查找成功时的平均查找长度ASL,每个元素被查找的概率相同,都是 1/15。每个元素的查找都需要先查分块索引,再顺序查找分块内各个元素。

查找元素9时,查找长度为 1+1

查找元素7时,查找长度为 1+2

查找元素10时,查找长度为 1+3

查找元素2时,查找长度为 1+4

查找元素19时,查找长度为 2+1

查找元素14时,查找长度为 2+2

查找元素15时,查找长度为 2+3

查找元素20时,查找长度为 3+1

查找元素25时,查找长度为 3+2

查找元素22时,查找长度为 3+3

查找元素30时,查找长度为 3+4

查找元素43时,查找长度为 4+1

查找元素36时,查找长度为 4+2

查找元素55时,查找长度为 4+3

查找元素40时,查找长度为 4+4

综上,查找成功的ASL=(2+3+4+5+3+4+5+4+5+6+7+5+6+7+8)×1/15=4.933

5.2 折半查找

注1:关于折半查找的算法效率分析,可参考基础课课件,重点复习“如何画查找分析树”、“如何计算查找成功、查找失败的ASL”

注2:关于折半查找的算法执行过程,可以去408快乐小网站玩一玩,传送门如下:https://www.cs.usfca.edu/~galles/visualization/Search.html

1720623010221-db16d462-10fa-4308-bd45-c56e4be835bb.png

5.2.1 自己设计一个折半查找的例子,不少于10个数据元素,画出对应的查找分析树

1720623039168-a2c9d868-5db3-4d40-b9f1-4aedc72e4db3.png

1720623065672-86b13f32-a27e-4481-a813-225e20ee3c3d.png

5.2.2 基于上述例子,计算查找成功的ASL、查找失败的ASL

1720623094397-19056253-8392-45d3-b312-45371587d280.png

5.3 散列查找

5.3.1 自己设计一个散列表,总长度由你决定,并设计一个合理的散列函数,使用线性探测法解决冲突

可参考基础课视频中的例子,设散列表长度为16,采用线性探测法解决冲突,从空表开始,依次插入关键字 {19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79},散列函数 H(key)=key%13

注:散列表的长度可以比散列函数的映射范围更大,散列函数的映射范围是 0~12,散列表的后面几个位置 13~15 不可能被直接映射,但是在线性探测法处理冲突时,可能被使用到

1720623120135-083498f3-4406-444a-a146-baed70bd2007.png

5.3.2 基于上述散列表,设计不少于10个元素的插入序列,依次插入散列表,画出散列表最终的样子(插入过程至少发生4次冲突)

19%13=6,插入元素 19,未发生冲突

14%13=1,插入元素 14,未发生冲突

23%13=10,插入元素 23,未发生冲突

1720623144580-a2b8f725-3841-4fd1-a4d7-f0041546e71f.png

1%13=1,插入元素1,发生冲突,线性探测法解决冲突

1720623173358-4e43c3af-5ab7-4322-9fae-4561bb6e7a57.png

68%13=3,插入元素 68,未发生冲突

20%13=7,插入元素 20,未发生冲突

1720623194469-e9639d22-b5e0-4a70-b89a-9378c932a067.png84%13=6,插入元素 84,发生冲突,线性探测法解决冲突

1720623217078-f7b9c6d8-c156-4f88-b878-167ebc66900e.png27%13=1,插入元素 27,发生冲突:

1720623239307-fa755b2b-8c12-417d-b9e5-cccfa5931119.png55%13=3,插入元素 55,发生冲突,线性探测法解决冲突1720623263811-bd310f8a-f575-4e7a-85a2-63bd23581a86.png11%13=11,插入元素 11,未发生冲突1720623286836-e41ecc81-93d9-4616-b9ef-94f513e94380.png10%13=10,插入元素 10,发生冲突,线性探测法解决冲突1720623313913-e4f5bc86-4cd3-4928-8c8a-58fa991f54e8.png79%13=1,插入元素 79,发生冲突,线性探测法解决冲突1720623362253-31acdab3-dff9-442c-a4a6-6805763f953f.png

5.3.3 基于上述例子,计算查找成功的ASL、查找失败ASL

考虑查找成功的所有情况。各元素查找成功时的查找长度如下:

1720623368772-d722c04b-058c-4f9a-84be-8eff5fd30460.png

1720623386461-24344bea-d81d-4ebb-bfb3-58e3ebeeadc7.png

考虑查找失败的所有情况。若目标元素的散列值为0,则只需对比位置0的关键字即可确定查找失败,查找长度为1;若目标元素的散列值为1,采用线性探测法,需对比位置1~位置13,才能确定查找失败,查找长度是13;若目标元素的散列值为2,采用线性探测法,需对比位置2~位置13,才能确定查找失败,查找长度是12;其他散列值同理。

1720623400303-6e9d6635-8365-40b6-b918-f08cbbed0e15.png

5.3.4 代码能力强的同学,尝试实现上述散列表的数据结构定义,并实现查找、插入、删除三个基本操作

略,有能力的同学自己实现

5.3.5 自己设计一个散列表,总长度由你决定,并设计一个合理的散列函数,使用拉链法解决冲突

可参考基础课视频中的例子,设散列表长度为13,采用拉链法解决冲突,从空表开始,依次插入关键字 {19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79},散列函数 H(key)=key%13

注:如果采用拉链法处理冲突,则散列表的长度应该和散列函数的映射范围相同,散列函数的映射范围是 0~12。

5.3.6 基于上述散列表,设计不少于10个元素的插入序列,依次插入散列表,画出散列表最终的样子(插入过程至少发生4次冲突)1720623437671-14a4a05d-549e-4066-bf5d-edbc1381c94b.png

5.3.7 基于上述例子,计算查找成功的ASL、查找失败的ASL

考虑查找成功的所有情况。元素 14、68、19、29、23、11 的查找长度为1;元素1、55、84、10 的查找长度为2;元素27的查找长度为3;元素79的查找长度为4,因此:

1720623460229-9b944ed0-6365-4762-a30b-4ad7e9d79a2e.png

考虑查找失败的所有情况。若目标元素的散列值为0,则无需对比任何关键字即可确定查找失败,查找长度为0;若目标元素的散列值为1,则需要依次对比关键字14、1、27、79才能确定查找失败,查找长度为4;其他散列值同理。

1720623460234-7b6a7065-2d2d-435e-a0fd-dee5ec8f0285.png

5.3.8 代码能力强的同学,尝试实现上述散列表的数据结构定义,并实现查找、插入、删除三个基本操作

略,有能力的同学自己实现,或参考强化课补充代码包。

Ch6 排序算法的分析和应用

6.1 希尔排序

6.1.1 自己设计一个长度不小于10的乱序数组,用希尔排序,自己设定希尔排序参数

6.1.2 画出每一轮希尔排序的状态

1720623511209-c89b4e24-8fcb-4ef4-82ec-d1076ae85bb7.png

6.2 堆排序

6.2.1 自己设计一个长度不小于10的乱序数组,用堆排序,最终要 生成升序数组 ,画出建堆后的状态

假设乱序数组的初始状态如下,元素从0开始存储👇:1720623561179-3043b294-ca88-4ceb-86e0-e30c0869549e.png

注:参考本文档 3.1.3~3.1.5

Warning

若顺序二叉树从数组下标1开始存储结点,则:

● 结点 i 的父结点编号为 i/2

● 结点 i 的左孩子编号为 i*2

● 结点 i 的右孩子编号为 i*2+1

若顺序二叉树从数组下标0开始存储结点,则:

● 结点 i 的父结点编号为 [(i+1)/2] - 1

● 结点 i 的左孩子编号为 [(i+1)2] - 1 = 2i + 1

● 结点 i 的右孩子编号为 [(i+1)2+1] - 1 = 2i + 2

在本例中,元素从数组下标0开始存储,因此,0号元素是根节点,1号元素是其左孩子,2号元素是其右孩子。其他元素间的关系如下:

1720623585596-a2db1377-0aa4-47d7-a7ec-b90990da570b.png

Warning

注:如果应用题让你画出一个乱序数组建堆后的样子,只需要画出数组形式的图示即可,不用画二叉树形态的图示。如下👇

1720623628561-7828a66e-416e-4752-af76-e0e914c82641.png

6.2.2 画出每一轮堆排序的状态1720623645085-963c12b7-1356-4d08-b76c-f02c4a1bffdb.png

1720623645304-b8537c5b-c0ab-4b17-aa4d-1400773c89f4.png

1720623645282-fd72e51d-64c9-410e-8218-81cefe85f085.png

1720623645307-79fff2ad-f202-4df6-90c2-f86a09e600f5.png1720623670741-37ff5f46-8faf-4d8a-9c01-43bf716715c3.png

1720623671021-a7aff796-40cc-462f-b51e-df206987ff64.png

1720623671018-afacd03b-d804-4600-b0ca-4348b0cc1e41.png

1720623671704-980e6aba-7769-4130-8f83-f8425e9d33da.png

1720623671439-ffa08f1b-11e6-4041-b36e-69ff1ebb10f2.png

1720623671618-04b2f5e0-8278-48ac-8bdf-c18a0567765d.png

1720623705802-be75c4fa-edaa-4bb2-9308-c0ae34ff083b.png

1720623706025-e17fd30a-55f1-4807-9688-86b7fbb11622.png

1720623705950-cfa3cf8b-0446-49c4-ba57-14376c60be4b.png

1720623706671-35936989-804d-44c7-8a34-429e80c3e9e5.png

6.3 快速排序

6.3.1 自己设计一个长度不小于10的乱序数组,用快速排序,最终要生成升序数组

6.3.2 画出每一轮快速排序的状态1720623727247-cb9aef10-6400-416b-bed6-1e66ee590858.png

6.4 基数排序

6.4.1 自己设计一个长度不小于15的乱序链表,每个数据元素取值范围0~99,用基数排序,最终要生成升序链表

6.4.2 画出每一轮基数排序的状态

1720623727397-585cddb8-b1f1-47bc-8fc1-9f104aea8feb.png

6.4.3 代码能力强的同学,尝试实现上述基数排序的相关代码

略,有能力的同学自己实现,或参考数据结构强化课补充代码包

6.5 外部排序

“外部排序”在历年真题中的考频不算高,因此许多考生并不重视对该考点的复习。但是 2023 年应用题突然深入考察了“外部排序”,让广大考生感到被偷袭,猝不及防。因此,我们需要重 视这个考点。以下是历年真题中,涉及到“外部排序”的题目:

【2016 年真题 11 题】(选择题)考察了“外部排序的思想”

【2019 年真题 11 题】(选择题)考察了“最佳归并树”

【2023 年真题 42 题】(应用题)考察了“置换-选择排序”

接下来,我们将 2023 年真题进行改编,用于回顾外部排序的三个重要考点:①置换-选择排序, ②最佳归并树,③败者树

Warning

对含有 19 个记录的文件进行外部排序,其关键字依次是 51, 94, 37, 92, 14, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100。假设每个文件记录刚好占一个磁盘块。请回答下列问题:

1)若采用置换-选择排序生成初始归并段,工作区中能保存 3 个记录,可生成几个初始归 并段?各是什么?请问置换-选择排序的过程中,读、写磁盘次数分别是几次?

2)若要对几个初始归并段进行 3 路归并,为实现最佳归并,需要补充的虚段个数是多少? 请画出最佳归并树的样子,并计算该归并树的 WPL。请问归并过程中,读、写磁盘次数分 别是多少次?磁盘 I/O 次数是多少次?

3)若要对几个初始归并段进行 4 路归并,为减少归并过程中关键字对比次数,需使用“败 者树”。请问构造初始败者树时,需要对比几次关键字?基于构造好的败者树,每次从 4 个 归并段中找到最小关键字所需的关键字对比次数是多少?

1)排序过程如下表所示:

输出文件 FO 工作区 WA 输入文件 FI
— — 51, 94, 37, 92, 14, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
— 51, 94, 37 92, 14, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37 51, 94, 92 14, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51 14, 94, 92 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 92 14, 94, 63 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 92, 94 14, 15, 63 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 92, 94# 14, 15, 63 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
14 99, 15, 63 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
14, 15 99, 48, 63 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
14, 15, 48 99, 56, 63 23, 60, 31, 17, 43, 8, 90, 166, 100
14, 15, 48, 56 99, 23, 63 60, 31, 17, 43, 8, 90, 166, 100
14, 15, 48, 56, 63 99, 23, 60 31, 17, 43, 8, 90, 166, 100
14, 15, 48, 56, 63, 99 31, 23, 60 17, 43, 8, 90, 166, 100
14, 15, 48, 56, 63, 99# 31, 23, 60 17, 43, 8, 90, 166, 100
23 31, 17, 60 43, 8, 90, 166, 100
23, 31 43, 17, 60 8, 90, 166, 100
23, 31, 43 8, 17, 60 90, 166, 100
23, 31, 43, 60 8, 17, 90 166, 100
23, 31, 43, 60, 90 8, 17, 166 100
23, 31, 43, 60, 90, 166 8, 17, 100 —
23, 31, 43, 60, 90, 166# 8, 17, 100 —
8 17, 100 —
8, 17 100 —
8, 17, 100 — —
8, 17, 100# — —
    可生成 4 个归并段,分别是:

37, 51, 92, 94

14, 15, 48, 56, 63, 99

23, 31, 43, 60, 90, 166

8,17,100

置换-选择排序的过程中,需要读磁盘 19 次,写磁盘 19 次。因为 19 条文件记录(即上表所示 的“输入文件 FI”)初始时存储在磁盘,每条记录占一个磁盘块,进行置换-选择排序时,这 19 条记录需要依次读入内存中(即上表所示的“工作区 WA”),再逐一写回外存(即上表所示的 “输出文件 FO”)。因此,整个过程需要读磁盘 19 次,写磁盘 19 次。

2)回顾“最佳归并树”的构造方法:

1720624750820-2f827f2a-1b56-47dd-a7bd-503919e6b53b.png

本题中,有 4 个初始归并段,进行 3 路归并,因此需要构造 3 叉最佳归并树。

(初始归并段数量-1) % (k-1) = (4-1)%(3-1)=1≠0,因此需要补充 (k-1)-u = (3-1)-1=1 个虚段。

补充 1 个虚段后,各初始归并段的长度为:

37, 51, 92, 94——归并段①长度为 4

14, 15, 48, 56, 63, 99——归并段②长度为 6

23, 31, 43, 60, 90, 166——归并段③长度为 6

8, 17, 100——归并段④长度为 3

NULL——归并段⑤为虚段,长度为 0

最佳归并树形态如下:

1720624865686-951cbb57-208d-42d3-95db-c602c22b6ed7.png

WPL = 树中所有叶节点的带全路径之和 = (0+3+4)2 + (6+6)1 = 26

注:最佳归并树形态不唯一,但 WPL 一定是 26

归并过程中,读磁盘次数=WPL=26 次

写磁盘次数=WPL=26 次

磁盘 I/O 总次数=WPL*2=52 次。

注:每个记录刚好占一个磁盘块,因此每读一个记录就需要一次读磁盘,每写一个记录就需要 一次写磁盘。在进行第一趟归并时,需要三个归并段中的记录依次读入磁盘、再按归并顺序依次写回磁盘。 因此第一趟归并带来了 0+3+4=7 次读磁盘、以及 7 次写磁盘。在进行第二趟归并时,需要将三个归并段中的记录依次读入磁盘、再按归并顺序依次写回磁盘, 因此第二趟归并带来了 7+6+6 = 19 次读、以及 19 次写。

综上,经过两趟归并,读磁盘次数 = 7+19=26 次,写磁盘次数=7+19=26 次。

3)本题要求构造 4 路归并的败者树,因此需要对比关键字 3 次。

注:三次关键字对比如图下所示

1720624989526-8e2bb107-6ee9-4b8d-8b1d-96233408c822.png

首先分别取出 4 个初始归并段中的最小值,用于构造初始败者树。

第一次关键字对比:将关键字 8、37 进行对比。37 更大,为“失败者”;8 更小,为“胜利者”, 晋级至下一轮对比。

第二次关键字对比:将关键字 14、23 进行对比。23 更大,为“失败者”;14 更小,为“胜利者”, 晋级至下一轮对比。

第三次关键字对比:将上一层的两个“胜利者”,即关键字 8、14 进行对比。14 更大,为“失败 者”;8 更小,为最终“胜利者”,也就找到了最小元素。

基于构造好的败者树,每次从 4 个归并段中找到最小关键字所需的关键字对比次数是 2 次。

举个例子,在初始败者树构造完成后,可知 4 个归并段中,最小元素为 8(来自于最左边一个 归并段)。接下来,基于已经构造好的败者树,要继续找到剩余元素中的最小关键字,仅需进行两次关键 字对比。

第一次关键字对比:将关键字 17、37 进行对比。37 更大,为“失败者”;17 更小,为“胜利者”, 晋级至下一轮对比。

第二次关键字对比:将关键字 14、17 进行对比。17 更大,为“失败者”;14 更小,为最终“胜利 者”,也就找到了最小元素。1720625045415-9ceb5e31-33a6-4aca-bc0d-8edeaa74c708.png

Warning

咸鱼认为,如果考题中考到败者树,最有可能的三个出题角度是:

①问你败者树的作用

②构造 k 路归并的败者树时,关键字对比次数是多少?

③基于构造好的败者树,每次从 k 个归并段中找到最小关键字,至多/至少需要对比关键字 多少次?

下面对着三个问题进行总结:

①败者树的作用是:在进行多路归并时,减少关键字对比次数。

②构造 k 路归并的败者树时,需要对比关键字 k-1 次。例如:构造 7 路归并的败者树时,需要对比关键字 6 次。

③基于构造好的败者树,每次从 k 个归并段中找到最小关键字所需的关键字对比次数,取 决于败者树的高度和形态。万一考到这类问题,可以先画出败者树的形态,再来分析。 k 路归并的败者树,形态上是一棵具有 k 个叶子结点的完全二叉树。

例如:7 路归并的败者树形态如下

1720625111679-5fe8fb14-4a81-460c-8abb-337b330e5438.png

总共有 7 个叶子结点,每个叶子对应一个归并段。

基于这棵构造好的败者树,从 7 个归并段中找到最小关键字,最多需要 3 次关键字对比; 最少需要 2 次关键字对比。

如果本轮新关键字来自于归并段 1,则只需要 2 次关键字对比即可找到 7 个归并段中的最 小关键字。如下所示:

1720625152544-1be59d0e-4612-45cc-ac03-c9034274e52b.png