数据结构课程设计是综合运用数据结构课程中学到的几种典型数据结构 , 以及程序设计语言 ( c语言) 自行实现一个较为完整的使用系统 。通过课程设计 ,即通过系统分析 、系统设计 、编程调试 ,写实验报告等环节,进一步掌握使用系统设计的方法和步骤,灵活运用并深刻理解典型数据结构在软件开发中的使用。
本课程设计目的是:1.解决约瑟夫环问题的实现,对数据结构这门学科加以学以致用。2.熟练掌握链表存储结构及其建立过程和常用操作;3.学会自己调试程序的方法并掌握一定的技巧。4.利用单链表存储结构模拟此问题,按照出列顺序打印各人的编号;添加顺序结构上的实现部分。(选做)同时还使用了流程图(Flow Chart)来表示算法的思路。而流程图作用:1.将工作过程的复杂性、有问题的地方、重复部分、多余环节以及可以简化和标准化的地方都显示出来;2.将实际的和想象的过程流程进行比较和对照、以便寻求改进过程的机会;3.提供了一个直观而通俗地展示复杂过程的工具
本课程设计第2章介绍了需求分析,大致分为问题分析和功能分析两大类;第3章介绍概要设计,其中3.1至3.3详细介绍了链表、单链表存储结构、单向循环链表存储结构的概念;第4章介绍了详细设计,包括算法思想,重点解释内容;第5章是调试与测试;第6章是总结与体会,主要描述本程序存在的问题和需要改善的地方;第7章是参考文献,介绍了完成本课程设计用到的参考文献;附录为整个程序的源代码。
著名犹太历史学家 Josephus在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式。41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
演示如图2-1所示:
说明:内圈为41个人的原始排序,外圈为这个人第几次死亡。例:1.2.3循环报数,每次报数为3的人自杀,所以序号为3的人第1个自杀、序号为4的人第2个自杀、序号为9的人第3次自杀……以此类推。
约瑟夫环问题是:编号为1,2,…,n的n个人按顺时针方向围坐一圈,每人持有一个密码。一开始任选一个正整数作为报数上限值m,从第一个人开始顺时针方向自1开始顺序报数,报到m时停止报数。报m的人出列,将他的密码作为新的m值,从他在顺时针方向上的下一个人开始重新从1报数,如此下去,直至所有人全部出列为止。
利用单链表存储结构模拟此问题,按照出列顺序打印各人的编号。
m的初值为20,n=7,7个人的密码依次为3,1,7,2,4,8,4。则首先出列人的值为6,依次,正确的出列顺序应该为6,1,4,7,2,3,5。
具体演示如下表格2-1:
什么是链表?线性表的链式存储结构生成的表,叫做链表。链表存储是在程序运行过程中动态的分配空间,只要存储器还有空间,就不会发生存储溢出问题,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点关系间的指针。
优点:
(1)存取某个元素速度慢。
(2)插入和删除速度快,保留原有的物理顺序,比如:插入或者删除一个元素时,只需要改变指针指向即可。
(3)没有空间限制,存储元素的个数无上限,基本只与内存空间大小有关。
此外在循环链表中,从任何一个结点的位置出发都可以找到其他所有结点,但单链表做不到,所以在创建约瑟夫环时选用的是循环链表存储数据;
所谓链式存储结构,相较于顺序存储结构的顺序表,顾名思义其存储方式不再是物理地址开辟一块连续空间存储所有结点的方式,见图3-1,而是通过指针将结点连接起来的存储方式。因此,单链表的每一个结点在物理空间可以不相邻,而在逻辑空间上连续存在;且每个结点除了存储了本身的数据Data,而且有且仅有一个指针Pointer指向下一个结点的位置(地址),是单向存储。
这里Head是头结点,是放在第一个元素的节点之前,头结点用来标记单链表的开始,它的数据域一般没有意义,并且它本身也不是链表必须要带的。它的设立单纯是为了操作的统一和方便。比如:有了头结点,我们在对第一个元素前插入或者删除结点的时候,它的操作与其它结点的操作就统一了。单链表最后一个结点的指针一般指向NULL,如果让其指向开头,则这个链表就成了循环单链表。
针对约瑟夫环,选用不包含头结点的循环链表,循环链表是由单链表转换而来,所以,循环链表拥有单链表的所有操作,主要包括如下几个操作。创建链表、 销毁链表、获取链表的长度、清空链表、获取结点、插入结点、删除结点。
链表都是由一个个结点组成,结点不同,链表也不同,根据题目要求,该结点的数据域为序号和密码,数据类型为整型。此外还有一个指针域,用于存放下一个结点的地址,如图4-1。
说明:struct L是包含3个成员的结构类型;struct L *next这个指针存储struct L结构类型的地址;typedef将这个结构类型重新起名为LNode;*LinkList表示指向LNode的指针。这里运用了嵌套定义
typedef struct L //链表
{
ELE data;
int seq;
struct L *next;
}LNode,*LinkList;
编号为1,2,…,n的n个人按顺时针方向围坐一圈,每人持有一个密码。
实现方案:利用尾插法创建一个单向循环链表。如图4-2
for(i=1;i<n;i++)
{
printf("输入第%d个人的密码:",i);
scanf("%d",&p->data);//输入密码
p->seq=i;//序号
p->next=(LinkList)malloc(sizeof(LNode));//指针域置空
p=p->next; //p往后走
}
因为该for循环最后创建的是n-1个结点,所以在for循环执行完后需要再打印出最后一个人的序号及其密码,代码如下:
printf("输入第%d个人的密码:",i);
scanf("%d",&p->data);
p->seq=n;//最后一个结点序号为n
为什么不使用i<=n作为循环条件?答案见4.3内容。
在所有结点创建好后,需要将此单链表变为循环链表,执行
p->next=head,做到首尾相连。
== 注意:在创建链表之前要进行其他链表的销毁,如下代码:==
void free_list(LinkList head)
{//销毁链表
LinkList p=head,q=head;
if(p==NULL)
return ;
if(p->next==head)
{
free(p);
head=NULL;
return ;
}
while(p->next!=head)//最后的尾结点不指向头结点,
{
q=p;//保存要删除结点的前一个地址
p=p->next;
free(q);
}
free(p);
free(head);
head=NULL;
}
考虑到不确定因素,在查看创建的单向循环链表时,设置了三个条件:(1)如果链表为空(2)如果只有一个结点(3)如果有多个结点,
就可以进入循环遍历。代码如下:
if(p==NULL)
if(head->next==head)
if(p->next!=head)
判断循环链表是否遍历完的方法是判断它的尾结点是否指向首结点,如果指向首结点,则循环结束。所以,此时还剩下尾结点没有打印出来。这也就解释了5.2的问题。解决如下代码。
while(p->next!=head)
{
printf("%d\t",p->data);//输出它的密码
p=p->next;
}
printf("%d\t",p->data);//输出最后一个
一开始任选一个正整数作为报数上限值m,从第一个人开始顺时针方向自1开始顺序报数,报到m时停止报数。报m的人出列,将他的密码作为新的m值,从他在顺时针方向上的下一个人开始重新从1报数,如此下去,直至所有人全部出列为止。
实现方案如图4-3:
n为一开始报的数,q为一个辅助指针。q和p保持一前一后的关系。
while(p->next!=p)
{
for(i=1;i<n;i++)//循环n次
{
q=p; //q保存删除的前一个结点
p=p->next;//p移动到下一个位置,直到移动到位置为n,for循环结束时,p->next就为出列的结点
}
q->next=p->next; //删除p的作用,此时的p->next为要删除结点的下一个地址 ,
printf("初始序号为%d的人出列,密码:%d\n",p->seq,p->data);//输出
n=p->data;//新的密码
free(p);//释放该节点
p=q->next;//更新p,q->next 为要删除结点的下一个地址 ,从此在该地址重新从1计数
}
printf("最后一个人出列,");
printf("密码:%d\t",p->seq);
return p;
}
int main()
{
system("color 74");
int n;
LinkList head=NULL;//创建一个指针类型的引用head
do
{
menu();
scanf("%d",&n);
while(n<0||n>3)
{
printf("范围错误,重新输入!\n");
scanf("%d",&n);
}
// system("cls");
switch(n)
{
case 1:
head=creat_list(head);//把函数类型赋给head
printf("\n\n");
break;
case 3:
head=kill_list(head);
system("pause");
printf("\n\n");
break;
case 2:
show_list(head);
printf("\n\n");
break;
}
}while(n!=0);
free_list(head);
return 0;
}
对于head=kill_list(head)的理解如下:
A a1 = new A();它代表A是类,a1是引用,a1不是对象,new A()才是对象,a1引用指向new A()这个对象。在JAVA里,“=”不能被看成是一个赋值语句,它不是在把一个对象赋给另外一个对象,它的执行过程实质上是将右边对象的地址传给了左边的引用,使得左边的引用指向了右边的对象。JAVA表面上看起来没有指针,但它的引用其实质就是一个指针,引用里面存放的并不是对象,而是该对象的地址,使得该引用指向了对象。在JAVA里,“=”语句不应该被翻译成赋值语句,因为它所执行的确实不是一个赋值的过程,而是一个传地址的过程,被译成赋值语句会造成很多误解,译得不准确。
再如:A a2;它代表A是类,a2是引用,a2不是对象,a2所指向的对象为空null;
再如:a2 = a1;它代表,a2是引用,a1也是引用,a1所指向的对象的地址传给了a2(传址),使得a2和a1指向了同一对象。
综上所述,可以简单的记为,在初始化时,“=”语句左边的是引用,右边new出来的是对象。在后面的左右都是引用的“=”语句时,左右的引用同时指向了右边引用所指向的对象。
见附录
1、使用说明
(1)用户运行程序后可以看到菜单。
(2)菜单列出了四个模块,用户可以根据提示输入对应编号进行系统的相关操作。
2、测试结果与分析
(1) 菜单界面:输入0-1选择相应的功能,输入其他数字要求重新输入,如图5-1。
在这次的数据结构课程设计中,我在每个阶段都能够较顺利并圆满地完成任务,认真完成课程设计。通过这次的课程设计,我学习到很多课堂上没有学习到的知识。当然,我们对数据结构方面的理论知识理解的更透彻,同时对常用画图软件、文档编辑等知识也掌握得更加熟练。尽管也会遇到一些难题,不过经过同学和老师的帮助以及共同商讨,也都解决了。
对于该程序还存在不足,美观方面,虽然在函数里面加了system(“cls”),但是清屏效果还是没能达到预期。其次,对于查看每个人的密码模块,在进行了报数模块之后,在进行查看时,只能查到最后一个人的密码。经查阅资料,约瑟夫环还可以用队列来实现,而这篇程序只用到了不带头结点的单向循环链表。对于不足,发现自己欠缺的知识还是很多,这还需要进一步学习和提升。
[1]严蔚敏,吴伟民.数据结构:C语言版[M].清华大学出版社,1997.4
[2] Stephen Prata,译者:张海龙、袁国忠.C Primer Plus(第6版)中文版.人民邮电出版社,2012.6.1
#include
#include
#define ELE int
typedef struct L //链表
{
ELE data;
int seq;
struct L *next;
}LNode,*LinkList;
void menu()//菜单
{
printf("\n\n");
printf("\t\t\t\t********************************************\t\t\t\t\n");
printf("\t\t\t\t| 1:创建约瑟夫环- |\t\t\t\t\n");
printf("\t\t\t\t| 2:查看每个人的密码或最后出列人的密码|\t\t\t\t\n");
printf("\t\t\t\t| 3:输入一开始的报数m: |\t\t\t\t\n");
printf("\t\t\t\t| 0:退出 |\t\t\t\t\n");
printf("\t\t\t\t******************欢迎使用******************\t\t\t\t\n");
}
void free_list(LinkList head)
{//销毁链表
LinkList p=head,q=head;
if(p==NULL)
return ;
if(p->next==head)
{
free(p);
head=NULL;
return ;
}
while(p->next!=head)//最后的尾结点不指向头结点,
{
q=p;//保存要删除结点的前一个地址
p=p->next;
free(q);
}
free(p);
free(head);
head=NULL;
}
LinkList creat_list(LinkList head)//创建循环链表
{
int n,i,k;
LinkList p=NULL;//进行定义指针时,没有申请空间,所以开始要赋予空值
free_list(head);//创建之前先销毁上一次的数据
printf("请输入要创建的人数:");
scanf("%d",&n);
while(n<=0)
{
printf("人数至少为1,请重新输入\n");
scanf("%d",&n);
}
head=(LinkList)malloc(sizeof(LNode));//给首元结点 一个空间
p=head;//p指向第一个 把head的地址赋给p
for(i=1;i<n;i++)
{
printf("输入第%d个人的密码:",i);
scanf("%d",&p->data);//输入结点密码
p->seq=i;//结点序号
p->next=(LinkList)malloc(sizeof(LNode));
p=p->next; //p往后走
}
printf("输入第%d个人的密码:",i);
scanf("%d",&p->data);
p->seq=n;//最后一个结点序号为n
p->next=head;//首位相连
return head;
}
void show_list(LinkList head)//输出链表元素
{
LinkList p=head;//引用创建好的链表,head为头指针,头指针就可以找到创建的链表
if(p==NULL)
{
printf("无任何元素\n");
return ;
}
if(head->next==head)//如果它的下一个就是头,为一个结点
{
printf("%d\n",head->data);
return ;
}
while(p->next!=head)
{
printf("%d\t",p->data);//输出它的密码
p=p->next;
}
printf("%d\t",p->data);//输出最后一个
printf("\n");
}
LinkList kill_list(LinkList head)//开始获取密码
{
int n,i;
LinkList p=head,q=head;//都为指针类型
if(head==NULL)
{
printf("无任何人\n");
return NULL;
}
printf("输入一开始的报数m:");
scanf("%d",&n);
while(n<=1)
{
printf("至少为2\n");
scanf("%d",&n);
}
if(head->next==head)//如果只有一个元素
{
printf("%d",head->seq);
return head;
}
printf("按照出列顺序打印各人的编号:\n\n");
while(p->next!=p)
{
for(i=1;i<n;i++)//循环n次
{
q=p; //q保存删除的前一个结点
p=p->next;//p移动到下一个位置,直到移动到位置为n,for循环结束时,p->next就为出列的结点
}
q->next=p->next; //删除p的作用,此时的p->next为要删除结点的下一个地址 ,
printf("初始序号为%d的人出列,密码:%d\n",p->seq,p->data);//输出
n=p->data;//新的密码
free(p);//释放该节点
p=q->next;//更新p,q->next 为要删除结点的下一个地址 ,从此在该地址重新从1计数
}
printf("最后一个人出列,");
printf("密码:%d\t",p->seq);
return p;
}
int main()
{
system("color 74");
int n;
LinkList head=NULL;//创建一个指针类型的引用head
do
{
menu();
scanf("%d",&n);
while(n<0||n>3)
{
printf("范围错误,重新输入!\n");
scanf("%d",&n);
}
// system("cls");
switch(n)
{
case 1:
head=creat_list(head);//吧函数类型赋给head
printf("\n\n");
break;
case 3:
head=kill_list(head);
system("pause");
printf("\n\n");
break;
case 2:
show_list(head);
printf("\n\n");
break;
}
}while(n!=0);
free_list(head);
return 0;
}