数据结构之双向链表

发布于:2025-07-21 ⋅ 阅读:(19) ⋅ 点赞:(0)

1.双向链表的概念

        双向链表是对单链表的一种改进数据结构。在单链表里,每个节点仅包含一个指向后继节点的指针域;而双向链表在此基础上,为每个节点额外设置了一个指向前驱节点的指针域。

        双向链表和单链表的显著区别在于节点的访问方式。在单链表中,若要访问某个节点的前驱节点,只能从链表的头节点开始,按照顺序依次遍历,直至找到目标节点的前驱,过程相对繁琐。但双向链表则不同,它的每个节点都能凭借自身的指针域,直接、便捷地访问其前驱节点和后继节点,无需从头节点开始按顺序逐个查找,大大提高了数据访问的灵活性和效率。

2.双向链表的结构与特点

双向链表的每个节点由三个部分组成

  (1)存储当前节点的数据---data

  (2)前驱指针---prev

  (3)后驱指针---next

带头双向循环链表具有以下显著特点:

头结点(哨兵位)的存在

        该链表设置了头结点作为哨兵位。这个特殊的头结点不存储实际的数据,却在链表的操作中扮演着至关重要的角色。它就像是链表的“守护者”,使得链表无论在何种操作下,都能保持一种稳定的结构。

链表永不为空

        得益于头结点的存在,带头双向循环链表在逻辑上永远不会为空。即使链表中没有存储有效数据的节点,头结点依然存在,保证了链表结构的完整性。这一特性避免了在处理空链表时复杂的边界条件判断,简化了代码逻辑,降低了编程难度。

无需二级指针

        在对链表进行插入、删除等操作时,通常不需要使用二级指针。由于头结点始终存在,我们可以直接通过头结点来操作链表,避免了使用二级指针传递地址的复杂性。这不仅让代码更加简洁易懂,还减少了因指针操作不当而引发错误的可能性。

节点访问的高效性

        在带头双向循环链表中,任意一个节点都可以方便地找到它的上一个节点和下一个节点。每个节点不仅有指向下一个节点的指针,还有指向前一个节点的指针,并且链表首尾相连形成循环。这种结构使得我们可以从任意一个节点出发,沿着指针的方向快速访问到其相邻节点,大大提高了数据的访问效率和操作的灵活性。

整体结构:

//定义双向链表中节点的结构
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;  //后继节点
	struct ListNode* prev;  //前驱节点

	LTDataType data;   //当前节点的数据

}LTNode;

3.双链表的总接口


//初始化链表
LTNode* LTInit();
//打印链表
void LTPrint(LTNode* phead);

//判断是否为空
bool LTEmpty(LTNode* 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 LTInsertAfter(LTNode* pos,LTDataType x);
// 删除pos位置的值
void LTErase(LTNode* pos);

//销毁
void LTDestroy(LTNode* phead);

3.1初始化链表

3.1.1创建节点

当我们需要插入元素是,只需要创建节点,然后直接把值存入,两个指针置为空,返回该节点的地址就可

//创建节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	//判断节点创建是否成功,避免野指针的出现
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;

}
3.1.2初始化链表

        我们可以采用一种灵活高效的策略。首先,创建一个哨兵位节点。这个哨兵位节点就如同一个稳固的“基石”,我们让它自成一个循环链表,形成一个初始的、稳定的结构框架。

        当后续有数据添加需求时,我们可采用尾插法来添加新数据。尾插法就像是在已有的队伍末尾依次加入新成员,操作简单且有序。借助这种方式,数据的添加变得极为自由,只要有需要,随时都能进行添加操作,无需提前规划复杂的添加流程。

        更为重要的是,采用这种方式无需我们为存储容量问题而烦恼。不像传统的一些存储方式需要频繁地进行扩容操作,过程繁琐且可能会造成资源的浪费。在这里,我们只需要根据实际的数据量不断开辟新的内存空间,真正做到了“用多少,开多少”,极大地提高了内存资源的使用效率,避免了不必要的资源闲置与浪费。

//初始化链表
LTNode* LTInit()
{
	//创建一个哨兵位
	LTNode*phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

3.2打印链表

我们从哨兵位的下一位开始打印,直到cur的下一位是phead打印完成

//打印链表
void LTPrint(LTNode* phead)
{
	assert(phead);

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

3.3尾插

我们只需要将tail->next连接到newnode上,再将newnode->prev连接到tail上,最后phead的prev和newnode的next连接即可

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

	tail->next = newnode;
	newnode->prev = tail;

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

}

3.4头插

头插就是将哨兵位和头一个节点断开,再进行和尾插类似的工作,特别注意我们要先保存好数据再进行相关操作,避免数据丢失。

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* first = phead->next;
	LTNode* newnode = BuyLTNode(x);

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

3.5尾删

尾删我们要先找到tail的前一个节点tailprev,然后将尾节点直接释放掉即可。

特别注意:哨兵位不能删除

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next);

	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;
	free(tail);

	phead->prev = tailprev;
	tailprev->prev = phead;

}

3.6头删

头删就是将哨兵位的后一位删除,同样哨兵位不能为空

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next);

	LTNode* first = phead->next;
	LTNode* firstnext = first->next;
	free(first);
	first = NULL;

	phead->next = firstnext;
	firstnext->prev = phead;

}

