数据结构篇三:双向循环链表

这篇具有很好参考价值的文章主要介绍了数据结构篇三:双向循环链表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

  前面我们学习了单链表的实现,我们发现它在进行从后往前查找的时候有很大的不便,为了解决这个问题,双向链表油然而生。它可以很好的解决单链表无法从后往前查找的困难。

双向链表的结构

数据结构篇三:双向循环链表

  如图所示,它是有两个指针域,一个指向后一个结点,一个指向前一个结点。它存储了前一个结点的地址与后一个结点的地址,所以可以很方便的进行向前遍历或者向后遍历。同时它还是一个循环链表,可以通过第一个结点直接找到最后一个结点。

功能的解析及实现

1. 双向链表的创建

  就像前文所说,它包含了两个指针域和一个数据域,用来存放它前一个结点的地址和后一个结点的地址以及本身的数据。

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

2. 创建头节点(初始化)

  此次双向链表的结构我是采用了带头结点的结构,好处就是头节点是malloc出来的,是在堆区上存放,不会因为出了函数就被销毁,也意味着后续的各种操作我们只需要传递一级指针就会有实现单链表时传递二级指针的效果。

LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		return NULL;
	}
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

3. 创建新结点

  每次插入新的数据都需要开辟新的结点,因此把它单独拿出来放到一个函数中实现。

LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		return NULL;
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

4. 尾插

  因为是循环链表,我们可以通过第一个头节点直接找到尾结点,而在连接的时候,需要将新结点分别连接到头节点的prev以及尾结点的next,同时自身的prev存放尾结点的地址,next存放头节点的地址。如图:
数据结构篇三:双向循环链表

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyListNode(x);
	
	newnode->data = x;
	newnode->next = phead;
	phead->prev = newnode;
	newnode->prev = tail;
	tail->next = newnode;
}

5. 尾删

  在创建头节点时,我们是将头节点的prev与next都指向了它自身,因此我们可以通过头节点的next指向的是不是自身来判断是否为存放了数据。(头节点自身不存放数据)。与尾插类似,如图:
数据结构篇三:双向循环链表

void ListPopBack(LTNode* phead)
{
	assert(phead);
	if (phead->next == phead)//判断链表是否存放了数据
	{
		return;
	}
	LTNode* tail = phead->prev;
	LTNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;
}

6. 头插

  与尾插类似,只不过这个放到了最前面。在尾插是我们是有tail与phead来与新结点连接,头插也一样。先保存当前的第一个结点地址,然后再将新结点连接到头节点与原第一个结点的中间即可。

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* next = phead->next;//保存当前的第一个结点地址
	LTNode* newnode = BuyListNode(x);

	newnode->prev = phead;
	phead->next = newnode;

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

7. 头删

  我们只需要保存第一个结点与第二节结点的地址,然后在将第二个与头节点连接,再释放掉第一个结点即可。同时还需要判断链表是否为空(即有没有元素存放其中)。

void ListPopFront(LTNode* phead)
{
	//assert(phead->next != phead);  //暴力解决

	//温和解决
	if (phead->next == phead)
	{
		return;
	}
	LTNode* prev = phead->next;
	LTNode* next = prev->next;

	phead->next = next;
	next->prev = phead;
	free(prev);
	prev = NULL;
}

8. 查找

  依次遍历链表即可,从phead开始,一直到再次遇到phead结束(循环链表)。

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	printf("该元素不存在\n");
	return NULL;
}

9. 在pos位置前插入

  与头插相似,这里只需要用prev保存pos位置的前一个结点地址,然后将新结点与prev与pos相连接即可。

