概念与结构

双向带头链表,分开来就是双向、带头、链表
之前了解了单链表,他只能由上一个节点找到下一个节点,不能由这个节点找到上一个,这个就是单向。
双向就是它的每一个节点都有两个指针,分别为前指针与后指针,分别指向上一个和下一个节点。头节点的前指针指向尾节点,尾节点的后指针指向头节点(哨兵位)。
带头就是它默认有一个头,也就是哨兵位,他是不存放有效数据的。
具体的结构如图:
在这里插入图片描述
head: 哨兵位
dn:有效节点
OK,话不多说,直接开始写代码。

链表实现

对小编个人来说,感觉比单链表还是要简单一些的。
因为一个一个详细介绍篇幅会太长,除了第一个函数以外,其他的就通过画图的方式来帮助理解代码,因为原理是相似的,所以就不再做过多文字介绍了。

源码链接:https://gitee.com/xiao-li-learning-c-language/c-language-exercisehomework/tree/master/10-11%E5%8F%8C%E5%90%91%E5%B8%A6%E5%A4%B4%E9%93%BE%E8%A1%A8/%E5%8F%8C%E5%90%91%E5%B8%A6%E5%A4%B4%E9%93%BE%E8%A1%A8

链表头文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType val;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;
LTNode* LTNodeInit();//链表初始化
bool LTEmpty(LTNode* phead);//检查是否只剩哨兵位
void LTNodePushBack(LTNode* phead, LTDataType x);//尾插
void LTNodePrint(LTNode* phead);//打印
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopBack(LTNode* phead);//尾删
void LTNodePopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置节点
void LTErase(LTNode* pos);
//销毁链表(包含哨兵位)
void LTDesTroy(LTNode** pphead);
void LTDesTroy2(LTNode* phead);//传一级,需要手动将plist置为NULL

链表初始化

初始化就是给我们的结构体赋一个初值,也就是让链表现有一个哨兵位。
这里我们要借助一个函数实现新链表的创建以及赋值
这里哨兵位的存放的是无效地址,我就用-1来代替这个无效数据。

LTNode* BuyNode(LTDataType x)//创建新节点
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	assert(newnode);
	newnode->val = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

LTNode* LTNodeInit()//为我们的链表创建哨兵位
{
	LTNode* phead = BuyNode(-1);
	return phead;
}

链表尾插

思路:
对链表尾插需要的数据有新节点、哨兵位的节点地址、原末尾节点的地址。
通过画图来遍历思考

在这里插入图片描述
如图,做标记的地方是我们代码要实现修改的地方,首先我们先修改新节点的两个指针,这样不会扰乱原链表。
将新节点的prev指向d3,将新节点的next指向head,形成环。
接下来将d3的next指向新节点,将head的prev指向新节点。
代码:

void LTNodePushBack(LTNode* phead,LTDataType x)
{
	LTNode* newnode = BuyNode(x);//创建新节点
	//新节点指针修改
	newnode->next = phead;
	newnode->prev = phead->prev;
	//修改原末尾节点的next指向
	phead->prev->next = newnode;
	//修改哨兵位的prev指向
	phead->prev = newnode;
}

链表头插

思路:
在这里插入图片描述

void LTPushFront(LTNode* phead, LTDataType x)
{
	LTNode* newnode = BuyNode(x);//创建新链表
	//修改新节点
	newnode->next = phead->next;
	newnode->prev = phead;
	//原头节点修改
	phead->next->prev = newnode;
	//哨兵位修改
	phead->next = newnode;
}

链表尾删

思路:
在这里插入图片描述
这里主要是修改哨兵位的前指针以及新尾节点的后指针,同时释放原尾节点。
注意:当链表只剩下哨兵位时,是不能再删除的。

代码:

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next != phead;//判断下一个节点是不是自己,是自己就返回flase,不是就返回ture
}
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(LTEmpty(phead));//判断是否只剩下哨兵位
	//暂存原末尾节点
	LTNode* tmp = phead->prev;

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

链表头删

思路:
在这里插入图片描述

代码:

void LTNodePopFront(LTNode* phead)
{
	assert(phead);
	assert(LTEmpty(phead));//检查链表是否只剩下哨兵位

	LTNode* tmp = phead->next;//暂存头节点地址
	phead->next = phead->next->next;//让哨兵位指向第二个节点
	phead->next->prev = phead;//让第二节点前指针指向哨兵位

	free(tmp);//释放头节点
	tmp = NULL;
}

