数据结构——双向链表

1 双向链表

1.1 概念与结构

双向链表(Doubly Linked List)是一种链式数据结构,它由一系列节点组成,每个节点包含三部分:数据部分(存储元素)、指向前一个节点的指针(prev)和指向下一个节点的指针(next)。与单向链表不同,双向链表的节点不仅可以通过下一个节点(next)进行访问,还可以通过前一个节点(prev)进行访问,因此它支持从两个方向进行遍历。

带头链表里面的头结点(head),实际为“哨兵位”,哨兵位结点实际不储存任何元素,只是站在这里“放哨”。

 结点点对应的结构体代码:

struct Node {
    int data;         // 数据部分
    struct Node* prev; // 指向前一个节点的指针
    struct Node* next; // 指向下一个节点的指针
};

这种结构使得在任意节点的插入、删除以及遍历操作都非常高效。希望这个简要的图示和说明可以帮助你理解双向链表的基本概念!

1.2 性质特点

  1. 双向链接:每个节点不仅可以访问到下一个节点,还可以访问到前一个节点。这样,可以在两个方向上遍历链表。

  2. 动态大小:与数组不同,双向链表的大小不固定,可以根据需要动态添加或删除节点,内存使用更加灵活。

  3. 插入和删除操作:在双向链表中,插入和删除节点的操作通常比数组更高效,因为不会涉及到大规模的数据移动。只需修改几个指针即可完成操作。

  4. 存储开销:由于双向链表的每个节点需要额外存储一个指向前一个节点的指针,因此其存储开销比单向链表大。

  5. 访问效率:虽然双向链表支持双向遍历,但由于访问特定元素仍需从头或尾开始遍历,因此随机访问的效率较低,通常是O(n)时间复杂度。

  6. 适合多种应用场景:双向链表非常适合需要频繁插入和删除的应用场景,比如实现队列、栈、LRU缓存等。

  7. 需要更多的内存管理:由于每个节点都需要维护两个指针,管理和操作双向链表时需要更小心内存的分配与释放,以避免内存泄漏和悬挂指针。

总结来说,双向链表既具有灵活的动态特性,又能支持双向遍历,但在空间效率和随机访问方面有所折中。

1.3 双向链表的遍历

双链表是循环结构,要想遍历双链表,需要找到双链表的特殊条件:哨兵位。

双向链表为空的情况下只有一个哨兵位,如果连哨兵位都没有的话,这不是双链表而是单链表。

while(pcur != head)
{
    printf("%d", pcur->data);
    pcur = pcur->next;
}

2 双向链表的实现

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


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


void LTPrint(LTNode* phead);

//双向链表的初始化   plist  &plist  
//void LTInit(LTNode** pphead);//传地址:形参的改变影响实参
LTNode* LTInit();
//为了保持接口一致性,建议统一参数,都传一级:手动将实参置为NULL
void LTDesTroy(LTNode* phead);
//传二级:未保持接口一致性
//void LTDesTroy(LTNode** pphead);

//尾插
//phead结点不会发生改变,参数传一级
//phead结点发生改变,参数传二级
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);

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

//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的结点
void LTErase(LTNode* pos);

bool LTEmpty(LTNode* phead);

2.1 尾插

在实现尾插法时,需要根据链表是否为空的情况进行不同的处理:

判断链表是否为空

  • 链表为空:如果 tail == NULL,说明链表中没有元素。这时新节点既是头节点也是尾节点,因此将 head 和 tail 都指向该节点。
  • 链表非空:如果链表不为空,说明链表已有元素。这时我们将新节点的 prev 指针指向当前尾节点,当前尾节点的 next 指针指向新节点,然后将 tail 更新为新节点。

链表为空:

链表非空:

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = buyNode(x);
	newnode->prev = phead->prev;
	newnode->next = phead;

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

2.2 头插

头插法的核心思想是:每次都将新节点插入到链表的头部,使得新节点成为链表的头节点。

判断链表是否为空,若为空则新节点成为头节点且尾节点;若非空,则新节点成为新的头节点,更新链表的头部指针和相关节点的指针。

判断链表是否为空

  • 链表为空:如果 head == NULL,说明链表没有任何节点。此时新节点既是头节点也是尾节点,因此我们将 head 和 tail 都指向该节点。
  • 链表非空:如果链表非空,则将新节点插入到链表的头部。我们需要做以下操作:
    • 新节点的 next 指向当前的头节点。
    • 当前头节点的 prev 指向新节点。
    • 将 head 更新为新节点。

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

	phead->next->prev = newnode;
	phead->next = newnode;
}
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

2.3 尾删

尾删操作实现

