
上一篇博客,我们了解并实现了单向+不带头+不循环链表,而本篇博客会讲解链表中的王者:双向+带头+循环链表。
概述
双向+带头+循环链表的特点是:
- 每个结点内部,既有指向上一个结点的前驱指针prev,也有指向下一个结点的后继指针next。
 - 第一个结点,是哨兵位的头结点,不存储有效数据。从第二个结点开始存储有效数据。
 - 最后一个结点的后继指针指向第一个结点,第一个结点的前驱指针指向最后一个结点。
 
这个结构,感觉上就是单向+不带头+不循环链表完全反过来!那么这么设计有什么优点呢?看过我的上一篇博客的朋友应该都能感觉到,单链表的实现相当麻烦,看起来结构简单,但是实现起来复杂、逻辑复杂、效率低。事实上,本篇博客讲解的双链表看起来结构复杂,但是实现起来逻辑简单、效率高。具体等实现之后大家体会更深。
双链表结点的声明如下:
// 链表存储的数据类型
typedef int LTDataType;
// 带头+双向+循环链表
typedef struct ListNode
{
	LTDataType data;       // 存储数据
	struct ListNode* next; // 指向下一个结点
	struct ListNode* prev; // 指向上一个结点
}ListNode;
 
申请结点
先写一个函数,向堆区申请一个结点,函数的声明如下:
ListNode* BuyListNode(LTDataType x);
 
使用malloc函数申请一个结点,并对data、next、prev进行初始化即可。
ListNode* BuyListNode(LTDataType x)
{
	// 创建新结点
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	// 判断是否创建成功
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	// 初始化
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}
 
初始化
接下来写一个函数来创建一个链表,并返回哨兵位的头结点。函数的声明如下:
ListNode* ListCreate();
 
根据双向+带头+循环链表的定义,我们需要开辟一个哨兵位的头结点,这个结点不存储有效的数据。根据“循环”的特点,当只有一个结点时,它的prev和next都是它自己。
ListNode* ListCreate()
{
	// 创建哨兵位
	ListNode* newnode = BuyListNode(0);
	// 链接
	newnode->next = newnode;
	newnode->prev = newnode;
	return newnode;
}
 
销毁
接下来写一个函数来销毁链表,函数声明如下:
void ListDestroy(ListNode* phead);
 
遍历双向链表,并依次销毁即可。遍历时需要注意:从phead->next开始,等于phead时结束。销毁之前要保存下一个结点,否则就找不到下一个了。把其他结点都销毁完后,再销毁哨兵位。
注意需要检查phead指针的有效性,因为哪怕链表为空,也有哨兵位,phead一定不为空,后面的函数同理。
void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* del = phead->next;
	// 遍历+删除
	while (del != phead)
	{
		ListNode* next = del->next;
		// 删除
		free(del);
		// 迭代
		del = next;
	}
	// 释放哨兵位
	free(phead);
	phead = NULL;
}
 
打印
下面写一个函数来打印链表中的数据,方便后面的测试。这是函数声明:
void ListPrint(ListNode* phead);
 
和上一个函数类似,也是从phead->next开始遍历,到phead结束。
void ListPrint(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	printf("哨兵位");
	// 遍历+打印
	while (cur != phead)
	{
		printf("<==>%d", cur->data);
		// 迭代
		cur = cur->next;
	}
	printf("\n");
}
 
判空
接下来写一个函数,来判断链表是否为空链表。这是函数声明:
bool ListEmpty(ListNode* phead);
 
链表什么时候为空呢?当链表只有一个哨兵位的头结点时,链表中是没有有效数据的,此时我们认为链表为空。具体的判断就只需要判断phead->next和phead是否相等即可。
bool ListEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
 
查找
再来个简单的函数,在链表中查找数据。函数的声明如下:
ListNode* ListFind(ListNode* phead, LTDataType x);
 
和前面的遍历完全一样。从phead->next开始遍历数据,遇到phead结束。
ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* cur = phead->next;
	// 遍历+查找
	while (cur != phead)
	{
		if (cur->data == x)
		{
			// 找到了
			return cur;
		}
		// 迭代
		cur = cur->next;
	}
	// 没找到
	return NULL;
}
 
插入
下面我们来实现一个函数,在链表中的指定结点前面插入一个新的结点。函数的声明如下:
void ListInsert(ListNode* pos, LTDataType x);
 
由于双向链表的特性,我们知道了pos,就可以找到pos的前一个结点prev,然后在prev和pos中间插入新结点即可。
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	// 链接prev<==>newnode<==>pos
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}
 
有了Insert函数,我们就可以实现尾插和头插了。尾插就是在哨兵位的头结点前面插入,头插就是在phead->next前面插入。
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead, x);
}
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}
 
删除
最后实现一个函数,删除链表中的指定结点,函数的声明如下:
void ListErase(ListNode* pos);
 
找到pos的前一个结点prev和后一个结点next,删除pos结点,链接prev和next即可。
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	// 释放pos
	free(pos);
	pos = NULL;
	// 链接
	prev->next = next;
	next->prev = prev;
}
 
有了Erase函数,就可以轻松实现尾删和头删了。尾删就是删除phead->prev,头删就是删除phead->next。
严谨起见,最好先断言一下链表非空。
void ListPopBack(ListNode* phead)
{
	assert(phead);
	// 断言链表非空
	assert(!ListEmpty(phead));
	ListErase(phead->prev);
}
void ListPopFront(ListNode* phead)
{
	assert(phead);
	// 断言链表非空
	assert(!ListEmpty(phead));
	ListErase(phead->next);
}
 
总结
呼,这就搞定了!大家可以对比一下上一篇博客中的单链表,那叫一个天上地下!真是没有对比就没有伤害。双向链表的结构确实比单链表的结构要复杂,但是由于其结构特点,没有死角,能够实现以O(1)的时间复杂度在任意位置插入删除数据,非常强大。不过链表还是有缺点的,它在内存中不是连续存放的,这就导致了其无法实现下标的随机访问,并且缓存命中率较低,存在缓存污染。
感谢大家的阅读!


