3.7判断是否为空

判断是否为空这里我们可以加上这个函数,如果phead下一个等于phead,说明链表为空只剩下哨兵位自己, 那么此时我们就不能继续删除了,这里我们可以用这个函数直接替换掉上面的断言

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

3.8查找

因为在循环链表中没有NULL,我们在查找过程中没有添加限制条件的话链表会无限循环,那么我们用phead作为条件,读取数据从第一位开始读,当读到phead表示已经把所有数据都遍历了一遍

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

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

3.9在pos插入数据(之前,之后,pos位)

我们通过pos->prev找到posprev的位置,然后将newnode与pos和1posprev联系起来即可

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* posprev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

}

同理可得其他两种情况

// 在pos之后插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* posnext = pos->next;
	LTNode* newnode = BuyLTNode(x);

	newnode->prev = pos;
	pos->next = newnode;
	newnode->next = posnext;
	posnext->prev = newnode;
}
// 删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;
	free(pos);

	posprev->next = posnext;
	posnext->prev = posprev;
}

3.10销毁

我们将链表的所有数值的清空

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

4.总结

至此,我们不难发现,无论是头插、头删操作,尾插、尾删操作,还是在指定的 `pos` 位置进行的相关操作,它们背后所遵循的原理实际上是相通的。本质上,头插、头删以及尾插、尾删操作都可以看作是 `pos` 位置操作的特殊情形。

以头插为例,它其实就是在链表头部这个特定的 `pos` 位置插入元素;头删则是删除链表头部这个 `pos` 位置的元素。同理,尾插是在链表尾部的 `pos` 位置插入元素,尾删则是删除尾部 `pos` 位置的元素。

基于这样的发现,我们完全有能力对现有的代码进行简化。通过将这些看似不同的操作归纳统一,提取出它们的共性部分,我们能够让代码的结构更加简洁清晰,减少冗余代码,提高代码的可维护性和可读性。如此一来,后续无论是对代码进行功能扩展还是故障排查,都会变得更加轻松高效。

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
 
	LTInsert(phead, x);
}
 
 
void LTPushFront(LTNode* phead, LTDataType x)
{
	LTInsert(phead->next, x);
}
 
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTErase(phead->prev);
}
 
 
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
 
	LTErase(phead->next);
 
}

5.完整代码

//List.h

#pragma once

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

//定义双向链表中节点的结构
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;  //后继节点
	struct ListNode* prev;  //前驱节点

	LTDataType data;   //当前节点的数据

}LTNode;


//初始化链表
LTNode* LTInit();
//打印链表
void LTPrint(LTNode* phead);

//判断是否为空
bool LTEmpty(LTNode* 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 LTInsertAfter(LTNode* pos, LTDataType x);
// 删除pos位置的值
void LTErase(LTNode* pos);

//销毁
void LTDestroy(LTNode* phead);
//Test.c

#include"List.h"

void TestList1()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);
}

void TestList2()
{
	LTNode* plist = LTInit();

	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

}

void TestList3()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		LTInsert(pos, 30);
	}
	LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;

}

int main() 
{
	TestList1();
	TestList2();
	TestList3();
	return 0;
}

//List.c

#include"List.h"

//创建节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	//判断节点创建是否成功,避免野指针的出现
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;

}

//初始化链表
LTNode* LTInit()
{
	//创建一个哨兵位
	LTNode*phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

//打印链表
void LTPrint(LTNode* phead)
{
	assert(phead);

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

//判断是否为空这里我们可以加上这个函数,如果phead下一个等于phead,说明链表为空只剩下哨兵位自己,
// 那么此时我们就不能继续删除了,这里我们可以用这个函数直接替换掉上面的断言
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

	tail->next = newnode;
	newnode->prev = tail;

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

}


//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* first = phead->next;
	LTNode* newnode = BuyLTNode(x);

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

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;
	free(tail);

	phead->prev = tailprev;
	tailprev->next = phead;

}

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

	LTNode* first = phead->next;
	LTNode* firstnext = first->next;
	free(first);

	phead->next = firstnext;
	firstnext->prev = phead;

}

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

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

// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* posprev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

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

}

// 在pos之后插入
void LTInsertAfter(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* posnext = pos->next;
	LTNode* newnode = BuyLTNode(x);

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

// 删除pos位置的值
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;
	free(pos);

	posprev->next = posnext;
	posnext->prev = posprev;
}

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

////代码简化
//void LTPushBack(LTNode* phead, LTDataType x)
//{
//	assert(phead);
//
//	LTInsert(phead, x);
//}
//
//
//void LTPushFront(LTNode* phead, LTDataType x)
//{
//	LTInsert(phead->next, x);
//}
//
//void LTPopBack(LTNode* phead)
//{
//	assert(phead);
//	assert(!LTEmpty(phead));
//	LTErase(phead->prev);
//}
//
//
//void LTPopFront(LTNode* phead)
//{
//	assert(phead);
//	assert(!LTEmpty(phead));
//
//	LTErase(phead->next);
//
//}

网站公告

今日签到

点亮在社区的每一天
去签到