尾删操作是指删除链表的尾节点,并更新相应的指针。对于双向链表,尾删的操作分为几种情况:

  1. 链表为空:如果 head == NULL,说明链表为空,无法删除节点。则先断言该节点不为空。
  2. 链表只有一个节点:如果 head == tail,说明链表中只有一个节点,删除该节点后,head 和 tail 都应指向 NULL
  3. 链表有多个节点:如果链表中有多个节点,我们需要将尾节点的前一个节点更新为新的尾节点,并且更新尾指针。

删除尾节点的步骤

  1. 如果 list->tail 是链表的最后一个节点,则需要先判断链表是否为空。
  2. 如果链表非空,则需要更新 tail 指针,指向倒数第二个节点。
  3. 更新尾节点的 next 指针为 NULL,并释放原尾节点。

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;
	//phead del->prev(d2) del(d3)
	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
}

2.4 头删

头删操作实现

头删操作是指删除链表的第一个节点,并更新相应的指针。对于双向链表,头删操作分为以下几种情况:

  1. 链表为空:如果 head == NULL,说明链表为空,无法删除节点。
  2. 链表只有一个节点:如果 head == tail,说明链表中只有一个节点,删除该节点后,head 和 tail 都应指向 NULL
  3. 链表有多个节点:如果链表中有多个节点,我们需要将头节点的 next 节点的 prev 指针更新为 NULL,并且更新 head 指针。

 删除头节点的步骤

  1. 如果链表为空(head == NULL),直接返回。
  2. 如果链表只有一个节点(head == tail),删除该节点后,head 和 tail 都应设置为 NULL
  3. 如果链表有多个节点,将头节点的 next 节点的 prev 指针设置为 NULL,并将 head 指针指向下一个节点。

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->next;
	//phead del del->next
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

2.5 在指定位置之后插入数据

在指定位置后插入数据时,首先我们需要找到目标位置的节点,然后将新节点插入到该节点之后。插入的步骤如下:

  1. 插入的位置是链表的头部:在这种情况下,插入节点需要成为新的头节点,更新头节点的指针。
  2. 插入的位置是链表的尾部:如果目标节点是尾节点,新的节点将成为新的尾节点,更新尾指针。
  3. 插入的位置是中间某个节点:需要调整目标节点的 next 和 prev 指针来插入新节点。

插入操作步骤

  1. 找到目标节点:从链表的头部开始,找到指定位置的节点。
  2. 创建新节点:分配内存并填充数据。
  3. 调整指针:更新目标节点的 next 和新节点的 prev 指针,最后将新节点的 next 指向目标节点的下一个节点(如果有)。

//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = buyNode(x);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

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

2.6 删除指定位置的结点

删除操作的核心是:

  1. 遍历链表找到目标位置的节点
  2. 调整相邻节点的指针,将目标节点从链表中移除。
  3. 处理三种特殊情况
    • 删除链表的头节点。
    • 删除链表的尾节点。
    • 删除中间节点。

删除操作的步骤

  • 目标节点是头节点

    • 更新头指针,使其指向下一个节点。
    • 如果链表只包含一个节点,删除后需要将尾指针也设置为 NULL
  • 目标节点是尾节点

    • 更新尾指针,使其指向前一个节点。
  • 目标节点是中间节点

    • 更新前一个节点的 next 指针,指向目标节点的 next
    • 更新下一个节点的 prev 指针,指向目标节点的 prev

//删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos);

	//pos->prev  pos  pos->next
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;
}

2.7 销毁

销毁双向链表的核心操作是释放每个节点占用的内存。我们从链表的头节点开始,逐个释放节点。每次访问一个节点时,我们保存下一个节点的指针,释放当前节点,然后移动到下一个节点。销毁过程中需要特别注意:

  • 遍历链表时需要小心处理指针。
  • 最后,链表的头指针和尾指针需要设置为 NULL,表示链表已被销毁。

void LTDesTroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

最后附上完整代码:

#include"List.h"

LTNode* buyNode(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;
}

//双向链表的初始化
//void LTInit(LTNode** pphead)
//{
//	*pphead = buyNode(-1);
//}

LTNode* LTInit()
{
	LTNode* phead = buyNode(-1);
	return phead;
}

void LTDesTroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}


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 = buyNode(x);
	//newnode phead->prev(尾结点) phead
	newnode->prev = phead->prev;
	newnode->next = phead;

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

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

	phead->next->prev = newnode;
	phead->next = newnode;
}
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;
	//phead del->prev(d2) del(d3)
	del->prev->next = phead;
	phead->prev = del->prev;

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

	free(del);
	del = NULL;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = buyNode(x);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

	pos->next->prev = newnode;
	pos->next = newnode;
}
//删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos);

	//pos->prev  pos  pos->next
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值