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

目录

一:前言

二:链表的表示

三:链表的实现

1:链表的创建(构建结构体,ListInit函数)

2:链表的打印(ListPrint)

3:链表的尾插(ListPushBack)

 4:链表的头插(ListPushFront)

5:链表的头删(ListPopFront)

 6:链表的尾删(ListPopBack)

7:链表的查找(ListFind)

8:链表指定位置之前插入(ListInsert)

9:指定位置删除(ListErase)


一:前言

在上节课学习过单链表后,我们能发现许多单链表结构的缺陷,比如:尾插要遍历整个链表才能实现,时间复杂度为0(N),这节课我们就来认识一种新链表,可以改善这种缺陷,这种链表就是带头循环双向链表。

二:链表的表示

我们先来对比一下两者的优略性:

无头单向非循环链表结构简单,一般不会用来存放数据,实际中更多是作为其他数据结构的子结构,如哈希桶,图的邻接表等等,另外这种结构在笔试面试中出现很多。

带头双向循环链表结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了,后面我们代码实现了就知道了。

另外我们这里说一下我们这里的带哨兵位的头节点:即第一个节点不存储有效值。

我们来看一下带头循环双向链表的逻辑图: 

 从图中我们可以看出来双向链表不仅可以找到某个节点的next节点,也可以找到某个节点的前驱节点(prev),那么我们在实现的过程中就要与单链表的实现方法有所不同。

三:链表的实现

1:链表的创建(构建结构体,ListInit函数)

我们在用结构体创建此链表时,比起单链表,我们又多出来了一个新的指针成员--prev

typedef int LTDataType;          //方便修改链表中存储数据的类型
typedef struct DListNode {
	struct DListNode* prev;      //指向该节点的前驱节点
	struct DListNode* next;      //指向该节点的后一个节点
	LTDataType data;
}ListNode;

  然后我们要创建该链表的头节点,通过ListInit实现

//创建新节点
ListNode* BuyListNode(LTDataType x) {       
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}
//链表初始化
ListNode* ListInit(ListNode* phead)
{
	phead = BuyListNode(0);         //给头节点随便赋值,这个0可以是任意数,因为头节点的
                                      data值无意义
	phead->next = phead;            //双向带头循环链表的phead的next节点和prev节点初始化
                                      设置的都是phead
	phead->prev = phead;
	return phead;
}
void TestList()
{
	ListNode* phead = NULL;
	phead = ListInit(phead);     //通过ListInit函数来初始化phead节点
}

TestList函数是调用所有我们写过的函数的地方。

2:链表的打印(ListPrint)

和单链表打印时while的临界值不同,我们打印带头双向循环链表中while的结束条件是cur!=phead结点,让我们来看一下为什么

 我们最后给出的代码

//链表的打印
void ListPrint(ListNode* phead) {
	ListNode* cur = phead->next;
	while (cur != phead) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

3:链表的尾插(ListPushBack)

我们先来看逻辑图:

插入前

插入后

按照这样的逻辑图我们很容易写出代码

//链表的尾插
void ListPushBack(ListNode* phead, LTDataType x) {
	ListNode* newnode = BuyListNode(x);               //给新节点赋值

	ListNode* tail = phead->prev;         //根据循环链表性质可知,尾节点是头节点的prev
	
    tail->next = newnode;                 
	newnode->prev = tail;                 //根据双向链表的性质,newnode节点是尾节点的
                                            next,尾节点是newnode节点的prev
	newnode->next = phead;                
	phead->prev = newnode;                //newnode成了新的尾节点,要把头节点的prev设
                                            置为newnode,把newnode的next设置为头节点
}

我们可以插入几个数并打印他们试试

void TestList()
{
	ListNode* phead = NULL;
	phead = ListInit(phead);
	ListPushBack(phead, 1);
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPushBack(phead, 5);
	ListPushBack(phead, 4);
	ListPrint(phead);
}

运行结果

 4:链表的头插(ListPushFront)

先看逻辑图:

插入前

 插入后

 根据插入前后的逻辑图我们可以写出如下代码

//链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	ListNode* newnode = BuyListNode(x);

	ListNode* first = phead->next;
	newnode->prev = phead;
	phead->next = newnode;
	newnode->next = first;
	first->prev = newnode;
}
//就算在插入前链表中只有一个头节点,依然可以正常插入,可以自行画逻辑图体会

