目录
一、定义:
逻辑结构上一个挨一个的数据,在实际存储时,并没有像顺序表那样也相互紧挨着。恰恰相反,数据随机分布在内存中的各个位置,这种存储结构称为线性表的链式存储。
1、链表特点:
由于分散存储,为了能够体现出数据元素之间的逻辑关系,每个数据元素在存储的同时,要配备一个指针,用于指向它的直接后继元素,即每一个数据元素都指向下一个数据元素(最后一个指向NULL(空))。
2、链表内数据元素结构:
- 本身的信息,称为“数据域”;
- 指向直接后继的指针,称为“指针域”。
这两部分信息组成数据元素的存储结构,称之为“结点”。n个结点通过指针域相互链接,组成一个链表。
图 3 中,由于每个结点中只包含一个指针域,生成的链表又被称为 线性链表 或 单链表。
3、头节点、头指针
头结点:有时,在链表的第一个结点之前会额外增设一个结点,结点的数据域一般不存放数据(有些情况下也可以存放链表的长度等信息),此结点被称为头结点。
若头结点的指针域为空(NULL),表明链表是空表。头结点对于链表来说,不是必须的,在处理某些问题时,给链表添加头结点会使问题变得简单。
首元结点:链表中第一个元素所在的结点,它是头结点后边的第一个结点。
头指针:永远指向链表中第一个结点的位置(如果链表有头结点,头指针指向头结点;否则,头指针指向首元结点)。
头结点和头指针的区别:头指针是一个指针,头指针指向链表的头结点或者首元结点;头结点是一个实际存在的结点,它包含有数据域和指针域。两者在程序中的直接体现就是:头指针只声明而没有分配存储空间,头结点进行了声明并分配了一个结点的实际物理内存。
单链表可以没有头节点,但是不能没有头指针。
二、程序实现
1、结构体定义
typedef int SLTDataType;
typedef struct SeqList{
SLTDataType data; //数据元素
struct SeqList* next;//指向下一个结构体的指针
}SeqList;
2、函数定义
SeqList* SeqListCreatNode(SLTDataType data); //创建节点
void SeqListPushBack(SeqList** pphead,SLTDataType data); //尾插函数
void SeqListPushFront(SeqList**phead,SLTDataType data); //头插函数
void SeqListPopBack(SeqList**phead); //尾删函数
void SeqListPopFront(SeqList**phead); //头删函数
SeqList* SeqListFindPosition(SeqList*phead,SLTDataType data); //删除某一个值前的数值
void SeqListPushPosition(SeqList**phead,SeqList*pos,SLTDataType data);//在某个数值前插入一个数
void SeqListShow(SeqList*phead);
3、创建结点
SeqList* SeqListCreatNode(SLTDataType data)
{
SeqList* temp=(SeqList*)malloc(sizeof(SeqList));
temp->data=data;
temp->next=NULL;
return temp;
}
4、尾插函数
插入结点前,若链表为空链表,则头指针(phead)的值为NULL。插入节点后,头指针(phead)为新结点的地址。并且,头指针的值在尾插函数外同步需要更新。若函数直接传递phead的值,此时只是传递的函数外的phead的拷贝值。因此,函数形参需要传递phead的地址值,这样才能做到更新函数外phead的值。
void SeqListPushBack(SeqList** phead,SLTDataType data)//尾插函数
{
SeqList* newcode=SeqListCreatNode(data); //此时涉及到刚开始插入时无节点情况,即头指针为NULL
//所以新建节点后必须把头指针地址改为第一个节点地址
if(!*phead){
*phead=newcode; //判断链表是否为空,与不空的区别是会改变头指针的值
}
else{
//找指向NULL的尾节点
SeqList* temp=*phead;
while(temp->next!=NULL)
{
temp=temp->next;
}
temp->next=newcode;
}
}
5、头插函数
void SeqListPushFront(SeqList**phead,SLTDataType data) //头插函数
{
SeqList* newcode=SeqListCreatNode(data);
newcode->next=*phead;
*phead=newcode;
}
6、尾删函数
void SeqListPopBack(SeqList**phead)//尾删函数
{
if(*phead==NULL){ //判断内部是否有数据进行删除,如果为空,
会造成对空指针取值,程序崩溃
printf("无数据可删除");
return;
}else if((*phead)->next==NULL){ //如果只有一个数据,删除后必定会造成头指针
数值改变。即由原结点的地址变为NULL,因此需单独情况判断
free(*phead);
*phead=NULL;
}else{ //两个及以上数据,尾删后头指针不会改变,因此
又是另外的情况
SeqList* cur,*pre;
cur=pre=*phead;
while(cur->next!=NULL){
pre=cur;
cur=cur->next;
}
pre->next=NULL;
free(cur);
cur=NULL;}
}
7、头删函数
(1)无数据时不能删除。(2)只存在一个数据时,(*phead)->next等于NULL;多个数据时,(*phead)->next取到下一数据地址。都不会发生错误,所以可以合并为一种情况。
void SeqListPopFront(SeqList**phead)//头删函数
{
if(*phead==NULL){ //无数据时不能删除
printf("无数据可删除");
return;
}
else{//只存在一个数据时,(*phead)->next等于NULL;多个数据时,(*phead)->next取到下一数据地址。都不会发生错误,所以可以合并为一种情况。
SeqList* cur=*phead;
*phead=(*phead)->next;
free(cur); //释放删除的结点
cur=NULL; //删除结点的指针设为空
}
}
8、查找某一个值位置并插入值。
(1)查找不涉及更改指针,因此可传递phead的拷贝.函数返回查找到的指针。查找不到返回NULL。
SeqList* SeqListFindPosition(SeqList*phead,SLTDataType data)//查找某一个值的位置
{
//首先,需要查找到该值的位置
SeqList* cur=phead;
while(cur){
if(cur->data==data){
return cur;
}
cur=cur->next;
}
printf("1未找到该数值");
return NULL;
}
(2)根据查找到的指针进行插入
pos为NULL的情况通过第二段代码进行过滤,保证调用函数SeqListPopPosition时,pos不为空。
此时,类似于头插操作。可能会造成头指针值更改,因此需进行分类。
void SeqListPushPosition(SeqList**phead,SeqList*pos,SLTDataType data)
{
if(pos==*phead){ //pos值为头指针值时,即为头插,头指针值改变。
SeqListPushFront(phead,data);
}
else{ //返回其他位置,头指针值不变
SeqList* pre=*phead;//两个指针一前一后,后指针找到插入位置后的元素地址,前指针则是插入前的位置地址。
SeqList* cur=*phead; //设置cur值等于头指针
while(cur!=pos){ //cur值不为pos值时,继续查找要插入的位置
pre=cur;
cur=cur->next;
}
SeqList* newone=SeqListCreatNode(data);
pre->next=newone;
newone->next=cur;
return;
}
}
if(!pos){
printf("2未找到该值");
}else{
SeqListPushPosition(&phead,pos,30);
}
三、总结
线性表的链式存储相比于顺序存储,有两大优势:
- 链式存储的数据元素在物理结构没有限制,当内存空间中没有足够大的连续的内存空间供顺序表使用时,可能使用链表能解决问题。(链表每次申请的都是单个数据元素的存储空间,可以利用上一些内存碎片)
- 链表中结点之间采用指针进行链接,当对链表中的数据元素实行插入或者删除操作时,只需要改变指针的指向,无需像顺序表那样移动插入或删除位置的后续元素,简单快捷。
链表和顺序表相比,不足之处在于,当做遍历操作时,由于链表中结点的物理位置不相邻,使得计算机查找起来相比较顺序表,速度要慢。因此有查找更方便的链表结构,采用双指针。