【数据结构】双向循环带头链表

一、什么是双向循环带头链表?

1. 概念

双向循环带头链表(Doubly Circular Linked List with Head Node) 是一种结合了双向指针、循环结构和**头节点(哨兵节点)**的复杂链表形式,兼具灵活性与操作便捷性。

2. 结构

链表整体结构:

在这里插入图片描述
链表节点结构:

双向链表相对于单链表来说,除了数据域和指向下一个节点的后继指针,还多了一个指向前一个节点的前驱指针。

typedef int DLTDataType;
typedef struct DListNode
{
	DLTDataType val; // 数据域
	struct DListNode* prev; // 前驱指针
	struct DListNode* next; // 后继指针
}DLTNode;

3. 与单链表的比较

3.1 结构与内存占用对比

特性双向循环链表单链表
节点结构包含 prev 和 next 两个指针仅包含 next 指针
循环性尾节点 next 指向头节点,头节点 prev 指向尾节点尾节点 next 指向 NULL
内存占用每个节点多一个指针(空间复杂度 O(n) 更高)空间占用较低

3.2 操作复杂度与效率对比

操作双向循环链表单链表
插入/删除任意位置 O(1)(直接操作前驱和后继指针)已知前驱节点时 O(1),否则需遍历 O(n)
遍历方向支持双向遍历(正向/反向)仅支持单向遍历(从头到尾)
头尾操作头插/头删、尾插/尾删均 O(1)头插/头删、尾插/尾删均 O(1)
查找特定节点双向遍历可能减少平均查找时间(灵活方向)必须从头开始遍历 O(n)

二、双向循环带头链表的实现

1. 初始化一个双线循环带头链表

申请一个节点

DLTNode* DLTBuyNode(DLTDataType x)
{
	DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
	node->val = x;
	node->prev = node; // 前驱和后继都指向自己
	node->next = node;
}

初始化链表

