数据结构课程设计报告
广州大学 计算机科学与网络工程学院
计算机系 19级网络工程专业网络194班
超级菜狗
(学号:19062000)
(班内序号:xxx)
完成时间:2021年1月11日
一.课程设计目的
数据结构课程设计是计算机类专业的工程基础课。课程设计的主要内容包括线性表、栈与队列、树、图等经典的数据结构实现,以及排序、查找等经典算法的设计与实现。
该门课程设计主要培养学生软件开发的基本能力,包括基本数据结构的设计与实现能力,基本算法的设计、编程与调试能力,算法时间和空间复杂度的基本分析能力。提高学生解决问题的能力,初步锻炼学生系统设计与分析能力。
- 课程设计题目及内容
神秘国度的爱情故事
[问题描述]
某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村间有路相连,更精确一点说,任意两村之间有且仅有一条路径。小村 A 中有位年轻人爱上了自己村里的美丽姑娘。每天早晨,姑娘都会去小村 B 里的面包房工作,傍晚 6 点回到家。年轻人终于决定要向姑娘表白,他打算在小村 C 等着姑娘路过的时候把爱慕说出来。问题是,他不能确定小村 C 是否在小村 B 到小村 A 之间的路径上。你可以帮他解决这个问题吗?
[基本要求]
(1)输入由若干组测试数据组成。每组数据的第 1 行包含一正整数 N ( l 《 N 《 50000 ) , 代表神秘国度中小村的个数,每个小村即从0到 N - l 编号。接下来有 N -1 行输入,每行包含一条双向道路的两个端点小村的编号,中间用空格分开。之后一行包含一正整数 M ( l 《 M 《 500000 ) ,代表着该组测试问题的个数。接下来 M 行,每行给出 A 、 B 、 C 三个小村的编号,中间用空格分开。当 N 为 O 时,表示全部测试结束,不要对该数据做任何处理。
(2)对每一组测试给定的 A 、 B 、C,在一行里输出答案,即:如果 C 在 A 和 B 之间的路径上,输出 Yes ,否则输出 No.
- 程序中使用的数据结构及主要符号说明
实验中使用的数据结构:图,队列,树
利用图的邻接矩阵的的下三角部分存储该村庄图,利用队列对该村庄图进行BFS遍历然后生成树。
每个村庄结点Node存储结构的初始化定义:
struct Node{
int _number;//记录该节点的下标
int _fa;//记录该节点的父亲节点
int _tier;//记录该节点在bfs生成树的第几层
bool _hvs=false;//用来记录在BFS遍历时该节点是否被访问过
};
我在Node的结构体中定义两个int 类型的变量_fa和_tier,它们分别用记录该结点的父亲结点的位置和它们在该村庄图的BFS遍历生成树中的层次(该生成树的层次我将根结点的层次设置为0)。bool类型的变量_hvs是为为了在对该村庄图进行BFS遍历是标记当前结点是否已经被访问过。int类型的变量_number是为了表示该结点的编号。
- 程序流程图,算法思想和带有注释的源程序
程序执行过程流程图
算法思想:
因为这道题很明显就是求树中两个结点的最近公共祖先的问题,所以如果C在A和B之间的路径上必须满足以下情况中的一种:
- A与C的最近公共祖先为C,B,C的最近公共祖先也为C,而且A,B的最近公共祖先也是C。
- A,C的最近公共祖先是C,但是B,C的最近公共祖先不是C
- B,C的最近公共祖先是C,但是A,C的最近公共祖先不是C
若出现其他情况时C都不在A和B之间的路径上。
带有注释的源程序:
我把解决方案全部封装在一个Slution类中,Slution类的定义如下:
class Slution{
public:
Slution(int n):n(n){}//调用构造函数初始化村庄的数量
void InitMap();//村庄地图的初始化
void CreateGraph();//建立村庄图的邻接矩阵的下三角
void BFS_Tree();//利用bfs算法生成该村庄图的树
int SearchFather(int u,int v);//寻找两个节点之间的最近公共祖先
private:
Node *node;//结点数组
int n;//村庄的数量
int **Map;//村庄图
};
在该类中我利用该类的构造函数来给该类的成员变量n(表示小村个数)进行初始化赋值。
在为该类创建了一个Node类型的一维动态数组node用来存储每个小村结点的基本信息,
因为这道题表示每一个小村之间有且仅有一条道路,很明显这是表示具有n个结点和n-1条边的最小无向连通图。正是因为其是最小无向连通图,而且该图的邻接矩阵是关于其邻接矩阵的主对角线对称的,所以创建了一个int类型的二维动态数组Map来存储其邻接矩阵的下三角部分。
接下来在该类中声明了四个公有的函数。InitMap()函数用于对存储该村庄图的邻接矩阵的下三角部分的动态数组Map开辟空间,以及先将其所有的数据元素全部置为0,(因为我是利用0来表示两个结点不是互为邻接点,利用1表示两个点互为邻接点),以此来完成对二维动态数组Map的初始化。
Map数组的初始化函数如下所示:
void Slution::InitMap()
{
//因为邻接矩阵存储无向连通图是对称的矩阵,故只要存储矩阵的下三角部分
Map =new int*[n];
for(int i=0;i<n;++i){Map[i]=new int[i+1];}
for(int i=0;i<n;++i)//将邻接矩阵的每一个元素都置0
{
for(int j=0;j<=i;++j)
{
Map[i][j]=0;
}
}
}
CreateGraph()函数是用于创建村庄图的邻接矩阵的下三角部分,并且为node开辟n个空间,并且对每个node结点的编号进行初始化。在该村庄图的邻接矩阵的下三角矩阵的创建过程中因为数组Map中没有第一个下标小于第二个下标的位置,故这里要对输入的两个结点u,v的大小进行比较最后在Map数组正确的位置中置1。以此来完成该村庄图的创建。
又因为该村庄图是一个最小无向连通图,故其的边数只有n-1条,故这里将会有n-1条数据的输入。
村庄图Map的创建的函数CreateGraph()的定义如下:
void Slution::CreateGraph()
{
cout<<"请依次输入村庄的"<<n-1<<"条道路,两点间用空格隔开!"<<endl;
node=new Node[n];
for(int i=0;i<n;++i){node[i]._number=i;}//初始化每一个结点的编号
int _edgeNum=n-1;//路径数量
for(int i=0;i<_edgeNum;++i)
{
int u ,v;//一条路径上的两个端点
cin>>u>>v;
if(u>=v){Map[u][v]=1;}//采用下三角矩阵存储时,没有第一个下标小于第二个下标的位置
else{Map[v][u]=1;}
}
}
BFS_Tree()函数是用来对该村庄图进行广度优先遍历进而生成该村庄图的树。因为对图进行广度优先遍历需要用到队列,所以我在这里直接调用了STL库中的queue来创建一个int类型的队列qu。该队列的作用是让每一个被访问的队列入队,并且在其所有邻接点均被访问完时把其出队。在每一个结点入队的之前,我分别把每一个结点的的父亲,和层次都记录下来并且将它们标记为已经访问过。每个结点在树中的层次是其父结点的层次数加1(这里我是从编号为0的小村开始进行BFS的,所以我将该点的父结点的设置为它本身。并且把其所在的层次树设置为0)
因为我是存储的是该图邻接矩阵的下三角部分,所以我用Map[u][i]==1||Map[i][u]==1
来查找每一队头结点的所有邻接点。等到队空的时候就证明该图的BFS遍历生成树创建完成。
村庄图的BFS遍历生成树的创建的函数BFS_Tree()的定义如下:
void Slution::BFS_Tree()//利用bfs算法生成该村庄图的树
{
queue<int>qu;
qu.push(0);//插入邻接矩阵的第一个结点
node[0]._hvs=true;//标志该节点已经被访问过
node[0]._fa=0;//第一个结点的父节点为它本身
node[0]._tier=0;//第一个结点在生成树的第0层
while(!qu.empty())//当队列不为空时
{
int u=qu.front();//记录下队头结点
qu.pop();//队列中的第一个结点出队列
for(int i=0;i<n;++i)
{
if((Map[u][i]==1||Map[i][u]==1)&&node[i]._hvs==false)
{
node[i]._fa=u;//记录结点的父亲结点的下标
node[i]._tier=node[u]._tier+1;//该结点在生成树的深度为父节点的深度加1
node[i]._hvs=true;//标志该节点已经被访问过
qu.push(i);//让该结点的所有邻接点依次进入队列
}
}
}
}
SearchFather(int u,int v)函数是在该村庄图的BFS遍历生成树已经创建好的情况下去寻找结点u,和结点v的最近公共祖先节点的函数。因为根据树的定义两个节点一定会有一个最近的公共祖先结点,并且只有两种情况:
1.最近公共祖先结点在两个结点的上层;
2.最近公共祖先结点在是两个结点中层次较为高的那个结点。
所以我的基本思路是,先对u,v两个结点在树中的层次进行比较,找出层次最高那个结点,并且找出其与另一个结点层次一样的祖先结点,然后判断其祖先结点是否是另一个结点本身,如果是则最近公共祖先寻找完成,返回该最近公共祖先结点的编号并且结束程序运行。否则,则让另一个结点和层次高结点的祖先结点同时出发找它们的第一个公共祖先结点,该第一个公共祖先结点即为u,v两个结点的最近公共祖先结点,查找成功返回该最近公共祖先的结点编号,并且结束函数执行。
int Slution::SearchFather(int u,int v)//寻找两个节点之间的最近公共祖先
{
//因为这是最小无向连通图的bfs生成树故两个结点一定有一个相同的祖先节点
int gap;//记录u和v之间在树的层次的差值
int _enu=u,_env=v;//分别记录两个结点的祖先节点
if(node[u]._tier>node[v]._tier)
{
gap=node[u]._tier-node[v]._tier;//让层次较高的结点开始寻找和另一个结点层次一样的祖先结点
for(int i=1;i<=gap;++i)
{
_enu=node[_enu]._fa;
}
}
else{
gap=node[v]._tier-node[u]._tier;//让层次较高的结点开始寻找和另一个结点层次一样的祖先结点
for(int i=1;i<=gap;++i)
{
_env=node[_env]._fa;
}
}
while(_enu!=_env)//如果层次较高的结点的与另一个结点层次一样的祖先结点不是这个层次低的结点,
//则让两个结点一起往树的上边方向去找他们共同的最近的祖先结点
{
_enu=node[_enu]._fa;
_env=node[_env]._fa;
}
return _enu;
}
主函数
int main()
{
int T;//表示输入测试数据的组数
int N;//表示村庄的个数
int M;//该组测试问题的个数
cout<<"请输入测试数据的组数:";
cin>>T;
while(T--)
{
cout<<"请输入村庄的个数:";
cin>>N;
Slution slu(N);
slu.InitMap();//初始化村庄图
slu.CreateGraph();//创建村庄图的邻接矩阵的下三角矩阵
slu.BFS_Tree();//利用bfs算法生成该村庄图的树
cout<<"请输入测试组数目:";
cin>>M;
while(M--)
{
int a,b,c;
cout<<"请依次输入村庄A,B,C的编号:";
cin>>a>>b>>c;//输入结点a,b,c
int ab=slu.SearchFather(a,b);
int ac=slu.SearchFather(a,c);
int bc=slu.SearchFather(b,c);
if(ac!=c&&bc!=c)//a,c最近公共祖先和b,c的最近公共祖先都不是c
{
cout<<"No"<<endl;
}
else
{
if(ac==c&&bc==c&&ab!=c)//若a,c的最近公共祖先与b,c的最近公共祖先都是c,但c不是a,b的最近公共祖先
//则c一定不在a,b的路径之间
cout<<"No"<<endl;
else
{
cout<<"Yes"<<endl;
}
}
}
}
}
五.执行程序名,并打印程序运行时的初值和运算结果
执行Mysterious_love_story.cpp文件,手动输入两个村庄图的每一个条边
输入数据如下:
第一个村庄的数据:
20
0 1
0 5
0 4
1 2
1 3
4 7
4 8
4 9
8 16
8 17
7 18
18 19
19 6
2 13
2 10
2 12
3 11
3 14
3 15
该村庄图经过BFS遍历生成的树如下:
程序输入边和测试数据的运行截图:
1.第一组测试数据,查询结点小村15是否在小村11和小村14之间。由生成树的图可知,11和15的最近公共祖先为3,14和15的最近公共祖先为3故小村15不在11和14之间的路径上。
2. 第二组测试数据,查询7是否在6和17之间的路径上。因为6和7的最近公共祖先是7,17和7的最近公共祖先为4,6和17的最近公共祖父为4所以小村7在6和17之间的路径上。
3. 第三组测试数据,查询1是否在11和14之间的路径上。因为11和1的最近公共祖先是1,14和1的最近公共祖先为1,11和14的最近公共祖父为3所以小村1不在11和14之间的路径上。
4. 第四组测试数据,查询5是否在18和11之间的路径上。因为11和5的最近公共祖先是0,18和5的最近公共祖先为0,所以小村5不在11和18之间的路径上。
第二个村庄的数据:
10
0 1
0 2
1 4
1 7
7 8
2 9
9 6
9 3
2 5
该村庄图经过BFS遍历生成的树如下:
程序输入边和测试数据的运行截图:
1.第一组测试数据,查询结点小村7是否在小村4和小村8之间。由生成树的图可知,4和7的最近公共祖先为1,8和7的最近公共祖先为1故小村7在4和8之间的路径上。
2. 第二组测试数据,查询0是否在4和8之间的路径上。因为4和0最近公共祖先是0,8和0的最近公共祖先为0,4和8的最近公共祖父为1所以小村0不在4和8之间的路径上。
3. 第三组测试数据,查询2是否在5和6之间的路径上。因为5和2的最近公共祖先是2,6和2的最近公共祖先为2,5和6的最近公共祖父为2所以小村2在5和6之间的路径上。
4. 第四组测试数据,查询9是否在5和0之间的路径上。因为9和5的最近公共祖先是2,9和0的最近公共祖先为0,所以小村9不在0和5之间的路径上。
六.实验结果分析,实验收获和体会
我在本次数据结构课程设计中选择的是第三道题目:神秘国度的爱情故事。
我本次编写的程序语句执行频度最大的为void Slution::InitMap()函数中的Map[i][j]=0;语句其执行频度为n(n+1)/2,故该程序的时间复杂度为O(n^2)。在本次实验中,我利用到了图,队列和树这三种数据结构。利用图的邻接矩阵的表示方式存储了图的基本结点信息,而且因为无向图的邻接矩阵是关于其矩阵的主对角线对称的所以我存储其邻接矩阵的下三角部分,通过对该村庄图的邻接矩阵的BFS搜索遍历进而生成该图的树,最后通过在树中分别寻找A和C,B和C,A和B的最近公共祖先然后判断C是否在村庄A和B之间的路径上,该种解决方案能准确的判断出C是否在A,B之间的路径上,程序输出结果的准确率为100%,通过该次数据结构的课程设计使我对在这一学期学到的数据结构的知识有了一次更为综合的应用,加深了我对各种常用数据结构的理解以及进一步熟悉不同数据结构的在不同场合的优缺,这次课程设计让我对该学科的应用有了进一步深刻的认识。
七.实验的改进意见和建议
因为我在本次实验中是利用图的邻接矩阵的存储方式来存储图的导致在开辟动态二维数组的时让整个程序的时间复杂度处于O(n^2),这导致在输入的村庄结点为50000村庄的图的创建时间达到了10.583s,所以总的来说该程序在面对数据量较大的情况时执行速度是一个硬伤。我想该程序可以利用图的邻接表存储方式来进行对该村庄图的存储,这时候该程序的时间复杂度就会处于O(n-1)即O(n-1)(因为这时候输入每一条边的语句的执行频度在此程序的执行频度最大,这时候将大大优化程序的执行速度。
源代码:
#include<iostream>
#include<queue>
using namespace std;
//利用邻接矩阵储存村庄的分布情况,0表示两点之间不互为邻接点,1表示互为邻接点
//因为邻接矩阵存储无向连通图是对称的矩阵故,只要存储矩阵的下三角部分
struct Node{
int _number;//记录该节点的下标
int _fa;//记录该节点的父亲节点
int _tier;//记录该节点在bfs生成树的第几层
bool _hvs=false;//用来记录在BFS遍历时该节点是否被访问过
};
class Slution{
public:
Slution(int n):n(n){}//调用构造函数初始化村庄的数量
void InitMap();//村庄地图的初始化
void CreateGraph();//建立村庄图的邻接矩阵的下三角
void BFS_Tree();//利用bfs算法生成该村庄图的树
int SearchFather(int u,int v);//寻找两个节点之间的最近公共祖先
private:
Node *node;//结点数组
int n;//村庄的数量
int **Map;//村庄图
};
void Slution::InitMap()
{
//因为邻接矩阵存储无向连通图是对称的矩阵,故只要存储矩阵的下三角部分
Map =new int*[n];
for(int i=0;i<n;++i){Map[i]=new int[i+1];}
for(int i=0;i<n;++i)//将邻接矩阵的每一个元素都置0
{
for(int j=0;j<=i;++j)
{
Map[i][j]=0;
}
}
}
void Slution::CreateGraph()
{
cout<<"请依次输入村庄的"<<n-1<<"条道路,两点间用空格隔开!"<<endl;
node=new Node[n];
for(int i=0;i<n;++i){node[i]._number=i;}//初始化每一个结点的编号
int _edgeNum=n-1;//路径数量
for(int i=0;i<_edgeNum;++i)
{
int u ,v;//一条路径上的两个端点
cin>>u>>v;
if(u>=v){Map[u][v]=1;}//采用下三角矩阵存储时,没有第一个下标小于第二个下标的位置
else{Map[v][u]=1;}
}
}
void Slution::BFS_Tree()//利用bfs算法生成该村庄图的树
{
queue<int>qu;
qu.push(0);//插入邻接矩阵的第一个结点
node[0]._hvs=true;//标志该节点已经被访问过
node[0]._fa=0;//第一个结点的父节点为它本身
node[0]._tier=0;//第一个结点在生成树的第0层
while(!qu.empty())//当队列不为空时
{
int u=qu.front();//记录下队头结点
qu.pop();//队列中的第一个结点出队列
for(int i=0;i<n;++i)
{
if((Map[u][i]==1||Map[i][u]==1)&&node[i]._hvs==false)
{
node[i]._fa=u;//记录结点的父亲结点的下标
node[i]._tier=node[u]._tier+1;//该结点在生成树的深度为父节点的深度加1
node[i]._hvs=true;//标志该节点已经被访问过
qu.push(i);//让该结点的所有邻接点依次进入队列
}
}
}
}
int Slution::SearchFather(int u,int v)//寻找两个节点之间的最近公共祖先
{
//因为这是最小无向连通图的bfs生成树故两个结点一定有一个相同的祖先节点
int gap;//记录u和v之间在树的层次的差值
int _enu=u,_env=v;//分别记录两个结点的祖先节点
if(node[u]._tier>node[v]._tier)
{
gap=node[u]._tier-node[v]._tier;//让层次较高的结点开始寻找和另一个结点层次一样的祖先结点
for(int i=1;i<=gap;++i)
{
_enu=node[_enu]._fa;
}
}
else{
gap=node[v]._tier-node[u]._tier;//让层次较高的结点开始寻找和另一个结点层次一样的祖先结点
for(int i=1;i<=gap;++i)
{
_env=node[_env]._fa;
}
}
while(_enu!=_env)//如果层次较高的结点的与另一个结点层次一样的祖先结点不是这个层次低的结点,
//则让两个结点一起往树的上边方向去找他们共同的最近的祖先结点
{
_enu=node[_enu]._fa;
_env=node[_env]._fa;
}
return _enu;
}
int main()
{
int T;//表示输入测试数据的组数
int N;//表示村庄的个数
int M;//该组测试问题的个数
cout<<"请输入测试数据的组数:";
cin>>T;
while(T--)
{
cout<<"请输入村庄的个数:";
cin>>N;
Slution slu(N);
slu.InitMap();//初始化村庄图
slu.CreateGraph();//创建村庄图的邻接矩阵的下三角矩阵
slu.BFS_Tree();//利用bfs算法生成该村庄图的树
cout<<"请输入测试组数目:";
cin>>M;
while(M--)
{
int a,b,c;
cout<<"请依次输入村庄A,B,C的编号:";
cin>>a>>b>>c;//输入结点a,b,c
int ab=slu.SearchFather(a,b);
int ac=slu.SearchFather(a,c);
int bc=slu.SearchFather(b,c);
if(ac!=c&&bc!=c)//a,c最近公共祖先和b,c的最近公共祖先一样但该最近公共祖先不是c
{
cout<<"No"<<endl;
}
else
{
if(ac==c&&bc==c&&ab!=c)//若a,c的最近公共祖先与b,c的最近公共祖先都是c,但c不是a,b的最近公共祖先
//则c一定不在a,b的路径之间
cout<<"No"<<endl;
else
{
cout<<"Yes"<<endl;
}
}
}
}
}