void ListInsert(LTNode* pos, LTDataType x)
{
	LTNode* prevPos = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

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

10. 删除pos位置的结点

  保存pos的前一个结点地址与后一个结点地址,然后将彼此相连接,然后free掉pos结点就完成了。

void ListErase(LTNode* pos)
{
	LTNode* nextPos = pos->next;
	LTNode* prevPos = pos->prev;

	nextPos->prev = prevPos;
	prevPos->next = nextPos;
	free(pos);
	pos = NULL;
}

11. 销毁

  动态开辟的结点在最后结束时都需要进行释放,防止出现内存泄漏。用cur保存当前准备要释放的结点,用next保存cur的下一个结点,释放完cur后,再将cur指向next,进行循环。

void ListDestroy(LTNode* phead)
{
	LTNode* cur = phead;
	LTNode* next = cur->next;

	while (cur)
	{
		free(cur);
		cur = NULL;

		if (cur != NULL)
		{
			cur = next;
			next = next->next;
		}
	}
}

代码实现

1.ListNode.h

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

typedef int LTDataType;

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

// 创建返回链表的头结点.
LTNode* ListInit();

// 双向链表销毁
void ListDestroy(LTNode* phead);

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

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

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

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

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

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

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

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

2. ListNode.c

#include"ListNode.h"

LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		return NULL;
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}
LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		return NULL;
	}
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyListNode(x);
	
	newnode->data = x;
	newnode->next = phead;
	phead->prev = newnode;
	newnode->prev = tail;
	tail->next = newnode;
}

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

void ListPopBack(LTNode* phead)
{
	assert(phead);
	if (phead->next == phead)
	{
		return;
	}
	LTNode* tail = phead->prev;
	LTNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;
}

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* next = phead->next;
	LTNode* newnode = BuyListNode(x);

	newnode->prev = phead;
	phead->next = newnode;

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

void ListPopFront(LTNode* phead)
{
	//assert(phead->next != phead);  //暴力解决

	//温和解决
	if (phead->next == phead)
	{
		return;
	}
	LTNode* prev = phead->next;
	LTNode* next = prev->next;

	phead->next = next;
	next->prev = phead;
	free(prev);
	prev = NULL;
}

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	printf("该元素不存在\n");
	return NULL;
}

