数据结构精录&总结Episode.2 数据结构入门之线性表详解(基于Visual C++)

祖安业余程序员又更新了我的精录总结系列!这一次的干货比较多,所有知识点我都一并总结在代码里了,设计代码编写&亲测Visual Studio 2019均可完美运行。

新文章的导火索来源于北理徐院C++及数据结构课程,最近迎来了疫情期间在家学习的第一次大作业,在小组成员@Wang@Zeng@Song@Wang同学的共同商议下,我们决定完成贪吃蛇研究学习程序的编写。贪吃蛇是一个十分适合C++面向对象以及数据结构初学者学习、练习的游戏开发实例,其中控制台版C++贪吃蛇蛇身的创建就是利用了最简单的链表完成。以下除了总结线性表的所有完整知识以外,我还附上我们小组的贪吃蛇程序开发思路分配以及开发详解。

文末为北理乐学数据结构线性表相关问题的代码解答。


线性表总结&基础代码详析:

感谢@LYYYM的分享,参考了许多C++与数据结构的书记后,加上我的总结,线性表的主要知识可以囊括以下几点:

线性表总结思维导图

其中,可以用详细的代码表示三大基本线性表顺序表、单链表和双链表的基础操作。循环链表由于只需要添加尾节点到头节点指针的操作,故此从略。总结及分析都已注释在代码中:

// 第二章 线性表.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。编写:JoeyBG,总结&编写&优化代码不易,白嫖不忘点赞哦~
//

#include <iostream>
#include<iomanip>
#include<string.h>
#include<math.h>
using namespace std;

#define Maxsize 100//定义最大表存储长度,之后的代码都可以用上

// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单

// 入门使用技巧: 
//   1. 使用解决方案资源管理器窗口添加/管理文件
//   2. 使用团队资源管理器窗口连接到源代码管理
//   3. 使用输出窗口查看生成输出和其他消息
//   4. 使用错误列表窗口查看错误
//   5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
//   6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件

/*
线性表基础知识点:

线性表由一组数据元素构成,数据元素的位置只取决于自己的序号,元素之间的相对位置是线性的。
在复杂线性表中,由若干项数据元素组成的数据元素称为记录,而由多个记录构成的线性表又称为文件。
非空线性表的结构特征:
(1)且只有一个根结点a1,它无前件;
(2)有且只有一个终端结点an,它无后件;
(3)除根结点与终端结点外,其他所有结点有且只有一个前件,也有且只有一个后件。结点个数n称为线性表的长度,当n=0时,称为空表。
线性表的顺序存储结构具有以下两个基本特点:
(1)线性表中所有元素的所占的存储空间是连续的;
(2)线性表中各数据元素在存储空间中是按逻辑顺序依次存放的。
ai的存储地址为:adr(ai)=adr(a1)+(i-1)k,,adr(a1)为第一个元素的地址,k代表每个元素占的字节数。
顺序表的运算:插入、删除。
*/

/*
线性链表知识点:

数据结构中的每一个结点对应于一个存储单元,这种存储单元称为存储结点,简称结点。
结点由两部分组成:(1)用于存储数据元素值,称为数据域;(2)用于存放指针,称为指针域,用于指向前一个或后一个结点。
在链式存储结构中,存储数据结构的存储空间可以不连续,各数据结点的存储顺序与数据元素之间的逻辑关系可以不一致,而数据元素之间的逻辑关系是由指针域来确定的。
链式存储方式即可用于表示线性结构,也可用于表示非线性结构。
线性链表,head称为头指针,head=null(或0)称为空表,如果是两指针:左指针(llink)指向前件结点,右指针(rlink)指向后件结点。
线性链表的基本运算:查找、插入、删除。
*/

/*
单链表定义:

指针域中存储的信息称做指针或链。N个结点链结成一个链表,由于此链表的每一个结点中包含一个指针域,故又称线性链表或单链表。
*/

/*
循环链表定义:

循环链表是单链表的变形。循环链表最后一个结点的next指针不为空,而是指向了表的前端。为简化操作,在循环链表中往往加入表头结点。
循环链表的特点是:只要知道表中某一结点的地址,就可搜寻到所有其他结点的地址。
*/

/*
双向链表的定义:

双向链表是指在前驱和后继方向都能游历(遍历)的线性链表。
在双向链表结构中,每一个结点除了数据域外,还包括两个指针域,一个指针指向该结点的后继结点,另一个指针指向它的前趋结点。通常采用带表头结点的循环链表形式。
*/

/*
指针与线性表:

用数组实现表时,利用了数组单元在物理位置上的邻接关系表示表元素之间的逻辑关系。
优点是:
无须为表示表元素之间的逻辑关系增加额外的存储空间。
可以方便地随机存取表中任一位置的元素。
缺点是:
插入和删除运算不方便,除表尾位置外,在表的其他位置上进行插入或删除操作都须移动大量元素,效率较低。
由于数组要求占用连续的存储空间,因此在分配数组空间时,只能预先估计表的大小再进行存储分配。当表长变化较大时,难以确定数组的合适的大小
*/

/*
顺序表与链表的重要分别:

顺序表的存储空间可以是静态分配的,也可以是动态分配的。链表的存储空间是动态分配的。顺序表可以随机或顺序存取。
链表只能顺序存取。顺序表进行插入/删除操作平均需要移动近一半元素。链表则修改指针不需要移动元素。
若插入/删除仅发生在表的两端,宜采用带尾指针的循环链表。存储密度=结点数据本身所占的存储量/结点结构所占的存储总量。顺序表的存储密度= 1,链表的存储密度< 1。
总结:顺序表是用数组实现的,链表是用指针实现的。用指针来实现的链表,结点空间是动态分配的,链表又按链接形式的不同,区分为单链表、双链表和循环链表。
*/


