【数据结构】—— 双向链表(超详细)

本文干货满满,你准备好了吗?

前面我们了解到链表总共有八种类型,我们已经学习了其中的一种,即不带头单向不循环链表,也就是单链表。那么今天我们又将学习一种链表的结构,即带头双向循环链表,简称双向链表。

接下来让我们一起进入双向链表的学习吧! \color{green}{接下来让我们一起进入双向链表的学习吧!} 接下来让我们一起进入双向链表的学习吧!

一、什么是双向链表?

带头双向循环链表简称双向链表,是链表结构的一种,其结构如下图所示:在这里插入图片描述
我们可以看到,双向链表与单链表不同的是双向链表的第一个节点之前有一个head节点,这个就是头节点,同时双向链表每个节点有两个指针,一个指向前一个节点(prev),一个指向后一个节点(next),它可以从头到尾,也可以从尾到头双向遍历,所以它是循环的,这大大增加了双向链表的灵活性

二、单链表和双向链表的不同点

在我们实际运用中,最常用的即单链表和双向链表
那么这两种结构具体有什么不同点呢?下面为大家总结一下。
节点指针
单链表:每个节点只有一个指向下一个节点的指针。这意味着你只能从当前节点访问下一个节点,而无法直接访问前一个节点。
双向链表:每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。这使得你可以从当前节点访问前一个节点和后一个节点。
遍历方向
单链表:由于每个节点只有一个指向下一个节点的指针,因此只能从头到尾单向遍历。
双向链表:由于每个节点有两个指针,因此可以从头到尾,也可以从尾到头双向遍历。
增删节点复杂度
单链表:增加和删除节点相对简单,只需调整一个指针。例如,删除一个节点时,只需将前一个节点的指针指向被删除节点的下一个节点。
双向链表:增加和删除节点较为复杂,需要调整两个指针。例如,删除一个节点时,需要将前一个节点的后指针指向被删除节点的后一个节点,并将后一个节点的前指针指向被删除节点的前一个节点。
内存占用
单链表:内存占用较少,因为每个节点只包含一个指针。
双向链表:内存占用较多,因为每个节点包含两个指针。
使用场景
单链表:适用于节点的增加和删除操作频繁的场景,因为这些操作相对简单。
双向链表:适用于需要双向查找节点值的场景,因为可以从两个方向遍历链表,提供了更大的灵活性。

三、双向链表的实现

1、定义节点结构

双向链表的节点也是结构体结构,别忘了双向链表每个节点有prev和next两个指针
注意
双向链表为空时,还剩下一个头结点,头结点的prev和next指针都指向自己,如下图所示:
在这里插入图片描述
双向链表头结点不存储有效数据

//定义节点结构
 typedef int LTDataType;
 typedef struct ListNode
 {
   LTDataType data;
   struct ListNode* next;//指向下一节点的指针
   struct ListNode* prev;//指向前一个节点的指针
   LTDataType data;//存储的数据
 }LTNode;
 

2、双向链表要实现的功能

对于双向链表,我们要实现以下功能:

 //初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();
//申请节点
LTNode* LTBuyNode(LTDataType x);
//销毁
void LTDesTroy(LTNode* phead);
//双向链表打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x); 
//头插
void LTPushFront(LTNode* phead, LTDataType x); 
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x);

3、双向链表功能的实现

3.1、申请节点

//申请节点
 LTNode* LTBuyNode(LTDataType x)
 {
   LTNode* node = (LTNode*)malloc(sizeof(LTNode));
   if(node == NULL)
   {
     perror("malloc fail!");
     exit(1);
   }
   node->data = x;
   node->next = node->prev = node;

   return node;
}

3.2、初始化链表

双向链表需要初始化,就是给双向链表创建一个头节点(哨兵位).

//初始化
 LTNode* LTInit()
 {
   //给双向链表创建一个哨兵位
   LTNode* phead = BuyListNode(-1);//哨兵位不存储有效的值,这里传参传-1(也可以传其他值)
   return phead;
 }

3.3、打印链表

//打印
 void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

3.4、双向链表尾插

在双向链表尾插过程中,哨兵位节点(即头结点)不能被删除,节点的地址也不能发生改变(哨兵位不能被修改),所以在传形参的时候传一级指针

//尾插
 void LTPushBack(LTNode* phead,LTDataType x)
 {
   assert(phead);//哨兵位不能为空
   LTNode* newnode = LTBuyNode(x);
   newnode->prev = phead->prev;
   newnode->next = phead;
   phead->prev->next = newnode;/1
   phead->prev = newnode;//2
   //1和2不能交换位置
 }
   

在这里插入图片描述

3.5、双向链表头插

头插不是往哨兵位前面去插入,而是往第一个有效的节点前面去插入新节点。

//头插
 void LTPushFront(LTNode* phead,LTDataType x)
 {
   assert(phead);
   LTNode* newnode = LTBuyNode(x);
   newnode->next = phead->next;
   newnode->prev = phead;
   phead->next->prev = newnode;
   phead->next = newnode;
 }

在这里插入图片描述

3.6、双向链表尾删

要注意链表必须有效且链表不能为空,链表为空即链表只有一个哨兵位。

 //尾删
 void LTPopBack(LTNode* phead)
 {
   assert(phead && phead->next != phead);//链表有效且不为空
   LTNode* del = phead->prev;
   del->prev->next = phead;
   phead->prev = del->prev;
   free(del);//删除del节点
   del = NULL;
 }

