(总结源自《大话数据结构》,初学数据结构推荐此书)
目录
静态链表
用数组描述的链表叫静态链表。
让数组的元素由两个数据域组成,data和cur,data存放数据,cur模拟链表里的指针,存放后继元素在数组中的下标。
#define MAXSIZE 1000;
typedef int ElemType;
typedef int Status;
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
另外,数组的第一个和最后一个元素有其他妙用,是我们的特殊元素,不存数据。
(备用链:数组是连续的,一般来说若作为链表,肯定会有未被使用的地方,我们把未被使用的数组元素称为备用链表。)
如图这样定义有什么好处呢? 这样我们就知道了头结点的位置,也就知道了链表在哪儿开始(若如图为0,则表明此链表为空),还知道了备用链第一个结点的位置(方便后来的插入操作)
//初始化数组
//space[0].cur为头指针, “0”表示空指针
Status InitList(StaticLinkList space)
{
int i;
for(i=0;i<MAXSIZE-1;i++)
{
space[i].cur=i+!;
}
space[MAXSIZE-1].cur=0;
return OK;
}
静态链表的插入操作
在链表中,结点的插入与删除分别用的是malloc()和free()两个函数。但在数组,不存在结点的申请和释放的问题,所以我们需要自己实现这两个函数,才可以进行插入和删除的操作。
插入的思路:我们知道下标为0的数组元素中存储着备用链的第一个结点的位置,那就是我们想要的插入位。
int Malloc_SLL(StaticLinkList space)
{
int i=space[0].cur; //我们想要存的位置,即第一个备用空闲的下标
if(space[0].cur)
space[0].cur=space[i].cur; //插入后第一个备用空闲没有了,转到它的cur去
return i; //返回分配的结点下标
}
那么这样我们就解决了链表里data的问题,接下来我们要解决的是链表里next的问题。
//在L中第i个元素之前插入新的数据元素e
Status ListInsert(StaticLinkList L,int i,ElemType e)
{
int j,k,l;
k=MAX_SIZE-1; //k是最后一个元素的下标 它的cur是第一个非空下标
if( i<1 || i>ListLength(L)+1 )
return ERROR;
j=Malloc_SSL(L); //j是空闲分量的下标
if( j )
{
L(j).data=e;
for(l=1;l<=i-1;l++) //找到第i个元素之前的位置
k=L[k].cur;
L(j).cur=L[k].cur;
L[k].cur=j;
return OK;
}
return ERROR;
}
例子如图,即实现了静态链表的插入操作。
静态链表的删除操作
删除操作和插入操作一样,需要自己实现free()函数
//删除在L中的第i个数据元素e
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
k=MAX_SIZE-1; //k是最后一个元素的下标 它的cur是第一个非空下标
if( i<1 || i>ListLength(L)+1 )
return ERROR;
j=Malloc_SSL(L); //j是空闲分量的下标
for(l=1;l<=i-1;l++) //找到第i个元素之前的位置
k=L[k].cur;
j=L[k].cur;
L[k].cur=L[j].cur;
Free_SSL(L,j);
return OK;
}
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur=space[0].cur;
space[0].cur=k;
}
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
turn j;
}
循环链表
循环链表,顾名思义,就是将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,头尾相接。
循环链表和单链表最主要的区别就在于循环的判断条件上,单链表是判断p->next是否为空,而循环链表是判断p->next是否等于头结点。
循环链表有什么用呢?
循环链表的有点是从链尾到链头比较方便,适合处理有环形结构特点的数据
在单链表里,我们若想访问头结点和最后一个结点,那么分别需要O(1)和O(n)的时间。而在循环链表中,我们若取消头指针,改为尾指针,如图,那么访问头结点与最后一个结点则均只需用O(1)的时间了。
尾指针有什么用呢?
将两个循环链表合并为一个链表的时候,尾指针非常方便。
双向链表
双向链表就是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
(上图来自极客时间的数据结构与算法之美专栏)
typedef struct DulNode
{
ElemTyoe data;
struct DuLNode *prior; //直接前驱指针
struct DuLNode *next; //直接后继指针
}DulNode,*DuLinkList;
双向链表相比存储同样多的单链表,多占了那么多的空间,具体有什么用呢?
先放结论:由于双向链表能够方便的找到结点的前驱结点,所以在某些情况下,它在插入与删除时比单链表要更加简单,高效
开玩笑的吧,单链表在插入和删除方面,时间复杂度都已经是O(1)了,还能怎么优化,欺负我读书少呢吧!
好,让我来仔细说说链表的两个操作:
1、删除操作
淡定淡定,我们说O(1)其实很片面,单纯指的是删除这个操作,可是在删除之前,我们得去遍历找到要删除的地方,这个地方 才是耗时大户,对应的时间复杂度是O(n),双向链表就是在这个地方进行优化滴。
从链表中删除⼀个数据⽆外乎这两种情况:删除结点中“值等于某个给定值”的结点; 删除给定指针指向的结点。
前者无论是单链表还是双向链表,做法都一样,必须从头结点开始一个个遍历,直至找到给定值的结点在进行删除操作,大家的操作都一样,这没得说。
我们来看看第二种情况:删除给定指针指向的结点。删除这个结点需要有这个结点的前驱结点帮忙,改改它的next指针的指向,可是我单链表找后继结点简单,前驱结点还真没辙,只能从头开始遍历,所以还是得遍历,这一遍了就是O(n)的时间复杂度。这时候双向链表笑嘻嘻的出来,向前一指,喏,这不是前驱结点么,居然还要遍历,太麻烦了。此时双向链表只需要O(1)的时间复杂度就能找到前驱结点进行删除操作,所以双向链表相比单链表,还是有优越感滴~
那么讲了循环链表和双向链表,我们自然而然的能想到超级大boss:双向循环链表,如下,我就不多讲啦