【C语言项目实战6——指针以及应用】

学生管理系统链表问题

项目功能需求

请选择排序字段:


(1)学号 (2)姓名 (3)性别 (4)年龄

(5)C语言 (6)英语 (7)高数 (8)平均分

(0)退出程序


(4)当用户正确选择反序字段后,进一步请用户选择排序方向,如下:

请选择排序方向:


(1)升序 (2)降序 (0)退出程序


(6)此操作可以反复进行,直到用户选择“退出程序”。

下框是在控制台下处理3个学生信息的运行效果示例(说明:加下划线的内容表示用户的输入,其余内容为系统运行时自动显示)。

请输入第 1 个学生信息:


学号(12个字符以内):201210409601

姓名(10个字符以内):刘子栋

性别(4个字符以内):男

年龄(整数):19

《C语言》成绩(整数):92

《英语》成绩(整数):85

《高数》成绩(整数):86


请选择操作:


(0)输入完成 (其他任意键):继续输入


/注:此处省略其他两位学生信息的输入显示/
请选择操作:


(0)输入完成 (其他任意键):继续输入


请选择排序字段:


(1)学号 (2)姓名 (3)性别 (4)年龄

(5)C语言 (6)英语 (7)高数 (8)平均分

(9)显示全部原始信息 (0)退出程序


请选择排序方向:


(1)升序(2)降序(0)退出程序


排序后的学生信息:

================

学号 姓名性别年龄 C语言 英语高数平均分


201210409603 刘子栋 女 19 92 85 86 87.7

201210409602 杨欣悦 女 18 78 93 7481.7

201210409601 童雨嘉 男 19 88 72 82 80.7


请选择排序字段:


(1)学号 (2)姓名 (3)性别 (4)年龄

(5)C语言 (6)英语 (7)高数 (8)平均分

(9)显示全部原始信息 (0)退出程序


原始输入的学生信息:

================

学号 姓名性别年龄 C语言 英语高数平均分


201210409603 刘子栋 女 19 92 85 86 87.7

201210409601 童雨嘉 男 19 88 72 82 80.7

201210409602 杨欣悦 女 18 78 93 7481.7


请选择排序字段:


(1)学号 (2)姓名 (3)性别 (4)年龄

(5)C语言 (6)英语 (7)高数 (8)平均分

(9)显示全部原始信息 (0)退出程序

知识点分析

程序处理的对象是学生,由于每个学生有姓名,性别等若干属性,所以属于复杂的数据类型,可以使用结构体的处理。
学生人数的确定,并且程序的运行中不能要求改变,可以考虑用数组介意储存。

  • 通过实验可达到如下目标:
    (1)进一步掌握数据导入声明和使用
    (2)掌握链表的使用和结构体的声明,结构体的变量声明和赋值
    (3)掌握的成员的访问
    (4)掌握双链表的使用
    (5)使用结构体处理复杂的数据结构

算法思想

(1)本项目主体功能与项目4.1基本相同,故基本的算法思想可参考项目5.1的相关分析。二者的重要区别是:学生人数是否固定。
(2)程序包括输入学生信息、显示操作菜单、排序、显示信息等功能,这些功能相对独立,根据结构化、模块化的编程思想,应该将它们单独编写函数。除输入学生成绩只运行一次外,其他的都应放到循环体中反复操作,直到程序中止。
(3)在这里插入图片描述

(4)
(5)由于输入的学生人数不确定,因此,这个过程应该考虑无限循环,直到特定输入退出。每次输入后给一个操作菜单,由于题目中没有明确要求,可以考虑如下:

请选择操作:


(0)输入完成(其他任意键):继续输入


但是很显然,需要注意的是:在输入第一个学生之前,是不应该出现这个菜单的。

(4)使用链表存储学生对象,可以达到动态大小的目的,链表节点的结构如下:

(5)在项目5.1中曾经讲到:如果希望能够保持或随时恢复原来输入学生的顺序,可以有两种方案,给存储结构增加一个输入顺序的属性,或者将排序操作的结果放到新的链表中,这种方式可以认为是“保护现场”。项目5.1采用第2种方式,为了演示不同的方法,本例采用前一种解决方案,即增加输入顺序的属性。因此,链表节点的结构改进如下:

在这里插入图片描述
在这里插入图片描述

  • 节点的结构如下:
    在这里插入图片描述

(6)从存储的数据结构来看,可以使用单向链表、双向链表、单向循环链表、双向循环链表等实现动态存储。单向链表的特点是相对简单,但不能获取前一个节点;双向链表的特点是能够获取前后的节点。考虑到本项目中有排序(交换节点)的操作,故采用双向链表实现。结构如图3-6-5所示(stu为学生结构体,存储一个学生的信息)。

在这里插入图片描述

其中:头节点head和尾节点tail用于标记链表的起始,并不存放实际数据,当然也不会参与排序、输出等操作。

(7)链表的操作。

项目4.1中已经从数组的角度介绍了数组的排序操作,从理论上讲,链表排序操作的原理也是同的,但事实上,由于数据结构不同,链表的排序操作要复杂得多。因为数组元素在交换时可以通过一个中间变量简单实现;而链表在交换的时候必须要考虑节点间的前后关系(前、后指针的指向)。下面对以双向链表为例介绍链表的相关操作。

a) 双向链表节点的移除

移除节点是链表操作中最简单的操作。例如,在链表中移除一个节点p,双向链表只需3步,步骤如下:例如,当新增一个学生时,需要在链表的最后增加一个节点p,即是在tail节点前插入该节点,步骤如下:

在这里插入图片描述
在这里插入图片描述

c) 双向链表节点的交换

