手撕数据结构 —— 带头双向循环链表(C语言讲解)

目录

0.前言

1.什么是带头双向循环链表

理解带头

​编辑

理解双向

理解循环

2.带头双向循环链表的实现

List.h文件中接口总览

具体实现 

结点的定义

申请结点

初始化

打印链表

尾插

尾删

头插

头删

​编辑​编辑

获取大小

查找 

在指定位置前插入

​编辑

删除指定位置的值

3.完整代码附录

List.h

List.c


0.前言

本篇文章旨在讲解带头双向循环链表的实现,如果读者并不了解链表的基础知识,推荐阅读 —— 手撕数据结构 —— 单链表(C语言讲解)

1.什么是带头双向循环链表

理解带头

什么是带头:带头的意思是链表多申请一个结点放在链表的起始位置,该结点并不存储有效元素。上图中的head结点就是头结点,该结点也往往称之为哨兵位。

带头的作用:该结点的主要作用是为了方便实现链表和操作链表。主要体现在两个方面:1、提供统一的操作方式。2、避免二级指针的使用。

  • 提供统一的操作方式。因为操作没有头结点的链表的时候,往往需要记录当前结点的上一个结点,但是第一个结点是没有上一个结点的,就需要特殊处理,设置哨兵位的头结点可以对所有存储有效元素的结点提供统一的操作方式。
  • 避免二级指针的使用。在实现单链表的时候,我们有时候需要改变的是结构体指针,这个时候就需要将参数设置为二级指针。有的时候需要改变的是结构体当中的成员,这个时候需要将参数设置为一级指针。这样不方便理解和实现,引入哨兵位的头结点之后,我们不需要改变结构体指针,避免了二级指针的使用。

关于带头的好处读者先了解,后续实现会深有体会。

理解双向

什么是双向:双向就是指结点中会包含两个指针域,一个指针域记录上一个结点的地址,一个指针域记录下一个结点的地址。 不像单链表,只是记录了下一个结点的地址。

双向的作用:在链表的实现中,往往需要使用当前结点的上一个结点(比如在某个位置之前插入节点)。对于单链表来说,只能在寻找指定节点的时候记录上一个结点,操作比较复杂,而双向链表中记录了上一个结点的地址,直接就能找到上一个结点,操作简单。

理解循环

什么是循环:循环的意思就是链表形成回路,最后一个结点的指针域指向第一个节点。

循环的作用:在操作链表的时候,我们往往知道头结点,需要寻找尾结点。单链表只能遍历链表去找,时间复杂度尾O(N);双向循环链表的头结点中记录了尾结点的地址,直接就能找到,时间复杂度为O(1),在需要找尾结点的操作中,大大提高了效率。

可以看出,带头双向循环链表是对单链表的升级,是一种提高链表效率的结构,是一种十分优秀的结构。

2.带头双向循环链表的实现

实现带头双向循环链表,我们主要实现List.h文件和List.c文件,List.h文件中存放声明,List.c文件中存放定义。

List.h文件中接口总览

具体实现 

结点的定义

带头双向循环链表的结点中需要记录数据、上一个结点以及下一个结点。

  • data用来记录有效数据。
  • prev记录上一个结点的地址。
  • next记录下一个结点的地址。

申请结点

我们使用malloc函数申请结点。

  • 申请的数据的类型是自定义的结点类型。
  • data设置为指定的值。
  • next指针和prev指针设置为空。

初始化

初始化就是申请一个哨兵位的头结点,该节点的prev和next都指向自己。

  • 初始化的时候,我们申请的结点是在堆空间上申请的,堆上申请的变量除非手动释放,否则一直存在。
  • 最后返回指向这块空间的指针变量。

打印链表

打印链表只需要遍历输出即可。

  • 注意phead的值不能为空,使用断言暴力判断。
  • cur指向要打印的元素,从哨兵位的下一个结点开始打印,当 cur == phead 的时候,说明所有的结点都打印了,退出循环,打印结束。

注意,所有涉及 LTNode* 类型的指针都不能为空! 

尾插

在链表的末尾插入结点,寻找尾结点的时候,直接一步到位,不需要遍历寻找。找到尾结点之后,依次和前一个结点连接,和后一个结点连接即可。

尾删

删除尾结点的时候,首先要找到尾结点和尾结点的前一个结点;释放尾结点后,将新的尾结点和哨兵位连接。

头插

头插是在哨兵位的后面,第一个有效结点的前面插入数据。需要注意的是:

  • 该代码中并没有记录phead的下一个结点,连接的时候需要从后往前连接。如果记录了phead的下一个结点,那么先连接和后连接哪个结点都可以。

头删

在头部删除数据时,我们删除的是哨兵位后面的第一个结点。

  • 依次记录哨兵位后面的第一个结点和第二个结点,删除的时候,只需要改变对于指针的指向即可。

获取大小

和打印链表的方法是一样的,只不过遍历的时候记录结点的个数并返回即可。

查找 

查找和打印差不多,通过遍历进行查找,当结点的数据等于指定元素时,返回该节点的地址即可,没找到返回NULL。

在指定位置前插入

我们先记录pos位置的前一个位置,然后连接即可。

删除指定位置的值

删除指定位置的结点,我们可以先记录指定位置的前一个结点和后一个结点,释放指定位置的节点,然后连接posPrev和posNext即可。

3.完整代码附录

List.h

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;                     //指向后一个结点 
	struct ListNode* prev;                     //指向前一个结点 
	LTDataType data;                           //存储数据 
}LTNode;


LTNode* BuyLTNode(LTDataType x);               //申请结点 

LTNode* LTInit();                              //初始化链表 

void LTPrint(LTNode* phead);                   //打印链表 

void LTPushBack(LTNode* phead, LTDataType x);  //尾插 

void LTPopBack(LTNode* phead);                 //尾删 

void LTPushFront(LTNode* phead, LTDataType x); //头插 

void LTPopFront(LTNode* phead);                //头删 

int LTSize(LTNode* phead);                     //获取链表大小 

LTNode* LTFind(LTNode* phead, LTDataType x);   //查找指定结点 

void LTInsert(LTNode* pos, LTDataType x);      //在指定结点位置插入 

void LTErase(LTNode* pos);                     //删除指定结点 

List.c

#include"List.h"

// 申请结点 
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

// 初始化 
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

// 打印 
void LTPrint(LTNode* phead)
{
	assert(phead);

	printf("phead<=>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

// 尾插 
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;      // 直接找到尾结点 
	LTNode* newnode = BuyLTNode(x);  // 申请一个新结点 

	// 连接newnode的tail 
	newnode->prev = tail;
	tail->next = newnode;
	// 连接newnode和phead 
	newnode->next = phead;
	phead->prev = newnode;
}

// 尾删 
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);  // 保证链表中有数据才删 

	LTNode* tail = phead->prev;    // 找到尾结点 
	LTNode* tailPrev = tail->prev; // 找到尾结点的前一个结点 
	free(tail);                    // 释放尾结点 
	
	// 将新的尾结点和哨兵位连接 
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

// 头插 
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x); // 申请新结点 
	
	// 连接新结点和哨兵位的下一个结点 
	newnode->next = phead->next;
	phead->next->prev = newnode;
	
	// 连接哨兵位和新结点 
	phead->next = newnode;
	newnode->prev = phead;
}

// 头删 
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 确保有存储数据的结点再删除 

	LTNode* first = phead->next;  // 记录第一个存储有效数据的结点 
	LTNode* second = first->next; // 记录第二个存储有效数据的结点 

	free(first);                  // 释放第一个存储有效数据的结点 

	// 将哨兵位和第二个存储有效数据的结点连接 
	phead->next = second;
	second->prev = phead;
}


// 查找 
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	
	LTNode* cur = phead->next; // 记录第一个存储有效元素的结点 
	while(cur != phead)
	{
		if(cur->data == x)
			return cur;
	}
	
	return NULL;
}

// 在指定位置之前插入数据 
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);                    // 指定的位置不能为空 

	LTNode* posPrev = pos->prev;    // 记录指定位置的前一个位置 
	
	LTNode* newnode = BuyLTNode(x); // 申请新结点 

	// 将新结点链接进链表中 
	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

// 删除指定位置的值 
void LTErase(LTNode* pos)
{
	assert(pos);                  // 指定位置不能为空 
	
	LTNode* posPrev = pos->prev;  // 记录pos的前一个位置 
	LTNode* posNext = pos->next;  // 记录pos的后一个位置 

	free(pos);                    // 释放pos指向的节点 

	// 连接posPrev和posNext 
	posPrev->next = posNext;
	posNext->prev = posPrev;
}