双向链表详解

一、什么是双向链表?

双向链表(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语言的指针操作。

相关推荐
码农Cloudy.1 小时前
数据结构 --- 顺序表
c语言·数据结构
1白天的黑夜111 小时前
数据结构之堆(topk问题、堆排序)
c语言·数据结构·算法
一点.点15 小时前
针对C语言的开发工具推荐及分析(涵盖编辑器、集成开发环境(IDE)、编译器、调试工具及辅助工具)
c语言·开发工具
Hello.Reader17 小时前
Redis C语言连接教程
c语言·数据库·redis
?!71417 小时前
多线程和并发之线程
linux·c语言·c++
whoarethenext17 小时前
图像卷积OpenCV C/C++ 核心操作
c语言·c++·opencv·卷积
黑不拉几的小白兔18 小时前
第十五届蓝桥杯大赛软件赛国赛Python 大学 C 组试做【本期题单: 设置密码、栈】
c语言·python·蓝桥杯
guguhaohao1 天前
编译与链接,咕咕咕
c语言
生活很暖很治愈1 天前
《函数栈帧的创建和销毁》
c语言·数据结构·c++·编辑器
OKkankan1 天前
类和对象(中1)
c语言·数据结构·c++·算法