数据结构:一篇文章带你学会双向链表

带头双向循环链表的的介绍

在前面学习了无头单向非循环链表,今天我们就来学习一下带头双向循环链表吧!

下图就是带头双向循环链表的结构

在这里插入图片描述

双向链表和单链表有一点点不一样,为什么这么说呢?因为双向链表的每个结点存在两个指针域>> 和一个数据域,两个指针域分别是指向前一个结点和后一个结点地址的指针,我们可以通过当前>> 结点找到前一个结点和它的后面一个结点。而且单链表能做的事情,它都可以做,并且它做起来>> 还更加的方便。那为什么又叫它为带头双向循环链表呢?带头和循环这两个分别是什么意思呢?>> 不要着急,等我为你一 一道来,带头的意思是说它有一个哨兵位的头结点,专门用来站岗的不存储>> 有效的数据。至于循环二字主要是因为头结点的prev指针是指向尾结点的,而尾结点的next指针是>> 指向头结点的,这就是循环二字的由来。

双向循环链表增删查改的实现
双向链表的函数接口声明
#pragma once

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

typedef int LTDataType;

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

//创建返回链表的头结点
LTNode* BuyListNode(LTDataType x);

//双向链表的初始化
LTNode* ListInit();

//双向链表的打印
void ListPrint(LTNode* phead);

//双向链表的尾插
void ListPushBack(LTNode* phead,LTDataType x);

//双向链表的头插
void ListPushFront(LTNode* phead,LTDataType x);

//双向链表的尾删
void ListPopBack(LTNode* phead);

//双向链表的头删
void ListPopFront(LTNode* phead);

//双向链表的查找
LTNode* ListFind(LTNode* phead, LTDataType x);

//双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);

//双向链表删除pos位置的节点
void ListErase(LTNode* pos);

//双向链表的销毁
void ListDestroy(LTNode* phead);
双向链表的实现
#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

//创建返回链表的头结点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* Newnode = (LTNode*)malloc(sizeof(LTNode));
	Newnode->data = x;
	Newnode->next = NULL;
	Newnode->prev = NULL;

	return Newnode;
}
//双向链表的初始化
LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//双向链表的尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	//LTNode* tail = phead->prev;
	//LTNode* Next = BuyListNode(x);
	LTNode* Next = (LTNode*)malloc(sizeof(LTNode));
	Next->data = x;
	//
	//tail->next = Next;
	//Next->prev = tail;
	//phead->prev = Next;
	//Next->next = phead;
	ListInsert(phead, x);

}

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

//双向链表的头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	//LTNode* Next = phead->next;
	//LTNode* Newnode = BuyListNode(x);
	///*LTNode* Newnode = (LTNode*)malloc(sizeof(LTNode));
	//Newnode->data = x;*/
	phead   Newnode    Next
	//phead->next = Newnode;
	//Newnode->prev = phead;

	//Newnode->next = Next;
	//Next->prev = Newnode;
	ListInsert(phead->next, x);
}


//双向链表的尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	/*LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
	tail = NULL;*/
	ListErase(phead->prev);
}



//双向链表的头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	//LTNode* next = phead->next;
	//LTNode* nextNext = next->next;
	 phead     next     nextNext
	//phead->next = nextNext;
	//nextNext->prev = phead;
	//free(next);
	//next = NULL;
	ListErase(phead->next);
}

//双向链表的查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	//找不到返回NULL
	return NULL;
}

//双向链表在pos位置的前面进行插入
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = BuyListNode(x);
	LTNode* posPrev = pos->prev;
	//posPrev   newnode    pos
	posPrev->next = newnode;
	newnode->prev = posPrev;

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

//双向链表删除pos位置的节点
void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	//posPrev    pos    posNext

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
	pos = NULL;
}


//双向链表的销毁
void ListDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* Next = cur->next;
		free(cur);
		cur = Next;
	}

	free(phead);
	phead = NULL;
}
双向链表的测试
#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

void TestList()
{
	LTNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPrint(plist);

	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPrint(plist);

	ListPopBack(plist);
	ListPrint(plist);

	ListPopFront(plist);
	ListPopFront(plist);
	ListPrint(plist);

	LTNode*pos = ListFind(plist, 2);
	ListInsert(pos, 5);
	ListPrint(plist);

	pos = ListFind(plist, 5);
	ListErase(pos);
	ListPrint(plist);


	ListDestroy(plist);
	//这里形参的改变不会引起实参的改变,所以我们在外面自己手动置空
	plist = NULL;

}

int main()
{
	TestList();
	return 0;
}
双向链表的初始化