寻找链表某个元素

思路:
从头节点也就是哨兵位的下一位开始循环,因为链表是循环的,当遍历到哨兵位时,就代表链表已经便利了一遍了。
代码:

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->val == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}

在某个位置之后插入节点

以在第二个节点之后插入为例
思路:
听过上面那个代码找到要插入数据的位置。
比如现在链表里面数据有1,2,3,4;
我要在2后面插入数据,就通过寻找元素2获取位置。
在这里插入图片描述
这样获取位置后就简单了,我们将该位置假设为哨兵位,是不是就跟头插的思路一样了。
代码:

void LTInsert(LTNode* pos, LTDataType x)
{
	LTNode* newnode = BuyNode(x);//创建新节点
	newnode->next = pos->next;//新节点指针修改
	newnode->prev = pos;

	pos->next->prev = newnode;//修改指定位置下一个位置的前指针,也就是图例d3
	pos->next = newnode;//修改指定位置的后指针
}

删除指定位置节点

还是以删除d2为例
思路:在这里插入图片描述

这个就很简单了,我们通过寻找元素获取地址进行传参,因为这是双向链表,我们可以通过它找到他前后的节点进行修改,修改完后我们直接释放这个节点的空间就行。
代码:

void LTErase(LTNode* pos)
{
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	pos = NULL;
}

销毁链表

从头节点遍历链表依次释放空间,然后释放哨兵位的空间,将指向哨兵位的指针置为NULL。

传参二级指针

void LTDesTroy(LTNode** pphead)
{
	assert(pphead&&*pphead);
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* tmp = pcur;
		pcur = pcur->next;
		free(tmp);
	}
	free(pcur);//这时pcur存放的就是哨兵位的地址
	*pphead = NULL;//这里*pphead就是主程序里存放哨兵位地址的指针
	pcur = NULL;
}

一级指针传参

因为一级指针传参只能通过这个指针找到所有的节点释放空间,但是无法对这个一级指针的实参进行修改,所以我们就需要在主程序里面对plist进行置空操作。

void LTDesTroy2(LTNode* phead)//传一级,需要手动将plist置为NULL
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* tmp = pcur;
		pcur = pcur->next;
		free(tmp);
	}
	free(pcur);
	pcur = NULL;
}

打印链表有效数据

思路:
从头节点的位置进行遍历,依次打印有效数据,当指针回到哨兵位就结束循环。
代码:

void LTNodePrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	printf("plist->");
	while (pcur!= phead)
	{
		printf("%d->", pcur->val);
		pcur = pcur->next;
	}
	printf("plist->...\n");
}

代码测试

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include"list.h"

void test1()
{
	LTNode* plist=LTNodeInit();
	LTNodePrint(plist);
	//LTNodePushBack(plist, 1);
	LTPushFront(plist, 1);
	LTNodePrint(plist);
	//LTNodePushBack(plist, 2);
	LTPushFront(plist, 2);
	LTNodePrint(plist);
	//LTNodePushBack(plist, 3);
	LTPushFront(plist, 3);
	LTNodePrint(plist);
	//LTNodePushBack(plist, 4);
	LTPushFront(plist, 4);
	LTNodePrint(plist);

	printf("\n");

	LTNodePopFront(plist);
	LTPopBack(plist);
	LTNodePrint(plist);
	LTNodePopFront(plist);
	LTPopBack(plist);
	LTNodePrint(plist);
	LTPopBack(plist);
	LTNodePopFront(plist);
	LTNodePrint(plist);
	
	LTNodePushBack(plist, 1);
	LTNodePushBack(plist, 2);
	LTNodePushBack(plist, 3);
	LTNodePushBack(plist, 4);

	LTNode* ret=LTFind(plist, 3);
	if (ret)
	{
		printf("YES\n");
	}
	else
	{
		printf("NO\n");
	}
	LTInsert(ret, 6);
	LTNodePrint(plist);
	
	LTDesTroy2(plist);
	plist = NULL;

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

在这里插入图片描述
结果如图,代码运行正常,plist是创建的哨兵位。


除了这些之外还有其他的一些可实现功能,这里就不再做过多介绍,基本原理都是差不多的。
谢谢各位观看,编写不易,赏赐一个三连吧。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部