【数据结构】双向链表的实现
深入理解双向链表:从创建到核心操作的完整指南
双向链表是数据结构中极具代表性的链式结构,它解决了单链表 “单向遍历” 的局限,让数据操作更灵活。本文将带你从概念理解到代码实现,彻底掌握双向链表的创建、初始化以及头插、尾插、头删、尾删等核心操作。
双向链表
- 深入理解双向链表:从创建到核心操作的完整指南
- 一、双向链表是什么?和单链表有何不同?
- 结构对比:单链表 vs 双向链表
- 二、双向链表的节点结构定义
- 三、双向链表的初始化与创建
- 四、核心操作 1:尾插(在链表尾部插入节点)
- 五、核心操作 1:头插(在链表头部插入节点)
- 六、核心操作 4:尾删(删除链表尾部节点)
- 七、核心操作 3:头删(删除链表头部节点)
- 八、查找节点(搭配指定位置添加和删除)
- 九、指定位置尾插
- 十、指定位置头插
- 十一、指定位置删除
一、双向链表是什么?和单链表有何不同?
如果把单链表比作 “单向行驶的火车”(只能从车头到车尾),那双向链表就是 “双向行驶的高铁”—— 它的每个节点不仅能指向 “下一个节点”,还能指向 “前一个节点”。
结构对比:单链表 vs 双向链表
- 单链表节点:仅包含 “数据域” 和 “指向下一节点的指针域”。
- 双向链表节点:包含 “数据域”、“指向下一节点的指针域(
next)”、“指向前一节点的指针域(prev)”。
这种结构让双向链表具备两大核心优势:
- 双向遍历:既能从前往后找,也能从后往前找。
- 插入 / 删除效率更高:无需像单链表那样遍历找前驱节点,通过
prev指针可直接定位。
二、双向链表的节点结构定义
首先定义双向链表的节点结构,这是实现所有操作的基础:
头文件:
#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。
形式:
//初始化
void ListInit(ListNode** PPhead);
实现函数:
//申请节点
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。
形式:
//尾删
void ListPopBack(ListNode* Phead);
//由于不能改变哨兵位,使用不用传入节点的地址,预防改变哨兵位
函数实现:
//尾删
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 deldel->prev = Phead;Phead->next = del->prev;//释放掉删除的节点free(del);del = NULL;
#endif
}
五、核心操作 1:头插(在链表头部插入节点)
头插的逻辑是:新节点成为 “新头”,原头节点成为新节点的next,新节点成为原头节点的prev。
形式:
//头插
void ListPushFront(ListNode* Phead, LTDataType x);
函数实现:
//头插
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,并释放内存。
//尾删
void ListPopBack(ListNode* Phead);
函数实现:
//尾删
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 delPhead->prev = del->prev;del->prev->next = Phead;//释放掉删除的节点free(del);del = NULL;
#endif
}
七、核心操作 3:头删(删除链表头部节点)
头删的逻辑是:将头指针后移一位,同时断开原头节点的prev和next,并释放内存。
形式:
//头删
void ListPopFront(ListNode* Phead);
函数实现:
//头删
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 1ListNode* del = Phead->next;//Phead del->next del//指向第二个节点Phead->next = del->next;//指向哨兵位del->next->prev = Phead;//手动释放删除的节点free(del);del = NULL;
#endif
}
八、查找节点(搭配指定位置添加和删除)
查找节点的逻辑是:循环遍历双向链表,如果节点中的数据等于要找的数据,就返回当前地址,否则返回NULL
形式:
//查找节点
ListNode* ListFind(ListNode* Phead, LTDataType x);
函数实现:
//查找节点
ListNode* ListFind(ListNode* Phead, LTDataType x)
{ListNode* pcur = Phead->next;while (pcur != Phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
九、指定位置尾插
指定位置尾插,无论插入的位置在哪都不会影响该结果,包括尾插也一样。函数实现可以查考尾插
形式:
//指定位置之后插入数据
void ListInsert(ListNode* pos, LTDataType x);
函数实现:
//指定位置之后插入数据
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;
}
十、指定位置头插
指定位置头插其实和指定位置尾插很类型,将条件改成相反即可
形式:
//指定位置之前插入数据
void ListInsertend(ListNode* pos, LTDataType x);
函数实现:
//指定位置之前插入数据
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;
}
十一、指定位置删除
形式:
//删除指定节点
void ListPop(ListNode* pos);
函数实现:
//删除指定节点
void ListPop(ListNode* pos)
{assert(pos);//pos->perv pos pos->nextpos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}