我们动态申请一个结点,作为我们的头结点。这个结点就是我们上面说的哨兵卫的头结点专门用来站岗的不存储有效数据,并且它的prev指针和next指针都分别指向自己。那么创建这个头结点有什么好处的呢?它会使得我们的尾插头插、尾删头删变得非常的简答。并且也不需要像单链表那样要用二级指针或者返回值的形式。

//双向链表的初始化
LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
创建一个新结点

由于尾插,头插或者说在pos位置的前面进行插入的时候都需要创建结点,那么为了方便,并且使得我们的代码减少冗余,我们就直接弄一个创建结点的函数出来方便我们插入。并且将这个结点的prev指针与next指针都指向NULL

//创建返回链表的头结点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* Newnode = (LTNode*)malloc(sizeof(LTNode));
	Newnode->data = x;
	Newnode->next = NULL;
	Newnode->prev = NULL;

	return Newnode;
}
双向链表的尾插

双向链表的尾插相较于单链表的尾插就方便多了,因为单链表尾插还需要遍历一次链表去找尾,而双向链表头结点的prev指针就是指向我们的尾结点,因此尾插起来就非常的方便。其次单链表尾插的时候还要单独处理链表为空插入的情况,但是带头的双链表就不需要单独去处理这种情况。

在这里插入图片描述

在这里插入图片描述

//双向链表的尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	//LTNode* tail = phead->prev;
	//LTNode* Next = BuyListNode(x);
	LTNode* Next = (LTNode*)malloc(sizeof(LTNode));
	Next->data = x;
	//
	//tail->next = Next;
	//Next->prev = tail;
	//phead->prev = Next;
	//Next->next = phead;
	ListInsert(phead, x);

}
双向链表的打印

双向链表的打印和单链表的打印也有一点点不一样,单链表的打印的循环条件是cur不为NULL,而双向链表的打印的循环条件则是cur不等于头结点即可,为什么会这样呢?因为在双向链表中,尾结点的next结点并不是指向NULL,而是指向头结点的,而头结点的prev结点又是指向尾结点的。

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

双向链表的头插比单链表的尾插也会方便一些,因为它不需要像单链表那样单独去处理链表为空时头插的情况。并且头插的时候只需要改变一下哨兵卫头结点和它的下一个结点的prev结点的指向就好了:首先将哨兵卫头结点的next结点指向我们要头插的结点,再将我们要头插的结点的prev结点指向哨兵卫的头结点,然后将要头插的结点的next结点指向原本哨兵卫头结点的下一个结点,最后再将原本哨兵卫头结点的下一个结点的prev结点指向我们要头插的结点即可。

在这里插入图片描述

//双向链表的头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	//LTNode* Next = phead->next;
	//LTNode* Newnode = BuyListNode(x);
	///*LTNode* Newnode = (LTNode*)malloc(sizeof(LTNode));
	//Newnode->data = x;*/
	phead   Newnode    Next
	//phead->next = Newnode;
	//Newnode->prev = phead;

	//Newnode->next = Next;
	//Next->prev = Newnode;
	ListInsert(phead->next, x);
}

双向链表的尾删

双向链表的尾删相较于单链表的尾删也方便得多,和尾插一样,都不需要去遍历找尾了。因为哨兵卫头结点的prev结点就是指向尾结点,只要我们将尾结点的上一个结点的next结点指向哨兵卫头结点,再将哨兵卫头结点prev结点指向它更新一下尾结点,最后再将原来的结点释放掉即可,最好再置空一下。但是在双向链表尾删的时候我们得再加上一个断言——哨兵卫头结点的next不能够指向自己,如果没有加上这个断言,假如说有人不小心然后又没有意识到这一点的话就会把哨兵卫的头结点也给删除了。

在这里插入图片描述

//双向链表的尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	/*LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
	tail = NULL;*/
	ListErase(phead->prev);
}
双向链表的头删

双向链表的头删比单链表的头删也会方便一些,只需要将哨兵卫头结点的next结点指向它下一个结点的下一个结点,然后再将这个结点的prev指向哨兵卫头结点,最后再将哨兵卫头结点的下一个结点释放掉即可,最好是再置空一下。但是在双向链表头删的时候我们得再加上一个断言——哨兵卫头结点的next不能够指向自己,如果没有加上这个断言,假如说有人不小心然后又没有意识到这一点的话就会把哨兵卫的头结点也给删除了。

在这里插入图片描述

//双向链表的头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	//LTNode* next = phead->next;
	//LTNode* nextNext = next->next;
	 phead     next     nextNext
	//phead->next = nextNext;
	//nextNext->prev = phead;
	//free(next);
	//next = NULL;
	ListErase(phead->next);
}
双向链表的查找

