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

双向循环带头链表

  • 一、什么是双向循环带头链表?
    • 1. 概念
    • 2. 结构
    • 3. 与单链表的比较
      • 3.1 结构与内存占用对比
      • 3.2 操作复杂度与效率对比
  • 二、双向循环带头链表的实现
    • 1. 初始化一个双线循环带头链表
    • 2. 尾插
    • 3. 头插
    • 4. 尾删
    • 5. 头删
    • 6. 查找
    • 7. 在指定位置之前插入数据
    • 8. 删除pos节点
    • 9. 销毁链表
  • 三、源码
    • DList.h
    • DLst.c
    • test.c

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

1. 概念

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

2. 结构

链表整体结构:

【数据结构】双向循环带头链表_第1张图片
链表节点结构:

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

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 
#include 

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;
}

你可能感兴趣的:(数据结构,链表)