线性表链式表示和实现(C语言)
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,而线性表的链式存储特点则是用一组任意的存储单元存储线性表的数据元素。这组存储单元既可以是连续的,也可以是不连续的。
对于链式存储的每个数据元素而言,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息,即直接后继的存储位置。这两部分信息组成了数据元素的存储映像,称为结点。
链式存储的结构包含两个域:一个用于存储数据元素的信息,另一个用于存储直接后继的存储位置;存储数据元素信息的域称为数据域,存储直接后继存储位置的域称为指针域。
图示
数据结构中,在单链表的开始结点之前一般要附设一个类型相同的结点,称之为头结点。头结点的数据域可以不存储任何信息,头结点的指针域存储指向开始结点的指针,即第一个元素结点的存储位置。
附设头结点的作用:
- 防止单链表是空的而设的。当链表为空的时候,带头结点的头指针就指向头结点,如果当链表为空的时候,单链表没有带头结点,那么它的头指针就为NULL
- 方便单链表的特殊操作,插入在表头或者删除第一个结点,加入头结点的话就可以保持单链表操作的统一性
- 当单链表加上头结点之后,无论单链表是否为空,头指针始终指向头结点,因此空表和非空表的处理也就统一了,方便了单链表的操作,也减少了程序的复杂性和出现bug的机会
链式表示要实现的功能:
实现工具:Dev C++
- 构造一个空的头结点
- 对线性表进行赋值
- 对线性表进行销毁
- 对线性表进行重置
- 判断线性表是否为空
- 获取线性表的长度
- 获取线性表某一位置对应的元素
- 在线性表某一位置插入元素
- 删除线性表某一位置的元素
- 求线性表某一元素的前驱
- 求线性表某一元素的后继
- 打印线性表
- 退出
准备工作:
打开Dev C++后新建一个Source File文件即可
File>new>Source File
代码实现:
在实现线性表的链式存储时导入的头文件有
#include<stdio.h>#include<stdlib.h>#include<malloc.h>#include<windows.h>
在这里我调用windows头文件是为了在后面的代码中修改控制台的名称,在实现线性表的链式存储时真正用到的只有前三个头文件
在写各种函数之前先对一些表示结果状态的字符进行预定义
//函数结果状态代码 #defineTRUE1//代码中出现TRUE相当于出现了1 #defineFALSE0//出现FALSE相当于出现了0 #defineOK1//出现OK相当于出现了1 #defineERROR0//出现ERROR相当于出现了0 #defineINFEASIBLE-1#defineOVERFLOW-2typedefintStatus; //定义函数的返回状态 typedefintElemType;
在这里使用了typedef定义了Status和ElemType为int类型,也就是说之后的代码当中如果出现了Status和ElemType,它们与int作用相同
1. 单链表的结点构造
typedefstructLNode{ ElemTypedata; //数据域 structLNode*next; //指针域,指向一整个结点(结构体,该结构体中包含数据域和指针域) }LNode , *LinkList;
代码中的* LinkList指的是结点的类型,在之后的代码中出现了它就相当于出现了指向这个结点的指针
2. 构造一个空的头结点
//构造一个空的头结点StatusInitList_L(LinkList&L){ L= (LinkList)malloc(sizeof(LNode)); //产生头结点,并使L指向该头结点(L也称头指针)if(!L) returnERROR; //如果存储空间分配失败,返回ERRORL->next=NULL; //将指针域赋值为NULLprintf("空的头结点创建成功\n"); returnOK; }
在该函数传参时一定要在L之前加"&“符号,表示引用传递,保证形参和实参同时改变。之前在写代码时因为没有输入”&"符号,没有使用引用传递也就意味着没有开辟了新的内存空间,所以在之后赋值的时候会出现无法赋值的情况
在这里要强调一点:"->"是一个指针类型的运算符,它是用于指向结构体子数据的指针
3. 对线性表进行赋值
//对线性表进行赋值 StatusValueList_L(LinkList&L,ElemTypee){ LinkLists,p; p=L; while(p->next){ p=p->next; } s= (LinkList)malloc(sizeof(LNode)); //生成一个新结点 s->data=e; //将e赋值给新结点的数据域 s->next=p->next; //将新结点与后一个结点的地址连接p->next=s; returnOK; }
因为要实现构造一个单链表,所以在main函数中会循环调用ValueList_L方法,所以通过以下的循环是用来使p指向要赋值的位置
while(p->next){
p = p->next;
}
之后利用malloc()函数开辟新的结点来存放数据和下一个结点的位置即可,因为malloc()函数开辟空间之后返回的不是LinkList结点类型,所以在利用malloc()函数开辟新的结点时要将其通过强制类型转换使其转换成LinkList类型
4.对线性表进行销毁
//对线性表进行销毁StatusDistoryList_L(LinkList&L) { if(!L){ //如果线性表不存在,返回ERROR printf("线性表不存在\n"); returnERROR; } LinkListq=L->next; //使q指向单链表的首元结点 while(q!=NULL){ //当q结点不为空时一直进入循环 free(L); //释放L结点 L=q; //将q结点赋值给L结点 q=L->next; //将q结点赋值给L结点以后使q结点指向L的下一个结点 } free(L); //此时q的值为NULL,L指向尾结点,将其释放L=NULL; printf("线性表已销毁\n"); }
在对单链表进行销毁操作时,从头结点开始逐一释放,释放前使q指向开始释放的结点,当开始结点不为空时,执行释放过程,先释放头结点,然后将L,q都向后移,依次释放,因为q始终是L的后继,所以最后一定是L留到最后,最后释放L结点
L = NULL;
为什么在feel(L);之后还要将L赋值为空?
因为free函数只是将之前动态分配给L的内存归还给系统,但是指针类型的结点L仍然存在,为了防止之后发生野指针访问,将L赋值为NULL
5.对线性表进行重置
//对线性表进行重置StatusClearList_L(LinkList&L) { if(!L->next){ printf("线性表为空表,不需要重置\n"); returnERROR; } LinkListp,q; p=L->next; //将单链表的头结点赋值给pwhile(p){ q=p->next; //将单链表的首元结点赋值给qfree(p); p=q; } L->next=NULL; //将头结点的指针域赋值为空 printf("线性表已重置\n"); returnOK; }
在对线性表进行重置前首先要判断线性表是为空表,当其不为空时构造两个LinkList类型的结点p和q,使p指向L的首元结点,当p不为空即单链表不为空时进入while循环,将p的下一个结点复制给q,将p释放后再将q赋值给p。p为空时说明此时单链表只剩下了头结点,将头结点的指针域设置为NULL,完成单链表的重置(因为LinkList为指针类型的数据,所以赋值的内容都是地址)
图示:
6.判断线性表是否为空
//判断线性表是否为空StatusListEmpty_L(LinkListL) { if(L){ if(L->next==NULL) //如果首元结点不存在 printf("线性表是空表\n"); elseprintf("线性表不是空表\n"); } else{ printf("线性表不存在,无法判断\n"); } returnOK; }
在判断线性表是否为空时,首先判断头结点是否存在,当头结点存在时看头结点的指针域是否为空,当指针域为空时说明首元结点不存在,单链表是空表;当指针域不为空时说明存在首元结点,单链表不是空表。如果头结点不存在的话说明单链表不存在,无法判断是否为空表。
7.获取线性表的长度
//获取线性表的长度StatusListLength_L(LinkListL,intcount) { //L为带头结点的单链表的头指针,count为计数器LinkListp=L->next; //定义p为单链表L的指针域 while(p){ p=p->next; count++; } returncount; }
获取线性表长度的核心思路是遍历单链表,定义LinkList类型的变量p,将单链表的首元结点赋值给p。在该函数中count为计数器,形参count传来的值始终为1,count的值为1代表从首元结点开始计数,所以才将L->next赋值给p,目的是为了让count与存储数据元素的结点位置对应一致。(在单链表中有头结点的存在,所以这种方法计算出的长度最后的值要比线性表的实际长度大1)进入循环后每当p不断向后移动,每当p后移一次,计数器count的值就加1,直到p为空,此时count的位置就对应着最后一个存储着数据元素的结点位置。
8.获取线性表某一位置对应的元素
//获取线性表某一位置对应的元素StatusGetElem_L(LinkListL,intindex) { LinkListp; p=L->next; //使p指向L的首元结点intcount=1; //count为计数器 ,赋值等于1的原因是从首元结点开始计数 while(p&&count<index){ //顺着指针向后查找,直到p指向第index个元素或p为空 p=p->next; count++; //此时p一直指向第count个元素 } if(!p||count>index){ printf("当前位置没有元素\n"); returnERROR; } printf("第%d个元素的值是:%d\n",index,p->data); returnOK; }
与获取单链表的长度思路一样,获取单链表某一位置的元素也需要遍历单链表,只不过什么时候停止遍历由自己决定,可能不需要全部遍历。定义LinkList类型的变量p并且将首元结点的地址赋值给p。定义计数器count的初始值为1之后,进入while循环,循环判断有两个:
- p 因为p一直指向第count个结点,所以此循环判断条件的意思是当第count个结点存在时才能进入循环
- count < index 当count还不是我们想要获取的结点位置时继续循环
退出循环以后p指向的位置就是我们想要获取的结点位置,这个时候要先进行越界判断,!p的意思是如果在之前的循环中index的值大于单链表的长度,那么退出循环的原因就是p为NULL,那么!p就为true,满足if语句中的条件,返回ERROR,所以 !p的作用就是限制index不大于单链表的长度。
count > index的目的是为了限制index的值小于1
9.在线性表某一位置插入元素
//在线性表某一位置插入元素StatusListInsert_L(LinkList&L,intindex,ElemTypee) { LinkListp,q; p=L; //将线性表的头结点赋值给pintcount=0; //count为计数器 while(p&&count<index-1){ //寻找第index-1个结点 p=p->next; //此时的p结点指向第index-1个结点 count++; } if(!p||count>index-1){ //越界判断,index小于1或是index大于表长加1 printf("当前结点无法插入元素\n"); returnERROR; } q= (LinkList)malloc(sizeof(LNode)); q->data=e; //将e赋值到q的数据域当中q->next=p->next; p->next=q; printf("元素插入成功\n"); returnOK; }
与寻找某个位置结点的值思路一致,需要先找到要插入结点的位置。但是这里不同的地方在于要插入结点的话,可以在单链表的表尾插入元素,也可以在头结点和首元结点间插入元素,所以计数器count的初值为0(为了保证从头结点开始遍历,count的值与实际的结点位置相匹配),所以判断条件变为index - 1。在结束循环和越界判断结束后p之后的位置就是要插入结点的位置,先构造出一个空结点并赋值给q,将p的下一个结点位置赋值给q的指针域,再将p的下一个结点位置赋值给q完成插入操作。
图示: