带头双向循环链表

       上篇博客重点讲解了单链表的相关知识,单链表(无头单向不循环)是链表里面最简单的一种结构,而今天我们要讲的带头双向循环链表则是链表中最复杂的一种结构,叫做带头双向循环链表

       事实上,链表一种有八种结构:单向/双向,带头/不带头,循环/不循环,2^3便是八种,所以我们就讲了链表中的两种极端结构,带头双向循环链表虽然结构复杂,但代码实现起来反而容易

带头双向循环链表结构

1.带头:带哨兵位的头节点,之所以叫哨兵位,是因为这个节点不存储有效数据,只存储链表头节点的地址,方便我们找头节点以及更改头节点

2.双向:和单链表每一节点只存下一个节点的地址不同的是,该链表每个节点有两个指针域,第一个指针域存储上一个节点的地址,而第二个指针域存储下一个节点的地址,因此双向链表很方便找到上一个节点(而单链表需要定义一个新的指针,遍历找到指定节点的前一个节点,时间复杂度高)

3.循环:我们知道,单链尾节点的next存储的是NULL,而循环链表尾节点的next存储的是哨兵位头节点的地址,哨兵位头节点的prev存储的是尾节点的地址,因此构成了循环结构

 带头双向循环链表实现

 全部代码如下

 List.h

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	LTDataType data;
	struct ListNode* next;
}LTNode;

//初始化
LTNode* LTInit();

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

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

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

//判空
bool LTEmpty(LTNode* phead);
 
//尾删
void LTPopBack(LTNode* phead);

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

//查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//插入
void LTInsertFront(LTNode* pos, LTDataType x);

//删除
void LTErase(LTNode* pos);

//销毁
void LTDestroy(LTNode* phead);

 List.c

#include"List.h"

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
	if (newNode == NULL)
	{
		perror("malloc");
		return;
	}
	newNode->data = x;
	newNode->prev = NULL;
	newNode->next = NULL;
	return newNode;
}


LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("guard<=>");
	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* newNode = BuyLTNode(x);
	LTNode* tail = phead->prev;

	tail->next = newNode;
	newNode->prev = tail;
	newNode->next = phead;
	phead->prev = newNode;
}


void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* head = phead->next;
	LTNode* newNode = BuyLTNode(x);

	phead->next = newNode;
	newNode->prev = phead;
	newNode->next = head;
	head->prev = newNode;
}


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


bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}



void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);

	phead->prev = tailPrev;
	tailPrev->next = phead;
}


void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* first = phead->next;
	LTNode* second = first->next;
	phead->next = second;
	second->prev = phead;

	free(first);
}

//void LTPopFront(LTNode* phead)
//{
//	assert(phead);
//	assert(!LTEmpty(phead));
//
//	LTNode* first = phead->next;
//
//	phead->next = first->next;
//	first->next->prev = phead;
//	free(first);
//}



LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while(cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}


void LTInsertFront(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;
	LTNode* posnext = pos->next;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
}


void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

 test.c

#include"List.h"

void TestList1()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPrint(plist);
}

void TestList2()
{
    LTNode* plist = LTInit();
    LTPushFront(plist, 1);
    LTPushFront(plist, 2);
    LTPushFront(plist, 3);
    LTPushFront(plist, 4);
    LTPrint(plist);
}


void TestList3()
{
    LTNode* plist = LTInit();
    LTPushFront(plist, 1);
    LTPushFront(plist, 2);
    LTPushFront(plist, 3);
    LTPushFront(plist, 4);
    LTPopBack(plist);
    LTPrint(plist);
}

void TestList4()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTPopFront(plist);
    LTPopFront(plist);
    LTPrint(plist);
}

void TestList5()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTNode* pos = LTFind(plist,3);
    pos->data = 30;
    LTPrint(plist);
}

void TestList6()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTNode* pos = LTFind(plist, 3);
    LTInsertFront(pos,10);
    LTPrint(plist);
}

void TestList7()
{
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushBack(plist, 4);
    LTNode* pos = LTFind(plist, 3);
    LTErase(pos);
    LTPrint(plist);
}

int main()
{
    //TestList1();
    //TestList2();
    //TestList3();
    //TestList4();
    //TestList5();
    //TestList6();
    TestList7();
}


1.定义结构

 有了单链表的基础,这块理解起来就容易多啦,无非多加了一个prev指针指向前一个节点

2.功能实现

       需要先给大家说明的是,在讲解单链表函数实现的时候,头插头删等等这些需要改变头节点的指针的函数,我们的形参都使用的是二级结构体指针,但是我们带头双向循环链表的所有函数形参使用一级指针即可,因为带哨兵位的头节点中存储了头节点地址,因此想要改变头节点,只需要改变哨兵位头节点这个结构体的成员变量(next)即可,不需要改变外部的plist,因此一级指针就行

创建节点

①链表初始化

       注意,带头双向循环链表为空不是没有一个节点,而是只有带哨兵位的头节点 ,因此我们对其进行初始化一下。

       虽然只有一个哨兵位头节点,但还是应该满足带头双向循环的特点,因此结构如下:

 说简单一点,就是其中的prev和next都指向了自己这个节点本身

 由于头节点不存储有效数据,因此我们随便给个数字就行

②打印链表

思路:和单链表一样,需要遍历整个链表,但关键点就是循环结束的条件,单链表是尾节点的next指向空,而带头双向循环链表特点是尾节点的next是phead

 为了避免改掉头节点,我们还是选择用新的变量tail/cur 进行遍历

注意两点:

1. cur 的初始值不是phead, 而是phead->next,因为哨兵位头节点不存储有效数据,所以无需打印

2.循环继续条件不是 cur->next != phead, 是cur!=phead,否则cur为尾节点的时候,循环不会进去,就会少打印尾节点的数据

③尾插

 

        思路很简单,就是链接新节点和尾节点,以及链接新节点和头节点,注意是双向链表,因此prev和next都要考虑到位

        还记得我们在单链表中讲的插入删除这些操作要注意什么吗?就是要注意链表中只有一个节点(不算头节点)和链表为空的情况,我们此处也需要考虑一下。当然由于不论链表为空还是只有一个节点,都是有哨兵位节点的,进行尾插时的逻辑和有多个节点都没有任何区别

因此上述代码适用链表的所有情况,我们应该能初步感知到带头双向循环的优势了

尾插逻辑测试

④头插

 思路:

1.先定义一个head头指针变量保存头节点地址,然后插入新节点进行链接

2.若不想定义另外的变量,则应该先链接新节点和头节点,再链接新节点和带哨兵位的头节点

法一:

  若链表为空,即phead->next 就是phead,因此head就是phead,该代码也没有任何问题

法二:

 若链表为空,即phead->next 就是phead,该代码也没有任何问题

头插测试结果

 ⑤判空

       已经提到过,链表为空的特点是phead的next还是phead,注意返回值是bool,而且判空函数的逻辑是为空返回真,不为空返回假,为空则返回真

 

 ⑥尾删

尾删逻辑:重点是定义一个指针变量 tailPrev,存储尾节点前一个节点的地址

       当然链表为空就不能删了,所以我们加了一个断言,用来处理链表为空的时候。这里重点理解assert(!LTEmpty(phead))这句代码,因为assert(条件)表示的是条件为真继续执行,条件为假会报错,我们此处想要的是链表不为空正常执行,链表为空(LTEmpty返回值为真)报错,因此加了!逻辑反操作这一操作符,表示返回值为假(链表不为空)正常执行

 尾删逻辑测试

 ⑦头删

 思路:先保存头节点地址,链接新的头节点和哨兵位头节点

法一

       当然,其中phead->next->prev这句代码就不是很直观了,所以不是很推荐这种写法,我们宁可多定义一个变量,使代码的可读性提高

法二

 

 头删逻辑测试

 ⑧查找

①cur初始值!

②while循环继续条件

 

 查找逻辑测试

 ⑨任意位置插入(pos之前rnei)

既然有了任意位置的插入,那么开始写的头插和尾插函数可以直接来调用该函数即可

头插:LTInsertFront(phead->next,x);

尾插:LTInsertFront(phead,x);

注意尾插的pos是phead,可能有些小伙伴会有疑问,不是尾插嘛,怎么插到了哨兵位头节点的前面

 要说明的是链表是靠指针链接的,而不是像数组是块连续的空间,画图把节点画在哪并不要紧,链接的时候是正常顺序就行

例如打印链表的时候依旧是从phead->next开始遍历,到cur == phead结束!!!

 任意位置插入逻辑测试

 ⑩任意位置删除

保存pos前一个节点和后一个节点的地址,释放pos,链接posprev和posnext

 

11.链表的销毁

 

记得在最后把哨兵位头节点释放了

好啦,带头双向循环链表的相关知识,大家就重点去感知该链表结构的设计巧妙之处,虽然结构复杂了,但是代码实现起来却更加地容易,也更加容易理解,本篇博客仍有许多不足,欢迎小伙伴们交流指正

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是Java实现带头双向循环链表的完整源码,供参考: ``` public class DoublyCircularLinkedList<T> { private Node<T> head; // 节点 // 节点类 private static class Node<T> { T data; Node<T> prev; Node<T> next; Node(T data) { this.data = data; this.prev = null; this.next = null; } } // 构造函数 public DoublyCircularLinkedList() { head = new Node<>(null); head.prev = head; head.next = head; } // 在链表末尾添加元素 public void add(T data) { Node<T> node = new Node<>(data); node.prev = head.prev; node.next = head; head.prev.next = node; head.prev = node; } // 在指定位置插入元素 public void insert(int index, T data) { Node<T> node = new Node<>(data); Node<T> p = head.next; int i = 0; while (p != head && i < index) { p = p.next; i++; } if (p == head || i > index) { throw new IndexOutOfBoundsException(); } node.prev = p.prev; node.next = p; p.prev.next = node; p.prev = node; } // 删除指定位置的元素 public void remove(int index) { Node<T> p = head.next; int i = 0; while (p != head && i < index) { p = p.next; i++; } if (p == head || i > index) { throw new IndexOutOfBoundsException(); } p.prev.next = p.next; p.next.prev = p.prev; p.prev = null; p.next = null; } // 获取指定位置的元素 public T get(int index) { Node<T> p = head.next; int i = 0; while (p != head && i < index) { p = p.next; i++; } if (p == head || i > index) { throw new IndexOutOfBoundsException(); } return p.data; } // 获取链表长度 public int size() { Node<T> p = head.next; int size = 0; while (p != head) { size++; p = p.next; } return size; } } ``` 该代码实现了带头双向循环链表数据结构,支持在链表末尾添加元素、在指定位置插入元素、删除指定位置的元素、获取指定位置的元素、获取链表长度等操作。在算法实现中,通过一个Node类来表示链表中的节点,包含数据域、前驱指针和后继指针。同时,链表节点也是一个Node对象,通过节点来连接链表的首尾,形成双向循环链表

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值