节点的交换通常有两种方式:一是采用中间变量(临时节点),通过数据复制的方式完成;二是通过调整节点为的前后指针指向完成。

对于简单的数据类型(如整数等),采用前者最简单,然而对于复杂的数据类型(如这里的节点为,节点中包括指针、结构体、内部结构体),这样复制操作开销太大,也很容易出错,通过修改指针反而非常简单,效率也最高。

要交换节点p和q,只需要将二者从链表中移除,再将p和q分别插入到原来q和p所在位置即可。这里需要注意的是,由于先移除、再插入,所以插入时找不到原来的参照物了,因此,应该在交换前,在参与交换的节点的前(或后)节点做标记(m和n),以便在插入时作为参照。;

系统流程图

在这里插入图片描述

图1学生管理系统问题的流程图

项目实现

在这里插入图片描述
在这里插入图片描述

运行效果图:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

项目小结

学习双链表的时候还是遇到了问题,就是前段时间刚刚学习了单向链表我写的关于学习单向链表的心得,单向链表其实比较简单,也是很常用的一种数据结构,单向链表相较于数组来说的话它的增加和删除都比较方便,但是单向链表在删除节点的时候吧总是得先获取要删除的节点的前一个节点,所以我们这里学习双向链表,使得在对链表进行增删改查时更加方便。

参考文献

《C语言综合项目实战》 科学出版社。

学生管理系统的代码:

#define IDLen 12  //ID的长度
#define NameLen 10  //姓名
#define SexLen 4 //性别
 
 
//学生成绩结构体
 
typedef  struct strScore {
	int cp; //C语言
	int en;  //英语
	int math;  //数学
}Score;
 
//学生信息结构体
typedef struct Student {
	char id[IDLen + 1];    //ID
	char name[NameLen + 1];    //姓名
	char sex[SexLen + 1];    //性别
	int age;      //年龄
	Score score;   //成绩结构体
	double avg;     // 平均分
};
 
 
//链表节点结构体
typedef struct theNode Node;
struct theNode
{
	Node* prev;   //指向上一个节点
	int order;   //输入的顺序
	Student stu;  //学生结构体的信息
	Node* next;  //指向下一个节点的
};
 
 
Node* head = NULL;  //双向链表的头指针
Node* tail = NULL;  //双向链表的尾指针
int num = 0;        //输入的学生总数
char choice = 0;     //用户的菜单选择
char dir = 0;       //排序方向(1:升序;(2:降序
 
 
void  InputStudentlb();     //输入学生的信息
void  DisplayMenulb();      //显示主菜单并获得用户选项
void  GetorderDirlb();      //显示排序方向的菜单并获取选项
void  SortStudentlb();      //进行排序的操作
void  OutputStudentlb();    //输出学生的信息
 
