输出形式:
中文提示按照m个猴子,数n个数的方法,输出为大王的猴子是几号,建立一个函数来实现此功能。
三、【算法设计】
这个问题实际上就是约瑟夫环问题。
相当于有m个猴子围成一圈,顺序排号,从第一个猴子开始报数,相隔n个数出圈一个猴子,最后留下来的就是猴子大王了。
由于有m个猴子,所以就需要动态申请m个整型内存,而不能使用整型数组。
为了实现围城一圈,可以将链表首尾相接形成单循环链表,链表中的每一个结点代表一个猴子,每相隔n个数退出一个猴子并将此结点从链表中删除,当链表中只剩下一个结点,即p->next=p时,说明p就是最后留下的结点即为猴子大王。
用到的抽象数据类型如下:
由于该程序较为简单所以用到的抽象数据类型为链表,为便与操作,设置单链表为循环单链表。
ADTList{
数据对象D:
一个集合,该集合中的所有元素具有相同的特性;
数据关系R:
循环链表中的数据关系为线性关系;}
基本操作P:
build(LinkList&p,intm)
初始条件:
m为整形数据;
操作结果:
构造一个数据个数为m的循环单链表。
find_king(LinkListp,intn)
初始条件:
链表L已存在,数据m,n已知。
操作结果:
依次输出猴子的出圈号,最后选出猴子大王。
}
四、【测试分析】
在编程过程中,发现使用单链表的时候很难实现多次循环,算法时间复杂度很高,后改用循环单链表的时候很好的解决了这个问题,便与实现多次循环。
本实验采用的是尾插法建立单链表,但是做的时候发现指针的位置很重要,特别要注意将单链表的首尾相接形成单循环链表,这也是本实验的一个重点。
涉及到的主要算法的时间是,在选出猴子的主要代码中主要利用了两个循环,一个循环判断链表是否为空,一个循环判断该猴子编号是否为出圈的编号。
故该算法的时间复杂度为O(n*n)。
不过该算法中频繁的删除结点,大大增加了算法的空间复杂度。
还有就是关于引用和传参的问题,虽然是老问题,但是每次都有新意。
五、【运行结果分析】
(1)输入猴子的总数m=7,相隔数目n=2,测试结果的正确性如下:
依次出圈的猴子为:
2、4、6、1、5、3.最后的猴子大王为:
7,结果正确。
(2)输入猴子的总数m=8,相隔数目n=3,测试结果如下:
依次出圈的猴子为:
3、6、9、4、8、5、2、7.猴子大王为:
1.结果是正确的。
(三)二叉排序树的基本操作
一、【设计题目】二叉排序树的基本操作
二、【题目描述】
任务:
编写算法实现对依次输入的关键字序列建立二叉排序树,并能实现二叉排序树的查找、插入和删除运算。
测试数据要多种,测试程序的可行性。
三、【算法设计】
二叉排序树(BinarySortTree)或者是一个空树,或者是具有以下性质的二叉树:
(1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左右子树也分别为二叉排序树。
正是基于二叉排序树的这些性质,在做程序的时候才有算法的思想对二叉排序树进行查找、插入、删除等运算。
例如下面的一棵树就是二叉排序树:
本实验中,二叉排序树在创建的时候,采用的是先序建立过程,‘#’代表的是空树,边插入,边创建。
查找的时候,首先将给的值与根的关键字比较,若相等,则查找成功;否则,将给定的值与根的关键字比较大小,如果小与根结点的值,就在左子树上继续查找,反之在右子树上继续查找。
插入的时候,首先要确定插入的位置,插入位置的确定,用指针的变换来实现,具体的操作还是比较给定值与根的关键字的大小。
删除的时候分三种情况:
(1)删除的是叶子结点,直接修改修改其双亲的指针就可以了,因为叶子结点的删除不会破坏树的有序性;
(2)删除的是但分支结点,直接用它的孩子指针代替它即可;
(3)删除的是双分支结点,就需要重接它的左右子树,即用被删结点的直接前驱或直接后继来代替被删结点。
用到的抽象数据类型:
ADTBST{
数据对象D:
D是具有相同的特性的数据元素的集合。
数据关系R:
……
基本操作P:
InsertBST(BiTree&T,chare)
操作结果:
按先序构造二叉排序树BST
SearchBST(BiTreeT,charkey,BiTreef,BiTree&p)
初始条件:
二叉排序树已存在
操作结果:
返回要查找的关键字的值
DeleteBST(BiTree&T,charkey)
初始条件:
二叉排序树已存在
操作结果:
删除一个结点元素
Delete(BiTree&p)
初始条件:
二叉排序树已存在
操作结果:
删除根结点,重接树的左右子树}
程序结构图:
四、【测试分析】
这是一个比较简单的程序,针对的是二叉排序树的基本操作。
在做的过程中,用到的最多的就是递归函数的调用,把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的能力在于用有限的语句来定义对象的无限集合。
用递归思想写出的程序往往十分简洁易懂。
但是递归算法解题的运行效率较低,在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出等。
该程序耗费时间的过程主要是查找过程,而查找过程和折半查找很类似。
含有n个结点的二叉排序树,最差的情况下的ASL=n+1/2(和顺序查找相同),显然,最好的情况就是和折半查找相同ASL与㏒N成正比。
性能不是很好,所以,一般都要对二叉树进行平衡处理。
五、【运行结果分析】
首先来测试一下各个功能是否可行:
没有输入任何值的时候测试。
然后输入想要建立的二叉排序树:
依次插入6、8、1、7.删除6.显示结果
再测试一下字符类型:
依次输入f、k、o、l。
显示为:
fklo操作后最后显示为:
fko。
可知,程序是可行的,结果也是想要的结果。
(四)最小生成树问题
一、【设计题目】最小生成树问题
二、【题目描述】
若要在n个城市之间建设通信网络,只需要假设n-1条线路即可。
如何以最低的经济代价建设这个通信网,是一个网的最小生成树问题。
【系统要求】
1.利用克鲁斯卡尔算法求网的最小生成树。
2.利用普里姆算法求网的最小生成树。
3.要求输出各条边及它们的权值。
【测试数据】
由学生任意指定,但报告上要求写出多批数据测试结果。
【实现提示】
通信线路一旦建成,必然是双向的。
因此,构造最小生成树的网一定是无向网。
设图的顶点数不超过30个,并为简单起见,网中边的权值设成小于100的整数,可利用C语言提供的随机函数产生。
图的存储结构的选取应和所作操作相适应。
为了便于选择权值最小的边,此题的存储结构既不选用邻接矩阵的数组表示法,也不选用邻接表,而是以存储边(带权)的数组表示图。
三、【算法设计】
本实验采用的是用邻接矩阵和邻接表存储图,主要是因为邻接矩阵便于稠密型的图的存储,而邻接表便于稀疏型图的存储。
假设G=(V,E)为一个网络,其中V为网中所有顶点的集合,E是网中所有带权边的集合。
设立两个新集合U和T,其中集合U用于存放G中最小生成树的顶点,T用于存放G的最小生成树的边。
集合U的初始值仅含有起始顶点,假设构造最小生成树时,从顶点u1开始,那么U={u1}。
集合T的初始值为T={}。
Prim算法的思想是,从所有u∈U、v∈V-U的边中,选取具有最小权值的边(u,v),将顶点v加入集合U,并将边(u,v)加入集合T中,如此不断重复,直到U=V时,最小生成树构造完毕。
为了实现Prim算法,需要设置两个辅助数组lowcost和closevex,其中lowcost为一维数组,用于保存集合V-U中各顶点与集合U中各个顶点构成的所有边中具有最小权值的边的权值;数组prevex也是一个一维数组,用于保存该边在集合U中的顶点。
我们利用这两个辅助数组来标记在生成树求解过程中已经加入到生成树中的顶点和边。
具体实现是:
假设初始状态为U={u1},这时lowcost[1]=0,表示顶点u1已经加入到集合U中;lowcost的其他分量的值是顶点u1到其余顶点所构成的直接边的权值。
然后不断选取权值最小的边(ui,uk),其中ui∈U而uk∈V-U,并将lowcost[k]置为0,表示顶点uk已经加入集合U中。
由于顶点由集合V-U进入集合U,从而引起这两个集合的内容发生变化,所以此时还需要根据具体情况对数组lowcost和prevex的部分分量进行更新。
设G=(V,E)是网络,它的最小生成树的初始状态是n个只有根结点的树林,将集合E中的边按权的递增顺序排列,从小到大依次选择顶点分别在两颗不同的树的边,连接两棵树,使之成为一棵树,依次类推,直到所有树林变成一棵树。
kruscal算法:
设N=(V,{E})是一个联通网络,则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图的每个顶点自成一格连通分量。
在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边,以此类推,直至T中所有顶点都在一个连通分量上为止。
在这个算法中,用一个数组标记已经访问过的顶点,访问过的顶点标记为0,否则返回根的下标。
抽象数据类型图的定义如下:
ADTGraph{
数据对象V:
V是具有相同特性的数据元素的集合,成为顶点集。
数据关系R:
R={VR}
VR={(v,w)|v,w属于V,(v,w}表示v和w之间存在路径}
基本操作P:
CreateGraph(&G,V,VR)
初始条件:
V是图的顶点集,VR是图中边的集合。
错作结果:
按V和VR定义构造图G。
LocateVex(G,u)
初始条件:
图G存在,,u和G中顶点有相同特性。
操作结果:
若G中存在顶点u,则返回该顶点在图中的位置;否则返回其他信息
GetVex(G,v)
初始条件:
图G存在,v是G中某个顶点。
操作结果:
返回v的信息。
FirstEdge(G,v)
初始条件:
图G存在,v是G中某个顶点。
操作结果:
返回依附于v的第一条边。
若该顶点在G中没有邻接点,则返回“空”。
NextEdge(G,v,w)
初始条件:
图G存在,v是G中某个顶点,w是v对邻接顶点。
操作结果:
返回依附于v的(相对于w)下一条边。
若不存在,则返回“空”。
InsretEdge(&G,v,w)
初始条件:
图G存在,v和w是G中两个顶点。
操作结果:
在G中添加边(v,w)。
DeleteEdge(&G,v,w)
初始条件:
图G存在,v和w是G中两个顶点。
操作结果:
在G中删除边(v,w)
}ADTGraph
程序模块:
本程序分三大模块,其调用关系为
主程序
↙↘
建立图模块最小生成树模块
↙↘↙↘
邻接矩阵建立图邻接表建立图Prim算法Kruscal算法
函数的调用关系:
Main
↙↘
CreatGraphMiniSpanTree
↙↘↙↘
creatMGraph_Lcreatadjprimkruscal_ar
↓
localvex
四、【测试分析】
做题目的过程中,曾经尝试了用不同的存储方式来存储图,但是效果都不是很好,并且参考书上的算法有的地方理解不是太好。
所以还是采用图的邻接矩阵和邻接表来存储图。
Prim算法的时间复杂度为O(n2),与网中的变数无关,因此适用于求边稠密的网的最小生成树;kruscal算法的时间复杂度为O(e㏒e)(e为网中的边数)因此相对于prim算法它适用于求边稠密的网的最小生成树。
除此之外,还有就是建立二叉树的时候,建立为先序序列,输入的时候要注意一下,如果序列的输入不正确,结果也是不正确的。
五【运行结果分析】
输入第一组数据4个顶点4条弧。
(016)、(024)、(038)、(129)测试结果如下所示:
通过运行的结果可以看出,程序是可执行的,结果正确。
输入第二组数据:
8个顶点11条边:
(ac5)、(ab7)、(cd13)、(de8)、(db4)、(ef21)、(gf14)、(hg3)、(bh9)、(hf11)、(bc6)。
测试结果如下:
(创建无向图输入的顶点就不再截屏)
(五)收获与体会
通过为期一周半的课程设计,我的感受颇深。
首先就是课设题目的选择。
在选择的时候,就觉得以前的知识掌握的还不是很牢固,因为有的地方课设的题目要求不能很好的理解,一些算法的思想不能明确的变成自己的东西写出来,这样在课设的时候就觉得课设有了一定的难度。
所以我选择了自己可以独立完成的,比较简单的题目来做。
一方面是因为简单的题目可以激发更浓厚的学习兴趣。
若是上来就写一个不知道东南西北的题目,肯定一下子就丧失了课设的信心了。
相反,相对比较简单的题目做起来不是很费力,不会的、不懂的地方,可以自己查资料,或者参考同学的意见,都会得到很好的结果。
这样的学习,即掌握了基础知识,又让自己对编程不是感到高不可攀。
虽然大一的学习已经结束了,但是觉得知识的联系性还是很紧密的,比如说算法的思想,排序思想,查找思想等。
另一方面,就是课设的时间比较紧张。
太抽象的系统在短时间内不能给出一个考虑全面的结果,免不了丢三落四。
不过这样的题目对于自己的编程的能力的提高还是很有帮助的。
猴子选大王的设计涉及到的是单循环链表的知识。
进一步深化了自己对链表建立、链表的查询和删除的理解和应用能力;
统计二叉树的结点个数设计涉及到的是二叉树知识。
不是很抽象,很容易理解,主要就是递归思想的应用。
不过函数的引用与调用也是一个知识点,在以后的学习中还是会用到的;
二叉排序树的基本操作设计涉及到的是排序树的知识。
主要就是二叉树的建立采取的是边插入边建立的思想。
查找、删除也都是递归函数实现的,比较容易懂,没有什么难得地方;
最小生成树设计涉及到的是prim和kruscal算法的应用。
其中图的存储结构的选择、算法的实现都是一个重点。
特别是kruscal算法的回路控制条件的选取。
在实验的时候,选取的是标记已经访问过的顶点的办法来实现的。
总之,通过课设,使我对数据结构有了一个新的认识和知识的提升,同时又复习到了以前没有掌握的地方,课设中同学之间的探讨,比如说用哪种思想比较合理等,也是很有意义的活动。
同时与老师之间的探讨也是获取知识的一个非常重要的途径。
我想,在以后的学习中,还是要继续努力,把自己的知识体系打的更牢固,对以前的知识做到会应用、能应用,才是最终的目的。
六【附加源代码】
(一)猴子选大王代码
#include
typedefstructLNode
{
intdata;
structLNode*next;
}LNode,*LinkList;//线性单链表的存储结构
voidbuild(LinkList&p,intm)//p为头结点指针,创建单循环链表
{
inti;
LinkListw=p;
for(i=0;i{
LinkListq=newLNode;//按顺序插入结点q
q->data=i+1;
q->next=NULL;
w->next=q;
w=q;//w后移
}
w->next=p->next;//单链表首位相接形成单循环链表
}
voidfind_king(LinkListp,intn)//p为头结点指针,查找大王
{cout<<"依次出圈的猴子为:
"<inti;
LinkListq=p->next;
while(q->next!
=q)//不只一个结点
{
for(i=1;i<(n-1);i++)
q=q->next;
if(q->next!
=q)
{LinkListw=q->next;
cout<data;
q->next=w->next;
deletew;
}
q=q->next;
}
cout<cout<<"大王为"<data<}
intmain()
{
intm,n;
LinkListp=newLNode;
p->next=NULL;
cout<<"输入猴子个数和"<<"相隔数目N"<<"的个数"<cin>>m>>n;
build(p,m);
find_king(p,n);
return0;
}
二、统计二叉树叶子结点个数代码
#include
#include
structBiTree{
chardata;
structBiTree*lchild;
structBiTree*rchild;
};//二叉树的结构体
structBiTree*CreatBiTree(){//创建二叉树的函数
charx;
structBiTree*p;
cin>>x;
if(x!
='#'){
p=newstructBiTree;
p->data=x;
p->lchild=CreatBiTree();
p->rchild=CreatBiTree();}
else
p=NULL;
returnp;
}
voidpreorder(BiTree*p)//先序遍历
{
if(p){
cout<data;
preorder(p->lchild);
preorder(p->rchild);}
}
voidInorder(BiTree*p)//中序遍历
{if(p){
Inorder(p->lchild);
cout<data;
Inorder(p->rchild);}
}
voidPostorder(BiTree*p)//后序遍历
{if(p){
Postorder(p->lchild);
Postorder(p->rchild);
cout<data;}
}
intLeafNum(structBiTree*T){//计算二叉树中叶子结点和个数
if(!
T)return0;
elseif(!
T->lchild&&!
T->rchild)
{cout<data<<'';
return1;
}
else
returnLeafNum(T->lchild)+LeafNum(T->rchild);
}
intNodeDouCount(BiTree*T)//求二叉树中度为2的结点和个数
{
intn,num1,num2,num;
if(!
T)return0;
elseif(!
T->lchild||!
T->rchild)n=0;//为单分支结点时,记0;
else
{
n=1;//为双分支结点时,记1
cout<data<<'';
}
num1=NodeDouCount(T->lchild);//递归
num2=NodeDouCount(T->rchild);
return(num1+num2+n);
}
voidmain(){
structBiTree*T,p;
cout<<"请输入结点以创建二叉树:
"<T=CreatBiTree();//创建二叉树
while(!
T){
cout<<"树为空:
"<T=CreatBiTree();
}
cout<