深入理解双向链表:从创建到核心操作的完整指南
双向链表是数据结构中极具代表性的链式结构,它解决了单链表 "单向遍历" 的局限,让数据操作更灵活。本文将带你从概念理解 到代码实现 ,彻底掌握双向链表的创建、初始化以及头插、尾插、头删、尾删等核心操作。
双向链表
- 深入理解双向链表:从创建到核心操作的完整指南
-
- 一、双向链表是什么?和单链表有何不同?
-
- 结构对比:单链表 vs 双向链表
- 二、双向链表的节点结构定义
- 三、双向链表的初始化与创建
- 四、核心操作 1:尾插(在链表尾部插入节点)
- 五、核心操作 1:头插(在链表头部插入节点)
- 六、核心操作 4:尾删(删除链表尾部节点)
- 七、核心操作 3:头删(删除链表头部节点)
- 八、查找节点(搭配指定位置添加和删除)
- 九、指定位置尾插
- 十、指定位置头插
- 十一、指定位置删除
一、双向链表是什么?和单链表有何不同?
如果把单链表比作 "单向行驶的火车"(只能从车头到车尾),那双向链表就是 "双向行驶的高铁"------ 它的每个节点不仅能指向 "下一个节点",还能指向 "前一个节点"。
结构对比:单链表 vs 双向链表
- 单链表节点:仅包含 "数据域" 和 "指向下一节点的指针域"。
- 双向链表节点 :包含 "数据域"、"指向下一节点的指针域(
next)"、"指向前一节点的指针域(prev)"。
这种结构让双向链表具备两大核心优势:
- 双向遍历:既能从前往后找,也能从后往前找。
- 插入 / 删除效率更高 :无需像单链表那样遍历找前驱节点,通过
prev指针可直接定位。
二、双向链表的节点结构定义
首先定义双向链表的节点结构,这是实现所有操作的基础:
头文件:
C
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{
LTDataType data; //数据域:存储节点数据
struct ListNode* next; //前驱指针:指向前一个节点
struct ListNode* prev; //后驱指针:指向后一个节点
}ListNode;
三、双向链表的初始化与创建
初始化的目标是创建一个空链表 ,让头、尾指针都指向NULL,长度置为 0。
形式:
C
//初始化
void ListInit(ListNode** PPhead);
实现函数:
C
//申请节点
ListNode* LTBuyNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("maloc fail!");
exit(1);
}
node->data = x;
//双向链表的节点要自己指向自己
node->next = node->prev = node;
return node;
}
//初始化
void ListInit(ListNode** PPhead)
{
//给链表创建一个哨兵位
*PPhead = LTBuyNode(-1);
}
四、核心操作 1:尾插(在链表尾部插入节点)
尾插的逻辑是:新节点成为 "新尾",原尾节点成为新节点的prev,新节点成为原尾节点的next。
形式:
C
//尾删
void ListPopBack(ListNode* Phead);
//由于不能改变哨兵位,使用不用传入节点的地址,预防改变哨兵位
函数实现:
C
//尾删
void ListPopBack(ListNode* Phead)
{
assert(Phead && Phead->next);
#if 0
//方案1:
//让被删除的上一个节点,指向头节点
Phead->prev->prev->next = Phead;
//指向好了,就把尾节点释放掉
ListNode* scr = Phead->prev;
free(scr);
scr = NULL;
//让头节点的尾指向被删除的上一个节点
Phead->prev = Phead->prev->prev;
#endif
#if 1
//方案2:
//创建一个被删除节点的变量
ListNode* del = Phead->prev;
//Phead del->prev del
del->prev = Phead;
Phead->next = del->prev;
//释放掉删除的节点
free(del);
del = NULL;
#endif
}
五、核心操作 1:头插(在链表头部插入节点)
头插的逻辑是:新节点成为 "新头",原头节点成为新节点的next,新节点成为原头节点的prev。
形式:
C
//头插
void ListPushFront(ListNode* Phead, LTDataType x);
函数实现:
C
//头插
void ListPushFront(ListNode* Phead, LTDataType x)
{
assert(Phead);
ListNode* newnode = LTBuyNode(x);
//与尾插的思维相同,画图分析
newnode->next = Phead->next;
newnode->prev = Phead;
//需改变的节点:Phead newnode Phead->next;
//两行代码不能完全交换
Phead->next->prev = newnode;
Phead->next = newnode;
}
六、核心操作 4:尾删(删除链表尾部节点)
尾删的逻辑是:将尾指针前移一位,同时断开原尾节点的prev和next,并释放内存。
C
//尾删
void ListPopBack(ListNode* Phead);
函数实现:
C
//尾删
void ListPopBack(ListNode* Phead)
{
assert(Phead && Phead->next != Phead);
#if 0
//方案1:
//让被删除的上一个节点,指向头节点
Phead->prev->prev->next = Phead;
//指向好了,就把尾节点释放掉
ListNode* scr = Phead->prev;
free(scr);
scr = NULL;
//让头节点的尾指向被删除的上一个节点
Phead->prev = Phead->prev->prev;
#endif
#if 1
//方案2:
//创建一个被删除节点的变量
ListNode* del = Phead->prev;
//Phead del->prev del
Phead->prev = del->prev;
del->prev->next = Phead;
//释放掉删除的节点
free(del);
del = NULL;
#endif
}
七、核心操作 3:头删(删除链表头部节点)
头删的逻辑是:将头指针后移一位,同时断开原头节点的prev和next,并释放内存。
形式:
C
//头删
void ListPopFront(ListNode* Phead);
函数实现:
C
//头删
void ListPopFront(ListNode* Phead)
{
assert(Phead && Phead->next != Phead);
#if 0
//让头节点指向被删除的下一个节点
//1.必须先把被删除的下一个节点用指针保存起来
//2.因为在释放内存时,空指针不能解引用
ListNode* PheadNext = Phead->next->next;
//手动释放被删除的空间
//1.将第一个节点释放时,需要一个指针接收
//2.因为在释放时,不用指针接收的地址释放,就会产生未初始化的指针解引用
ListNode* scr = Phead->next;
free(scr);
scr = NULL;
//让头节点指向被删除的下一个节点
Phead->next = PheadNext;
//让被删除的下一个节点,指向头节点
Phead->next->next->prev = Phead;
#endif
#if 1
ListNode* del = Phead->next;
//Phead del->next del
//指向第二个节点
Phead->next = del->next;
//指向哨兵位
del->next->prev = Phead;
//手动释放删除的节点
free(del);
del = NULL;
#endif
}
八、查找节点(搭配指定位置添加和删除)
查找节点的逻辑是:循环遍历双向链表,如果节点中的数据等于要找的数据,就返回当前地址,否则返回NULL
形式:
C
//查找节点
ListNode* ListFind(ListNode* Phead, LTDataType x);
函数实现:
C
//查找节点
ListNode* ListFind(ListNode* Phead, LTDataType x)
{
ListNode* pcur = Phead->next;
while (pcur != Phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
九、指定位置尾插
指定位置尾插,无论插入的位置在哪都不会影响该结果,包括尾插也一样 。函数实现可以查考尾插
形式:
C
//指定位置之后插入数据
void ListInsert(ListNode* pos, LTDataType x);
函数实现:
C
//指定位置之后插入数据
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
//接收新节点
ListNode* newnode = LTBuyNode(x);
//让新节点指向pos节点
newnode->prev = pos;
//让新节点指向pos前一个节点
newnode->next = pos->next;
//让pos节点前一个节点的后面指向新节点
pos->next->prev = newnode;
//让pos节点指向新节点
pos->next = newnode;
}
十、指定位置头插
指定位置头插其实和指定位置尾插很类型,将条件改成相反即可
形式:
C
//指定位置之前插入数据
void ListInsertend(ListNode* pos, LTDataType x);
函数实现:
C
//指定位置之前插入数据
void ListInsertend(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = LTBuyNode(x);
//让新节点前面指向pos节点
newnode->next = pos;
//让新节点后面指向pos后一个节点
newnode->prev = pos->prev;
//让pos后一个节点的前面指向新节点
pos->prev->next = newnode;
//让pos后一个指向新节点
pos->prev = newnode;
}
十一、指定位置删除
形式:
C
//删除指定节点
void ListPop(ListNode* pos);
函数实现:
C
//删除指定节点
void ListPop(ListNode* pos)
{
assert(pos);
//pos->perv pos pos->next
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}