我们可以发现,在链表的尾插里,我们不用像单链表那样还需要找出特殊情况来单独讨论(你可以自己画一些特殊的情况,把代码代入试试会不会出错。),这就是双向带头循环链表相比较于单链表的优势-----容易实现。

5:链表的头删(ListPopFront)

先看逻辑图

删去前

删去后

我们写出的代码如下

//链表的头删
void ListPopFront(ListNode * phead) {
	ListNode* first = phead->next;
	ListNode* second = first->next;
	phead->next = second;
	second->prev = phead;

	free(first);   //最后free掉first,以免造成空间浪费。
}

 6:链表的尾删(ListPopBack)

先上逻辑图

删除前 

删除后

 实现的代码

//链表的尾删
void ListPopBack(ListNode* phead) {
	assert(phead);
	assert(phead->next != phead);
	ListNode* tail = phead->prev;
	ListNode* tailprev = tail->prev;  //找出尾节点的前一个结点

	phead->prev = tailprev;
	tailprev->next = phead;           //把头节点和找出的倒数第二个结点联系起来

	free(tail);
}

7:链表的查找(ListFind)

给定一个值,我们要在链表中找到这个值并返回该结点的地址

我们可以采用遍历的方法,只要找到即可返回结点并结束程序。

代码如下

//链表的查找
ListNode* ListFind(ListNode* phead, LTDataType x) {
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

8:链表指定位置之前插入(ListInsert)

在用find函数找到pos的位置之后,我们用ListInsert函数在pos前面插入一个data值为x的结点

//链表指定位置前插入
void ListInsert(ListNode* phead, ListNode* pos, LTDataType x) {
	assert(pos);
	ListNode* newnode = BuyListNode(x);
	ListNode* prev = pos->prev;


	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
 }

具体的运行结果如下

如图是在data值为3的结点前面插入data值为100的新结点

9:指定位置删除(ListErase)

在用find函数找到pos的位置后,我们可以对该结点进行删除,这和之前讲的尾删头删有异曲同工之妙,话不多说直接上代码

//链表指定位置删除
void ListErase(ListNode* phead, ListNode* pos) {
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	return;
}

 运行结果如下

以上就是带头双向循环链表的基础操作了,最后附上总代码

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct DListNode {
	struct DListNode* prev;
	struct DListNode* next;
	LTDataType data;
}ListNode;
//创建新节点
ListNode* BuyListNode(LTDataType x) {
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}
//链表初始化
ListNode* ListInit(ListNode* phead)
{
	phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
//链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	ListNode* newnode = BuyListNode(x);

	ListNode* first = phead->next;
	newnode->prev = phead;
	phead->next = newnode;
	newnode->next = first;
	first->prev = newnode;
}
//链表的尾插
void ListPushBack(ListNode* phead, LTDataType x) {
	ListNode* newnode = BuyListNode(x);

	ListNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}
//链表的头删
void ListPopFront(ListNode * phead) {
	assert(phead);
	assert(phead->next != phead);
	ListNode* first = phead->next;
	ListNode* second = first->next;
	phead->next = second;
	second->prev = phead;

	free(first);
}
//链表的尾删
void ListPopBack(ListNode* phead) {
	assert(phead);
	assert(phead->next != phead);
	ListNode* tail = phead->prev;
	ListNode* tailprev = tail->prev;

	phead->prev = tailprev;
	tailprev->next = phead;

	free(tail);
}
//链表的查找
ListNode* ListFind(ListNode* phead, LTDataType x) {
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//链表指定位置删除
void ListErase(ListNode* phead, ListNode* pos) {
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	return;
}
//链表指定位置前插入
void ListInsert(ListNode* phead, ListNode* pos, LTDataType x) {
	assert(pos);
	ListNode* newnode = BuyListNode(x);
	ListNode* prev = pos->prev;


	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
 }
//链表的打印
void ListPrint(ListNode* phead) {
	ListNode* cur = phead->next;
	while (cur != phead) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
void TestList()
{
	ListNode* phead = NULL;
	phead = ListInit(phead);
	ListPushBack(phead, 1);
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPushBack(phead, 5);
	ListPrint(phead);
	ListNode* pos=ListFind(phead,3);
	if (pos != NULL) {
		ListErase(phead, pos);
		ListPrint(phead);
	}
}
int main()
{
	TestList();
	return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Boletb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值