void manageStudentlb()
{
	InputStudentlb(); //输入学生信息
	while (1)
	{
		DisplayMenulb();   //显示菜单及获取菜单选项
		if ('0' == choice)
		{
			printf("\n感谢使用,再见!");
			_getch();
			exit(0);
		}
		else if ('9' == choice)
		{
			dir = '1';    // 输出原始信息,按照升序
		}
		else {
			GetorderDirlb();   //获取排序方向
			if ('0' == dir)
				continue;
		}
		SortStudentlb();    //排序操作
		OutputStudentlb();  //输出学生信息
	}
}
 
 
//输入学生的信息
void  InputStudentlb() 
{
	Node* p = NULL;  //指向新增学生的节点
	//初始化头指针和尾指针,形成只有两空节点的双向链表
	head = (Node*)malloc(sizeof(Node));
	tail = (Node*)malloc(sizeof(Node));
	head->prev = NULL;
	head->next = tail;
	tail->prev = head;
	tail->next = NULL;
 
	while (1)
	{
		printf("请输入第%d个学生的信息:\n",++num);
		p = (Node*)malloc(sizeof(Node));
		p->order = num;
		printf("---------------------------------------------------");
		printf("\n学号(12个数字字符以内):", IDLen);
		scanf_s("%s", p->stu.id,12);
		p->stu.id[IDLen] = '\0';    //最后一个字符强行加一个串的结束符
 
		printf("\n姓名(10个数字字符以内):\t");
		scanf_s("%s", p->stu.name,12);
		p->stu.name[NameLen] = '\0';    //最后一个字符强行加一个串的结束符
 
		printf("\n性别(整数):\t");
		scanf_s("%s", p->stu.sex,4);
		p->stu.sex[SexLen] = 0;    //最后一个字符强行加一个串的结束符
 
		printf("\n年龄(整数)\t\t");
		scanf_s("%d", &p->stu.age,4);
 
		printf("\n《C语言》成绩(整数)\t\t");
		scanf_s("%d", &p->stu.score.cp,4);
 
		printf("\n《英语》成绩(整数)\t\t");
		scanf_s("%d", &p->stu.score.en,4);
 
		printf("\n《高数》成绩(整数)\t\t");
		scanf_s("%d", &p->stu.score.math,4);
 
		p->stu.avg = (p->stu.score.cp + p->stu.score.en + p->stu.score.math);
		printf("---------------------------------------------------");
		//将新的学生节点添加到链表数据节点最后,即tail节点之前
		p->next = tail;  //p的后节点指向tail
		p->prev = tail->prev;   //p的前节点指向tail的前节点
		tail->prev->next = p;   //tail的原来前节点的后节点指向p
		tail->prev = p;   //tail节点的前节点指向p
 
		//是否结束输入
		printf("\n请选择操作\n");
		printf("\n---------------------------------------------------\n");
		printf("\n(0) 输入完成\t\t(其中按任意键):继续输入\n");
		printf("\n---------------------------------------------------\n");
		if (_getche() == '0')
			return;
 
	}
}
//显示主菜单并获得用户选项
void  DisplayMenulb() 
{
	while (1)
	{
		printf("\n请选择排序的字段\n");
		printf("\n-----------------------------------------------------------\n");
		printf("(1)学号\t\t(2)姓名\t\t(3)性别\t\t(4)年龄\n");
		printf("(5)C语言\t\t(6)英语\t\t(7)高数\t\t(8)平均分\n");
		printf("(9)显示全部原始信息\t\t(0)退出程序");
		printf("\n-----------------------------------------------------------\n");
		choice = _getche();
		if ((choice - '0') < 0 || (choice - '0') > 9)
			printf("\n请选择正确的操作!");
		else
			return;
	}
}
 
 
//显示排序方向的菜单并获取选项
void  GetorderDirlb()
{
	while (1)
	{
		printf("\n请选择排序的方向:\n");
		printf("\n-----------------------------------------------------------\n");
		printf("(1)升序\t(2)降序\t(0)退出程序\n");
		printf("\n-----------------------------------------------------------\n");
		dir = _getche();
		if (dir - '0' < 0 || (dir - '0') > 2)
			printf("\n请选择正确的操作!");
		else
			return;
	}
}
 
 
//进行排序的操作
void SortStudentlb()  
{
	Node *m;  //左边比较的节点的前一个节点
	Node *p;  //左边比较的节点
	Node *q;  //右边比较的节点
	Node *n;   //右边比较的节点的后一个节点
 
	int f;  //大小标志,0表示前者大,1表示后者大
	int swap;   //是否交换,0不交换,1交换
 
	//如果节点数小于2,不用做交换操作
	if (num <= 1)
		return;
	m = head;
	p = m->next;
	while (p->next != tail)
	{
		q = p->next;
		n = q->next;
		while (q != tail)
		{
			f = 0;
			switch(choice)
			{
			case '1'://学号
				if (strcmp(p->stu.id,q->stu.id)<0)
					f = 1;
				break;
			case '2'://姓名
				if (strcmp(p->stu.name,q->stu.name)<0)
					f = 1;
				break;
			case '3'://性别
				if (strcmp(p->stu.sex,q->stu.sex)<0)
					f = 1;
				break;
			case '4'://年龄
				if (p->stu.age<q->stu.age)
					f = 1;
				break;
			case '5'://C语言
				if (p->stu.score.cp < q->stu.score.cp)
					f = 1;
				break;
			case '6'://英语
				if (p->stu.score.en <q->stu.score.en)
					f = 1;
				break;
			case '7'://高数
				if (p->stu.score.math < q->stu.score.math)
					f = 1;
				break;
			case '8'://平均分
				if (p->stu.avg <q->stu.avg)
					f = 1;
				break;
			case '9':  //原始输入顺序
				if (p->order < q->order)
					f = 1;
			default:  //其他,错误
				printf("错误,请选择正确的操作!");
				break;
		
			}
			swap = 0;
			if (1 == f && '2' == dir)
				swap = 1;
			if (0 == f && '1' == dir)
				swap;
			//需要交换
			if (1 == swap)
			{
				//左节点p移出
				m->next = p->next;
				p->next->prev = m;
				//右节点q移出
				q->prev->next = n;
				n->prev = q->prev;
				//将q插入到原来p的位置 (即m节点之后)
				p->next = m->next;
				q->prev = m;
				m->next->prev = q;
				m->next = q;
				//将p插入到原来q的位置 (即n节点之后)
				p->next = n;
				 p->prev = n->prev;
				 n->prev->next = p;
				 n->prev = p;
 
				//恢复p,q原来的位置
				 p = m->next;
				 q = n->prev;
			}
			//一趟比较中,内循环的指针移动
			q = n;
			n = q->next;
		}
		//外循环的指针移动
		m = p;
		p = m->next;
	}
}
//输出学生的信息
void  OutputStudentlb()  
{
	int i = 1;  //循环变量
	Node* p;  //循环指针
	if ('9' == choice)
	{
		printf("\n原始输入的学生信息:\n===========\n");
	}
	else
	{
		printf("\n排序后输入的学生信息:\n===========\n");
	}
 
	printf("学号\t姓名\t性别\t年龄\tC语言\t英语\t高数\n");
	printf("\n-----------------------------------------------------------\n");
 
 
	//遍历链表,输出全部的信息
	p = head->next;
	do
	{
		printf("%-15s%-14s%-7s%-7d", p->stu.id, p->stu.name, p->stu.sex, p->stu.age);
		printf("%-7d%-7d%-7d%4.lf\n", p->stu.score.cp, p->stu.score.en, p->stu.score.math);
		p = p->next;
	}while(p != tail);
	printf("\n-----------------------------------------------------------\n");
}
 

约瑟夫链表问题

项目功能需求
任意个人N围成一圈,每个人有一个密码k(整数);从第一个人(其密码为k1)开始往后计数,数到第k1个人退出;接着,将退出者的密码k2作为下一个退出的依据,继续从下一个人开始计数,数到第k2个人退出;退出者的密码再作为下一个退出的依据……依次循环,直到全部人员出列。

要求:
(1)人数N不确定,用户可以输入任意个人
(2)用户分别依次输入任意个人名,同时输入每个人的密码k
(3)用单向、循环链表处理
(4)输入完成后,给出操作菜单让用户选择操作,菜单项如下:

请选择操作:


(1)显示信息
(2)重新输入每个人的密码
(3)开始“数N退出”游戏
(4)退出程序
(5)此操作可以反复进行,直到用户选择“退出程序”。

