一、什么是双向链表?
双向链表(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;
}
五、注意事项与常见错误
-
内存管理 :每次
malloc
后都要free
,防止内存泄漏。 -
断言与空指针检查:操作前应检查指针有效性。
-
循环链表优势:带头结点的循环双向链表能简化插入、删除等边界情况的处理。
-
插入与删除的O(1)特性:只要已知节点指针,插入和删除操作时间复杂度均为O(1)。
六、总结
双向链表是链表家族中非常重要的一员,适合需要频繁在两端或中间插入、删除元素的场景。掌握其实现原理和常见操作,有助于更好地理解数据结构的本质和C语言的指针操作。