//顺序表的代码操作详解
//顺序表的优点:操作简单、存储密度高、可以随机存取、O(1)复杂度的高速取出元素的能力
//顺序表的缺点:首先预分配空间太大,最大空间数估计过大或者过小会造成空间浪费或者溢出,同时插入和删除的移动元素操作比较复杂麻烦
typedef struct
{
	int* elem;
	int length;
}SqList;//顺序表动态分配的结构体定义,存100号整形数据,数据基地址为elem,并记录顺序表长度信息为length

bool InitList(SqList& L)
{
	L.elem = new int[Maxsize];
	if (!L.elem)
	{
		return false;
	}
	L.length = 0;
	return true;
}//构造空的顺序表L并且进行初始化,同时可以检测建立是否成功,不成功返回false程序会被终止,保证了代码的稳定性

bool CreateList(SqList& L)
{
	int x = 0, i = 0;//x用来接受控制台输入的外部数据,并且负责写入L.elem,i用来进行顺序表项数的递增工作
	while (x != -1)
	{
		if (L.length = Maxsize)
		{
			cout << "顺序表已满!";
			return false;
		}//顺序表满了的时候,证明顺序表的表长和顺序表开辟的空间个数一致,不能继续存入更多数据了
		cin >> x;
		L.elem[i++] = x;
		L.length++;//每写入一项,数据项数+1,/n空符项数-1
	}
	return true;
}//顺序表的创建函数

bool GetElem(SqList L, int i, int& e)
{
	if (i < 1 || i > L.length)
	{
		return false;
	}
	e = L.elem[i - 1];
	return true;
}//顺序表中,一定是可以直接一步到位搜索出结果的,这里只需要要求用if语句验证i是否合法,算法时间复杂度为O(1)

int LocateElem(SqList L, int e)
{
	int i = 0;
	for (i = 0; i < L.length; i++)
	{
		if (L.elem[i] == e)
		{
			return (i + 1);//下标为i才是第i+1个元素
		}
	}
	return -1;//如果没有找到,那么返回-1
}//顺序表的查找,直接一一比对看数据是否相同方式实现,算法的时间复杂度O(n)

bool ListInsert_Sq(SqList& L, int i, int e)
{
	if (i<1 || i>L.length + 1)
	{
		return false;//i值不合法直接给退出
	}
	if (L.length == Maxsize)
	{
		return false;//存储空间已满直接给退出
	}
	for (int j = L.length - 1; j = i - 1; j--)
	{
		L.elem[j + 1] = L.elem[j];//从最后一个元素开始往后移,直到第i个元素及之后的全部给移动一项,空出那个需要插入的位置
	}
	L.elem[i - 1] = e;//e则放入空出来的第i位置
	L.length++;//插入元素后表长+1
	return true;
}//顺序表的插入操作,算法的时间复杂度为O(n)

bool ListDelete_Sq(SqList& L, int i, int& e)
{
	if (i<1 || i>L.length)
	{
		return false;//i值非法直接退出
	}
	e = L.elem[i - 1];//将e输入i-1的位置
	for (int j = i; j <= L.length - 1; j++)
	{
		L.elem[j - 1] = L.elem[j];//将删除后元素的从当前项全部往前挪一位
	}
	L.length--;//表长-1
	return true;
}//顺序表的删除节点操作,这里的一个不足是必须保证操作者键入的i和e数值都是正确的!否则将会出现错误数据覆盖正确数据删除的模式

void print(SqList L)
{
	cout << "输出顺序表" << endl;
	for (int j = 0; j <= L.length - 1; j++)
		cout << L.elem[j] << "   ";
	cout << endl;
}//直接输出+空格分隔的方式将顺序表打印在控制台上,算法时间复杂度O(n)

void DestroyList(SqList& L)
{
	if (L.elem) delete[]L.elem;    //释放存储空间
}//清除工作,可作为顺序表类中的析构函数存在,这里我们一直是面向过程的解释+学习,故单独列出


//单链表的基本操作代码详解
/*
链表优点和缺点如下:
优点:在插入和删除操作时,只需要修改被删节点上一节点的链接地址,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量来元素的缺点。
缺点:
1、没有解决连续存储分配带来的表长难以确定的问题。
2、失去了顺序存储结构随机存取的特性。
*/
typedef struct LNode
{
	int data;
	struct LNode* next;//指向下一个节点的指针
}LNode,*LinkList;//单链表节点的结构体定义,LinkList为单链表,Lnode为过度的中间节点

bool InitList_L(LinkList& L)
{
	L = new LNode;
	if (!L)
	{
		return false;
	}
	L->next = NULL;
	return true;
}//单链表的初始化函数,初始化失败则会返回0从而退出,初始化成功则将创建节点指向NULL

void CreateList_H(LinkList& L)
{
	int n;
	LinkList s;
	L = new LNode;
	L->next = NULL;
	cout << "请输入元素的个数n:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "头插法创建单链表......." << endl;
while (n--)
{
	s = new LNode;
	cin >> s->data;
	s->next = L->next;
	L->next = s;//生成新节点,输入元素赋值给新节点的数值域,同时将新节点s插入头节点之后
}
}//头插法创建链表的函数,创建成功的链表将会是我们键入数据的反序链表

