数据结构(四)-双向链表
双向链表
一、概念
对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可大大增加链表操作的便捷性。 因此,双向循环链表是在实际运用中最常见的链表形态。
二、基本操作
与普通的链表完全一致,双向循环链表虽然指针较多,但逻辑是完全一样的。基本的操作包括:
-
节点设计
-
初始化空链表
-
增删节点
-
链表遍历
-
销毁链表
三、代码实现
dlist.h
#ifndef _DLIST_H #define _DLIST_H #include <stdio.h> #include <stdlib.h> #include <string.h> typedef int DATA; //定义双向链表节点 typedef struct node {DATA data; //节点数据struct node *prev; //前驱指针struct node *next; //后继指针 }NODE; /*** 创建链表(初始化)* @param head 待操作的链表(默认头指针(头节点的指针))* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ extern int dlist_create(NODE **head, DATA data); /*** 向链表的头部插入数据(头插法)* @param head 待操作的链表(默认头指针)* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ extern int dlist_addHead(NODE **head, DATA data); /*** 向链表的尾部插入数据(尾插法)* @param head 待操作的链表(默认头指针)* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ extern int dlist_addTail(NODE **head, DATA data); /*** 向链表的指定位置插入数据(中间插法)* @param head 待操作的链表(默认头指针)* @param pos 待插入节点的目标位置* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ extern int dlist_insert(NODE **head, DATA pos, DATA data); /*** 遍历链表数据* @param head 待遍历的链表*/ extern void dlist_showAll(const NODE *head); /*** 根据data找到对应的节点* @param head 待查找的链表* @param data目标数据* @return 目标数据匹配到的节点*/ extern NODE *dlist_find(const NODE *head, DATA data); /*** 根据old修改对应节点的数据为newdata* @param head 待操作的链表* @param old 待修改的节点数据* @param newdata 修改节点后的数据* @return 成功返回0,否则返回-1*/ extern int dlist_update(const NODE *head, DATA old, DATA newdata); /*** 根据data删除对应的节点* @param head 待操作的链表* @param data 目标数据* @return 成功删除返回0,否则返回1*/ extern int dlist_delete(NODE **head, DATA data); /*** 销毁双向链表* @param head 待操作链表*/ extern void dlist_destroy(NODE **head); #endif //_DLIST_H
dlist.c
#include "dlist.h" /*** 创建链表(初始化)* @param head 待操作的链表(默认头指针(头节点的指针))* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ int dlist_create(NODE **head, DATA data) {//检测头节点是否存在if(*head){perror("链表已经存在!");return -1;}//创建新节点NODE *p = (NODE*)malloc(sizeof(NODE));//校验新节点是否创建成功if(!p){perror("链表创建失败!");return -1;}//初始化新节点p->data = data;p->prev = p->next = NULL; //双向链表特性:新节点的前驱和后继指针初始化为NULL//将新节点作为链表的头节点*head = p; //头节点指向新创建的节点return 0;} /*** 向链表的头部插入数据(头插法)* @param head 待操作的链表(默认头指针)* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ int dlist_addHead(NODE **head, DATA data) {//创建新节点NODE *p = (NODE*)malloc(sizeof(NODE));//校验新节点是否创建成功if(!p){perror("链表创建失败!");return -1;}//初始化新节点p->data = data;p->prev = NULL; //新节点作为头节点,前驱指针必须置空(双向链表特性)p->next = *head; //新节点的后继指针指向原头节点(连接链表主体)//处理原头节点的前驱指针(需防止空指针解引用)if(*head)(*head)->prev = p; //原头节点的前驱指针指向新节点(建立双向链接)//更新头指针指向新节点*head = p;return 0; } /*** 向链表的尾部插入数据(尾插法)* @param head 待操作的链表(默认头指针)* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ int dlist_addTail(NODE **head, DATA data) {//遍历链表的时候需要用到pNODE *pNew = (NODE*)malloc(sizeof(NODE));if(!pNew){fprintf(stderr, "节点创建失败!\n");return -1;}pNew->data = data;pNew->prev = NULL;pNew->next = NULL;//查找尾节点(从链表头开始遍历)NODE *p = *head; //p指向当前遍历的节点//情景1:空链表(直接插入新节点)if(!p){//让头指针指向pNew*head = pNew;return 0;}else //非空链表{//判断是否存在下一个节点while(p->next) //当p的next为NULL时,那么p一定是尾节点{p = p->next;} //此时p指向尾节点,p是尾指针}//更改指针的指向p->next = pNew; //原尾节点的next指向新节点pNew->prev = p; //新节点的prev指向原尾节点return 0; } /*** 向链表的指定位置插入数据(中间插法,插入到目标位置前)* @param head 待操作的链表(默认头指针)* @param pos 待插入节点的目标位置* @param data 待插入的数据* @return 成功返回0,否则返回-1*/ int dlist_insert(NODE **head, DATA pos, DATA data) {//遍历链表的时候需要用到pNODE *pNew = (NODE*)malloc(sizeof(NODE));if(!pNew){fprintf(stderr, "节点创建失败!\n");return -1;}pNew->data = data;pNew->prev = NULL;pNew->next = NULL;NODE *p = *head;//空链表if(!p){*head = pNew; //空链表时头指针指向新节点return 0;}//非空链表while(p){//寻找目标位置if(memcmp(&(p->data), &pos, sizeof(DATA)) == 0){//目标节点是头节点if(p->prev == NULL){pNew->next = p; //新节点后继指向原头节点p->prev = pNew; //原头节点前驱指向新节点*head = pNew; //更新头指针为新节点}//目标节点是非头节点else{pNew->next = p; //新节点后继指向目标节点pNew->prev = p->prev; //新节点前驱指向目标节点的前驱p->prev->next = pNew; //目标节点的上一个节点的后继指向新节点p->prev = pNew; //目标节点的前驱指向新节点}return 0;}p = p->next;}return -1; } /*** 遍历链表数据* @param head 待遍历的链表*/ void dlist_showAll(const NODE *head) {const NODE *p = head;//正序遍历(从头节点---尾节点)printf("正序遍历:");while(p){printf("%d\t", p->data);p = p->next;}printf("\n");//逆序遍历(从尾节点---头节点)printf("逆序遍历:");p = head;while(p->next) //先让指针跑到尾节点p = p->next;while(p){printf("%d\t", p->data);p = p->prev;}printf("\n");} /*** 根据data找到对应的节点* @param head 待查找的链表* @param data目标数据* @return 目标数据匹配到的节点*/ NODE *dlist_find(const NODE *head, DATA data) {const NODE *p = head;//空链表if(!p)return NULL;while(p){if(memcmp(&(p->data), &data, sizeof(DATA)) == 0)return (NODE*)p;p = p->next;}return NULL; } /*** 根据old修改对应节点的数据为newdata* @param head 待操作的链表* @param old 待修改的节点数据* @param newdata 修改节点后的数据* @return 成功返回0,否则返回-1*/ int dlist_update(const NODE *head, DATA old, DATA newdata) {NODE *p = NULL;if((p = dlist_find(head, old))){//更新数据p->data = newdata;return 0;}return -1;} /*** 根据data删除对应的节点* @param head 待操作的链表* @param data 目标数据* @return 成功删除返回0,否则返回1*/ int dlist_delete(NODE **head, DATA data) {//创建遍历指针指向链表头NODE *p = *head;//空链表if(!p)return -1;//遍历链表查找目标节点while(p){//查找节点if(memcmp(&(p->data), &data, sizeof(DATA)) == 0){//查找到的是头节点if(p->prev == NULL){//若该头节点是唯一节点if(p->next == NULL){//链表置空*head = NULL;}//若此链表包含其他节点else{p->next->prev = NULL;//更新头指针*head = p->next;}}//查找的非头节点else{p->prev->next = p->next;//当删除的目标不是尾节点时(非尾节点需要更新后继节点)if(p->next)p->next->prev = p->prev; //尾节点的next是NULL,不能往前指}free(p); //释放节点内存return 0;}p = p->next;}return -1; } /*** 销毁双向链表* @param head 待操作链表*/ void dlist_destroy(NODE **head) {//用指针尾随法NODE *p = *head, *q = NULL;while(p){q = p;p = p->next;free(q);}*head = NULL; }
app.c
#include "dlist.h" int main(int argc, char const *argv[]) {NODE *head = NULL; // 创建链表dlist_create(&head, 111); // 111 // 向链表插入数据dlist_addHead(&head, 222); // 222 111dlist_addHead(&head, 333); // 333 222 111dlist_addHead(&head, 444); // 444 333 222 111dlist_addTail(&head, 5555); // 444 333 222 111 5555dlist_insert(&head, 333, 8888);// 444 8888 333 222 111 5555 // 遍历链表dlist_showAll(head); // 修改链表数据dlist_update(head, 8888, 6666);dlist_showAll(head); // 444 6666 333 222 111 5555 // 删除节点数据dlist_delete(&head, 444); // 6666 333 222 111 5555dlist_delete(&head, 5555); // 6666 333 222 111 dlist_delete(&head, 222); // 6666 333 111 dlist_showAll(head); // 6666 333 111 // 销毁链表dlist_destroy(&head);return 0; }
四、优缺点总结
优点
-
双向遍历:可以从头到尾或从尾到头遍历链表,灵活性高。
-
删除操作高效:在已知节点指针时,删除节点的时间复杂度为 O(1)(无需遍历前驱节点)
-
插入操作灵活:可在给定节点前或后快速插入新节点。
-
支持反向操作:适用于需要逆向操作的场景(如撤销功能)
缺点
-
内存占用高;每个节点需存储前驱和后继指针,空间开销比单链表大
-
操作复杂度略高:插入和删除需维护两个指针,代码实现稍复杂。
-
缓存不友好:节点分散存储,可能引发缓存命中率下降。
对比单链表:
-
单链表:节省空间,但删除/插入需遍历前驱节点(O(n) 时间)
-
双向链表:以空间换时间,适合频繁双向操作的场景(如浏览器历史记录)。
典型应用:
-
需要双向遍历的数据结构(如LRU缓存、浏览器历史)
-
频繁在任意位置插入/删除的场景