一、 线性表的链式存储结构
- 1. 顺序存储结构不足的解决办法
前面我们讲的线性表的顺序存储结构。它是有缺点的,最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。能不能想办法解决呢?
要解决这个问题,我们就得考虑一下导致这个问题的原因。
为什么当插入和删除时,就要移动大量的元素,仔细分析后,发现原因就在于相邻两个元素的存储位置也是具有邻居关系。它们编号是1,2,3,....,n,它们在内存中的位置也是挨着的,中间没有空隙,当然就无法快速介入,而删除后,当中就会留出空隙,自然需要弥补。问题就出在这里。
A同学思路:让当中每个元素之间都留有一个空的位置,这样要插入时,就不至于移动。可一个空隙的位置如何解决多个相同位置插入数据的问题呢?所以这个想法显然不行。
B同学的思路:我们反正也是要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有元素我们都可以通过遍历而找到。
- 2. 线性表链式存储结构定义
线性表的链式存储结构,如图所示,在逻辑上紧邻的元素在物理上不一定紧邻。
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存储在内存未被占用的任意位置。如上图所示。
以前在顺序结构中,每个元素只需要存数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后面元素的存储地址。
因为,为了表示每个元素与其直接后继元素之间的逻辑关系,对数据元素来说,除了存储其本身信息之外,还要存储一个指示直接后继的信息。我们把存储数据元素信息的域成为数据域,把存储直接后继位置的域成为指针域。指针域中存储的信息称做指针或链。这两个部分信息组成数据元素的存储映像,称为节点。
N个结点链接成一个链表,即为线性表(a1,a2,...,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起,如图所示:
对于线性表来说,总得有个头尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后每个结点,其实就是上一个的后继指针的位置。想像一下,最后一个结点,它的指针指向哪里?
最后一个,当然就意味着直接后继不存在了,所以我们规定,线性表的最后一个结点的指针为“空”(通常用NULL或“^”符号表示,如图所示)。
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,谁叫它是第一个呢。也可以存储如线性表的长度等信息,头结点的指针域存储指向第一个结点的指针,如图所示:
- 3. 头指针与头结点的异同
如图所示:
- 4. 线性表链式存储结构代码描述
若线性表为空表,则头结点的指针域为”空”,如图所示:
这里我们大概地用图示表达了内存中单链表的存储状态。而我们真正关心它在内存中的实际存储位置吗?不是的,这只是它所表示的线性表中的数据元素及数据元素之间的逻辑关系。所以我们改用更方便的存储示意图来表示单链表,如图所示:
带有头结点的单链表,如图:
空链表如图:
单链表中,我们在C语言中可以用结构指针来描述。
从这个结构定义中,我们也就知道,结点有存放数据元素的数据域存放后继结点地址的指针域组成。假设P是指向线性表第i个元素的指针,则该结点ai 的数据域我们可以用P->data来表示,p->data的值是一个数据元素,结点ai 的指针域可以用p->next 来表示,p->next的值是一个指针。P->next指向谁呢?当然是指向地i+1个元素,即指向ai+1的指针。也就是说,如果p->data=ai,那么p->next->data=ai+1元素,即指向ai+1的指针。也就说,如果p->data=ai,那么p->next->data=ai+1 (如图所示)
二、 单链表的读取
为什么要学习链表?
链表是线性表的链式存储,在平时工作中使用频率也很高的一种数据结构。
在线性表的顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。但在单链表中,由于第i个元素到底在哪?没办法一开始就知道,必须得从开头找。因此,对于单链表的实现获取第i个元素的操作GetElem,在算法上,相对要麻烦一些。
获得链表第i个数据的算法思路:
- 声明一个结点P指向链表第一个结点,初始化j从1开始;
- 当就j<i时,就遍历链表,让p的指针向后移动,不断向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,返回结点p的数据;
实现代码算法如下:
void output()//输出节点
{
struct nobe *p;
p=head;
if(head==NULL)
{
printf("暂无数据
");
}else
{
while(p)
{
printf("%d ",p->data);
p=p->next;
}
printf("
");
}
}
说白了,就是从开头开始找,直到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度是O(n).
由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此就不方便用for来控制循环。其主要核心思想就是”工作指针后移”,这其实就是很多算法的常用技术。
三、 单链表的插入与删除
- 1. ACM算法:单链表的插入
先来看单链表的插入。假设存储元素e的结点为s,要实现结点P,p->next 和s之间逻辑关系的变化,只需将结点s插入到结点p和p->next之间即可。可如何插入呢?如图:
根本不用惊动其他结点,只需让s->next和p->next的指针做一点改变即可。
s->next=p->next;
p->next=s;
解读这两句代码,也就是说让p的后继结点改成s的后继结点,再把结点s变成p的后继结点(如图所示)。
考虑一下,这两句的顺序可不可以交换?
如果先p->next=s;再s->next=p->next;会怎样?此时第一句使得将p->next给覆盖成s的地址了,那么s->next=p->next,其实就等于s->next=s,这样真正的拥有ai+1数据元素的结点就没了上级。这样的插入操作就是失败的,造成了临时掉链子的局面。所以这两句是无论如何不能反过来,这点初学的同学注意一下。
插入结点s后,链表如图所示。
对于单链表的表头和表尾的特殊情况,操作是相同的。如图所示:
单链表第i个元素数据插入结点的算法思路:
(1) 声明一个结点p指向链表第一个结点,初始化j从1开始;
(2) 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
(3) 若到链表末尾p为空,则说明第i个元素不存在;
(4) 否则查找成功,在系统生成一个空结点s;
(5) 将数据元素e赋值给s->data;
(6) 单链表的插入标准语句s->next=p->next p->next=s ;
(7) 返回成功了;
实现代码如下:
void input() { int n; struct nobe *p,*pa; p=(struct nobe *)malloc(sizeof(struct nobe)); pa=head; printf("请输入要插入在第几个之后:"); scanf("%d",&n); printf("输入数据:"); scanf("%d",&p->data); if(n==0) { head=p; p->next=pa; }else{ for(i=1;i<n;i++) { pa=pa->next; } p->next=pa->next; pa->next=p;} printf("插入成功 "); }
这段代码中,我们用到了C语言的malloc标准函数,它的作用是生成一个新结点,其类型与Node是一样的,其实质就是在内存中找一小块空地,准备用来存储e数据s结点;
- 2. ACM算法:单链表的删除
现在我们再来看单链表的删。设存储元素ai的结点为q,要实现结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可,如图所示。
我们所要做的,实际上就是一步,p->next=p->next->next,用q来取代p->next即是
q=p->next;p->next = q->next;
解读这两句代码,也就是说让p的后继结点改变成p的后继结点。
单链表第i个数据删除结点的算法思路:
(1) 声明一个结点p指向链表第一个结点,初始化j从1开始;
(2) 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
(3) 若到链表末尾p为空,则说明第i个元素不存在;
(4) 否则查找成功,将欲删除的结点p->next赋值给q;
(5) 单链表的删除标准语句p->next=q->next;
(6) 将q结点中的数据赋值给e,作为返回;
(7) 释放q结点;
(8) 返回成功;
void delet() { struct nobe *pa,*pb; int a; if(head==NULL) { printf("暂无数据 "); }else { printf("请输入您准备删除的数据:"); scanf("%d",&a); pa=head; while(pa) { if(pa->data==a) { break; } pb=pa;//pa等于他前一个节点 pa=pa->next; } if(pa==NULL) { printf("您输入的数据有误! "); } else if(pa->next==NULL) { pb->next=NULL; free(pa); }else{ pb->next=pa->next; free(pa); printf("删除成功! "); } } }
这段代码里,我们又用到另一个c语言标准库函数free。它的作用是让系统回收一个Node结点,释放内存。
分析一下刚才我们讲的单链表插入和删除算法,我们发现,它们其实都是有两个部分组成:第一个部分就是遍历查找第i个元素;第二个部分就是插入和删除元素;
从整个算法来说,我们很容易推导出:它们的时间复杂度都是O(n)。如果在我们不知道第i个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大的优势的。但如果,我们希望从第i个位置,插入10个元素,对于顺序存储结构意味着,每次插入都需要移动n-i个元素,每次都是O(n)。而单链表,我们只需要在第一次时,扎到第i个位置的指针,此时为O(n),接下来只是简单地通过移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。
四、 ACM算法:单链表的整表创建
回顾一下,顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。而单链表和顺序存储结构就不一样,它不像顺序存储机构的这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置 不需要预先分配划定的,可以根据系统的情况和实际需求即时生成。
所以创建单链表的过程是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表创建的算法思路:
1、声明一个结点p和计数器变量i;
2、初始化一个空链表L;
3、让L的头结点的指针指向NULL,建立一个带头结点的单链表;
4、循环;
(1) 生成一新节点赋值给p;
(2) 随机生成一个数字赋值给p的数据域p->data;
(3) 将p插入到头结点与前一新结点之间;
实现代码算法如下:
#include "stdio.h" #include "stdlib.h" int i;//循环变量 struct nobe { int data; struct nobe *next; }*head; void lines(struct nobe *p);//输入结点 void output();//输出节点 void delet();//删除一个节点 void cls(); void input(); void main() { struct nobe *p; int bh; for(;;){ printf("请选择链表功能: "); printf("1.创建链表 "); printf("2.输出链表 "); printf("3.删除节点 "); printf("4.清空链表 "); printf("5.插入节点 "); printf("0.退出 "); printf("请输入功能编号:"); scanf("%d",&bh); switch(bh) { case 1: printf("您选择的是常见链表功能: "); for(i=0;i<5;i++) { p=(struct nobe*)malloc(sizeof(struct nobe )); if(!p) { printf("分配失败!"); } printf("请输入第%d个节点的数据域的数据: ",i+1); scanf("%d",&p->data); fflush(stdin); lines(p); } break; case 2: output(); break; case 3: delet(); break; case 4: cls(); break; case 5: input(); break; case 0: exit(0); break; default : printf("您输入的功能编号有误! "); break; } } } void lines(struct nobe *p)//输入结点 { struct nobe *pa,*pb; pa=head; if(head==NULL) { head=p;//将第一个节点当作表头 }else { while(pa) { pb=pa; pa=pa->next; } pb->next=p; } p->next=NULL; } void output()//输出节点 { struct nobe *p; p=head; if(head==NULL) { printf("暂无数据 "); }else { while(p) { printf("%d ",p->data); p=p->next; } printf(" "); } } void delet() { struct nobe *pa,*pb; int a; if(head==NULL) { printf("暂无数据 "); }else { printf("请输入您准备删除的数据:"); scanf("%d",&a); pa=head; while(pa) { if(pa->data==a) { break; } pb=pa;//pa等于他前一个节点 pa=pa->next; } if(pa==NULL) { printf("您输入的数据有误! "); } else if(pa->next==NULL) { pb->next=NULL; free(pa); }else{ pb->next=pa->next; free(pa); printf("删除成功! "); } } } void cls() { struct nobe *pa,*pb; if(head==NULL) { printf("暂无数据 "); }else { pa=head; while(pa) { pb=pa;//pa等于他前一个节点 pa=pa->next; free(pb); } head=NULL; } } void input() { int n; struct nobe *p,*pa; p=(struct nobe *)malloc(sizeof(struct nobe)); pa=head; printf("请输入要插入在第几个之后:"); scanf("%d",&n); printf("输入数据:"); scanf("%d",&p->data); if(n==0) { head=p; p->next=pa; }else{ for(i=1;i<n;i++) { pa=pa->next; } p->next=pa->next; pa->next=p;} printf("插入成功 "); }
这段算法代码里,我们其实用的是插队的办法,就是始终让新节点在第一的位置。我也可以把这种算法简称为头插法,如图所示:
可事实上,我们还是可以不这样干,为什么不把新结点都放到最后呢,这才是排队时的正常思维,所谓的先来后到。我们每次新结点都插再终端结点后面,这种算法称之为尾插法。
注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随循环增长为一个多结点的链表。
这里需要解释一下,r->next=p的意思,其实就是将刚才的尾结点r的指针指向新结点p,如图所示,当1位置的连线就是表示这个意思。
R->next=p; 这句话应该还好理解,有很多学生不理解后面这一句 r=p;是什么意思?请看下图:
它的意思,就是本来r是在ai+1元素的结点,可现在它已经不是最后的结点了现在最后的结点是a1,所以应该要让将p结点这个最后的结点赋值给r。此时r又是最后的尾姐点了。
循环结束后,那么应该让这个链表的指针域置空,因此有了”r->next=NULL;”,以便以后遍历时可以确认其是尾部。
五、 ACM算法:单链表的整表删除
当我们不打算使用这个单链表时,我们需要把它销毁,其实也就是在内存中将它释放掉,以便留出空间给其他程序或软件使用。
单链表整表删除的算法思路如下:
1、声明一个结点p和q;
2、将一个结点赋值给p;
3、循环:
(1) 将下一个结点赋值给q;
(2) 释放p;
(3) 将q赋值给p;
实现代码算法如下:
这段算法代码里,常见的错误就是有同学会觉得q变量没有存在的必要。在循环体内直接写free(p); p=p->next; 即可。可这样会带来什么问题?
要知道p是一个结点,它除了有数据域,还有指针域。你在做free(p);时,其实就是队它整个结点进行删除和内存释放工作。
六、 单链表结构与顺序存储结构优缺点
简单地对单链表结构和顺序存储结构做对比:
通过上面的对比,我们可以得出一些经验性的结论:
- 1. 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。如此说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能随时增加或删除,此时再用顺序存储就不太合适了,单链表结构就可以大展拳脚。当然,这只是简单的类比,现实的软件开发,要考虑的问题会复杂很多。
- 2. 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。
总之,线性表顺序结构和单链表结构各有其优缺点,不能简单的说哪个好,哪个不好,需要根据实际情况,来综合平和采用哪种数据更能满足和达到需求和性能。
拳皇游戏代码
/* Note:Your choice is C IDE */ #include "stdio.h" #include "stdlib.h" #include "time.h" #include "windows.h" #include "string.h" int color(int c) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),c); return 0; } void menu1(){ int i,j; printf(" "); for(i=15;i>1;i--){ system("cls"); for(j=0;j<i;j++){ printf(" "); } printf(" O "); for(j=0;j<i;j++){ printf(" "); } printf("<H> "); for(j=0;j<i;j++){ printf(" "); } printf("I I "); Sleep(20); } printf("点击任意处开始 "); } void menu2(){ int i,j; printf(" "); for(i=30;i>15;i--){ system("cls"); printf(" O "); printf("<H> "); printf("I I "); color(j-1); for(j=i;j>0;j--){ printf(" "); } printf(" O "); for(j=i;j>0;j--){ printf(" "); } printf("<H> "); for(j=i;j>0;j--){ printf(" "); } printf("I I "); Sleep(20); } printf("点击任意处开始 "); } void main() { int i,j=3; int p1=100,p2=100; char arr[20],brr[20]; int a,b; system ( "mode con cols=53 lines=22" ); system("title.拳皇"); srand(time(NULL)); printf("请输入A玩家的昵称:"); scanf("%s",&arr); printf("请输入B玩家的昵称:"); scanf("%s",&brr); b=rand()%2; /* 谁先动手 */ system("pause"); for(i=1;i>0;i++){ menu1(); menu2(); a=rand()%11+5; /* 技能 */ printf("=======第%d回合====== ",i); if(a==13){ a-=20; /* 加7滴血 */ } if(b==1){ if(i%2!=0){ printf("%s使用技能 ",arr); p1-=a; }else{ printf("%s使用技能 ",brr); p2-=a; } }else{ if(i%2==0){ printf("%s使用技能 ",arr); p1-=a; }else{ printf("%s使用技能 ",brr); p2-=a; } } color(j); /* 控制颜色 */ if(j<15){ j++; }else{ j=3; } switch(a){ case 0:printf("========雷霆半月斩 "); break; case 1:printf("========九天雷霆三脚蹬 "); break; case 2:printf("========恒星螺旋连丸 "); break; case 3:printf("========橡胶机关枪 "); break; case 4:printf("========螺旋丸 "); break; case 5:printf("========超大玉螺旋丸 "); break; case 6:printf("========别天神 "); break; case 7:printf("========天照 "); break; case 8:printf("========月读 "); break; case 9:printf("========尾兽玉 "); break; case 10:printf("========盖亚能量炮 "); break; case 11:printf("========天马流星拳 "); break; case 12:printf("========天碍震星 "); break; case 13:printf("========铜墙铁壁 "); break; case 14:printf("========澳洲绵羊油 "); break; case 15:printf("========万物归宗 "); break; } printf("==================== "); printf("☆☆☆☆☆☆☆☆☆☆ "); printf("『『『%s还剩%d血 ",arr,p1); printf("』』』%s还剩%d血 ",brr,p2); printf("☆☆☆☆☆☆☆☆☆☆ "); printf("==================== "); if(p1<=0){ printf("%s阵亡,%s胜利",arr,brr); break; }else if(p2<=0){ printf("%s阵亡,%s胜利",brr,arr); break; } Sleep(1500); system("cls"); } }
彩虹字母
#include <conio.h> #include <windows.h> #include <stdlib.h> #include <stdio.h> #include "time.h" void gotoxy(int x, int y) //指定y行,x列 { COORD c; c.X=x-1; c.Y=y-1; SetConsoleCursorPosition (GetStdHandle(STD_OUTPUT_HANDLE), c); } int color(int c) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),c); return 0; } int check() { int x; x=rand()%26+97; return x; } void main() { int x=2,y,i,z,j,p,k; char zm; srand((unsigned)time); system ( "mode con cols=80 lines=28" ); for(i=0;i<20;i++){ /* x=rand()%81; y=rand()%29;*/ for(p=1;p<29;p++){ for(k=1;k<80;k++){ for(j=0;j<p;j++){ z=rand()%16; gotoxy(k,p+j); color(z); printf("%c",check()); } } Sleep(100); system("cls"); } } }
闪烁的心
#include <stdio.h> #include <math.h> #include <windows.h> #include "time.h" int color(int c) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),c); return 0; } int main() { //FILE *fp = fopen("graph.txt", "w+"); float x, y, f; int z; srand((unsigned)time); for(;;){ z=rand()%15+1; color(z); for(y = 1.6; y >= -1.6; y -= 0.15){ for(x = -1.1; x <= 1.1; x += 0.05){ f = x*x + pow(y - pow(x*x, 1.0/3), 2) - 1; //函数方程 //fputc(f <= 1E-5 ? '*' : ' ', fp); putchar(f <= 1E-5 ? '*' : ' ');//1E-5等价于1x10^(-5) } //fputc(' ', fp); putchar(' '); } //fclose(fp); if(z==4) { Sleep(1000); }else{ Sleep(100);} system("cls"); } return 0; }