void CreateList_R(LinkList& L)
{
	int n;
	LinkList s, r;
	L = new LNode;
	L->next = NULL;
	r = L;//初始化r尾节点就是空链表的头节点
	cout << "请输入元素的个数n:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "尾插法创建单链表......." << endl;
	while (n--)
	{
		s = new LNode;
		cin >> s->data;
		s->next = NULL;
		r->next = s;
		r = s;//生成新节点,将键入数据存进新节点的数值域,将新节点s插入尾节点r之后,再将r指向新的尾节点s
	}
}//尾插法创建链表的函数,创建成功的链表将会是我们键入数据的正序链表

bool GetElem_L(LinkList L, int i, int& e)
{
	//在带头节点的单链表L中查找第i个元素,用e记录L中第i个元素的值
	int j;
	LinkList p;
	p = L->next;
	j = 1;//j是一个计数器,并没有存取功能
	while (j < i && p)
	{
		p = p->next;
		j++;
	}
	if (!p || j > i)
	{
		return false;
	}
	e = p->data;
	return true;
}//单链表的取值函数,通过顺着链表向后扫描,直到p指向第i个元素或者p为空结束。前提是需要先判断i是否合法,如果不合法程序直接退出,算法时间复杂度为O(n)
//P.S.这里可以明显看出顺序表在取值方面效率的巨大优势

bool LocateElem_L(LinkList L, int e)
{
	LinkList p;
	p = L->next;
	while (p && p->data != e)
	{
		p = p->next;
	}//沿着节点扫描,直到p为空或者p的数据域和e匹配
	if (!p)
	{
		return false;
	}
	return true;
}//单链表的查找函数,如果查找失败,p为NULL,则返回false,算法的时间复杂度仍然是O(n)

bool ListInsert_L(LinkList& L, int i, int e)
{
	int j;
	LinkList p, s;
	p = L;
	j = 0;
	while (p && j < i - 1)
	{
		p = p->next;
		j++;
	}
	if (!p || j > i - 1)
	{
		return false;
	}
	s = new LNode;
	s->data = e;
	s->next = p->next;
	p->next = s;//生成新节点,将e放入新节点的数据域,将新节点的指针域指向第i个节点,将节点p的指针域最后又指回s从而完成了绕一圈插入的原理
	return true;
}//单链表插入元素的函数实现,算法思路是把i-1的next链接到i,然后再将i的next节点链接到原来i号节点,实现插入,算法的时间复杂度为O(1)

bool ListDelete_L(LinkList& L, int i)
{
	LinkList p, q;
	int j;
	p = L;
	j = 0;
	while ((p->next) && (j < i - 1))
	{
		p = p->next;
		j++;
	}
	if (!(p->next) || (j > i - 1))
	{
		return false;
	}
	q = p->next;
	p->next = q->next;
	delete q;//释放被删除的空间是有必要的
	return true;
}//单链表删除元素的函数实现,方法就是直接删除节点,将上一个节点的指针域直接连接到下一个节点即可,算法时间复杂度仍然是O(1)
//由此即可看出在插入删除的方面链表相对于顺序表效率的绝对优势

void Listprint_L(LinkList L) 
{
	LinkList p;
	p = L->next;
	while (p)
	{
		cout << p->data << "\t";
		p = p->next;
	}
	cout << endl;
}//单链表的输出,类同单链表的取值,只不过通过控制台打表输出而已,算法时间复杂度仍为O(n)


//双向链表的实现代码总结
/*
双链表虽然复杂,但是相对于单链表是具有优势的:

删除单链表中的某个结点时,一定要得到待删除结点的前驱,得到该前驱有两种方法,
第一种方法是在定位待删除结点的同时一路保存当前结点的前驱。
第二种方法是在定位到待删除结点之后,重新从单链表表头开始来定位前驱。
尽管通常会采用方法一,但其实这两种方法的效率是一样的,指针的总的移动操作都会有2*i次。而如果用双向链表,则不需要定位前驱结点。因此指针总的移动操作为i次。
*/
typedef struct DuLNode {
	int data; //结点的数据域
	struct DuLNode* prior, * next; //结点的指针域
}DuLNode, * DuLinkList; //双链表结构体创建,LinkList为指向结构体LNode的指针类型

bool InitDuList_L(DuLinkList& L)
{
	L = new DuLNode;     //生成新结点作为头结点,用头指针L指向头结点
	if (!L)
		return false;     //生成结点失败
	L->prior = L->next = NULL;   //头结点的两个指针域置空
	return true;
}//双链表的初始化函数实现

void CreateDuList_H(DuLinkList& L)
{
	//输入n个元素的值,建立到头结点的单链表L
	int n;
	DuLinkList s; //定义一个指针变量
	L = new DuLNode;
	L->prior = L->next = NULL; //先建立一个带头结点的空链表
	cout << "请输入元素个数n:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "前插法创建单链表..." << endl;
	while (n--)
	{
		s = new DuLNode; //生成新结点s
		cin >> s->data; //输入元素值赋给新结点的数据域
		if (L->next)
			L->next->prior = s;
		s->next = L->next;
		s->prior = L;
		L->next = s; //将新结点s插入到头结点之后
	}
}//前插法创建双向链表函数,同单链表一样,插入数据和控制台键入数据仍然是相反的,时间复杂度仍然是O(n)