知识点分析

(1)进一步掌握结构体的声明,结构体变量的声明
(2)掌握结构体指针的声明和使用
(3)掌握使用结构体指针访问结构体成员和赋值
(4)掌握双链表的操作

算法思想

(1)创建循环链表算法思想

务必考虑到从空链表开始创建,并注意为每个新节点分配内存空间(malloc()函数),此外节点之间的“连接”并形成“环”是很重要的操作。详细实现请参见InputPersons()函数及相关解释。

(2)循环链表遍历算法思想

实际上就是依次读取链表节点的信息并显示,核心思想在于指针的移动。具体操作是:让一个临时指针p指向头节点head,然后读取该节点信息并显示;接下来向下一个节点移动(p=p->next),继续读取和显示操作;继续往后移动指针,直到完成一个循环(p再次指向head)。详细实现请参见DisplayLink()函数及相关解释。

(3)节点的修改算法

在遍历链表过程中,对每一个节点信息进行修改,也就是对指针所指向的结构体的成员进行修改。比如:要修改当前指针p所指向的结构体的密码pwd为当前用户的输入,语句如下:

scanf(“%d”,&p->pwd);

详细实现请参见ResetPwd()函数及相关解释。

(4)约瑟夫环算法思想

利用循环链表,根据要出列密码进行数数,遍历每个节点,找到节点并出队;将出列者的密码作为下一个出列的依据,继续循环寻找下一个节点,如此反复,直到最后一个节点。

详细算法思想及实现请参见CountAndQuit()函数及相关解释。

(5)节点的删除。

首先找到需要删除的节点,该过程同样需要遍历链表,然后找到被删除的节点,删除节点。函数free()完成节点的释放

系统流程图

在这里插入图片描述

图1约瑟夫问题的流程图

项目实现

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

项目小结

学习双链表的时候还是遇到了问题,就是前段时间刚刚学习了单向链表我写的关于学习单向链表的心得,单向链表其实比较简单,也是很常用的一种数据结构,单向链表相较于数组来说的话它的增加和删除都比较方便,但是单向链表在删除节点的时候吧总是得先获取要删除的节点的前一个节点,所以我们这里学习双向链表,使得在对链表进行增删改查时更加方便。

参考文献

《C语言综合项目实战》 科学出版社。

约瑟夫代码:

#define LEN 21
#define OVER "exit"

typedef struct ps Person;
struct ps {
   char name[LEN];
   int pwd;
   struct ps* next;
};


void InputPersons();   //输入操作,初始化链表
void ShowMenu();       //显示操作菜单
void DisplayLink();    //显示链表信息
void ResetPwd();       //重输密码
void CountAndQuit();   //开始约瑟夫游戏


Person* head = NULL;   //指向表节点的指针
Person* tail = NULL;   //指向末节点的指针
char choi = 0;           //用户选择的菜单项
int num1 = 0;



//约瑟夫问题
void yuesefulb()
{
   InputPersons();
   while (true) 
   {
   	ShowMenu();
   	switch (choi)
   	{
   	case '1':
   		DisplayLink();     //显示链表信息
   		break;
   	case '2':
   		ResetPwd();        //重输密码
   		break;
   	case '3':
   		CountAndQuit();    //开始约瑟夫游戏
   		break;
   	default:
   		printf("输入错误,请重新输入\n");
   		break;
   	}
   }

}

//输出操作,初始化链表
void InputPersons()
{
   char name[LEN];
   Person* p;        //新增加的人
   head = tail;      //一开始,首尾都是NULL
   while (1)
   {
   	printf("请输入第%d个人的信息,输入%s结束:\n", num1 + 1, OVER);
   	printf("人名<= %d字符:", LEN - 1);
   	scanf("%s", name);
   	name[LEN - 1] = '\0';
   	if (strcmp(name, OVER) == 0)
   		return;
   	p = (Person*)malloc(sizeof(Person));
   	strcpy(p->name, name);
   	//输入密码k
   	printf("密码(整数):");
   	scanf("%d", &p->pwd);
   	p->next = head;       //首尾相连,形成循环
   	//创建链表
   	if (NULL == head)
   	{
   		head = tail = p;
   		p->next = head;
   	}
   	else
   	{
   		tail->next = p;
   		tail = p;
   	}
   	num1++;       //新人添加到链表中以后,计数器才+1;
   	printf("\n");
   }
}


//显示操作菜单
void ShowMenu() {
   while (true) {
   	printf("\n\n请选择操作:");
   	printf("\n==============================");
   	printf("\n(1)显示信息");
   	printf("\n(2)重新输人每个人的密码");
   	printf("\n(3)开始“数N退出”游戏");
   	printf("\n(0)退出程序");
   	printf("\n========================\n"); choi = _getche();
   	if ('O' == choi)
   		exit(0);
   	else if ((choi - '0') >= 1 && (choi - '0') <= 3)return;

   }
}

//显示链表信息
void DisplayLink() {
   int i = 0;
   Person* p = head;
   //如果没有就终止
   if (NULL == p) return;
   printf("\n\n当前来链表信息如下:");
   //先输出头节点
   printf("\n%2d:\t%s\t%d", ++i, p->name, p->pwd);
   p = p->next;
   while (p != head) {
   	printf("\n%2d:\t%s\t%d", ++i, p->name, p->pwd);
   	p = p->next;
   }
   printf("===================================");
}

