双向链表详解

一、什么是双向链表?

双向链表(Doubly Linked List)是一种链式数据结构,每个节点包含三个部分:数据域、指向前驱节点的指针(prev)、指向后继节点的指针(next)。与单向链表相比,双向链表可以方便地进行前向和后向遍历,插入和删除操作也更加灵活高效。

图解:

二、双向链表的结构定义

在C语言中,双向链表的节点通常定义如下:

复制代码
// 数据类型定义
typedef int LTDataType;
​
// 双向链表节点结构体
typedef struct LTNode {
    LTDataType data;      // 数据域
    struct LTNode* prev; // 指向前驱节点
    struct LTNode* next; // 指向后继节点
} LTNode;

三、双向链表的基本操作

1. 初始化链表

初始化时,通常创建一个带有哨兵(头结点)的循环双向链表,便于操作。

复制代码
LTNode* ListInit() {
    LTNode* phead = BuyListNode(0); // 头结点数据可设为0或特殊值
    phead->next = phead;
    phead->prev = phead;
    return phead;
}

2. 判断链表是否为空

复制代码
//判断双链表是否为空
bool ListEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

3. 计算链表长度

复制代码
//计算双向链表大小
size_t ListSize(LTNode* phead)
{
	assert(phead);
	size_t n = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		++n;
		cur = cur->next;
	}
	return n;
}

4. 节点的动态申请

复制代码
// 动态申请一个节点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

5. 插入操作

尾插
复制代码
//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead, x);
}
头插
复制代码
//双向链表头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	ListInsert(phead->next, x);
}
在指定位置前插入

图解:

代码实现:

复制代码
// 在pos之前去插入
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

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

6. 删除操作

尾删
复制代码
//双向链表尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

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

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

	ListErase(phead->prev);
}
头删
复制代码
//双向链表头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

	ListErase(phead->next);
}
删除指定位置节点
复制代码
// 删除pos位置
void ListErase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);
	pos = NULL;

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

7. 查找操作

复制代码
//双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

8. 打印链表

复制代码
// 双向链表打印
void ListPrint(LTNode* phead)
{
	assert(phead);

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

9. 销毁链表

复制代码
//销毁双向链表
void ListDestroy(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		//cur = NULL;
		cur = next;
	}

	free(phead);
	//phead = NULL;  
	// 这里其实置空不置空都可以的,因为处理函数作用,没人能访问phead
	// 其次就是phead形参的置空,也不会影响外面的实参
}

四、完整示例

头文件:

复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;//后继
	struct ListNode* prev;//前驱
	LTDataType data;
}LTNode;


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

// 动态申请一个节点
LTNode* BuyListNode(LTDataType x);

//void ListInit(LTNode** pphead);

//链表初始化
LTNode* ListInit();

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

bool ListEmpty(LTNode* phead);
size_t ListSize(LTNode* phead);

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

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

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

// 删除pos位置
void ListErase(LTNode* pos);

// 在pos之前去插入
void ListInsert(LTNode* pos, LTDataType x);

.c文件

复制代码
#include "List.h"

//判断双链表是否为空
bool ListEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}
//计算双向链表大小
size_t ListSize(LTNode* phead)
{
	assert(phead);
	size_t n = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		++n;
		cur = cur->next;
	}
	return n;
}

// 动态申请一个节点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

//void ListInit(LTNode** pphead)
//{
//	*pphead = BuyListNode(-1);
//	(*pphead)->next = *pphead;
//	(*pphead)->prev = *pphead;
//}

//链表初始化
LTNode* ListInit()
{
	LTNode* phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

//销毁双向链表
void ListDestroy(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		//cur = NULL;
		cur = next;
	}

	free(phead);
	//phead = NULL;  
	// 这里其实置空不置空都可以的,因为处理函数作用,没人能访问phead
	// 其次就是phead形参的置空,也不会影响外面的实参
}

//void ListDestroy(LTNode** pphead)
//{
//	LTNode* cur = (*pphead)->next;
//	while (cur != *pphead)
//	{
//		LTNode* next = cur->next;
//		free(cur);
//		cur = next;
//	}
//
//	free(*pphead);
//	*pphead = NULL;
//}


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

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

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

//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead, x);
}
//双向链表头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	ListInsert(phead->next, x);
}
//双向链表尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

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

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

	ListErase(phead->prev);
}
//双向链表头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));

	ListErase(phead->next);
}
//双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

// 删除pos位置
void ListErase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);
	pos = NULL;

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

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

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

测试文件:

复制代码
#include"List.h"

void test()
{
	LTNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPrint(plist);

}
int main()
{

	test();
	return 0;
}

五、注意事项与常见错误

  1. 内存管理 :每次malloc后都要free,防止内存泄漏。

  2. 断言与空指针检查:操作前应检查指针有效性。

  3. 循环链表优势:带头结点的循环双向链表能简化插入、删除等边界情况的处理。

  4. 插入与删除的O(1)特性:只要已知节点指针,插入和删除操作时间复杂度均为O(1)。

六、总结

双向链表是链表家族中非常重要的一员,适合需要频繁在两端或中间插入、删除元素的场景。掌握其实现原理和常见操作,有助于更好地理解数据结构的本质和C语言的指针操作。

相关推荐
超级大福宝4 小时前
使用 LLVM 16.0.4 编译 MiBench 中的 patricia遇到的 rpc 库问题
c语言·c++
闭着眼睛学算法7 小时前
【华为OD机考正在更新】2025年双机位A卷真题【完全原创题解 | 详细考点分类 | 不断更新题目 | 六种主流语言Py+Java+Cpp+C+Js+Go】
java·c语言·javascript·c++·python·算法·华为od
麦麦在写代码8 小时前
动态内存管理 干货2
c语言
say_fall8 小时前
C语言底层学习(2.指针与数组的关系与应用)(超详细)
c语言·开发语言·学习
祐言QAQ9 小时前
(超详细,于25年更新版) VMware 虚拟机安装以及Linux系统—CentOS 7 部署教程
linux·运维·服务器·c语言·物联网·计算机网络·centos
Ziyoung9 小时前
【探究】C语言-类型转换问题
c语言
JasmineX-111 小时前
数据结构——静态链表(c语言笔记)
c语言·数据结构·链表
学不动CV了12 小时前
ARM单片机中断及中断优先级管理详解
c语言·arm开发·stm32·单片机·嵌入式硬件·51单片机
自信的小螺丝钉13 小时前
Leetcode 4. 两两交换链表中的节点 递归 / 迭代
leetcode·链表
番茄大杀手13 小时前
C/C++柔性数组
c语言·柔性数组