2021年HFUU数据结构期末考试
一.算法设计题(30分)
1.有序的顺序表合并
大概的步骤就是用三个指针ia ib ic
分别指向三个顺序表,然后比较顺序表A B
的当前的值的大小,把小的或者相等的存到顺序表C中,再移动对应的指针,直到顺序表A B
中所有的元素都进入顺序表C。
//按照课本来的,数据从data数组下标为0开始存,所以last是顺序表最后一个元素的下标,表长为 list -> last + 1
#define maxlen 100
typedef struct
{
int data[maxlen];
int last;
}Orderlist;
//基于以上的定义方式进行顺序表合并,考试的时候老师会把结构体给你的
Orderlist *Qmerge(Orderlist *A, Orderlist *B)
{
Orderlist *C;//合并至顺序表C
int ia = 0, ib = 0, ic = 0;//三个指针分别指向三个顺序表的当前位置
while(ia != A -> last && ib != B -> last)
{
if(A -> data[ia] < B -> data[ib])
C -> data[ic] = A -> data[ia ++];
else if(A -> data[ia] > B -> data[ib])
C -> data[ic] = B -> data[ib ++];
else
{
C -> data[ic] = B -> data[ib ++];
ia ++;
}
ic ++;
}
while(ia != A -> last) C -> data[ic ++ ] = A -> data[ia ++ ];
while(ib != B -> last) C -> data[ic ++ ] = B -> data[ib ++ ];
C -> last = ic - 1;//最后一步合并ic多自加了一次
return C;
}
2.有序的链表的合并
特点:
不占用新的存储空间,直接合并,时间复杂度O(n + m)
具体思路:
还是用两个指针,从A B
的第一个元素开始比较,如果A
链表中的指向的元素比B
小,A链表的指针就后移;如果A
链表中指向的元素比B
大,就直接把B
的当前结点插入A
链表当前节点的前面;如果相等,就直接保留A链表中的,将B链表的指针后移。注意,每次删除B链表中的节点需要释放空间。
//课本上的链表构造方式是第一个节点也就是链表名称指向的节点是不存任何元素的
typedef struct node
{
int data;
struct node *next;
}LinkList;
//有序合并两个递增的链表
LinkList *Lmerge(LinkList *A, LinkList * B)
{
LinkList *p, *q, *pre;//p指针指向的是A链表当前元素,q链表指向的是B链表当前元素,pre是q指针的前驱节点
p = A -> next;//指向A链表中的第一个元素
q = B -> next;//指向B链表中的第一个元素
pre = A;//指向p前面
free(B);//释放B的空间
B = q;
while(p != NULL && q != NULL )
{
if(p -> data < q -> data)//A链表中的当前元素比B链表小
{
pre = p;
p = p -> next;
}
else
{
q = q -> next;//这是q已经指向之前q的后面一个位置了
if(p -> data > B -> data)//所以这边不能写成(p -> data > q -> data,这个地方课本上是错的)//A链表中的当前元素比B链表大
{
B -> next = p; pre -> next = B; pre = pre -> next//把B链表的那个节点插到A链表当前元素前面
}
else free(B);
B = q;
}
}
if(q != NULL) pre -> next = q;
return A;
}
3.已知长度为n的线性表A采用顺序存储结构,请写一个时间复杂度为\(O(n)\)、空间复杂度为\(O(1)\)的算法,该算法可删除线性表中所有值为\(item\)的数据元素。
具体思路:
因为是顺序存储,直接按照数组下标查找,如果值为\(item\),则删除。
删除操作:把数组后面的元素向前移动一个位置即可
时间复杂度:最坏的情况下为\(O(n)\), 其中n为顺序表长,也就是\(L -> last + 1\)。
代码:
//基于以下存储结构的顺序表
typedef struct
{
int data[maxlen];
int last;
}Sequenlist;
Sequenlist *delete_item(Sequenlist *L, int item)
{
for(int i = 0; i <= L -> last;i ++)
{
if(L -> data[i] == item)
{
for(int j = i; j < L -> last; j ++ ) L -> data[j] = L-> data[j + 1];
L -> last --;
i --;//这一步操作是因为你已经把数组后面的元素前移一个位置,那么第i个位置就是以前的第i + 1个位置的数,没有判断过
}
}
}
4.设计一个算法,通过一趟遍历确定长度为n的单链表中最大的结点,返回该结点的数据域。
数据域:指的就是\(data\)。
思路:
遍历整个单链表,假设最大的数据域是头节点指向的下一个结点的数据域,然后遍历单链表,如果找到一个比该数据域还大的就更新。
代码:
//基于以下的存储方式
typedef struct node
{
int data;
struct node *next;
}LinkList;
int findmax(LinkList *L)
{
int maxn = L -> next -> data;
L = L -> next;//L 指向的第一个结点是空的结点
while(L != NULL)
{
if(L -> data > maxn) maxn = L -> data;
L = L -> next;
}
return maxn;
}
5.设计一个算法将一个带头结点的单链表A分解为两个具有相同结构的链表B和C,其中B表的结点为A表中值小于零的结点,而C表的结点为A表中大于零的结点(链表A中的元素为非零整数,要求B、C表利用A表的结点)。
思路:
单纯的遍历A链表,然后数据域为负数的插到B链表中,数据域为正数的插到C链表中。
代码:
//基于以下的存储方式
typedef struct node
{
int data;
struct node *next;
}LinkList;
void divide(LinkList *A, LinkList *B, LinkList *C)
{
LinkList *p;
p = (LinkList *)malloc(sizeof (LinkList));
p = A -> next;
LinkList *r;
r = (LinkList *)malloc(sizeof (LinkList));
while(p != NULL)
{
r = p -> next; //暂存p的后继,因为在拆分链表的时候p会发现变化
if(p -> data > 0)//插到C链表中
{
p -> next = C -> next;
C -> next = p;
}
else//插到B链表中
{
p -> next = B -> next;
B -> next = p;
}
p = r;
}
}
6.设计一个算法,将链表中所有结点的链接方向“原地”逆转,即要求仅利用原表的存储空间,换句话说,要求算法的空间复杂度为\(O(1)\)。
思路:
逆转我们能想到:单链表头插法,插完后元素刚好与插入顺序相反(其实根本想不到,看题解才想到的(QwQ.jpg))
所以这题我们只需要对原链表中的结点按照顺序进行一次头插法就能逆转链表。
代码:
LinkList *reverse(LinkList *L)
{
LinkList *p;
p = (LinkList *)malloc(sizeof (LinkList));
p = L -> next;
LinkList *q;
q = (LinkList *)malloc(sizeof (LinkList));
L -> next = NULL;
while(p != NULL)
{
q = p -> next;
p -> next = L -> next;
L -> next = p;
p = q;
}
return L;
}
7.已知两个链表A和B分别表示两个集合,其元素递增排列。请设计一个算法,用于求出A与B的交集,并存放在A链表中。
思路:
创建一个新的链表用来存交集,题目要保证,所有的结点都要是A链表的结点,再创建两个指针\(pa\) \(pb\)遍历\(A\)与\(B\)链表,由于链表是递增的,所以如果数据域相同,就把\(pa\)指向的结点给C链表。如果\(pa\)的数据域比\(pb\)的数据域小,\(pa\)指针就后移。如果\(pa\)的数据域比\(pb\)的数据域大,\(pb\)指针就后移,并释放\(B\)链表中的结点的空间。当某一个链表遍历完毕,交集也就找完了,释放掉剩余结点的空间。
代码:
//基于以下的存储模式
typedef struct node
{
int data;
struct node *next;
}LinkList;
LinkList *merge(LinkList *A, LinkList *B, LinkList *C)
{
LinkList *pa, *pb, *pc, *u;
pa = A -> next;
pb = B -> next;
C = pc = A;
while(pa != NULL && pb != NULL)
{
if(pa -> data == pb -> data)
{
pc -> next = pa, pc = pa, pa = pa -> next;
u = pb; pb = pb -> next; free(u);
}
else if(pa -> data < pb -> data)
{
u = pa;
pa = pa -> next;
free(u);
}
else
{
u = pb;
pb = pb -> next;
free(u);
}
}
while(pa)
{
u = pa;
pa = pa -> next;
free(u);
}
while(pb)
{
u = pb;
pb = pb -> next;
free(u);
}
pc -> next = NULL;
free(pb);
return C;
}
8.已知两个链表\(A\)和\(B\)分别表示两个集合,其元素递增排列。请设计算法求出两个集合A和B 的差集(即仅由在A中出现而不在B中出现的元素所构成的集合),并以同样的形式存储,同时返回该集合的元素的个数。
思路:
用两个工作指针分别遍历A和B链表,并记录下A链表的前驱节点(为了删除用的),如果当前A链表中的元素等于B链表中的元素就删除A中的结点,如果A链表中的元素小于B链表中的元素,则B链表一定不会出现和A链表当前结点相同的结点(因为链表是递增的),差集个数 \(++\),如果A链表的元素大于B链表的元素,B链表的工作指针就后移。最后A链表如果还有剩余,剩余的也都是差集。
代码:
//基于以下的存储模式
typedef struct node
{
int data;
struct node *next;
}LinkList;
void QwQ(LinkList *A, LinkList *B, int *n)
{
LinkList *pa, *pb, *pre;
pa = A -> next;
pb = B -> next;
while(pa && pb)
{
if(pa -> data < pb -> data)//如果A集合中的元素小于B集合中的元素,A集合的元素不会在B中出现(因为链表是递增的)
{
n ++;
pre = pa;
pa = pa -> next;
}
else if(pa -> data > pb -> data)//如果A集合中的元素大于B集合中的元素B的工作指针后移
pb = pb -> next;
else//如果A集合和B集合中的元素相同删除A集合的元素
{
pre -> next = pa -> next;
LinkList *u = pa;
pa = pa -> next;
free(u);
}
}
while(pa)//A链表可能没有遍历完
{
pa = pa -> next;
n ++;
}
}
9.设计一个算法,删除递增有序链表中值大于\(mink\)且小于\(maxk\)(\(mink\)和\(maxk\)是给定的两个参数,其值可以和表中的元素相同,也可也不同)的所有元素。
思路:
遍历链表,找到第一个大于\(mink\)的和最后一个小于\(maxk\)的,然后一一删除就行。
代码:
//基于以下的存储模式
typedef struct node
{
int data;
struct node *next;
}LinkList;
void delete(LinkList *L, int mink, int maxk)
{
LinkList *p = L -> next;
LinkList *pre;//最后一个小于等于mink的结点
while(p && p -> data <= mink)
{
pre = p;
p = p -> next;
}
while(p && p -> data < maxk)
p = p -> next;//第一个大于maxk的结点
q = pre -> next;//需要删除的第一个结点
while(q != p)
{
LinkList *u = q -> next;
free(q);
q = u;
}
}
10二叉树的基本算法:
1.求叶子结点数目
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
//1.求叶子结点数目
int countleaf(Bitree *bt)
{
if(bt == NULL) return 0;
else if(bt -> lchild == NULL && bt -> rchild == NULL) return 1;
else return countleaf(bt -> lchild) + countleaf(bt -> rchild);
}
2.求结点数目
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
//2.求结点数目
int count(Bitree *bt)
{
if(bt == NULL) return 0;
else return 1 + count(bt -> lchild) + count(bt -> rchild);
}
3.交换左右子树
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
//3.交换所有结点的左右子树
void swap(Bitree *T)
{
Bitree *temp;
if(T == NULL) return;
else
{
temp = T -> lchild;
T -> lchild = T -> rchild;
T -> rchild = temp;
swap(T -> lchild);
swap(T -> rchild);
}
}
4.求一棵二叉树的深度
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
//求一棵二叉树的深度
int treedepth(Bitree * bt)
{
if(bt == NULL) return 0;
else return (max(treedepth(bt -> lchild), treedepth (bt -> rchild)) + 1);
}
11.以二叉链表作为二叉树的存储结构,判断两棵树是否相等
思路:递归判断两棵树的所有结点是否一样。不一样的情况有:
(1)两个结点只有一个是空结点
(2)两个结点的数据域不一样
代码:
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
int is_same(Bitree T1, Bitree T2)
{
if(T1 == NULL && T2 == NULL) return 1;
if((T1 == NULL && T2 != NULL) || (T2 == NULL && T1 != NULL)) return 0;
if(T1 -> data != T2 -> data) return 0;
return is_same(T1 -> lchild, T2 -> lchild) && is_same(T1 -> rchild, T2 -> rchild);
}
12.计算二叉树的最大宽度(二叉树的最大宽度是指二叉树所有层中结点个数的最大值)
思路:利用队列进行宽度优先遍历
定义队列:一开始队头和队尾都是指向1,当队头大于队尾,\(BFS\)结束,
每次遍历完每一层后更新最大值
小插曲:本来想画图帮忙理解的,\(sai\)真难用(我是弱智.jpg)
代码:
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
int get_width(Bitree *T)
{
if(T == NULL) return 0;
Bitree Q[100010];//开到足够大就行,我习惯这么开
int front = 1, rear = 1, last = 1, temp = 0, maxn = 0;
Q[1] = T;
while(front <= last) //BFS
{
p = Q[front ++];
temp ++;
if(p -> lchild != NULL) Q[ ++ rear] = p -> lchild;
if(p -> rchild != NULL) Q[ ++ rear] = p -> rchild;
if(front > last)//一层遍历结束
{
last = rear;
if(temp > maxn) maxn = temp;
temp = 0;
}
}
return maxn;
}
13.用层次顺序遍历二叉树的方法,统计树中具有度为1的结点数目。
思路:本题直接\(BFS\)求,度为一的结点显然两种情况,左右孩子只有一个不是\(NULL\)。
代码:
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
int get(Bitree *T)
{
int num = 0;
if(T == NULL) return 0;
queue<Bitree>q;//直接上C++了,自己C语言手写队列也行。
q.push(T);
while(!q.empty())
{
auto it = q.top();
q.pop();
if((it -> lchild == NULL && it -> rchild != NULL) || (it -> rchild == NULL && it -> lchild != NULL)) num ++;
if(it -> child != NULL) q.push(it -> lchild);
if(it -> rchild != NULL) q.push(it.rchild);
}
return num;
}
14.求任意二叉树中第一条最长的路径长度,并输出此路径上各结点的值
思路:用栈模拟全过程,先遍历完左子树,然后再依次遍历右子树求最大长度(又是我这个弱智想不出来的)
代码:
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
void getlongestpath(Bitree T)
{
int tag[100010];//记录右孩子是否遍历过
Bitree p = T, l[100010], s[100010];//数组l和s存的是栈,而l里面存的是最长的路径
int top = 0, length = 0;//top栈顶, length是最长路径的长度
while(p || top > 0)
{
while(p != NULL)//先把左子树遍历完
{
s[++ top] = p;
tag[top] = 0;
p = p -> lchild;
}
if(tag[top] == 1)//如果右子树已经遍历完
{
if(!s[top] -> lchild && !s[top] -> rchild)//如果是叶子结点就能求最大长度
if(top > length)
{
for(int i = 1; i <= top; i ++ )
l[i] = s[i];
length = top;
}
top --;//出栈
}
else if(top > 0)//遍历当前栈顶的右子树
{
tag[top] = 1;
p = s[top] -> rchild;
}
}
for(int i = length; i >= 1; i -- ) printf("%d ", l[i] -> data);//输出答案
printf("\n");
}
15.输出二叉树中从每个叶子结点到根结点的路径。
思路:对整棵二叉树进行深度优先遍历,并存储路径,要记得恢复现场。遍历到叶子结点输出路径。
代码:
//基于以下存储结构
typedef struct node
{
int data;
struct node *lchild, *rchild;
}Bitree;
void dfs(Bitree T, int path[], int length)
{
if(T == NULL) return;
if(T -> lchild == NULL && T -> rchild == NULL)
{
printf("%d ", T -> data);
for(int i = length - 1; i >= 0; i -- ) printf("%d ", path[i]);
printf("\n");
}
else
{
path[length] = T -> data;
length ++;
dfs(T -> lchild, path. length);
dfs(T -> rchild, path, length);
length --;
}
}
二.解答题+分析题(70分)
图论基础知识:给你一张有向图,画出其邻接矩阵、邻接表、逆邻接表,会画出从某个点开始的\(dfs\) \(bfs\)的搜索图(看清题目)。
一.最小生成树(MST)
定义:
给你n个结点,和m条已知长度的无向边,让你构造一棵树,这棵树要满足两个条件:
①这棵树连接所有n个结点
②这棵树总边权之和最小