//重输密码
void ResetPwd() {
   Person* p = head; //从头节点开始
   //如果没有,就终止
   if (NULL == p)  return;
   printf("\n\n请重新输入每个人的密码:");
   printf("\n============================\n");
   //先处理第一个人的密码
   printf("%s", p->name);
   printf("%d", &p->pwd);
   p = p->next;
   while (p != head) {
   	printf("%s", p->name);
   	scanf("%d", &p->pwd);
   	p = p->next;
   }
   printf("\n===========================");
}

//开始约瑟夫游戏
void CountAndQuit() {
   int i, n, pwd;                                //用于计数和保存当前密码

   //先复制一个链表
   Person* p, * c, * q;                          //分别指向临时节点,新链表的当前节点,当前节点的前节点

   Person* head2 = NULL, * tail2 = NULL;			//分别指向新链表的头,尾节点
   c = head;     //首节点不能为NULL
   if (NULL == head)   return;
   //先复制首节点
   p = (Person*)malloc(sizeof(Person));
   strcpy(p->name, c->name);
   p->pwd = c->pwd;
   p->next = p;
   head2 = tail2 = p;                       //第一个节点首尾在一起
   c = c->next;
   //复制其他节点
   while (c != head) {
   	p = (Person*)malloc(sizeof(Person));
   	strcpy(p->name, c->name);
   	p->pwd = c->pwd;
   	p->next = head2;
   	tail2->next = p;
   	tail2 = p;
   	c = c->next;
   }
   //开始数N退出
   printf("\n\n游戏处理结果:");
   printf("\n==================================");
   i = 0;
   c = head2;               //记录当前指针指向首节点
   q = tail2;               //q记录当前节点前的一个节点
   pwd = c->pwd;                //记录首节点的pwd
   while (c->next != c) {
   	n = 1;
   	while (n < pwd) {
   		q = c;          //记录下前节点的前一个节点
   		c = c->next;
   		n++;
   	}
   	//当前指针c所指的应该退出
   	q->next = c->next;
   	printf("\n第%2d个退出者:%s", ++i, c->name);
   	pwd = c->pwd;          //退出者的密码作为下一个退出的依据
   	free(c);
   	c = q->next;

   }
   //退出最后一个
   printf("\n第%2d个退出者:%s", ++i, c->name);
   free(c);
   printf("\n==============================");
}

贪吃蛇链表问题

项目功能需求

贪吃蛇游戏要求:在一个密闭的围墙空间内有一条蛇,和随机出现的一个食物,通过按WSAD键控制蛇向上下左右四个方向移动。如果蛇头碰到食物,表示蛇吃掉食物,蛇的身体会增长一节;接着又出现新的食物,等待被蛇吃掉。游戏进行的过程中,蛇身会变的越来越长。如果蛇在移动过程中,蛇头碰到自己的身体咬到自己,则游戏结束。

要求:
(1)实现蛇的表示:用矩形块表示蛇的一节身体。游戏初始,蛇只有蛇头,用白色矩形块表示;当吃掉一个食物后,蛇身增加一节,即增加一个黑色的矩形块。

(2)实现蛇的移动:必须沿蛇头方向在密闭围墙内上下左右移动;如果游戏者无按键动作,蛇自行沿当前方向前移;当游戏者按了WSAD键后,需要重新确定蛇每节的位置,然后移动。

(3)实现根据坐标绘制蛇、食物;食物要求在密闭围墙中随机生成。

(4)处理上下左右的方向按键。

(5)实现双向链表表示,和单向链表的操作。

知识点分析

(1)进一步掌握结构体的声明,结构体变量的声明
(2)掌握结构体指针的声明和使用
(3)掌握使用结构体指针访问结构体成员和赋值
(4)掌握双链表的操作

算法思想

(1)创建循环链表算法思想

务必考虑到从空链表开始创建,并注意为每个新节点分配内存空间(malloc()函数),此外节点之间的“连接”并形成“环”是很重要的操作。详细实现请参见InputPersons()函数及相关解释。

(2)循环链表遍历算法思想

实际上就是依次读取链表节点的信息并显示,核心思想在于指针的移动。具体操作是:让一个临时指针p指向头节点head,然后读取该节点信息并显示;接下来向下一个节点移动(p=p->next),继续读取和显示操作;继续往后移动指针,直到完成一个循环(p再次指向head)。

系统流程图

在这里插入图片描述

图1贪吃蛇问题的流程图

项目实现

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

项目小结

学习双链表的时候还是遇到了问题,就是前段时间刚刚学习了单向链表我写的关于学习单向链表的心得,单向链表其实比较简单,也是很常用的一种数据结构,单向链表相较于数组来说的话它的增加和删除都比较方便,但是单向链表在删除节点的时候吧总是得先获取要删除的节点的前一个节点,所以我们这里学习双向链表,使得在对链表进行增删改查时更加方便。

参考文献

《C语言综合项目实战》 科学出版社。

贪吃蛇代码

#define frame_height 30//地图尺寸
#define frame_width 50
#define UP 'w'//移动
#define DOWN 's'
#define LEFT 'a'
#define RIGHT 'd'

int v, j, k, sp, score;
char ch = UP, state = UP, choo, n;//初始化方向
int grow = 0;

struct Food//食物
{
   int x;//横坐标
   int y;//纵坐标
} food;

struct Snake//蛇
{
   /*用数组储存蛇的每一部分的坐标*/
   int x[100];
   int y[100];
   int len;//长度
   int speed;//速度
} snake;

void map(void);//地图
void update_food(void);//更新食物
void move_snake(void);//蛇的移动
int alive(void);//判断蛇是否死亡
void get_speed(void);//更新速度
void gotoxy(int x, int y);//移动光标,进行游戏界面的打印


