数组具有随机存储的优点,查询方便,然而插入删除效率低下,必须提前开辟固定大小的空间,此限制经常造成资源和空间浪费,因此我们可以想出一个办法在不移动其他数据的情况下实现数据的插入和删除,并且不用预先开辟空间,用多少开辟多少——链表
链表:保证数据的逻辑顺序不变得前提下,一种新的存储方式
数组访问元素的实质是通过地址计算实现的,而指针本身就是地址,因此我们考虑借助指针,也即是数据的地址来构造一种新的存储结构
数组是一种顺序的存储方式,即一段连续的存储空间存储数据,链表则不是按照逻辑关系顺序存放的,而是零散的分布在存储空间里
如何让机器知道他们内在的逻辑关系呢?
我们可以为每一个数据元素增加一项信息,即它后面跟着的元素的地址信息,可以通过这个信息可以找到它后面跟着的数据元素,如给a1增加a2的地址,给a2增加a3的地址,为了找到第一个元素a1还需要一个头指针,我们用一个指针变量来存储第一个数据元素a1的地址,称它为头指针变量,简称头指针。由于在这样的存储结构中只有一个向后的一个方向的链接关系,所以称它为单链表
当我们增加一个节点空间(头结点)这个头结点的数据域中并没有实际的数据,指针域中用来存这个链表中第一个节点的地址,头指针用来存放这个头结点的地址
为什么要增加这样一个头结点呢?
首先
我们把单链表中的每一个元素成为一个节点,其包括了两部分:1、用户需要的实际数据 2、下一个节点的地址
表头:存放第一个节点的地址
表尾:它的地址存放NULL,表示链表的结束
显然这样一个节点类型可以通过一个结构体类型来实现
void LnsList(Linklist L,Node *p,Datatype e)//参数有三个,分别为单链表的表头指针,待插位置指针p以及待插节点的data的值e
{
Node *pNew;
pNew=(Node*)malloc(sizeof(Node));
pNew->data=e;
pNew->next=p->next;
p-next=pNew;
}
单链表的删除思想
删除一个节点,即改变链表中的指向关系,进行改链运算
待删除节点的前一个节点的位置即为前驱位置,因此需要先找到待删节点的前驱位置,而是一个单链表,通过p无法找到前驱位置,我们可以将pre(前驱)作为一个搜索指针,初始设置为头指针,从第一个节点开始判断是否为pre,通过pre=pre->next,向后进行搜索,这里使用循环,利用搜索指针的移动去寻找需要的位置
while(pre->next!=p)
pre=pre->next;
一条改链和一条释放操作
void DelList(Linklist L,Node *p)//两个参数,单链表的头指针L和待删节点的位置指针p
{
Node *pre;
pre=L->next;
while(pre->next!=p)
pre=pre->next;
pre->next=p->next;
free(p);
}
单链表的查找代码实现:
Node *Search_x(Linklist,Datatype x)//由于函数的返回值是一个指针,因此前面我们定义的是指针类型的函数
{
Node*p;//设置搜索指针
p=L->next;
While(p)
{
if(p->data!=x)
p=p->next;
else
break;
}
if(p==NULL)
{
printf("没有找到");
}
return p;
}
求单链表的表长(有多少个节点)
在这里我们需要一个计数器和一个搜索指针
int ListLength(Linklist L)//由于需要得到链表的长度,因此需要得到长度的返回值
{
int count=0;
Node *p;
p=L->next;
while(p)
{
count++;
p=p->next;
}
return count;
}
并不像数组定义初始化那样简单,而是一个从无到有的过程,在单链表插入的基础之上产生
1、头插法(将新节点插入到当前链表的表头之后,也就是将插入算法中的指针p改为L头指针即可)
实现:从一个空表开始,重复读入数据,生成新节点,将读入数据存放到新节点的数据域中,然后将新节点插入到当前链表的表头之后,直至读入结束标志为止。
初始化空表
L=(Linklist)malloc(sizeof(Node));
L->next=NULL;
生成要插入的新节点
pNew=(Node*)malloc(sizeof(Node));
pNew->data=1;
pNew->next=L->next;
L->next=pNew;
再生成一个要插入的新节点
pNew=(Node*)malloc(sizeof(Node));
pNew->data=2;
pNew->next=L->next;
L->next=pNew;
我们发现头插法建立的单链表与输入的节点是反序的,因此我们形象的称它为反向建立单链表
代码整理
Linklist Creat_Head()//因为要返回创建的单链表的表头指针,因此函数的类型为表头指针类型
{
Linklist L;//定义表头指针L
Node *pNew;//新插节点位置指针pNew
int x;//用于输入的临时变量x
L=(Linklist)malloc(sizeof(Node));
L->next=NULL;//生成空表,开辟头结点空间,头结点的指针域赋为空值
scanf("%d",&x);输入要插入节点的值,若不是输入的结束标志值进入循环
while(x!=-1)
{
pNew=(Node*)malloc(sizeof(Node));//为新插入的节点开辟空间
pNew->data=x;//给他的data域赋值
pNew->next=L->next;//然后将pNew所指 向的节点插入到头结点之后重新输入新值
L->next=pNew;
scanf("%d",&x);
}
return 1;//最后返回单链表的表头指针
}
2、尾插法(将新节点插入到当前链表的表尾节点之后,也就是将插入算法中的指针p改为表尾指针即可)
实现:
从一个空表开始,重复读入数据,生成新的节点,将读入数据存放到新节点的数据域中,然后将新节点插入到当前链表的表尾,直至读入结束标志为止。
由于尾插法,每次插入的位置为表尾,因此我们需要增加一个指针r,使它始终指向当前链表的表尾
初始化空表
L=(Linklist)malloc(sizeof(Node));
L->next=NULL;//此时该表的表尾就是头结点,表尾指针的初值从L开始,,,头结点的百度解释:数据结构中 在单链表的第一个结点之前附设一个结点,称之为头结点。
r=L;
然后生成要插入的新节点
pNew=(Node*)malloc(sizeof(Node));
pNew->data=1;//将这个节点插入到尾节点之后即
pNew->next=r->next;
r->next=pNew;//在这里r的作用是跟踪当前链表的表尾节点,因此r需要随着建立过程随时进行调整,即新插入一个节点之后,尾指针就跟随它
r=pNew;
接下来重复,再生成一个要插入的新节点
pNew=(Node*)malloc(sizeof(Node));
pNew->data=2;//将这个节点插入到尾节点之后,也就是
pNew->next=r->next;实际上是给新建立的节点的地址域了一个空值,只要这个节点不是链表真正结束的节点那么这个值始终会被刷新,它没有实际的意义,可剔除
r->next=pNew;
r=pNew;
//依次我们重复建立所有的节点
尾插法建立的单链表与输入的节点次序是同向的,因此我们称为正向建立单链表
完整代码实现:
单链表的逆置问题
根据单链表的建立方法之一——头插法
我们可以考虑将原单链表重新建立,即重新建立链接关系,从第一个节点开始搜索单链表中的每一个节点,每搜索到一个就将其头插到头结点之后,那么从前向后搜索到的节点就会反向建立出一条新的单链表,即原单链表的逆单链表
首先将原单链表的表头节点断开,生成一个带头结点的空表
p=L->NULL;
L->next=NULL;//由于我们还需要搜索单链表中的节点,所以在断开这条链之前,一定要先预存一下后继节点的位置,即用搜索指针p去预存第一个节点的位置,然后通过p找到的节点作为新插入的节点,
单链表逆置代码的实现:
void RevLink(Linklist L)//参数为原单链表的表头指针L;
{
Node *p=L->next,q;//搜索指针p从第一个节点L->next开始,首先是空表,然后循环保存p的第一个节点位置,头插p所指向的节点,p指针移动到后一个节点q的位置,循环继续,直到链表结束
L->next=NULL;
while(p)
{
q=p->next;
p->next=L->next;
L->next=p;
p=q;
}
}
单链表的合并
有两个单链表LA和LB,其元素均为非递减有序排列,编写算法,将它们合并成一个单链表LC,要求LC也是非递减有序排列(同向)。
要求:新表LC利用原来的存储空间
考虑到单链表的建立——尾插法恰好是一个正向即同向建立单链表的过程。从一个空表开始,每次将新节点插入到当前链表的表尾节点之后,即正向建立单链表的过程。
r->next=pNew;
r=pNew;
那么在我们这道题目里,每次新插入的节点,又有谁来充当呢,显然每次是由两个链表中值最小的那个节点来充当,那么值最小的节点该如何找到呢,由于这两个单链表都是非递减有序的,即意味着这两个单链表中第一个节点的值都是单链表中值最小的,所以值最小的那个节点就是在这两个节点中比较产生,谁的值小,我们就将选择谁进行尾插,后面依次类推。
完整代码:
Linklist MergeLinkList(Linklist LA,Linklist LB)//参数两个,已知单链表的两个表头指针,由于最后返回的是两个单链表合并之后的单链表,因此函数的类型头指针类型Linklist
{
Node *pa,*pb;
Linklist LC;
pa=LA->next;
pb=LB->next;
LC=LA;
LC->next=NULL;
r=LC;
while(pa&&pb)//核心部分,链表没有结束的情况下循环比较找到值最小的节点进行尾插,循环结束后没有结束的表结点直接串联到尾指针之后,释放B表的表头节点空间,返回合并后的表头指针LC
{
if(pa->data<=pb->data)
{
r->next=pa;
r=pa;
pa=pa->next;
}
else
{
r->next=pb;
r=pb;
pb=pb->next;
}
}
if(pa) r->next=pa;
else r->next=pb;
free(LB);
return (LC);
}
插入头指针的官方定义
(头指针一般用于处理数组,链表,队列等数据结构。 头指针是指的指向上述数据结构的起始数据的指针,如指向数组首地址的指针,指向链表表头结点的指针。在线性表的链式存储结构中,头指针指链表的指针,若链表有头结点则是链表的头结点的指针,头指针具有标识作用,故常用头指针冠以链表的名字。 )
循环链表:
是一个首尾相接的链表,即单链表最后一个节点的指针域由NULL改为指向头结点或线性表中的第一个节点,就得到了单链表形式的循环链表,并称为循环单链表,在循环单链表中,表中所有节点被链在一个环上。
循环单链表和单链表非常相似,只是首尾状态不相同,因此循环单链表所涉及的基本操作除了判断表尾节点和单链表有所差异,其他的操作都非常类似
思考:
我们之前学习的单链表都是带头指针的,为什么这里会提出有尾指针的单链表?先看一个例题:
思路:将第一个链表的表尾和第二个表的表头连接起来,再将第二个表的表尾连接到第一个表的表头。首先要获得这两个表的表尾,因为它是单向的链表,所以我们只能通过循环从头开始进行搜索,搜索到表尾,分别用RA,RB来表示这两个表的表尾,那么将第一个表尾与第二个表头连接起来,RA->next=LB->next;再将第二个表尾连接到第一个表头RB->next=LA;最后释放掉第二个表的表头。
完整代码如下:
Linklist Merge_1(Linklist LA,Linklist LB)
{//函数名为merge_l,参数为两个循环单链表的表头指针LA,LB;,因为最后我们要返回合并后的表头,因此函数的类型为表头指针类型Linklist
Node *RA,*RB;//定义两个指针RA,RB,用来找两个表的表尾
RA=LA;
RB=LB;
while(RA->next!=LA) RA=RA->next;
while(RB->next!=LB) RB=RB->next;
RA->next=LB->next;//因为是循环单链表,所以循环判断条件为表尾的next不等于表头,指针后移,通过两个循环,分别找到了循环链表的表尾,此时就可以进行这两条改链语句,最后释放空间返回表头指针。
RB->next=LA;
free(LB);
return (LA);
}
由此看出,该合并过程时间主要花在表尾指针的查找,因此我们将题目改成已知两个带尾指针的循环单链表,RA,RB,将此合并为循环单链表,我们只需要先通过尾指针将表头位置预存下来,即LA=RA->next,然后就可以直接进行合并即RA->next=RB->next->next;free(RB->next);RB->next=LA;
优化完整代码如下:
Linklist Merge_2(Linklist RA,Linklist RB)
{
Linklist LA;//定义一个表头指针,用于保存第一个表的表头位置,然后进行改链 释放 以及合并工作,最后返回尾指针RB
LA=RA->next;
RA->next=RB->next->next;
free(RB->next);
RB->next=LA;
return (RB);
}
由以上两个指针,带尾指针的显然要比带头指针的循环单链表使用方便,因为有了尾指针,只需一次操作即可找到头指针,但是有了头指针没有那么方便找到尾指针,因此在使用循环单链表时习惯使用带尾指针的循环单链表
后继节点:就是中序遍历排序中相对的后一个,前驱结点则与此相反。
单链表查找后继节点非常方便,但是要查找前驱结点,需要利用循环从头进行查找,在表长为n的表中查找最后一个节点的前驱点需要O(n)的时间代价,因此我们提出了带有两种链接关系的链表 即双向链表。
双向链表:在单链表的每个结点里再增加一个指向其前驱的指针prior,这样形成的链表中就有两条方向不同的链,我们称之为双向链表。它的数据类型依然是结构体类型
代码实现:
//在双向链表L中p所指定的节点之前插入值为e的节点
void DLinkIns(Dlinklist,DNode*p,Datatype e)
{
DNode *pNew;
pNew=(DNode*)malloc(sizeof(DNode));
pNew->data=e;
pNew->prior=p->prior;
p->prior->next=pNew;
pNew->next=p;
p->prior=pNew;
}
//删除操作
p->prior->next=p->next;
p->next->prior=p-prior;
//在此之前需要查找到p的位置,类似于单链表,不再赘述
双向链表的创建不仅要像创建单项链表那样在插入新节点的时候建立节点的后继关系(先),和还需要建立前驱关系(后)。
之前学习的链表都是通过指针实现的,链表中节点的分配和释放都是由系统提供的标准函数malloc和free动态实现,因此也被称为动态链表,而实际上有一些语言,并没有提供指针这种类型,如果仍想采用链表作为存储结构,我们可以采用数组来模拟实现链表。
如此,每一个数组元素除了存储基本数据信息外,还存放了逻辑上的后继节点位置,而这里的位置信息不再是后继节点实际的地址值了 即指针型数据,而是后继节点在数组中的相对位置,也即是数组的下标值。
一般数组的第0个元素我们设定它为表头节点,地址域没有实际的有效数据,地址域存放第一个节点的下标值,第一个节点的地址域放第二个节点的下标值…表尾节点的地址域存放-1来表示静态链表的结束,如果我们给表尾节点的地址域存放的下标值是第0节点的地址域的值,那么就是第一个节点的相对地址值,则就构成了一个循环链表,可见在这个数组里,物理上相邻的数据元素逻辑上并不一定相邻,我们称这样的链表为静态链表。
定义一个较大的数组,作为节点空间的存储池,每个节点包含两个域,data域和next域,和动态链表不同的是这里的next域是整型的,用来记录数组元素的下标值,即相对地址
结构定义:
#define Maxsize 10
typedef int Datatype;
typedef struct SNode
{
Datatype data;
int next; //游标 cursor模拟指针
}
Snode,StaticList[Maxsize+1];
静态链表的插入删除类似于单链表的操作,不过这时的指针可以改为游标来进行
我们在data 值为a1,a2节点之间插入data值为x的节点,即这些节点的逻辑关系发生了改变。首先将x放入存储池,如放入到第九号元素中即L[9].data=x,地址域可以存放data值为a2的地址,即L[9].next=L[4].next,再来改变data值为a1的节点的地址域,应被重新赋值为L[4].next=9,即完成了插入工作
如删除刚刚插入的x,节点之间的逻辑关系又发生了改变。通过单链表的学习,我们知道要删除必须要知道待删节点的前驱位置,因此我们查找的时候要找到x节点的前驱位置,即data值为a1的。直接修改它的地址域。
但是仅仅这样,实际上在存储池中并没有着呢正删除该节点,也就是释放掉它的空间,多次这样就会造成静态链表的假满状态,即表中有很多空闲空间但却无法插入元素,原因是未对已删除的节点空间进行释放。
解决办法是:
将所有未被分配的节点空间以及因删除操作而释放的节点空间通过游标链呈一个空闲节点链表,当进行插入操作时,先从空闲节点链表取一个分量来存放待插入的节点,然后将其插入到已用链表的相应位置。
删除:
这种方法是指在已申请的大的存储空间中用一个已用的静态链表(表头指针设置为0)还有一个空闲节点链表(表头指针另外设置变量av)。
与数组相比
静态链表插入、删除方便(避免元素大量移动),查询不方便(沿着静态链搜索)(不具有随机存储特点)。