void ListInsert(LTNode* pos, LTDataType x)
{
	LTNode* prevPos = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

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

void ListErase(LTNode* pos)
{
	LTNode* nextPos = pos->next;
	LTNode* prevPos = pos->prev;

	nextPos->prev = prevPos;
	prevPos->next = nextPos;
	free(pos);
	pos = NULL;
}

void ListDestroy(LTNode* phead)
{
	LTNode* cur = phead;
	LTNode* next = cur->next;

	while (cur)
	{
		free(cur);
		cur = NULL;

		if (cur != NULL)
		{
			cur = next;
			next = next->next;
		}
	}
}

3. test.c

#include"ListNode.h"

void test()
{
	LTNode* phead = ListInit();
	if (phead == NULL)
	{
		return;
	}
	ListPushBack(phead, 1);//测试:尾插
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPrint(phead);

  	ListPopBack(phead);//测试:尾删
	ListPopBack(phead);
	ListPopBack(phead);
	ListPrint(phead);

	ListPopBack(phead);//测试:如果链表为空继续删除会不会报错
	ListPopBack(phead);

	ListPushBack(phead, 1);//尾插一个数据来对比头插
	ListPushFront(phead, 1);//测试:头插
	ListPushFront(phead, 2);
	ListPushFront(phead, 3);
	ListPushFront(phead, 4);
	ListPrint(phead);

	ListPopFront(phead);//测试:头删
	ListPopFront(phead);
	ListPopFront(phead);
	ListPrint(phead);

	ListPopFront(phead);//测试:如果链表删除完毕,继续删除会不会报错
	ListPopFront(phead);
	ListPopFront(phead);
	ListPrint(phead);

	ListPushBack(phead, 1);//插入新元素进行后续测试
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPrint(phead);
	ListFind(phead, 5);

	LTNode* pos = ListFind(phead, 2);//测试:在2前面插入数字5
	ListInsert(pos, 5);
	ListPrint(phead);

	pos = ListFind(phead, 2);//测试:删除结点2
	ListErase(pos);
	ListPrint(phead);

	ListDestroy(phead);//测试:销毁链表
}

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

总结

  总体而言难度不大,并且双向链表解决了单链表的很多问题,值得好好学习一下。并且在这里总结一下数据结构中形参能对实参产生影响的三种方式:二级指针,头节点(在堆区),返回值。
  双向循环链表就先告一段落了,如果发现文章哪里有问题可以在评论区提出来或者私信我嗷。接下来我会继续学习栈与队列,开启新的篇章,那么本期就到此结束,让我们下期再见!!觉得不错可以点个赞以示鼓励喔!!文章来源地址https://www.toymoban.com/news/detail-434446.html

到了这里,关于数据结构篇三:双向循环链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

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

    什么是双向带头循环链表? 上面简单的一个非空 带头循环双向链表逻辑图 如何定义一个双向链表? 根据图和代码可以看双向链表就是单链表的每个结点中,在设置一个指向前驱节点的指针 简单认识之后,对他进行初始化(申请一个头节点,让前驱和后驱指针都指向自己) 代码

    2024年02月07日
    浏览(55)
  • 【数据结构】实现带头双向循环链表

    之前我们已经学习了单链表,有了单链表的基础,现在开始学习带头双向循环链表~ 结构最复杂 ,一般用在单独存储数据。 实际中使用的链表数据结构,都是带头双向循环链表 。另外这个结构虽然结构复杂,但是使用代码实现以后会发现 结构会带来很多优势 ,实现反而简单

    2024年02月10日
    浏览(38)
  • 数据结构之带头双向循环链表

    目录 链表的分类 带头双向循环链表的实现 带头双向循环链表的结构 带头双向循环链表的结构示意图 空链表结构示意图 单结点链表结构示意图  多结点链表结构示意图 链表创建结点 双向链表初始化 销毁双向链表 打印双向链表  双向链表尾插 尾插函数测试 双向链表头插

    2024年02月08日
    浏览(65)
  • 数据结构_带头双向循环链表

    相较于之前的顺序表和单向链表,双向链表的逻辑结构稍微复杂一些,但是在实现各种接口的时候是很简单的。因为不用找尾,写起来会舒服一点。(也可能是因为最近一直在写这个的原因) 在实现接口的时候,除了没有找尾,其他的操作和单向链表是差不多的,这里就不多

    2024年04月14日
    浏览(52)
  • 数据结构:手撕图解双向循环链表

    在前面学完单链表后,我们思考这样一个问题,单链表和顺序表比起来,功能确实相当强大,有很多优势,但是于此同时,我们也应思考下面的问题 单链表有什么不足的地方? 如果你把单链表的各个函数都自己实现过,那么下面的问题你一定有相同的感悟 单链表实现尾插尾

    2024年02月15日
    浏览(76)
  • 【数据结构】带头双向循环链表及其实现

    目录 1.带头双向循环链表 2.带头双向循环链表实现 2.1初始化 2.2销毁 2.3头插 2.4链表打印 2.5头删数据 2.6尾插数据 2.7尾删数据 2.8链表判空  2.9查找一个数据 2.10在pos位置前插入数据 2.11删除pos位置 2.12求链表的长度 2.顺序表和链表的比较 我们已经实现了无头单向循环链表 带头双

    2024年02月10日
    浏览(31)
  • 数据结构---手撕图解双向循环链表

    在前面学完单链表后,我们思考这样一个问题,单链表和顺序表比起来,功能确实相当强大,有很多优势,但是于此同时,我们也应思考下面的问题 单链表有什么不足的地方? 如果你把单链表的各个函数都自己实现过,那么下面的问题你一定有相同的感悟 单链表实现尾插尾

    2024年02月15日
    浏览(43)
  • 【数据结构】线性表——带头双向循环链表

    带头双向循环链表的优点 1.支持任意位置时间复杂度为O(1)的插入和删除。 2.按照需求申请释放空间,无需担心空间不够用,无需担心浪费。 3.带头可以省去链表为空时的判断,可以使代码更加简约 带头双向循环链表的缺点 1.不可以进行下标随机访问。 2.缓存利用率低 带头双

    2024年02月03日
    浏览(59)
  • 数据结构入门指南:带头双向循环链表

    目录 文章目录 前言 1.结构与优势 2.链表实现       2.1 定义链表 2.2 创建头节点 2.3 尾插 2.4 输出链表 2.5 尾删 2.6 头插 2.7头删 2.8 节点个数 2.9 查找 2.10 位置插入 2.11 位置删除 2.12 销毁链表  3. 源码 总结         链表一共有8种结构,但最常用的就是无头单向链表、和带头

    2024年02月14日
    浏览(38)
  • 【数据结构】双向带头循环链表的实现

    前言:在前面我们学习了顺序表、单向链表,今天我们在单链表的基础上进一步来模拟实现一个带头双向链表。 💖 博主CSDN主页:卫卫卫的个人主页 💞 👉 专栏分类:数据结构 👈 💯代码仓库:卫卫周大胖的学习日记💫 💪关注博主和博主一起学习!一起努力! 带头双向循环链

    2024年01月15日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包