int tanchishelb()
{
   do
   {
   	score = 0;//初始化分数为0
   	/*让用户进行难度选择,有彩蛋*/
   	printf("Choose the degree of difficulty:\n1:easy\t2:middle  3:difficult\n");
   	n = _getch();
   	switch (n)
   	{
   	case '1':
   	{
   		sp = 300;
   		break;
   	}
   	case '2':
   	{
   		sp = 230;
   		break;
   	}
   	case '3':
   	{
   		sp = 180;
   		break;
   	}
   	default:
   	{
   		printf("Congratulations!Welcome to Devil's difficulty\n");
   		sp = 120;
   		break;
   	}
   	}
   	system("cls");//每次新一局游戏先清屏,包含在<stdlib.h>
   	map();//打印地图
   	/*开始游戏*/
   	while (1)
   	{
   		update_food();//生产食物
   		get_speed();//获取速度
   		move_snake();//移动
   		Sleep(snake.speed);//延时函数,speed数值越大延时越长
   		if (!(alive()))//判断蛇是否死亡
   		{
   			break;//死亡则退出循环
   		}
   	}
   	printf("Game Over!\n");
   	printf("1:Restart\t2:exit");
   	choo = _getch();
   } while (choo == '1');
   return 0;
}

void map()
{
   srand(time(NULL));
   /*打印第一个食物*/
   /*Attention!此处留了一个bug:可能食物
   的位置与初始的蛇重合,然后食物就会消
   失,读者可以加以改进*/
   food.x = rand() % (frame_height - 2) + 1;
   food.y = rand() % (frame_width - 2) + 1;//在框内
   gotoxy(food.x, food.y);//把光标移动到该坐标
   printf("$");//打印食物

   /*snake的初始化*/
   snake.x[0] = frame_height / 2;
   snake.y[0] = frame_width / 2;
   gotoxy(snake.x[0], snake.y[0]);
   printf("@");
   snake.len = 3;
   snake.speed = 200;
   for (k = 1; k < snake.len; k++)
   {
   	snake.x[k] = snake.x[k - 1] + 1;
   	snake.y[k] = snake.y[k - 1];
   	gotoxy(snake.x[k], snake.y[k]);
   	printf("@");
   }
   /*墙壁*/
   for (j = 0; j < frame_width; j++)
   {
   	gotoxy(0, j);
   	printf("#");
   	gotoxy(frame_height - 1, j);
   	printf("#");
   }
   for (v = 0; v < frame_height - 1; v++)
   {
   	gotoxy(v, 0);
   	printf("#");
   	gotoxy(v, frame_width - 1);
   	printf("#");
   }
   gotoxy(2, frame_width + 3);
   if (n == '1') printf("Difficulty:   easy");
   else if (n == '2') printf("Difficulty:   middle");
   else if (n == '3') printf("Difficulty:   difficult");
   else printf("Welcome to the Devil's difficulty");
   gotoxy(4, frame_width + 3);
   printf("UP:   w");
   gotoxy(6, frame_width + 3);
   printf("DOWN: s");
   gotoxy(8, frame_width + 3);
   printf("LEFT: a");
   gotoxy(10, frame_width + 3);
   printf("RIGHT:d");
   gotoxy(12, frame_width + 3);
   printf("Your score:%d", score);
   gotoxy(28, frame_width + 3);
   printf("Made by Zhao Hejie");
}
/*食物*/
void update_food()
{
   if (snake.x[0] == food.x && snake.y[0] == food.y)//吃到食物
   {
   	score += 10;
   	gotoxy(12, frame_width + 3);
   	printf("Your score:%d", score);
   	srand(time(NULL));
   	/*以下是更新食物的代码,里面排除了
   	食物与蛇重合的情况,读者可以参考以
   	下代码完成对上述bug的改进*/
   	int flag = 1;//标记变量
   	do
   	{
   		food.x = rand() % (frame_height - 2) + 1;
   		food.y = rand() % (frame_width - 2) + 1;//在框内
   		for (v = 0; v < snake.len; v++)
   		{
   			if (food.x == snake.x[v] && food.y == snake.y[v])
   			{
   				flag = 0;//有重合
   				break;
   			}
   		}
   	} while (flag == 0);
   	/*打印食物*/
   	gotoxy(food.x, food.y);
   	printf("$");
   	snake.len++;
   	grow = 1;//表明长了,在move_snake函数中有用到
   }
}
/*移动蛇*/
void move_snake()
{
   while (_kbhit())//键盘有输入
   {
   	ch = _getch();
   }
   if (!grow)//没有长
   {
   	gotoxy(snake.x[snake.len - 1], snake.y[snake.len - 1]);
   	printf(" ");//走了,在数组的最后打印空格,清除原有的蛇尾
   }
   for (k = snake.len - 1; k > 0; k--)//更新蛇的坐标,除了蛇头,其余位置继承上一个点的坐标
   {
   	snake.x[k] = snake.x[k - 1];
   	snake.y[k] = snake.y[k - 1];//移动位置
   }
   switch (ch)//改变方向
   {
   case UP:
   {
   	if (state == DOWN)//如果此时方向向下,输入向上的作用要被无视
   	{
   		snake.x[0]++;
   		break;
   	}
   	else
   	{
   		snake.x[0]--;
   		state = UP;//其余的改变状态为向上
   		break;
   	}
   }
   case DOWN:
   {
   	if (state == UP)
   	{
   		snake.x[0]--;
   		break;
   	}
   	else
   	{
   		snake.x[0]++;
   		state = DOWN;
   		break;
   	}
   }
   case LEFT:
   {
   	if (state == RIGHT)
   	{
   		snake.y[0]++;
   		break;
   	}
   	else
   	{
   		snake.y[0]--;
   		state = LEFT;
   		break;
   	}
   }
   case RIGHT:
   {
   	if (state == LEFT)
   	{
   		snake.y[0]--;
   		break;
   	}
   	else
   	{
   		snake.y[0]++;
   		state = RIGHT;
   		break;
   	}
   }
   /*摁其余键,保持原有状态*/
   default:
   {
   	if (state == DOWN)
   	{
   		snake.x[0]++;
   		break;
   	}
   	else if (state == UP)
   	{
   		snake.x[0]--;
   		break;
   	}
   	else if (state == LEFT)
   	{
   		snake.y[0]--;
   		break;
   	}
   	else if (state == RIGHT)
   	{
   		snake.y[0]++;
   		break;
   	}

   }
   }
   gotoxy(snake.x[0], snake.y[0]);
   printf("@");//打印蛇头
   grow = 0;//初始成长状态为0
   gotoxy(frame_height, 0);//光标移动到地图左下角下方
}
/*存活状态*/
int alive(void)
{
   if (snake.x[0] == 0 || snake.x[0] == frame_height - 1 || snake.y[0] == 0 || snake.y[0] == frame_width - 1)//撞墙
   	return 0;
   for (k = 1; k < snake.len; k++) //咬到自己
   {
   	if (snake.x[0] == snake.x[k] && snake.y[0] == snake.y[k])
   		return 0;
   }
   return 1;
}
/*加速*/
/*speed越大,蛇的速度越小*/
void get_speed()
{
   if (snake.len <= 6)
   	snake.speed = sp;
   else if (snake.len <= 10)
   	snake.speed = sp - 20;
   else if (snake.len <= 20)
   	snake.speed = sp - 50;
   else if (snake.len <= 30)
   	snake.speed = sp - 60;
   else
   	snake.speed = sp - 70;
}
/*移动光标*/
void gotoxy(int x, int y)
{
   HANDLE hout;
   COORD cor;
   /*
   typedef struct _COORD
   {
   	SHORT X; // horizontal coordinate
   	SHORT Y; // vertical coordinate
   } COORD;
   用该结构体来储存坐标
   */
   hout = GetStdHandle(STD_OUTPUT_HANDLE);//从标准输出设备中取得一个句柄
   /*这其中x,y的赋值对象要注意*/
   cor.X = y;
   cor.Y = x;
   SetConsoleCursorPosition(hout, cor);//定位光标的函数
}

银行排号系统

项目功能需求

银行大厅办理业务时,客户需要根据先后次序领取排号单,然后邓丹广播里喊到自己的号码,才办理业务.本程序模拟此排号流程,要求如下:
1.操作人员通过键盘输入每个各户的ID号(假设ID号不超过10个字符且唯一),即输入时不考虑重复问题),表示有一个客户排队
2.可以输入任意个客户ID号,并且可以随时停止输入
3.停止输入后,安排对顺序依次输出现有客户的ID信息

知识点分析

(1)进一步掌握结构体的声明,结构体变量的声明
(2)掌握结构体指针的声明和使用
(3)掌握使用结构体指针访问结构体成员和赋值

算法思想

(1)创建循环链表算法思想

务必考虑到从空链表开始创建,并注意为每个新节点分配内存空间(malloc()函数),此外节点之间的“连接”并形成“环”是很重要的操作。详细实现请参见InputPersons()函数及相关解释。

(2)循环链表遍历算法思想

实际上就是依次读取链表节点的信息并显示,核心思想在于指针的移动。具体操作是:让一个临时指针p指向头节点head,然后读取该节点信息并显示;接下来向下一个节点移动(p=p->next),继续读取和显示操作;继续往后移动指针,直到完成一个循环(p再次指向head)。

(3)节点的删除。

首先找到需要删除的节点,该过程同样需要遍历链表,然后找到被删除的节点,删除节点。函数free()完成节点的释放

系统流程图

在这里插入图片描述

图1银行管理系统问题的流程图

项目实现

在这里插入图片描述
在这里插入图片描述

项目小结

学习双链表的时候还是遇到了问题,就是前段时间刚刚学习了单向链表我写的关于学习单向链表的心得,单向链表其实比较简单,也是很常用的一种数据结构,单向链表相较于数组来说的话它的增加和删除都比较方便,但是单向链表在删除节点的时候吧总是得先获取要删除的节点的前一个节点,所以我们这里学习双向链表,使得在对链表进行增删改查时更加方便。

参考文献

《C语言综合项目实战》 科学出版社。

银行排号系统代码

person* head2;                    //链表头指针 
person* body;					 //用于创建新链表
person* last;					 //指向最后一个节点,以方便创建下一个节点
char option;
int j = 0;                           //链表计数
int n = 1;								 //用于呼叫定位

void bankAappointmentAsystem()
{                 //排号系统菜单
   do {
   	printf("\n");
   	printf("\t\t\t银行排号系统\n");
   	printf("\t\t*********************************\n");
   	printf("\t\t*\t1.查看排号列表\t\t*\n");
   	printf("\t\t*\t2.输入客户信息\t\t*\n");
   	printf("\t\t*\t3.删除客户信息\t\t*\n");
   	printf("\t\t*\t4.修改客户信息\t\t*\n");
   	printf("\t\t*\t5.呼叫客户\t\t*\n");
   	printf("\t\t*\t0.退出系統\t\t*\n");
   	printf("\t\t*********************************\n");
   	printf("\t\t请选择:");
   	getchar();
   	scanf("%c", &option);
   	switch (option) {
   	case '1':
   		checkList();                 //查看排号列表
   		option = 0;
   		break;
   	case '2':
   		if (addMessage())             //输入用户
   			printf("\t\t添加成功\n\n\n");
   		else
   			printf("\t\t添加失败\n\n\n");
   		option = 0;
   		break;
   	case '3':                           //删除用户
   		if (deleMessage())
   			printf("\t\t删除成功\n\n\n");
   		else
   			printf("\t\t删除失败\n\n\n");
   		option = 0;
   		break;
   	case '4':                           //更改信息
   		if (alterMessage())
   			printf("\t\t更改成功\n\n\n");

   		else
   			printf("\t\t删除失败\n\n\n");
   		option = 0;
   		break;
   	case '5':                           //呼叫用户
   		call();
   		break;
   	case '0':
   		printf("\t\t谢谢你的使用!\n\n\n");
   		exit(0);
   		break;
   	default:
   		printf("\n\t\t选择出错!\n\n\n");
   		option = 0;
   		break;
   	}
   } while (true);
}