bool GetElem_L(DuLinkList L, int i, int& e)
{
	//在带头结点的双向链表L中查找第i个元素
	//用e记录L中第i个数据元素的值
	int j;
	DuLinkList p;
	p = L->next;//p指向第一个结点,
	j = 1; //j为计数器
	while (j < i && p) //顺链域向后扫描,直到p指向第i个元素或p为空
	{
		p = p->next; //p指向下一个结点
		j++; //计数器j相应加1
	}
	if (!p || j > i)
		return false; //i值不合法i>n或i<=0
	e = p->data; //取第i个结点的数据域
	return true;
}//双向链表的取值函数,时间复杂度为O(n)

bool LocateElem_L(DuLinkList L, int e) 
{
	//在带头结点的双向链表L中查找值为e的元素
	DuLinkList p;
	p = L->next;
	while (p && p->data != e)//顺链域向后扫描,直到p为空或p所指结点的数据域等于e
		p = p->next; //p指向下一个结点
	if (!p)
		return false; //查找失败p为NULL
	return true;
}//双向链表的查找函数,时间复杂度为O(n)

bool ListInsert_L(DuLinkList& L, int i, int& e)
{
	//在带头结点的单链表L中第i个位置之前插入值为e的新结点
	int j;
	DuLinkList p, s;
	p = L;
	j = 0;
	while (p && j < i) //查找第i个结点,p指向该结点
	{
		p = p->next;
		j++;
	}
	if (!p || j > i)//i>n+1或者i<1
		return false;
	s = new DuLNode;     //生成新结点
	s->data = e;       //将新结点的数据域置为e
	p->prior->next = s;
	s->prior = p->prior;
	s->next = p;
	p->prior = s;
	return true;
}//双向链表的插入函数,实现复杂度为O(1)

bool ListDelete_L(DuLinkList& L, int i) //双向链表的删除
{
	//在带头结点的双向链表L中,删除第i个位置
	DuLinkList p;
	int j;
	p = L;
	j = 0;
	while (p && (j < i)) //查找第i个结点,p指向该结点
	{
		p = p->next;
		j++;
	}
	if (!p || (j > i))//当i>n或i<1时,删除位置不合理
		return false;
	if (p->next) //如果p的直接后继结点存在
		p->next->prior = p->prior;
	p->prior->next = p->next;
	delete p;        //释放被删除结点的空间
	return true;
}//双向链表的删除函数,实现复杂度为O(1)

void Listprint_L(DuLinkList L) 
{
	DuLinkList p;
	p = L->next;
	while (p)
	{
		cout << p->data << "\t";
		p = p->next;
	}
	cout << endl;
}//双链表的输出函数,这里没有怎么利用双链表双向同行的能力,同单链表相同,打表输出的操作如同直接遍历取值一样,时间复杂度为O(n)

//循环链表总结
//循环链表其实就是双链表or单链表,改动为尾节点不是指向NULL而是指向Head,从而转成圈。这有利于转圈循环筛选问题的解答,算法时间复杂度均易于计算,此略


//以下是主程序的实现方案
//三大表类型调用函数
int Dulmain()
{
	int i, x, e, choose;
	DuLinkList L;
	InitDuList_L(L);
	choose = -1;
	while (choose != 0)
	{
		cout << "1. 初始化\n";
		cout << "2. 创建双向链表(前插法)\n";
		cout << "3. 取值\n";
		cout << "4. 查找\n";
		cout << "5. 插入\n";
		cout << "6. 删除\n";
		cout << "7. 输出\n";
		cout << "0. 退出\n";
		cout << "请输入数字选择:";
		cin >> choose;
		switch (choose)
		{
		case 1: //初始化一个空的双向链表
			if (InitDuList_L(L))
				cout << "初始化一个空的双向链表!\n";
			break;
		case 2: //创建双向链表(前插法)
			CreateDuList_H(L);
			cout << "前插法创建双向链表输出结果:\n";
			Listprint_L(L);
			break;
		case 3: //双向链表的按序号取值
			cout << "请输入一个位置i用来取值:";
			cin >> i;
			if (GetElem_L(L, i, e))
			{
				cout << "查找成功\n";
				cout << "第" << i << "个元素是:" << e << endl;
			}
			else
				cout << "查找失败\n\n";
			break;
		case 4: //双向链表的按值查找
			cout << "请输入所要查找元素x:";
			cin >> x;
			if (LocateElem_L(L, x))
				cout << "查找成功\n";
			else
				cout << "查找失败! " << endl;
			break;
		case 5: //双向链表的插入
			cout << "请输入插入的位置和元素(用空格隔开):";
			cin >> i;
			cin >> x;
			if (ListInsert_L(L, i, x))
				cout << "插入成功.\n\n";
			else
				cout << "插入失败!\n\n";
			break;
		case 6: //双向链表的删除
			cout << "请输入所要删除的元素位置i:";
			cin >> i;
			if (ListDelete_L(L, i))
				cout << "删除成功!\n";
			else
				cout << "删除失败!\n";
			break;
		case 7: //双向链表的输出
			cout << "当前双向链表的数据元素分别为:\n";
			Listprint_L(L);
			cout << endl;
			break;
		}
	}
	return 0;
}//双向链表的调用函数

