一、链表的概念与结构

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑结构是通过链表中的指针链接次序实现的。

下面是链表结构的逻辑图:

C语言实现链表_链表

1、从上图可以看出,链式结构在逻辑上是连续的,但是在物理结构上不一定连续。

2、链表中节点使用的空间一般都是从堆上申请的。

3、从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续也可能不连续。

二、链表的分类

1、单向或者双向

C语言实现链表_双向循环链表_02

C语言实现链表_双向循环链表_03

2、带头或者不带头

C语言实现链表_双向循环链表_04

C语言实现链表_双向循环链表_02

3、循环或非循环

C语言实现链表_头结点_06

C语言实现链表_双向循环链表_02

这么多的链表结构,最常用的无非两种

第一种是不带头单向非循环链表

C语言实现链表_双向循环链表_02

这种链表结构简单,一般不会用来单独存数据。实际中更多是作为其他数据结构的子结构,如希哈桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

另一种是带头双向循环链表

C语言实现链表_头结点_09

这种链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然很复杂,但是使用代码实现后会发现结构带来了很多优势,实现反而更简单了。

三、不带头单向非循环链表的实现

首先我们用结构体来定义节点类型

//定义节点
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、头插

我们在插入和删除时,一般需要考虑链表是空或者非空两种情况。

当链表为空时,

C语言实现链表_头结点_10

我们想要插入新节点,只需要让新节点的next指向头指针指向的地址,然后再让头指针指向新节点就好了,逻辑图如下

C语言实现链表_头结点_11

当链表非空时,

C语言实现链表_链表_12

其实也是让新节点的next指向头指针指向的地址,然后再让头指针指向新节点,逻辑图如下

C语言实现链表_双向循环链表_13

由此,我们可以写出代码

void SLPushFront(SLTNode** pphead, int x)
 //因为头插需要改变头指针,所以我们传参传的是指向头指针的指针
{
	assert(pphead);
	SLTNode* newnode = SLBuyNewNode(x);
	newnode->next = *pphead;//让新节点next指头指针指向的地址
	*pphead = newnode;//让头指针指向新节点
}

2、尾插

当链表为空时,

C语言实现链表_头结点_14

只需要让头指针指向新的节点就好了。

当链表非空时,

C语言实现链表_头结点_15

首先要找到最后一个节点,让最后一个节点的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、头删

在删除时,链表不能为空。

C语言实现链表_链表_16

首先,我们先定义一个指针指向第一个节点(这个节点就是我们要删除的节点),然后让头指针指向第一个节点的next,然后用free函数把第一个节点释放掉,就完成头删了。一个节点或多个节点都是同样的方法。

由此,我们可以写出代码

void SLPopFront(SLTNode** pphead)
{
	assert(pphead);//指向头指针的指针不能为空
	assert(*pphead);//头指针也不能为空
	SLTNode* freenode = *pphead;//指向要删除的节点
	*pphead = (*pphead)->next;//让指针指向第一个节点的next
	free(freenode);//释放掉要删除的节点
	freenode = NULL;
}

4、尾删

C语言实现链表_双向循环链表_17

C语言实现链表_头结点_18

C语言实现链表_双向循环链表_19

定义一个指针cur遍历链表,当cur->next->next==NULL时停止(此时cur指向的节点的下一个节点就是我们要删除的尾结点),我们释放掉cur->next(尾结点),最后让cur->next=NULL即可。

但是有一种特殊情况,就是链表只有一个节点时。

C语言实现链表_双向循环链表_20

此时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,这个指针指向链表的一个节点,我们需要在这个节点前插入新节点。

C语言实现链表_头结点_21

C语言实现链表_链表_22

C语言实现链表_头结点_23

只需要用一个指针来遍历链表,直到找到pos指向节点的前一个节点,然后就可以开始插入了。

有两种特殊情况,第一种情况时pos指向的节点时第一个节点,

C语言实现链表_头结点_24

此时只需要复用我们上面写的头插即可。

另一种情况是pos指向空,

C语言实现链表_双向循环链表_25

此时复用上面写的尾插即可。

由此可以写出代码

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、销毁

C语言实现链表_头结点_26

C语言实现链表_头结点_27

C语言实现链表_链表_28

C语言实现链表_头结点_29

销毁时我们要使用两个指针,一个指针指向要销毁的节点,另一个指针指下一个节点,就这样从第一个节点开始依次销毁所有节点,最后再把链表置空即可。

代码如下

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、初始化

C语言实现链表_双向循环链表_30

我们需要给链表设置一个头结点,头结点储存的数据没有任何意义,因为是循环链表,所以头结点的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、头插

C语言实现链表_头结点_31

C语言实现链表_双向循环链表_32

定义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指向新节点
}

当链表为空时,我们按照上面的来画出逻辑图,

C语言实现链表_链表_33

C语言实现链表_链表_34

C语言实现链表_头结点_35

C语言实现链表_链表_36

C语言实现链表_链表_37

由此观之,当链表为空时,上面的代码也行之有效。

3、尾插

在尾插时,

C语言实现链表_链表_38

定义tail指针指向最后一个节点d2(头结点的prev就指向最后一个节点),

C语言实现链表_头结点_39

先让新节点的next指向头结点,

C语言实现链表_链表_40

然后让头结点的prev指向新节点,

C语言实现链表_头结点_41

再让新节点的prev指向d2,

C语言实现链表_头结点_42

最后让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、头删

头删时,

C语言实现链表_双向循环链表_43

定义指针frist指向第一个节点d1,指针second指向第一个节点的后一个

d2,

C语言实现链表_头结点_44

先让头结点的next指向d2,

C语言实现链表_头结点_45

再让d2的prev指向头结点,

C语言实现链表_头结点_46

最后释放掉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);
}

当链表只有一个节点时,

C语言实现链表_链表_47

C语言实现链表_头结点_48

C语言实现链表_链表_49

C语言实现链表_链表_50

根据逻辑图,我们写的代码在只有一个节点时依旧适用。

5、尾删

C语言实现链表_双向循环链表_51

定义指针tail指向最后一个节点d2,指针tailPrev指向最后一个节点的前一个节点,

C语言实现链表_双向循环链表_52

让头节点的prev指向d1,

C语言实现链表_头结点_53

让d2的next指向头结点,

C语言实现链表_头结点_54

释放掉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的前面插入一个新节点。

C语言实现链表_双向循环链表_55

pos指向d2,定义指针posPrev指向pos指向节点的的前一个节点d1。

C语言实现链表_双向循环链表_56

让新节点的next指向d2,再让d2的prev指向新节点,

C语言实现链表_头结点_57

让新节点的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、删除

C语言实现链表_双向循环链表_58

定义指针posPrev指向pos指向的节点d2的前一个节点d1,指针posNext指向pos指向节点d2的下一个节点d3,

C语言实现链表_头结点_59

让d1的next指向d3,

C语言实现链表_链表_60

让d3的prev指向d1,

C语言实现链表_双向循环链表_61

最后释放掉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);
}

注意,我们传的是一级指针,只是头指针的一份临时拷贝,无法在函数内把头结点置空,所以需要使用销毁函数的人自己置空。

总的来说,带头双向循环链表是优于不带头单向非循环链表的。