3.7、双向链表头删

//头删
 void LTPopFront(LTNode* phead)
 {
   assert(phead && phead->next != phead);
   
   LTNode* del = phead->next;
   phead->next = del->next;
   del->next->prev = phead;
   free(del);//删除del节点
   del = NULL;
 }

3.8、查找指定数据

//查找
 LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//没有找到
	return NULL;
}

3.9、在指定位置之后插入数据

//在指定位置之后插入数据
 void LTInsert(LTNode* pos,LTDataType x)
 {
   assert(pos);
   LTNode* newnode = LTBuyNode(x);
   newnode->next = pos->next;
   newnode->prev = pos;

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

在这里插入图片描述

3.10、删除pos节点

 //删除pos节点
 void LTErase(LTNode* pos)
 {
   assert(pos);
   pos->next->prev = pos->prev;
   pos->prev->next = pos->next;

   free(pos);
   pos = NULL;
}

3.11、销毁链表

//销毁链表
 void LTDesTroy(LTNode* phead)
 {
    assert(phead);
    
    LTNode* pcur = phead->next;
    while(pcur != phead)
    {
      LTNode* next = pcur->next;
      free(pcur);
      pcur = next;
    }
    free(phead);
    phead = NULL;
 }

LTErase和LTDestroy参数理论上要传二级指针,因为我们需要让形参的改变影响到实参,但是为了保持接口的一致性才传的一级指针,传一级指针存在的问题是:当形参phead置为NULL后,实参plist不会被修改为NULL,因此解决办法是:调用完方法后手动将实参置为NULL。

四、总代码

1、List.h

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

typedef int LTDataType;

//定义双向链表节点的结构
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//初始化
//void LTInit(LTNode** pphead);
LTNode* LTInit();
//申请节点
LTNode* LTBuyNode(LTDataType x);
//销毁
void LTDesTroy(LTNode* phead);
//双向链表打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x); 
//头插
void LTPushFront(LTNode* phead, LTDataType x); 
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x);
 

2、List.c

 #include"List.h"

 //申请节点
 LTNode* LTBuyNode(LTDataType x)
 {
   LTNode* node = (LTNode*)malloc(sizeof(LTNode));
   if(node == NULL)
   {
     perror("malloc fail!");
     exit(1);
   }
   node->data = x;
   node->next = node->prev = node;

   return node;
}

//初始化
 LTNode* LTInit()
 {
   //给双向链表创建一个哨兵位
   LTNode* phead = BuyListNode(-1);//哨兵位不存储有效的值,这里传参传-1(也可以传其他值)
   return phead;
 }
  
  //打印
  void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾插
 void LTPushBack(LTNode* phead,LTDataType x)
 {
   assert(phead);//哨兵位不能为空
   LTNode* newnode = LTBuyNode(x);
   newnode->prev = phead->prev;
   newnode->next = phead;
   phead->prev->next = newnode;/1
   phead->prev = newnode;//2
   //1和2不能交换位置
 }

 //头插
 void LTPushFront(LTNode* phead,LTDataType x)
 {
   assert(phead);
   LTNode* newnode = LTBuyNode(x);
   newnode->next = phead->next;
   newnode->prev = phead;
   phead->next->prev = newnode;
   phead->next = newnode;
 }

  //尾删
 void LTPopBack(LTNode* phead)
 {
   assert(phead && phead->next != phead);//链表有效且不为空
   LTNode* del = phead->prev;
   del->prev->next = phead;
   phead->prev = del->prev;
   free(del);//删除del节点
   del = NULL;
 }
  
 //头删
 void LTPopFront(LTNode* phead)
 {
   assert(phead && phead->next != phead);
   
   LTNode* del = phead->next;
   phead->next = del->next;
   del->next->prev = phead;
   free(del);//删除del节点
   del = NULL;
 }

 //查找
 LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//没有找到
	return NULL;
}

//在指定位置之后插入数据
 void LTInsert(LTNode* pos,LTDataType x)
 {
   assert(pos);
   LTNode* newnode = LTBuyNode(x);
   newnode->next = pos->next;
   newnode->prev = pos;

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

//删除pos节点
 void LTErase(LTNode* pos)
 {
   assert(pos);
   pos->next->prev = pos->prev;
   pos->prev->next = pos->next;

   free(pos);
   pos = NULL;
}

//销毁链表
 void LTDesTroy(LTNode* phead)
 {
    assert(phead);
    
    LTNode* pcur = phead->next;
    while(pcur != phead)
    {
      LTNode* next = pcur->next;
      free(pcur);
      pcur = next;
    }
    free(phead);
    phead = NULL;
 }

3、test.c

 #include"List.h"

void ListTest01()
{
	//LTNode* plist = NULL;
	//LTInit(&plist);
	LTNode* plist = LTInit();

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPrint(plist);

	LTNode* find = LTFind(plist, 3);
	//LTInsert(find, 66);
	LTErase(find);
	find = NULL;

	LTPrint(plist);
	LTDesTroy(plist);
	//测试尾删
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
}

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

那么到这里就结束啦!感谢您的阅读,点个小赞再走吧。后面将持续为您更新哦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值