int Linkmain()
{
	int i, x, e, choose;
	LinkList L;
	InitList_L(L);
	cout << "1. 初始化\n";
	cout << "2. 创建单链表(前插法)\n";
	cout << "3. 创建单链表(尾插法)\n";
	cout << "4. 取值\n";
	cout << "5. 查找\n";
	cout << "6. 插入\n";
	cout << "7. 删除\n";
	cout << "8. 输出\n";
	cout << "0. 退出\n";
	choose = -1;
	while (choose != 0)
	{
		cout << "请输入数字选择:";
		cin >> choose;
		switch (choose)
		{
		case 1: //初始化一个空的单链表
			if (InitList_L(L))
				cout << "初始化一个空的单链表!\n";
			break;
		case 2: //创建单链表(前插法)
			CreateList_H(L);
			cout << "前插法创建单链表输出结果:\n";
			Listprint_L(L);
			break;
		case 3: //创建单链表(尾插法)
			CreateList_R(L);
			cout << "尾插法创建单链表输出结果:\n";
			Listprint_L(L);
			break;
		case 4: //单链表的按序号取值
			cout << "请输入一个位置i用来取值:";
			cin >> i;
			if (GetElem_L(L, i, e))
			{
				cout << "查找成功\n";
				cout << "第" << i << "个元素是:" << e << endl;
			}
			else
				cout << "查找失败\n\n";
			break;
		case 5: //单链表的按值查找
			cout << "请输入所要查找元素x:";
			cin >> x;
			if (LocateElem_L(L, x))
				cout << "查找成功\n";
			else
				cout << "查找失败! " << endl;
			break;
		case 6: //单链表的插入
			cout << "请输入插入的位置和元素(用空格隔开):";
			cin >> i;
			cin >> x;
			if (ListInsert_L(L, i, x))
				cout << "插入成功.\n\n";
			else
				cout << "插入失败!\n\n";
			break;
		case 7: //单链表的删除
			cout << "请输入所要删除的元素位置i:";
			cin >> i;
			if (ListDelete_L(L, i))
				cout << "删除成功!\n";
			else
				cout << "删除失败!\n";
			break;
		case 8: //单链表的输出
			cout << "当前单链表的数据元素分别为:\n";
			Listprint_L(L);
			cout << endl;
			break;
		}
	}
	return 0;
}//单链表的调用函数

int Sqmain()
{
	SqList myL;
	int i, e, x;
	cout << "1. 初始化\n";
	cout << "2. 创建\n";
	cout << "3. 取值\n";
	cout << "4. 查找\n";
	cout << "5. 插入\n";
	cout << "6. 删除\n";
	cout << "7. 输出\n";
	cout << "8. 销毁\n";
	cout << "0. 退出\n";
	int choose = -1;
	while (choose != 0)
	{
		cout << "请选择:";
		cin >> choose;
		switch (choose)
		{
		case 1://初始化顺序表
			cout << "顺序表初始化..." << endl;
			if (InitList(myL))
				cout << "顺序表初始化成功!" << endl;
			else
				cout << "顺序表初始化失败!" << endl;
			break;
		case 2://创建顺序表
			cout << "顺序表创建..." << endl;
			cout << "输入整型数,输入-1结束" << endl;
			if (CreateList(myL))
				cout << "顺序表创建成功!" << endl;
			else
				cout << "顺序表创建失败!" << endl;
			break;
		case 3://取值
			cout << "输入整型数i,取第i个元素输出" << endl;
			cin >> i;
			if (GetElem(myL, i, e))
				cout << "第i个元素是: " << e << endl;
			else
				cout << "顺序表取值失败!" << endl;;
			cout << "第i个元素是: " << e << endl;
			break;
		case 4://查找
			cout << "请输入要查找的数x:";
			cin >> x;
			if (LocateElem(myL, x) == -1)
				cout << "查找失败!" << endl;
			else
				cout << "查找成功!" << endl;
			break;
		case 5://插入
			cout << "请输入要插入的位置和要插入的数据元素e:";
			cin >> i >> e;
			if (ListInsert_Sq(myL, i, e))
				cout << "插入成功!" << endl;
			else
				cout << "插入失败!" << endl;
			break;
		case 6://删除
			cout << "请输入要删除的位置i:";
			cin >> i;
			if (ListDelete_Sq(myL, i, e))
				cout << " 删除成功!" << endl;
			else
				cout << "删除失败!" << endl;
			break;
		case 7://输出
			print(myL);
			break;
		case 8://销毁
			cout << "顺序表销毁..." << endl;
			DestroyList(myL);
			break;
		}
	}
	return 0;
}//顺序表的主要输出函数

int main()
{
	int judgenumber = 0;
	char judge = '\n';
	cout << "线性表学习笔记+总结代码本—编写&调试:JoeyBG,算法尚有大量不足之处,还望不吝指正!" << endl;
	cout << "------------------------------------------------------------------------------------" << endl;
	cout << endl;
labelstart:
	cout << "1、顺序表" << endl;
	cout << "2、单链表" << endl;
	cout << "3、双向链表" << endl;
	cout << "键入你需要的线性表类型数字:";
	cin >> judgenumber;
	if (judgenumber == 1)
	{
		system("cls");
		Sqmain();
	}
	else if (judgenumber == 2)
	{
		system("cls");
		Linkmain();
	}
	else if (judgenumber == 3)
	{
		system("cls");
		Dulmain();
	}
	else
	{
		cout << "键入数字有误!" << endl;
	}
	cout << "是否继续查看其它链表或者退出(Y/N)?";
	cin >> judge;
	if (judge == 'Y')
	{
		system("cls");
		goto labelstart;
	}
	else if (judge == 'N')
	{
		system("cls");
		exit(0);
	}
	else
	{
		cout << "键入字符有误,系统默认退出!" << endl;
		exit(0);
	}
	return 1;
}


/*
参考资料:
1、隆曦:双链表相比单链表的优点,https://blog.csdn.net/a_long_/article/details/50999805?utm_source=blogxgwz8
2、乐观的志6:链表存储的优缺点,https://zhidao.baidu.com/question/485716702.html
3、陈小玉:趣学数据结构,人民邮电出版社,2019.09
*/