bool addMessage() {     //输入客户信息
   char id[100], Name[100];
   if (head2 == NULL) {
   	// 创建第一个节点,链表的头指针为head2
   	head2 = (person*)malloc(sizeof(person));
   	printf("\n\t\t请输入客户信息:\n");
   	printf("\t\tId(不超过10个字符):");
   	scanf("%s", id);
   	printf("\t\t姓名(不超过5个字符):");
   	scanf("%s", Name);
   	if (strlen(id) <= IdLEN && strlen(Name) <= nameLEN) {     //判断输入大小是否过界
   		strcpy(head2->Id, id);
   		strcpy(head2->name, Name);
   	}
   	else {
   		printf("\t\t输入字符过长\n");
   		return false;
   	}
   	head2->next = NULL;
   	head2->prior = NULL;
   	head2->numeral = 1;
   	last = head2;
   	j++;
   	return true;
   }
   else {
   	body = (person*)malloc(sizeof(person));
   	printf("\n\t\t请输入客户信息:\n");			 //初始化用户信息
   	printf("\t\tId(不超过10个字符):");
   	scanf("%s", id);
   	printf("\t\t姓名(不超过5个字符):");
   	scanf("%s", Name);
   	if (strlen(id) <= IdLEN && strlen(Name) <= nameLEN) {
   		strcpy(body->Id, id);
   		strcpy(body->name, Name);
   	}
   	else {
   		printf("\t\t输入字符过长\n");
   		return false;
   	}
   	if (judge(body->Id)) {
   		body->numeral = last->numeral + 1;        //当前序列的排号是上一个排号加一
   		last->next = body;                        //上一个节点的next指向下一个节点
   		body->prior = last;
   		body->next = NULL;                        //使最后当前节点一个节点指向空
   		last = body;                              //last始终指向最后一个节点以方便创建链表
   		j++;
   		return true;
   	}
   	else {
   		printf("\t\t输入Id错误\n");
   		return false;
   	}
   }
   return false;
}

void checkList() {       //查看客户队列
   person* check = head2;
   printf("\t\t序号\t姓名\t\tId\n");
   while (check != NULL) {
   	printf("\t\t%-8d%-16s%s\n", check->numeral, check->name, check->Id);
   	check = check->next;
   }

}

bool deleMessage() {							//删除队列
   int i = 0;
   person* check = head2;
   printf("\n\n\t\t请输入要删除的序号:");
   scanf("%d", &i);
   if (i > 1 && i < j) {
   	while (check->numeral != i) check = check->next;				//定位到序号所在的链表
   	(check->prior)->next = check->next;     //使上一个链表next指向下一个链表
   	(check->next)->prior = check->prior;    //使下一个链表prior指向上一个链表
   	sort(j);
   	free(check);
   	return true;
   }
   else if (i == 1) {
   	sort(j);
   	free(check);
   	return true;
   }
   else if (i == j) {
   	while (check->numeral != i) check = check->next;	//定位到序号所在的链表
   	(check->prior)->next = NULL;			            //使上一个链表next指向空
   	sort(j);
   	free(check);
   	return true;
   }
   return false;

}

bool alterMessage() {     //修改
   int num;
   char id[IdLEN], Name[nameLEN];
   person* check = head2;
   printf("\t\t请输入你要修改的序号");
   scanf("%d", &num);
   if (num > j) {
   	printf("\t\t输入序号过大\n");
   	return false;
   }
   while (check->numeral != num) check = check->next;	//定位到序号所在的链表
   printf("\t\t输入用户Id:");
   scanf("%s", id);
   printf("\n\t\t输入用户姓名:");
   scanf("%s", Name);
   if (strlen(id) > IdLEN || strlen(Name) > nameLEN) {
   	printf("\t\t输入字符过长\n");
   	return false;
   }
   if (judge(id)) {
   	strcpy(check->name, Name);
   	strcpy(check->Id, id);
   	return true;
   }
   return false;
}

void call() {                                           //呼叫用户
   person* check = head2;
   if (n > j) {
   	printf("\t\t已呼叫所有用户\n\n");
   	return;
   }
   for (int i = 1; i <= n; i++) {
   	if (i == n) {
   		printf("\t\t请%d号%s到前台办理业务", check->numeral, check->name);
   		n++;
   		return;
   	}
   	check = check->next;
   }
}

void sort(int j) {           //整理节点序号
   j--;
   int i = 0;
   person* check = head2;
   for (int k = 0; i < j; k++) {
   	check->numeral = ++i;
   	check = check->next;
   }
}

bool judge(char id[]) {      //判断输入Id是否与之前输入的Id是否相同
   person* check = head2;
   for (int i = 0; i < j; i++) {
   	if (strcmp(id, check->Id) == 0)
   		return false;
   	check = check->next;
   }
   return true;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值