数据结构 —— 双向循环链表

在上次文章中讨论了单向链表的实现,这次我们来讨论一个更复杂的链表即双向循环链表。

循环与双向

1、循环链表

之前我们实现的单向链表是由第一个节点开始指向最后一个节点结束。而循环链表的对吼一个节点next并不指向NULL,而是重新指向头节点。

2、双向链表

双向链表的每个节点都包含有两个指针,一个指向后一个节点,另一个指向前一个节点。


实现

了解了循环与双向的概念,下面我们进行具体实现。我们还是先从头文件开始。

头文件

cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int ListType;

struct Node
{
	ListType data;
	struct Node* next;
	struct Node* prev;
};

struct List
{
	Node* _head;
};

void InitList(List* p);
void Destroy(List* p);
void PrintList(List* p);
Node* BuyNewNode(ListType x);
void HeadInsert(List* p, ListType x);
void TailInsert(List* p, ListType x);
void HeadDelete(List* p);
void TailDelete(List* p);
Node* FindElem(List* p, ListType x);

和之前的单向链表相同,只不过节点中多了一个指向前一个节点的prev指针。


源文件

1、创建新节点

这次我们先来实现创建新节点的函数。

cpp 复制代码
Node* BuyNewNode(ListType x)
{
	Node* newnode = (Node*)malloc(sizeof(ListType));
	if (newnode == NULL)
	{
		perror("malloc newnode falied!");
		return;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

原理相同,malloc创建内存后将节点的数据初始化。

2、初始化

下面进行初始化。这里我们采取使用哨兵位头节点的方式来进行。

什么是哨兵位?

哨兵位节点并不是一个实际的数据,它的存在可以简化操作,能大大提高效率;它满足下面三个特点:

  • 不存有效数据。即任意填入一个数据占位即可。
  • 永远存在。若不销毁,链表初始化后就永远存在。
  • 简化代码。使用哨兵位节点之后,插入第一个节点,删除最后一个节点,判断空链表等操作就不必使用繁琐的if判断,从而大大简化代码。这一点我们从后面功能的实现中就能慢慢体会到。

因此,初始化函数的实现如下:

cpp 复制代码
void InitList(List* p)
{
	assert(p);
	p->_head = BuyNewNode(0);//哨兵位头节点
	p->_head->next = p->_head;
	p->_head->prev = p->_head;
	//双向循环初始化
}

我们给哨兵位塞入了0的无效数据,将前后指针都指向它本身来实现循环。

3、销毁

下面我们来实现销毁。因为是循环的,最后一个节点会重新指向头节点,因此我们可以使用while循环,用cur标记当前节点,遍历释放后如果cur走到最后一个节点时,会指向头节点,此时说明完成遍历,还需要跳出循环释放头节点。具体实现如下:

cpp 复制代码
void DestroyList(List* p)
{
	assert(p);

	Node* cur = p->_head->next;//标记第一个有效节点
	while (cur != p->_head)
	{
		Node* next = cur->next;
		free(cur);
		cur = next;
	}

	free(p->_head);
	p->_head = NULL;
}

4、头插

直接看代码:

cpp 复制代码
void HeadInsert(List* p, ListType x)
{
	assert(p);
	Node* newnode = BuyNewNode(x);
	//新节点的前后
	newnode->next = p->_head->next;
	newnode->prev = p->_head;
	//双向
	p->_head->next->prev = newnode;
	p->_head->next = newnode;
}

有了前后指针插入就很方便了。要注意的是需要修改的有四条线,新节点的前后要链,而双向链表同样也要修改两条,所以需要修改四条线。

5、头删

直接上代码:

cpp 复制代码
void HeadDelete(List* p)
{
	assert(p);

	//空链表直接返回
	if (p->_head->next == p->_head)
		return;

	Node* tmp = p->_head->next;//标记要删除的节点

	p->_head->next = tmp->next;
	tmp->next->prev = p->_head;

	free(tmp);
}

这里加if判断是因为如果是空链表的情况会将哨兵位也删掉,所以需要使用if来判断一下。

6、尾插

下面实现尾插。和之前的单向不同,单向链表还需要遍历找到最后一个节点,而双向链表中头节点的前指针就指向上一个节点。因此,尾插的实现可以是下面的方式:

cpp 复制代码
void TailInsert(List* p, ListType x)
{
	assert(p);

	Node* tail = p->_head->prev;//标记最后一个节点

	Node* newnode = BuyNewNode(x);
	//新节点两条
	newnode->next = p->_head;
	newnode->prev = tail;
	//双向两条
	tail->next = newnode;
	p->_head->prev = newnode;
}

7、尾删

直接上代码:

cpp 复制代码
void TailDelete(List* p)
{
	assert(p);
	// 空表直接返回
	if (p->_head->next == p->_head)
		return;
	Node* tail = p->_head->prev;
	Node* prev = tail->prev;

	prev->next = p->_head;
	p->_head->prev = prev;

	free(tail);
}

其实双向循环链表的实现相比单向链表实现更简单,只是逻辑上更复杂。

8、查找对应值的元素

查找对应值的元素就只能进行遍历了。原理和单向链表大同小异。

cpp 复制代码
Node* FindElem(List* p,ListType x)
{
	assert(p);

	if (p->_head->next == p->_head)
	{
		printf("链表为空!\n");
		return NULL;
	}
	Node* cur = p->_head->next;
	while (cur != p->_head)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	printf("未找到对应值的元素!\n");
	return NULL;

我们也可以将查找和删除结合起来,写一个删除对应值的元素的函数:

cpp 复制代码
void ElemDelete(List* p, ListType x)
{
	assert(p);

	if (p->_head == p->_head->next)
	{
		perror("the list is empty!");
		return;
	}

	Node* cur = p->_head->next;
	while (cur != p->_head)
	{
		if (cur->data == x)
		{
			Node* next = cur->next;
			Node* prev = cur->prev;
			prev->next = next;
			next->prev = prev;
			free(cur);
			return;
		}
		cur = cur->next;
	}
	printf("未找到对应值的元素!删除失败!\n");
	return;
}

不过该函数只能删除第一个值为对应值的节点,不能将重复的节点全部删除。如果需要全部删除,我们可以标记一个变量来判断是否执行删除操作:

cpp 复制代码
void ElemDeleteAll(List* p, ListType x)
{
	assert(p);

	if (p->_head == p->_head->next)
	{
		perror("the list is empty!");
		return;
	}

	Node* cur = p->_head->next;
	int tmp = 0;
	while (cur != p->_head)
	{
		if (cur->data == x)
		{
			Node* next = cur->next;
			Node* prev = cur->prev;
			prev->next = next;
			next->prev = prev;
			free(cur);
			tmp++;
			cur = next;//删除也要让cur继续走
		}
		else
			cur = cur->next;
	}
	if (tmp == 0)
		printf("未找到对应值的元素!删除失败!\n");
	else
		printf("已成功删除了%d个元素。\n", tmp);
	return;
}

这样就能将对应值的元素全部删除。

9、打印链表

最后别忘了还有一个打印链表元素功能。直接上代码:

cpp 复制代码
void PrintList(List* p)
{
	assert(p);

	Node* cur = p->_head->next;
	while (cur != p->_head)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

运行

最后再main函数中调用:

cpp 复制代码
int main()
{
	Test();
	return 0;
}
cpp 复制代码
void Test()
{
	List lt;
	InitList(&lt);
	HeadInsert(&lt, 1);
	HeadInsert(&lt, 2);
	HeadInsert(&lt, 3);
	HeadInsert(&lt, 4);
	TailInsert(&lt, 1);
	TailInsert(&lt, 2);
	TailInsert(&lt, 3);
	TailInsert(&lt, 4);
	PrintList(&lt);
    return;
}

运行结果如下:


完整代码

1、头文件

cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int ListType;

typedef struct Node
{
	ListType data;
	struct Node* next;
	struct Node* prev;
}Node;

typedef struct List
{
	Node* _head;
}List;


void InitList(List* p);
void DestroyList(List* p);
void PrintList(List* p);
Node* BuyNewNode(ListType x);
void HeadInsert(List* p, ListType x);
void TailInsert(List* p, ListType x);
void HeadDelete(List* p);
void TailDelete(List* p);
Node* FindElem(List* p, ListType x);
void ElemDelete(List* p, ListType x);
void ElemDeleteAll(List* p, ListType x);

2、源文件

cpp 复制代码
#include "List.h"

void PrintList(List* p)
{
	assert(p);

	Node* cur = p->_head->next;
	while (cur != p->_head)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

Node* BuyNewNode(ListType x)
{
	Node* newnode = (Node*)malloc(sizeof(Node));
	if (newnode == NULL)
	{
		perror("malloc newnode falied!");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

void InitList(List* p)
{
	assert(p);
	p->_head = BuyNewNode(0);//哨兵位头节点
	p->_head->next = p->_head;
	p->_head->prev = p->_head;
	//双向循环初始化
}

void DestroyList(List* p)
{
	assert(p);

	Node* cur = p->_head->next;//标记第一个有效节点
	while (cur != p->_head)
	{
		Node* next = cur->next;
		free(cur);
		cur = next;
	}

	free(p->_head);
	p->_head = NULL;
}

void HeadInsert(List* p, ListType x)
{
	assert(p);
	Node* newnode = BuyNewNode(x);
	//新节点的前后
	newnode->next = p->_head->next;
	newnode->prev = p->_head;
	//双向
	p->_head->next->prev = newnode;
	p->_head->next = newnode;
}

void HeadDelete(List* p)
{
	assert(p);

	//空链表直接返回
	if (p->_head->next == p->_head)
		return;

	Node* tmp = p->_head->next;//标记要删除的节点

	p->_head->next = tmp->next;
	tmp->next->prev = p->_head;

	free(tmp);
	printf("头删元素成功。\n");
}

void TailInsert(List* p, ListType x)
{
	assert(p);

	Node* tail = p->_head->prev;//标记最后一个节点

	Node* newnode = BuyNewNode(x);
	//新节点两条
	newnode->next = p->_head;
	newnode->prev = tail;
	//双向两条
	tail->next = newnode;
	p->_head->prev = newnode;
}

void TailDelete(List* p)
{
	assert(p);
	// 空表直接返回
	if (p->_head->next == p->_head)
		return;
	Node* tail = p->_head->prev;
	Node* prev = tail->prev;

	prev->next = p->_head;
	p->_head->prev = prev;

	free(tail);
	printf("尾删元素成功。\n");
}

Node* FindElem(List* p,ListType x)
{
	assert(p);

	if (p->_head->next == p->_head)
	{
		printf("链表为空!\n");
		return NULL;
	}
	Node* cur = p->_head->next;
	while (cur != p->_head)
	{
		if (cur->data == x)
		{
			printf("成功找到值为%d的元素。\n");
			return cur;
		}
		cur = cur->next;
	}
	printf("未找到对应值的元素!\n");
	return NULL;
}

void ElemDelete(List* p, ListType x)
{
	assert(p);

	if (p->_head == p->_head->next)
	{
		perror("the list is empty!");
		return;
	}

	Node* cur = p->_head->next;
	while (cur != p->_head)
	{
		if (cur->data == x)
		{
			Node* next = cur->next;
			Node* prev = cur->prev;
			prev->next = next;
			next->prev = prev;
			free(cur);
			printf("找到值为%d的元素,已执行删除操作。\n", x);
			return;
		}
		cur = cur->next;
	}
	printf("未找到对应值的元素!删除失败!\n");
	return;
}

void ElemDeleteAll(List* p, ListType x)
{
	assert(p);

	if (p->_head == p->_head->next)
	{
		perror("the list is empty!");
		return;
	}

	Node* cur = p->_head->next;
	int tmp = 0;
	while (cur != p->_head)
	{
		if (cur->data == x)
		{
			Node* next = cur->next;
			Node* prev = cur->prev;
			prev->next = next;
			next->prev = prev;
			free(cur);
			tmp++;
			cur = next;//删除也要让cur继续走
		}
		else
			cur = cur->next;
	}
	if (tmp == 0)
		printf("未找到对应值的元素!删除失败!\n");
	else
		printf("已成功删除了%d个值为%d的元素。\n", tmp,x);
	return;
}

3、测试函数

cpp 复制代码
#include "List.h"

void Test()
{
	List lt;
	InitList(&lt);
	HeadInsert(&lt, 1);
	HeadInsert(&lt, 2);
	HeadInsert(&lt, 3);
	HeadInsert(&lt, 4);
	TailInsert(&lt, 1);
	TailInsert(&lt, 2);
	TailInsert(&lt, 3);
	TailInsert(&lt, 4);
	PrintList(&lt);
	TailDelete(&lt);
	TailDelete(&lt);
	PrintList(&lt);
	Node* Four = FindElem(&lt, 4);
	ElemDeleteAll(&lt, 2);
	PrintList(&lt);

	DestroyList(&lt);
	return;
}

int main()
{
	Test();
	return 0;
}

4、运行结果

相关推荐
程序员阿鹏2 小时前
怎么理解削峰填谷?
java·开发语言·数据结构·spring·zookeeper·rabbitmq·rab
LYFlied2 小时前
【每日算法】LeetCode 300. 最长递增子序列
前端·数据结构·算法·leetcode·职场和发展
fpcc3 小时前
跟我学C++中级篇—Linux内核中链表分析
linux·c++·链表
2401_877274244 小时前
2025数据结构实验八:排序
数据结构·算法·排序算法
youngee115 小时前
hot100-53搜索旋转排序数组
数据结构·算法·leetcode
C雨后彩虹5 小时前
ConcurrentHashMap 核心锁机制:CAS+Synchronized 的协同工作原理
java·数据结构·哈希算法·集合·hashmap
集芯微电科技有限公司5 小时前
DC-DC|40V/10A大电流高效率升压恒压控制器
c语言·数据结构·单片机·嵌入式硬件·fpga开发
C雨后彩虹6 小时前
HashMap的线程安全问题:原因分析与解决方案
java·数据结构·哈希算法·集合·hashmap
im_AMBER6 小时前
Leetcode 87 等价多米诺骨牌对的数量
数据结构·笔记·学习·算法·leetcode