从哨兵卫头结点的下一个结点开始找,循环的条件是cur不等于哨兵卫头结点,为什么呢?还是上面说的那个:尾结点的next结点是指向哨兵卫头结点的,如果找到了就返回该结点即可,没有找到则返回NULL

在这里插入图片描述
在这里插入图片描述

//双向链表的查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	//找不到返回NULL
	return NULL;
}
双向链表在pos位置的前面进行插入

这个函数接口的实现相较于单链表而言就方便许多了,因为双向链表是双向的既有一个指针指向它的前一个结点,又有一个指针指向它的下一个结点,因此我们只需要改变一下pos结点的前一个结点和pos结点的指向就好了。但是单链表就会稍微麻烦一点在遍历找pos的时候必须还得用一个指针去记录一下pos前面的结点才行。并且我们双向链表实现了这个函数接口之后,双向链表的尾插或者头插只需要通过调用这个函数就可以实现。

双向链表的尾插:ListInsert(phead, x);

双向链表的头插:ListInsert(phead->next,x);

可能这个时候就会有人有疑问了,为什么会这样呢?

1.因为哨兵卫头结点的前面不就是我们的尾结点嘛!因为哨兵卫头结点的prev结点是指向尾结点的呀。那么把pos变成我们的哨兵卫头结点,那么在pos位置之前插入就是相当于尾插。并且对于双向链表来说链表为空也没关系不需要去单独处理

2.因为哨兵卫头结点的next结点是指向双向链表中的头结点的,因此把pos变成我们的哨兵卫头结点的下一个结点,那在pos位置之前插入就是相当于头插。并且对于双向链表来说链表为空也没关系不需要去单独处理

在这里插入图片描述

//双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = BuyListNode(x);
	LTNode* posPrev = pos->prev;
	//posPrev   newnode    pos
	posPrev->next = newnode;
	newnode->prev = posPrev;

	newnode->next = pos;
	pos->prev = newnode;
}
双向链表删除pos位置的结点

这个函数接口的实现相较于单链表而言就方便许多了,并且我们双向链表实现了这个函数接口之后,双向链表的尾插或者头插只需要通过调用这个函数就可以实现。

双向链表的尾删:ListErase(phead->prev);

双向链表的头删:ListErase(phead->next);

这是为什么呢?

1.哨兵卫头结点的prev结点是指向尾结点的,那么把pos变成哨兵卫头结点的prev结点之后,这样就相当于是我们的尾删了

2.哨兵卫头结点的next结点是指向双向链表中的头结点的,那么把pos变成哨兵卫头结点的next结点之后,这样就相当于是我们的头删了

在这里插入图片描述

//双向链表删除pos位置的节点
void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	//posPrev    pos    posNext

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
	pos = NULL;
}
双向链表的销毁

创建了一些链表之后,在使用完之后一定要养成一个销毁的好习惯,否则就会造成内存泄漏。将每一个结点取下来然后给释放掉,但是需要注意的是将哨兵卫头结点释放掉并置空的时候可以使用二级指针也可以不使用二级指针,但是如果不使用二级指针的话就得自己得test.c中手动置空一次。作者这里没有使用二级指针的方式,但是我建议大家使用二级指针的方式,以防忘记。

//双向链表的销毁
void ListDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* Next = cur->next;
		free(cur);
		cur = Next;
	}

	free(phead);
	phead = NULL;
}

总结:顺序表和链表(双向带头循环)的区别

顺序表和链表,这两个结构各有优势,很难说谁更优,严格来说他们俩是相辅相成的两个结构

顺序表

优点:1.支持随机访问。需要随机访问结构支持的算法可以很好地适用

​ 2.cpu高速缓存命中率更高

缺点:1.头部中部插入或删除时间效率低O(N)

​ 2.连续的物理空间,空间不够了以后需要增容。

​ a.增容有一定程度的消耗

​ b.为了避免频繁增容,一般我们都按倍数去增容,用不完可能存在一定的空间浪费

链表(双向带头循环链表)

优点:1.任意位置插入删除时间效率高O(1)

​ 2.按需申请释放空间

缺点:1.不支持随机访问(用下标访问)意味着:一些排序,二分查找等在这种结构上不使用

​ 2.链表存储一个值,同时要存储链接指针,也有一定的消耗。

​ 3.cpu高速缓存命中率更低

下面我将通过一个表格来进行总结

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持O(N)
任意位置插入或删除元素可能需要搬移元素,效率低O(N)只需要修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

如果大家对缓存相关知识感兴趣的话,我给大家推荐一篇大佬写的文章:
与程序员相关的CPU缓存知识
好了以上就是我们双向链表的全部内容了,如果觉得对你有帮助的话可以给作者点赞关注评论一波,感谢你的支持。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值