基于基本线性表知识的贪吃蛇游戏构建:

首先,经过了线上会议的详细讨论,我们初步拟定贪吃蛇游戏的团队分配方案如下:

char direction_a, direction_b;			//方向a、b,用于方向的限制 
int scores, num, fool_x, fool_y, speed = 100;	//得分、num用于蛇身起步、食物x坐标、食物y坐标 
bool endmark;								//结束标记 

struct node 							//蛇身结点 
{
	int x, y;
	node* next;
}*head = NULL, * p, * tail;

void init();							//初始化开始界面 
void start();							//游戏开始入场 
void init_snake();						//初始化蛇身 
void delete_snake();					//删除蛇身 
void control();							//方向控制 
void move();							//蛇身移动 
void limit();							//方向限制
void panduan();							//配合limit限制方向 
void fool();							//食物的出现以及食物被吞
void isEnd();							//结束判断 
void zhuangwei();						//撞尾判断 
void zhuangqiang();						//撞墙判断 

其中前者为贪吃蛇蛇身的链表节点,这需要组员在分工合作时统一变量名。其余的变量名可由组员自行学习、完成,并由我最终汇总修正&调试。

此外,由于我需要完成汇总工作,因而首先给出初始化开始界面、游戏开始入场、初始化蛇身、删除蛇身的函数。由另外四位组员共同分工完成控制蛇移动的八个不同的操作函数。

void init()
{
	gotoxy(35, 8);
	cout << "★贪  吃  蛇★";
	gotoxy(36, 10);
	cout << "开始请输入y:";
}//开始界面
void start()
{
	for (int i = 0; i <= 79; i++)
	{
		Sleep(10);
		cout << "*";
		gotoxy(i + 1, 24);
		cout << "*";
		gotoxy(i + 2, 1);
	}
	gotoxy(1, 2);
	for (int i = 0; i <= 21; i++)
	{
		Sleep(20);
		cout << "*";
		for (int j = 0; j <= 77; j++)	cout << " ";
		cout << "*";
	}
}//创建游戏框图
void init_snake()
{
	int n = 3;
	head = new node;
	tail = head;
	head->x = 40;
	head->y = 12;
	while (n--)
	{
		p = new node;
		tail->next = p;
		p->x = tail->x - 1;
		p->y = tail->y;
		tail = p;
	}
	tail->next = NULL;
	node* q = head->next;
	gotoxy(head->x, head->y);
	cout << '#';
	while (q != NULL)
	{
		gotoxy(q->x, q->y);
		cout << '*';
		q = q->next;
	}
}//创建蛇对象
void delete_snake()
{
	while (head != NULL)
	{
		node* q = head;
		head = q->next;
		delete q;
	}
}//删除蛇对象

此处对部分函数及头文件给出解释:

①函数:其它编译器:kbhit();VC++6.0及Visual Studio编译器:_kbhit();用 法:int _kbhit(void);

功能及返回值: 检查当前是否有键盘输入,若有则返回一个非0值,否则返回0。C++语言包含头文件: include <conio.h>。C语言不需要额外头文件。

②头文件:gotoxy.h,用以实现整个图形界面的创建以及引入基本的x、y坐标到控制台显示界面上去。在编译器中新建一个C++头文件,键入以下代码,命名为gotoxy.h,保存到与主函数文件相同的目录下面去,然后在主程序cpp文件中运行即可:

//头文件gotoxy.h代码
#include <windows.h>
void gotoxy(int x, int y)
{
	COORD pos;
	pos.X = x - 1;
	pos.Y = y - 1;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

对于我引入的贪吃蛇游戏,我给出了一个主程序cpp初稿的完整代码版本,可供参考。此版本由于时间较早,可能有部分功能操作不同于研究性学习要求之处,可微调改正。

 


 在贪吃蛇游戏的最终版本中,小组成员结合了C++现在比较适合新手的图形绘制包EasyX,做出了一款有那么一点图形界面风格的贪吃蛇游戏,游戏设定蛇的运行速度为300ms每步,可在代码中进行更改。

P.S.重要操作!!!

https://easyx.cn/downloads/
前往以上EasyX官网下载graphics.h头文件的包,选择2020年3月15日的测试版,然后安装,选择自己对应的Visual Studio版本进行安装即可。
安装完成后重启Visual studio,重新打开本代码,其中的graphics.h包错误将会自动消失。
因为我也不是很熟悉这个EasyX的绘图包的内核,所以有些地方用的也很烂,但是至少不是黑框框了,等到本次课程答辩结束后有空继续研究。
 

//!!!!!!!重要操作,否则程序无法运行
/*
https://easyx.cn/downloads/
前往以上EasyX官网下载graphics.h头文件的包,选择2020年3月15日的测试版,然后安装,选择自己对应的Visual Studio版本进行安装即可。
安装完成后重启Visual studio,重新打开本代码,其中的graphics.h包错误将会自动消失。
因为我也不是很熟悉这个EasyX的绘图包的内核,所以有些地方用的也很烂,但是至少不是黑框框了,等到本次课程答辩结束后有空继续研究。
编写&调试—JoeyBG,不足之处敬请指正!
*/

// 第二章 线性表—实例 贪吃蛇游戏 终极版.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <stdlib.h>
#include <time.h>
#include <conio.h>
#include <graphics.h>
#define N 100
using namespace std;

// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单

// 入门使用技巧: 
//   1. 使用解决方案资源管理器窗口添加/管理文件
//   2. 使用团队资源管理器窗口连接到源代码管理
//   3. 使用输出窗口查看生成输出和其他消息
//   4. 使用错误列表窗口查看错误
//   5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
//   6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件

enum moved { UP, DOWN, LEFT, RIGHT };
class Snake {
private:
	struct {      //整条蛇的信息
		int x;
		int y;
	}snake[100];
	struct {
		int life;   //为1代表还活着,为0代表已经死了
		int length; //代表蛇的长度,初始值为3
		enum moved direction;   //前进方向
	}snake_head;
	struct {       //食物的信息
		int x;
		int y;
	}food;
public:
	void display();    //显示界面
	void initSnake(); //随机生成蛇
	void move();//蛇移动
	void boundary_check();//边界判断
	void _food();//生成食物
	int food_eatcheck();//检查是否吃到食物,吃到则返回1,否则返回0
	int snake_eat();//判断贪吃蛇是否咬到自己,咬到则返回1,否则返回0
	void run();     //主要运行函数
};
void Snake::display() {
	initgraph(800, 600);
	setbkcolor(WHITE);   //设置背景颜色为白色
	cleardevice();       //将背景颜色刷新到窗口上
	setfillcolor(BLACK); //设置填充的颜色为黑色,之后话填充的图形都是这个颜色
	solidrectangle(20, 560, 560, 20);//这个区域每20*20为一个单位,一共有27*27个单位
}
//构造函数
void Snake::initSnake() {
	srand((unsigned)time(NULL));
	//因为一开始蛇是向右走的,所以不能让蛇初始化在太靠右边的地方
	int x = rand() % 22 + 3;  //范围是3-24
	int y = rand() % 22 + 3;  //加三是因为初始的长度是3,必须让整条蛇都在范围内
	this->snake[0].x = x * 20 + 10;//加十是因为要确定圆心的位置
	this->snake[0].y = y * 20 + 10;
	//默认蛇一开始是横着的所以三段的y坐标相同
	this->snake[1].y = this->snake[2].y = this->snake[0].y;
	this->snake[1].x = this->snake[0].x - 20;
	this->snake[2].x = this->snake[0].x - 40;
	setfillcolor(GREEN);   //设置填充色为蓝色
	solidcircle(this->snake[0].x, this->snake[0].y, 10); //画圆
	solidcircle(this->snake[1].x, this->snake[1].y, 10);
	solidcircle(this->snake[2].x, this->snake[2].y, 10);
	this->snake_head.length = 3;
	this->snake_head.life = 1;
	this->snake_head.direction = RIGHT;
}
void Snake::move() {
	char ch;
	if (_kbhit()) {   //如果有输入的话就返回1,没有输入的话就返回0
		ch = _getch();//获取输入的字符
		switch (ch) {
		case 'w':if (this->snake_head.direction != DOWN) this->snake_head.direction = UP; break;
		case 'W':if (this->snake_head.direction != DOWN) this->snake_head.direction = UP; break;
		case 's':if (this->snake_head.direction != UP) this->snake_head.direction = DOWN; break;
		case 'S':if (this->snake_head.direction != UP) this->snake_head.direction = DOWN; break;
		case 'a':if (this->snake_head.direction != RIGHT) this->snake_head.direction = LEFT; break;
		case 'A':if (this->snake_head.direction != RIGHT) this->snake_head.direction = LEFT; break;
		case 'd':if (this->snake_head.direction != LEFT) this->snake_head.direction = RIGHT; break;
		case 'D':if (this->snake_head.direction != LEFT) this->snake_head.direction = RIGHT; break;
		default:break;
		}
	}
	//将蛇尾变成黑色
	int i = this->snake_head.length - 1;
	setfillcolor(BLACK);
	solidcircle(snake[i].x, snake[i].y, 10);
	//接下来遍历每个身体,每个身体都更新为前一个身体,蛇头除外
	for (; i > 0; i--) {
		this->snake[i].x = this->snake[i - 1].x;
		this->snake[i].y = this->snake[i - 1].y;
	}
	switch (this->snake_head.direction) {
	case RIGHT:this->snake[0].x += 20; break;
	case LEFT:this->snake[0].x -= 20; break;
	case UP:this->snake[0].y -= 20; break;
	case DOWN:this->snake[0].y += 20; break;
	default:break;
	}
	setfillcolor(GREEN);
	solidcircle(this->snake[0].x, this->snake[0].y, 10);//绘制蛇头
	Sleep(300);//蛇每走一步的间隙,单位ms,时间越短游戏难度越高
}
void Snake::boundary_check() {
	if (this->snake[0].x <= 30 || this->snake[0].x >= 550 || this->snake[0].y <= 30 || this->snake[0].y >= 550) {
		this->snake_head.life = 0;
	}
}
void Snake::_food() {
	srand((unsigned)time(NULL));
	int x = rand() % 21 + 3;   //范围是3-23
	int y = rand() % 21 + 3;
	this->food.x = x * 20 + 10;
	this->food.y = y * 20 + 10;
	setfillcolor(RED);
	solidcircle(this->food.x, this->food.y, 10);
}
int Snake::food_eatcheck() {
	if (this->snake[0].x == this->food.x && this->snake[0].y == this->food.y) {
		//如果满足条件就是吃到食物了
		this->snake_head.length++;//长度加一
		setfillcolor(GREEN);
		solidcircle(food.x, food.y, 10);
		int k = this->snake_head.length;
		//吃到食物之后最后要在尾巴处加一个长度
		switch (this->snake_head.direction) {
		case RIGHT:this->snake[k - 1].x = this->snake[k - 2].x - 20; this->snake[k - 1].y = this->snake[k - 2].y; break;
		case LEFT:this->snake[k - 1].x = this->snake[k - 2].x += 20; this->snake[k - 1].y = this->snake[k - 2].y; break;
		case UP:this->snake[k - 1].x = this->snake[k - 2].x; this->snake[k - 1].y = this->snake[k - 2].y + 20; break;
		case DOWN:this->snake[k - 1].x = this->snake[k - 2].x; this->snake[k - 1].y = this->snake[k - 2].y - 20; break;
		default:break;
		}
		setfillcolor(GREEN);
		solidcircle(this->snake[k - 1].x, this->snake[k - 1].y, 10);
		return 1;
	}
	return 0;
}
int Snake::snake_eat() {
	int i;
	for (i = 1; i < this->snake_head.length; i++) {
		if (this->snake[i].x == this->snake[0].x && this->snake[i].y == this->snake[0].y) {
			return 1;
		}
	}
	return 0;
}
void Snake::run() {
	display();  //显示游戏界面
	initSnake();
	_food();    //生成第一个食物
	while (true) {
		move();   //蛇移动
		if (snake_eat() == 1) {
			//自己吃到自己了,游戏失败
			for (int i = 0; i <= 17; i++)
			{
				cout << " ";
			}
			cout << "自己吃到自己了,游戏失败" << endl;
			break;
		}
		boundary_check();//判断是否撞墙
		if (this->snake_head.life == 0) {
			//撞墙了
			for (int i = 0; i <= 20; i++)
			{
				cout << " ";
			}
			cout << "撞墙了,游戏结束" << endl;
			break;
		}
		if (food_eatcheck() == 1) {
			_food();  //吃到食物就重新生成一个食物
		}
	}
}
int main() 
{
	system("color F0");
	Snake s;
	char judge;//用于以下判定是否开始游戏还是再见北理工
	for (int i = 0; i <= 10; i++)
	{
		cout << endl;
	}
	for (int i = 0; i <= 10; i++)
	{
		cout <<" ";
	}//懒得去复制上一版代码的gotoxy了,因为s.run中的程序已有EasyX包中的绘图函数完成
	cout << "------贪吃蛇游戏的C++OOP思路实现------" << endl;
	for (int i = 0; i <= 10; i++)
	{
		cout << " ";
	}
	cout << "       北京理工大学徐特立学院" << endl;
startlabel:
	for (int i = 0; i <= 10; i++)
	{
		cout << " ";
	}
	cout << "         是否开始游戏(Y/N):";
	cin >> judge;
	if (judge == 'Y' || judge == 'y')
	{
		s.run();
	}
	else if (judge == 'N' || judge == 'n')
	{
		for (int i = 0; i <= 10; i++)
		{
			cout << " ";
		}
		cout << "    这么好玩你都不玩,再见北理工吧" << endl;
		exit(0);
	}
	else
	{
		for (int i = 0; i <= 10; i++)
		{
			cout << " ";
		}
		cout << "输入错误,重新输入!" << endl;
		goto startlabel;
	}
	return 0;
}//定义程序接口的主函数

 

 

 


附:线性表部分北京理工大学乐学平台:徐特立学院C++数据结构习题解答

Q1.删除结点:

对给定的单链表 L ,设计一个算法,删除 L 中值为 x 的结点的直接前驱结点。

1 由键盘输入值,换行表示输入结束,根据输入建立单链表。

2 输入链表中的一个元素值,从链表中删除这个元素的前驱结点。

3 对删除元素后的链表元素在屏幕上显示。

#include<iostream>    
 
#include<list>    
  
using namespace std;    
  
int main()    
  
{    
  
    list<int> LA,LB;    
  
    list<int>::iterator j,n;    
  
    int x,y;  
  
    do   
  
    {   cin>>x;  
  
        LA.push_back(x);    
  
    } while(getchar()!='\n');  
  
        
  
   
  
for (j = LA.begin();j!= LA.end();j++)     
  
    if(*j==y)  
  
    { LA.erase(--j); break;}  
  
    n=LA.begin();  
  
    cout<<*n;n++;  
  
for (;n!= LA.end();n++)  
  
    {   
  
     cout<<" "<<*n;  
  
    }  
  
    cout<<endl;  
  
    return 0;  
  
}  

Q2.求交集:

已知两个单链表 LA 和 LB 分别表示两个集合,其元素递增排序,设计算法求出 LA 和 LB 的交集 C ,要求 C 同样以元素递增的单链表形式存储。

#include<iostream>  

#include<list>  

using namespace std;  

int main()  

{  

    list<int> LA,LB,LC;  

    list<int>::iterator j,m,n;  

    int x;

	do 

    {   cin>>x;

        LA.push_back(x);  

    } while(getchar()!='\n');

    do

    {   cin>>x;  

        LB.push_back(x);  

    }while(getchar()!='\n');

    

for (j = LA.begin(),m= LB.begin(); j!= LA.end()&&m!= LB.end(); )   

    if(*j==*m)  {LC.push_back(*j);j++;m++;}  

    else *j>*m?m++:j++; 

if(LC.empty()) cout<<"没有交集";

else 

	for (n = LC.begin(); n!= LC.end(); ++n)   

    cout<<*n <<" ";  

    cout<<endl;

    return 0;

}

 这是两个很简单的线性表运用,对于上面总结的线性表操作知识,只需要结合最简单的算法即可实现。算法思路详析略。

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值