一、链表的概念与结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑结构是通过链表中的指针链接次序实现的。
下面是链表结构的逻辑图:
1、从上图可以看出,链式结构在逻辑上是连续的,但是在物理结构上不一定连续。
2、链表中节点使用的空间一般都是从堆上申请的。
3、从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续也可能不连续。
二、链表的分类
1、单向或者双向
2、带头或者不带头
3、循环或非循环
这么多的链表结构,最常用的无非两种
第一种是不带头单向非循环链表
这种链表结构简单,一般不会用来单独存数据。实际中更多是作为其他数据结构的子结构,如希哈桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
另一种是带头双向循环链表
这种链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然很复杂,但是使用代码实现后会发现结构带来了很多优势,实现反而更简单了。
三、不带头单向非循环链表的实现
首先我们用结构体来定义节点类型。
//定义节点
typedef struct SListNode
{
int date;//date用来存放数据
SLTNode* next;//next指向下一个节点
}SLTNode;
再写一个生成新节点的函数,这个函数的功能是在堆上申请空间给新的节点用,在新节点初始化后,返回这个节点。
SLTNode* SLBuyNewNode(int x)//生成新节点
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);//
newnode->next = NULL;
newnode->data = x;
return newnode;
}
1、头插
我们在插入和删除时,一般需要考虑链表是空或者非空两种情况。
当链表为空时,
我们想要插入新节点,只需要让新节点的next指向头指针指向的地址,然后再让头指针指向新节点就好了,逻辑图如下
当链表非空时,
其实也是让新节点的next指向头指针指向的地址,然后再让头指针指向新节点,逻辑图如下
由此,我们可以写出代码
void SLPushFront(SLTNode** pphead, int x)
//因为头插需要改变头指针,所以我们传参传的是指向头指针的指针
{
assert(pphead);
SLTNode* newnode = SLBuyNewNode(x);
newnode->next = *pphead;//让新节点next指头指针指向的地址
*pphead = newnode;//让头指针指向新节点
}
2、尾插
当链表为空时,
只需要让头指针指向新的节点就好了。
当链表非空时,
首先要找到最后一个节点,让最后一个节点的next指向新节点就好了。
由此可以写出代码
void SLPushBack(SLTNode** pphead, int x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNewNode(x);
SLTNode* cur = *pphead;
if (cur == NULL)//链表为空
{
cur = newnode;//让头指针指向新节点
}
else//链表非空
{
while (cur->next)//找到最后一个节点
{
cur = cur->next;
}
cur->next = newnode;//让最后一个节点的next指向新节点
}
}
3、头删
在删除时,链表不能为空。
首先,我们先定义一个指针指向第一个节点(这个节点就是我们要删除的节点),然后让头指针指向第一个节点的next,然后用free函数把第一个节点释放掉,就完成头删了。一个节点或多个节点都是同样的方法。
由此,我们可以写出代码
void SLPopFront(SLTNode** pphead)
{
assert(pphead);//指向头指针的指针不能为空
assert(*pphead);//头指针也不能为空
SLTNode* freenode = *pphead;//指向要删除的节点
*pphead = (*pphead)->next;//让指针指向第一个节点的next
free(freenode);//释放掉要删除的节点
freenode = NULL;
}
4、尾删
定义一个指针cur遍历链表,当cur->next->next==NULL时停止(此时cur指向的节点的下一个节点就是我们要删除的尾结点),我们释放掉cur->next(尾结点),最后让cur->next=NULL即可。
但是有一种特殊情况,就是链表只有一个节点时。
此时cur指向的就是我们要删除的节点,所以此时我们直接释放掉cur,把头指针置空即可。
由此可以写出代码
void SLPopBack(SLTNode** pphead)//尾删
{
assert(pphead);
assert(*pphead);
SLTNode* cur = *pphead;
if (cur->next == NULL)//链表只有一个节点
{
free(cur);
*pphead = NULL;
}
else//链表有多个节点
{
while (cur->next->next)
{
cur = cur->next;
}
free(cur->next);
cur->next = NULL;
}
}
5、查找
在查找时,我们希望输入一个数,如果在链表中有节点的data为这个数,那就返回这个节点的地址,否则返回空指针。
查找的实现很简单,只需要遍历链表即可。由此写出代码
SLTNode* SLFind(SLTNode* phead, int x)
{
SLTNode* cur = phead;//让cur来遍历链表
while (cur)
{
if (cur->data == x)
{
return cur;//返回指向找到节点的指针
}
cur = cur->next;
}
return NULL;//没找到,返回空指针
}
6、插入
实现插入时,会提供一个指针pos,这个指针指向链表的一个节点,我们需要在这个节点前插入新节点。
只需要用一个指针来遍历链表,直到找到pos指向节点的前一个节点,然后就可以开始插入了。
有两种特殊情况,第一种情况时pos指向的节点时第一个节点,
此时只需要复用我们上面写的头插即可。
另一种情况是pos指向空,
此时复用上面写的尾插即可。
由此可以写出代码
void SLInsert(SLTNode** pphead, SLTNode* pos, int x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)//头插
{
SLPushFront(pphead, x);
}
else if (pos == NULL)//尾插
{
SLPushBack(pphead, x);
}
else
{
SLTNode* newnode = SLBuyNewNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)//找到pos的前一个节点
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
7、删除
删除也是提供一个指针pos,这个指针指向链表的一个节点,我们需要删除节点。如果pos指向头结点,则用上面写的头删。否则,就先找到删除节点的前一个节点,然后删除。
代码如下,
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)//头删
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)//找pos的前一个节点
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
8、销毁
销毁时我们要使用两个指针,一个指针指向要销毁的节点,另一个指针指下一个节点,就这样从第一个节点开始依次销毁所有节点,最后再把链表置空即可。
代码如下
void SLDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* freeNode = *pphead;//指向要被销毁的节点
SLTNode* freeNext = NULL;//指向已被销毁节点的下一个节点
while (freeNode)
{
freeNext = freeNode->next;
free(freeNode);
freeNode = freeNext;
}
*pphead = NULL;//最好把头指针置空
}
四、带头双向循环链表的实现
先定义节点
typedef struct ListNode
{
struct ListNode* next;//指向后一个节点
struct ListNode* prev;//指向前一个节点
int data;
}LTNode;
生成新节点函数
LTNode* LTBuyNewNode(int x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = -1;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
1、初始化
我们需要给链表设置一个头结点,头结点储存的数据没有任何意义,因为是循环链表,所以头结点的head和prev都指向自己,头结点存在的意义就是不需要在考虑头指针是否为空。
代码如下
void LTInitial(LTNode** pphead)
{
*pphead = LTBuyNewNode(-1);
(*pphead)->next = *pphead;
(*pphead)->prev = *pphead;
}
上面这样写的话,传参传的是二级指针,考虑到接下来写的函数传的都是一级指针,所以我们避免传二级指针,我们换另一种写法。
LTNode* LTInitial()
{
LTNode* phead = LTBuyNewNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
这段代码会返回初始化好的链表,而且不需要传参。
2、头插
定义frist指针指向第一个节点,然后让新节点的next指向头结点的下一个节点d1,d1那的prev指向新节点,让新节点的prev指向头结点,让头结点的next指向新节点。
由此我们可以写出代码,注意,带头的链表即使为空,至少也有一个头结点,所以需要检查头指针是否为空
void LTPushFronts(LTNode* phead,int x)
{
assert(phead);
LTNode* newnode = LTBuyNewNode(x);
LTNode* frist = phead->next;//指向第一个节点
newnode->next = frist;//新节点的next指向第一个节点
frist->prev = newnode;//第一个节点的prev指向新节点
newnode->prev = phead;//新节点的prev指向头结点
phead->next = newnode;//头结点的next指向新节点
}
当链表为空时,我们按照上面的来画出逻辑图,
由此观之,当链表为空时,上面的代码也行之有效。
3、尾插
在尾插时,
定义tail指针指向最后一个节点d2(头结点的prev就指向最后一个节点),
先让新节点的next指向头结点,
然后让头结点的prev指向新节点,
再让新节点的prev指向d2,
最后让d2的next指向新节点,
由此可写出代码,
void LTPushBack(LTNode* phead, int x)
{
assert(phead);
LTNode* newnode = LTBuyNewNode(x);
LTNode* tail = phead->prev;
newnode->next = phead;//让新节点的next指向头结点
phead->prev = newnode;//让头结点的prev指向新节点
newnode->prev = tail;//让新节点的prev指向最后一个节点
tail->next = newnode;//最后一个节点的next指向新节点
}
4、头删
头删时,
定义指针frist指向第一个节点d1,指针second指向第一个节点的后一个
d2,
先让头结点的next指向d2,
再让d2的prev指向头结点,
最后释放掉frist指向的d1即可。
代码如下,
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next!=phead);//如果头结点的next指向头结点,那么链表为空
LTNode* frist = phead->next;//指向第一个节点
LTNode* second = frist->next;//指向第一个节点的后一个节点
phead->next = second;
second->prev = phead;
free(frist);
}
当链表只有一个节点时,
根据逻辑图,我们写的代码在只有一个节点时依旧适用。
5、尾删
定义指针tail指向最后一个节点d2,指针tailPrev指向最后一个节点的前一个节点,
让头节点的prev指向d1,
让d2的next指向头结点,
释放掉tail指向的最后一个节点,
代码如下,
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead);
LTNode* tail = phead->prev;//指向最后一个节点
LTNode* tailPrev = tail->prev;//指向最后一个节点的前一个节点
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
}
6、查找
查找的实现非常简单,只需要设置一个指针遍历链表即可,找到了就返回节点的地址,没找到就返回空指针。
LTNode* LTFind(LTNode* phead, int x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)//遍历链表
{
if (cur->data == x)//如果这个节点的data等于x
{
return cur;//返回这个节点的地址
}
cur = cur->next;
}
return NULL;//没有找到就返回空指针
}
7、插入
在使用插入函数时,需要传一个指针pos,我们需要在pos的前面插入一个新节点。
pos指向d2,定义指针posPrev指向pos指向节点的的前一个节点d1。
让新节点的next指向d2,再让d2的prev指向新节点,
让新节点的prev指向d1,再让d1的next指向新节点,
代码如下
void LTInsert(LTNode* pos, int x)
{
assert(pos);
LTNode* newnode = LTBuyNewNode(x);
LTNode* posPrev = pos->prev;
newnode->next = pos;
pos->prev = newnode;
newnode->prev = posPrev;
posPrev->next = newnode;
}
我们前面写的头插函数和尾插函数都可以直接复用插入函数
void LTPushFronts(LTNode* phead,int x)
{
assert(phead);
LTInsert(phead, phead->next, x);
}
头插是pos指向第一个节点,
void LTPushBack(LTNode* phead, int x)
{
assert(phead);
LTInsert(phead, phead, x);
}
尾插时pos指向头结点。
8、删除
定义指针posPrev指向pos指向的节点d2的前一个节点d1,指针posNext指向pos指向节点d2的下一个节点d3,
让d1的next指向d3,
让d3的prev指向d1,
最后释放掉pos指向的节点d2。
代码如下,
void LTErase(LTNode* phead,LTNode* pos)
{
//因为删除时需要检查链表是否为空
//所以需要把头指针传过来
assert(pos);
assert(pos != phead);//不能删头结点
assert(phead->next != phead);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
}
前面写的头删函数和尾删函数也可以复用删除函数,
void LTPopFront(LTNode* phead)
{
assert(phead);
LTErase(phead, phead->next);
}
头删时pos指向第一个节点,
void LTPopBack(LTNode* phead)
{
assert(phead);
LTErase(phead, phead->prev);
}
尾删是pos指向最后一个节点。
9、销毁
销毁时只需要定义一个指针cur从第一个节点开始遍历链表,遍历到哪个节点就销毁掉那个节点,遍历完后再销毁头结点,
代码如下
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
LTNode* Next = NULL;
while (cur != phead)
{
Next = cur->next;//记录下一个节点
free(cur);
cur = Next;
}
free(phead);
}
注意,我们传的是一级指针,只是头指针的一份临时拷贝,无法在函数内把头结点置空,所以需要使用销毁函数的人自己置空。
总的来说,带头双向循环链表是优于不带头单向非循环链表的。