DLTNode* InitLink()
{
	//return DLTBuyNode(0); // 因为后续操作和申请节点的操作完全一样,所以其实可以直接调用申请节点

	DLTNode* phead = (DLTNode*)malloc(sizeof(DLTNode));
	phead->val = 0;
	// 初始化时,虚拟头节点的前驱和后继指针均指向自己
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

为什么虚拟头节点的前驱和后继指针都指向自己呢?

假设存在多个节点,那么虚拟头节点的前驱指向的是链表尾节点,此时只有它自己,所以它自己就是自己的尾
同理,尾节点的后继指向的是链表的头节点,它既是头也是尾,所以均指向自己

2. 尾插

因为是双向循环带头链表,所以虚拟头节点的前驱即为尾节点。
那么要修改几个指针呢?
四个!新节点的前驱和后继指针、虚拟头节点的前驱指针、尾节点的前驱指针。
我们又该以什么样的顺序去修改这四个指针呢?
个人建议先修改newnode的两个指针,再去修改头尾节点,对于不熟的同学来说,这样能在一定程度上避免链表断连。

void DLTPushBack(DLTNode* phead, DLTDataType x)
{
	DLTNode* node = DLTBuyNode(x);
	// 我建议先修改新节点的指针
	// 总共需要修改四个指针
	// 新节点的后继为头节点
	node->next = phead;
	// 新节点的前驱为原来的尾节点
	node->prev = phead->prev;
	// 原来尾节点的后继为新节点
	phead->prev->next = node;
	// 头节点的前驱为新节点
	phead->prev = node;
}

3. 头插

双向循环带头链表相比于单链表来说,虽然看着多了个指针,貌似结构会更复杂,但是链表的插入和删除极其简单,所以后续便不做过多赘述,都参考尾插和尾删即可。实在是不理解的话,那还是通过画图去理解吧。

void DLTPushFront(DLTNode* phead, DLTDataType x)
{
	DLTNode* node = DLTBuyNode(x);
	// 新节点的后继为原来的头节点
	node->next = phead->next;
	// 新节点的前驱为虚拟头节点
	node->prev = phead;
	// 原来头节点的前驱为新节点
	phead->next->prev = node;
	// 虚拟头节点的后继为新节点
	phead->next = node;
}

4. 尾删

先用cur记录尾节点,再将虚拟头节点的前驱置为尾节点的前驱,尾节点的前驱的后继(新尾节点的后继)置为虚拟头节点,最后释放cur。

void DLTPopBack(DLTNode* phead)
{
	if (phead->prev == phead)
	{
		perror("The linked list is empty\n");
		exit(1);
	}
	// 记录尾节点
	DLTNode* cur = phead->prev;
	// 新尾节点为原尾节点的前驱
	phead->prev = cur->prev;
	// 新尾节点的前驱为虚拟头节点
	cur->prev->next = phead;
	free(cur);
}

5. 头删

void DLTPopFront(DLTNode* phead)
{
	if (phead->next == phead)
	{
		perror("The linked list is empty\n");
		exit(1);
	}

	// 记录头节点
	DLTNode* cur = phead->next;
	// 虚拟头节点的后继为头节点的后继
	phead->next = phead->next->next;
	// 新头节点的前驱为虚拟头节点
	cur->next->prev = phead;
	free(cur);
}

6. 查找

DLTNode* DLTFind(DLTNode* phead, DLTDataType x)
{
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->val == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

7. 在指定位置之前插入数据

void DLTInsert(DLTNode* phead, DLTNode* pos, DLTDataType x)
{
	DLTNode* node = DLTBuyNode(x);
	// 新节点的后继为pos
	node->next = pos;
	// 新节点的前驱为pos的前驱
	node->prev = pos->prev;
	// pos的前驱的后继为新节点
	pos->prev->next = node;
	// pos的前驱为新节点
	pos->prev = node;
}

8. 删除pos节点

void DLTErase(DLTNode* pos)
{
	// pos的前驱的后继为pos的后继
	pos->prev->next = pos->next;
	// pos的后继的前驱为pos的前驱
	pos->next->prev = pos->prev;
	free(pos);
}

9. 销毁链表

// 上层调用时记得手动置空虚拟头节点
void DLTDesTroy(DLTNode* phead)
{
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		DLTNode* prev = cur;
		cur = cur->next;
		free(prev);
	}
	free(phead);
}

三、源码

还是老三样:头文件、源文件、测试文件

DList.h

#pragma once

#include <stdio.h>
#include <stdlib.h>

typedef int DLTDataType;
typedef struct DListNode
{
	DLTDataType val;
	struct DListNode* prev;
	struct DListNode* next;
}DLTNode;

// 打印
void DLTPrint(DLTNode* phead);

// 初始化一个双向带头循环链表
DLTNode* InitLink();

// 尾插
void DLTPushBack(DLTNode* phead, DLTDataType x);

// 头插
void DLTPushFront(DLTNode* phead, DLTDataType x);

// 尾删
void DLTPopBack(DLTNode* phead);

// 头删
void DLTPopFront(DLTNode* phead);

// 查找
DLTNode* DLTFind(DLTNode* phead, DLTDataType x);

//在指定位置之前插入数据
void DLTInsert(DLTNode* phead, DLTNode* pos, DLTDataType x);

// 删除pos节点
void DLTErase(DLTNode* pos);

//销毁链表
void DLTDesTroy(DLTNode* phead);

DLst.c

#include "DList.h"


DLTNode* DLTBuyNode(DLTDataType x)
{
	DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
	node->val = x;
	node->prev = node;
	node->next = node;
}

void DLTPrint(DLTNode* phead)
{
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

DLTNode* InitLink()
{
	//return DLTBuyNode(0);

	DLTNode* phead = (DLTNode*)malloc(sizeof(DLTNode));
	phead->val = 0;
	// 初始化时,虚拟头节点的前驱和后继指针均指向自己
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

void DLTPushBack(DLTNode* phead, DLTDataType x)
{
	DLTNode* node = DLTBuyNode(x);
	// 我建议先修改新节点的指针
	// 总共需要修改四个指针
	// 新节点的后继为头节点
	node->next = phead;
	// 新节点的前驱为原来的尾节点
	node->prev = phead->prev;
	// 原来尾节点的后继为新节点
	phead->prev->next = node;
	// 头节点的前驱为新节点
	phead->prev = node;
}


void DLTPushFront(DLTNode* phead, DLTDataType x)
{
	DLTNode* node = DLTBuyNode(x);
	// 新节点的后继为原来的头节点
	node->next = phead->next;
	// 新节点的前驱为虚拟头节点
	node->prev = phead;
	// 原来头节点的前驱为新节点
	phead->next->prev = node;
	// 虚拟头节点的后继为新节点
	phead->next = node;
}


void DLTPopBack(DLTNode* phead)
{
	if (phead->prev == phead)
	{
		perror("The linked list is empty\n");
		exit(1);
	}
	// 记录尾节点
	DLTNode* cur = phead->prev;
	// 新尾节点为原尾节点的前驱
	phead->prev = cur->prev;
	// 新尾节点的前驱为虚拟头节点
	cur->prev->next = phead;
	free(cur);
}

void DLTPopFront(DLTNode* phead)
{
	if (phead->next == phead)
	{
		perror("The linked list is empty\n");
		exit(1);
	}

	// 记录头节点
	DLTNode* cur = phead->next;
	// 虚拟头节点的后继为头节点的后继
	phead->next = phead->next->next;
	// 新头节点的前驱为虚拟头节点
	cur->next->prev = phead;
	free(cur);
}

DLTNode* DLTFind(DLTNode* phead, DLTDataType x)
{
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->val == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void DLTInsert(DLTNode* phead, DLTNode* pos, DLTDataType x)
{
	DLTNode* node = DLTBuyNode(x);
	// 新节点的后继为pos
	node->next = pos;
	// 新节点的前驱为pos的前驱
	node->prev = pos->prev;
	// pos的前驱的后继为新节点
	pos->prev->next = node;
	// pos的前驱为新节点
	pos->prev = node;
}

void DLTErase(DLTNode* pos)
{
	// pos的前驱的后继为pos的后继
	pos->prev->next = pos->next;
	// pos的后继的前驱为pos的前驱
	pos->next->prev = pos->prev;
	free(pos);
}

// 上层调用时记得手动置空虚拟头节点
void DLTDesTroy(DLTNode* phead)
{
	DLTNode* cur = phead->next;
	while (cur != phead)
	{
		DLTNode* prev = cur;
		cur = cur->next;
		free(prev);
	}
	free(phead);
}

test.c

#include "DList.h"

void test_PushBack()
{
	DLTNode* phead = InitLink();
	DLTPushBack(phead, 1);
	DLTPushBack(phead, 2);
	DLTPushBack(phead, 3);
	DLTPushBack(phead, 4);
	DLTPushBack(phead, 5);
	DLTPrint(phead);
}

void test_PushFront()
{
	DLTNode* phead = InitLink();
	DLTPushFront(phead, 1);
	DLTPushFront(phead, 2);
	DLTPushFront(phead, 3);
	DLTPushFront(phead, 4);
	DLTPushFront(phead, 5);
	DLTPrint(phead);
}

void test_PopBack()
{
	DLTNode* phead = InitLink();
	DLTPushBack(phead, 1);
	DLTPushBack(phead, 2);
	DLTPushBack(phead, 3);
	DLTPushBack(phead, 4);
	DLTPushBack(phead, 5);
	DLTPrint(phead);

	DLTPopBack(phead);
	DLTPopBack(phead);
	DLTPrint(phead);
}


void test_DLTInsert()
{
	DLTNode* phead = InitLink();
	DLTPushBack(phead, 1);
	DLTPushBack(phead, 2);
	DLTPushBack(phead, 3);
	DLTPushBack(phead, 4);
	DLTPushBack(phead, 5);
	DLTPrint(phead);

	DLTNode* cur = DLTFind(phead, 3);
	DLTInsert(phead, cur, 6);
	DLTPrint(phead);
}

void test_DLTErase()
{
	DLTNode* phead = InitLink();
	DLTPushBack(phead, 1);
	DLTPushBack(phead, 2);
	DLTPushBack(phead, 3);
	DLTPushBack(phead, 4);
	DLTPushBack(phead, 5);
	DLTPrint(phead);

	DLTNode* cur = DLTFind(phead, 3);
	DLTErase(cur);
	DLTPrint(phead);
}

void test_DLTDesTroy()
{
	DLTNode* phead = InitLink();
	DLTPushBack(phead, 1);
	DLTPushBack(phead, 2);
	DLTPushBack(phead, 3);
	DLTPushBack(phead, 4);
	DLTPushBack(phead, 5);
	DLTPrint(phead);

	DLTDesTroy(phead);
	phead = NULL;
}


int main()
{
	//test_PushBack();
	//test_PushFront();
	//test_PopBack();
	//test_DLTInsert();
	//test_DLTErase();
	test_DLTDesTroy();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值