当前位置: 首页 > news >正文

数据结构--双链表

目录

一、引言

二 、链表的分类

1.单向或双向

2.带头或不带头 

3.循环或不循环 

 三、双链表的概念与基本结构

1.概念

2.基本结构 

三、双链表的常见操作

1.创建节点

2.初始化 

3.头插

4.尾插

5.头删

6.尾删

7.打印

8.查找

9.插入节点

10.删除节点

11.销毁链表

五、完整代码

1.List.h

2.List.c

3.test.c

六、总结


一、引言

双链表是一种在节点之间通过两个指针进行连接的数据结构,每个节点都有两个指针:一个指向前一个节点,另一个指向下一个节点。带头节点的双链表在实际应用中非常常见,本文将详细介绍其实现方法,并提供完整的 C 语言代码示例。

二 、链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

1.单向或双向

  • 单向链表:

    • 每个节点包含一个指向下一个节点的指针。
    • 只能从头到尾单向遍历。
    • 插入和删除操作较简单,但需要从头开始查找节点。
  • 双向链表:

    • 每个节点包含两个指针,一个指向下一个节点,一个指向前一个节点。
    • 可以从头到尾或尾到头双向遍历。
    • 插入和删除操作更灵活,但每个节点占用更多内存。

2.带头或不带头 

  • 带头节点:

    • 链表有一个额外的头节点,它不存储实际数据,只作为链表的起始点。
    • 操作如插入和删除更简单,因为头节点简化了边界条件处理。
  • 不带头节点:

    • 链表从第一个实际数据节点开始,没有额外的头节点。
    • 需要特别处理空链表和边界情况。

3.循环或不循环 

  • 循环链表:

    • 链表的尾节点指向头节点,形成一个循环结构。
    • 遍历时可以从任何节点开始,不会遇到“末尾”问题。
  • 非循环链表:

    • 链表的尾节点指向 NULL,表示链表的结束。
    • 遍历时需要检查是否到达链表末尾。

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表和双向带头循环链表
1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

本节我们所讲的双链表即为双向带头循环链表。 

 三、双链表的概念与基本结构

1.概念

双链表简介
双链表是一种链表变体,每个节点都包含三个部分:
存储的数据。
指向前一个节点的指针(prev)。
指向下一个节点的指针(next)。
带头节点的双链表有一个特殊的节点称为头节点,它不存储有效数据,只是作为链表的一个起始辅助节点存在。头节点的 prev 指针指向自己,next 指针指向链表的第一个有效节点。

2.基本结构 

双链表的基本结构如下:

typedef struct ListNode
{DataType data;//数据域struct ListNode* prev;//指针域,指向前一个节点struct ListNode* next;//指针域,指向后一个节点
}LN;

三、双链表的常见操作

1.创建节点

//申请节点
LN* ListBuyNode(DataType x)
{LN* node = (LN*)malloc(sizeof(LN));if (node == NULL){perror("malloc fail!");exit(1);}node->data = x;node->prev = node->next = node;//前后指针都指向自己,使得链表循环起来
}

2.初始化 

//初始化
void ListInit(LN** pphead)
{*pphead = ListBuyNode(-1);//头节点,随便给一个值(用不到)
}

3.头插

//头插
void ListPushFront(LN* phead, DataType x)
{assert(phead);LN* newnode = ListBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;//上面两行代码位置不能交换
}

4.尾插

//尾插
void ListPushBack(LN* phead, DataType x)
{assert(phead);LN* newnode = ListBuyNode(x);newnode->prev = phead->prev;//新节点prev指向原来的尾节点newnode->next = phead;//新节点next指向头节点phead->prev->next = newnode;//原来的尾节点next指向newnodephead->prev = newnode;//头节点的prev指向newnode//上面两行代码位置不能交换
}

5.头删

//头删
void ListPopFront(LN* phead)
{assert(phead && phead->next != phead);//链表必须有效且链表不为空LN* del = phead->next;del->next->prev = phead;phead->next = del->next;//删除del节点free(del);del = NULL;
}

 

6.尾删

//尾删
void ListPopBack(LN* phead)
{assert(phead&&phead->next!=phead);//链表必须有效且链表不为空LN* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;//删除del节点free(del);del = NULL;
}

7.打印

//打印
void ListPrint(LN* phead)
{LN* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}

8.查找

//查找
LN* ListFind(LN* phead, DataType x)
{LN* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;//找到了}pcur = pcur->next;}return NULL;//没有找到 
}

9.插入节点

//插入,pos节点后插入
void ListInsert(LN* pos, DataType x)
{assert(pos);LN* newnode = ListBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}

10.删除节点

//删除
void ListErase(LN* pos)//这里不传二级指针,是为了保持接口一致性
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}

11.销毁链表

//销毁链表
void ListDestory(LN* phead)
{assert(phead);LN* pcur = phead->next;while (pcur != phead){LN* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;
}

注:

LTErase和LTDestroy参数理论上要传一级,因为我们需要让形参的改变影响到实参,但是为了保持接口一致性才传的一级。传一级存在的问题是,当形参phead置为NULL后,实参plist不会被修改为NULL,因此解决办法是:调用完方法后手动将实参置为NULL~ 

五、完整代码

1.List.h

该部分放顺序表结构定义、函数的声明

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int DataType;
typedef struct ListNode
{DataType data;//数据域struct ListNode* prev;//指针域,指向前一个节点struct ListNode* next;//指针域,指向后一个节点
}LN;
//初始化
void ListInit(LN** pphead);
//尾插
void ListPushBack(LN*phead,DataType x);
//头插
void ListPushFront(LN*phead,DataType x);
//打印
void ListPrint(LN* phead);
//尾删
void ListPopBack(LN* phead);
//头删
void ListPopFront(LN* phead);
//查找
LN* ListFind(LN* phead, DataType x);
//插入,pos节点后插入
void ListInsert(LN* pos, DataType x);
//删除
void ListErase(LN* pos);
//销毁链表
void ListDestory(LN* phead);

2.List.c

该部分是函数功能的实现

#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
//申请节点
LN* ListBuyNode(DataType x)
{LN* node = (LN*)malloc(sizeof(LN));if (node == NULL){perror("malloc fail!");exit(1);}node->data = x;node->prev = node->next = node;//前后指针都指向自己,使得链表循环起来
}
//初始化
void ListInit(LN** pphead)
{*pphead = ListBuyNode(-1);//头节点,随便给一个值(用不到)
}
//尾插
void ListPushBack(LN* phead, DataType x)
{assert(phead);LN* newnode = ListBuyNode(x);newnode->prev = phead->prev;//新节点prev指向原来的尾节点newnode->next = phead;//新节点next指向头节点phead->prev->next = newnode;//原来的尾节点next指向newnodephead->prev = newnode;//头节点的prev指向newnode//上面两行代码位置不能交换
}
//头插
void ListPushFront(LN* phead, DataType x)
{assert(phead);LN* newnode = ListBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;//上面两行代码位置不能交换
}
//尾删
void ListPopBack(LN* phead)
{assert(phead&&phead->next!=phead);//链表必须有效且链表不为空LN* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;//删除del节点free(del);del = NULL;
}
//头删
void ListPopFront(LN* phead)
{assert(phead && phead->next != phead);//链表必须有效且链表不为空LN* del = phead->next;del->next->prev = phead;phead->next = del->next;//删除del节点free(del);del = NULL;
}
//打印
void ListPrint(LN* phead)
{LN* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
//查找
LN* ListFind(LN* phead, DataType x)
{LN* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;//找到了}pcur = pcur->next;}return NULL;//没有找到 
}
//插入,pos节点后插入
void ListInsert(LN* pos, DataType x)
{assert(pos);LN* newnode = ListBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}
//删除
void ListErase(LN* pos)//这里不传二级指针,是为了保持接口一致性
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}
//销毁链表
void ListDestory(LN* phead)
{assert(phead);LN* pcur = phead->next;while (pcur != phead){LN* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;
}

3.test.c

测试,函数的调用

#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
void test01()
{LN* plist = NULL;ListInit(&plist);/*ListPushBack(plist, 3);ListPushBack(plist, 2);ListPushBack(plist, 1);ListPushBack(plist, 0);*/ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);// ListPopBack(plist);//ListPopFront(plist);LN* find = ListFind(plist, 3);//if (find == NULL)//	printf("没找到\n");//else//	printf("找到了\n");ListInsert(find, 99);ListErase(find);find = NULL;ListPrint(plist);ListDestory(plist);plist = NULL;}
int main()
{test01();return 0;
}

六、总结

带头节点的双链表在进行节点的插入和删除操作时具有较好的灵活性。头节点的存在简化了链表操作的边界条件,减少了对空链表和链表边界的特殊处理。通过本文的实现和示例代码,你应该能掌握双链表的基本操作,并能够将其应用于实际的编程任务中。
希望这个博客对你有帮助!如果你有任何问题或者需要进一步的解释,欢迎在评论区留言。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 《C Primer Plus》第 2 章复习题和编程练习
  • 如何用静态住宅代理实现分布式代理网络
  • (学习总结16)C++模版2
  • 基于Python的B站热门视频可视化分析与挖掘系统
  • Ansible使用kubeadm方式一键安装k8s
  • 详解TCP的三次握手
  • git 合并分支并解决冲突
  • Kubernetes 常用命令、资源配置整理
  • IHostedLifecycleService是如何管理后台任务的
  • 学生请假管理系统
  • 执行机构是怎么运作的
  • 超详细!!!electron-vite-vue开发桌面应用之应用更新版本提示(十三)
  • 软件测试学习笔记丨Docker 安装、管理、搭建服务
  • ASP.net core 8.0网站发布
  • Linux软件包循环依赖解决 彻底删除i386架构 更新软件源
  • Apache的基本使用
  • Docker入门(二) - Dockerfile
  • JavaScript函数式编程(一)
  • JAVA之继承和多态
  • ng6--错误信息小结(持续更新)
  • 程序员最讨厌的9句话,你可有补充?
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 力扣(LeetCode)965
  • 聊聊flink的BlobWriter
  • 浅谈web中前端模板引擎的使用
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 协程
  • 字符串匹配基础上
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • ()、[]、{}、(())、[[]]命令替换
  • (¥1011)-(一千零一拾一元整)输出
  • (html转换)StringEscapeUtils类的转义与反转义方法
  • (Matalb时序预测)WOA-BP鲸鱼算法优化BP神经网络的多维时序回归预测
  • (Pytorch框架)神经网络输出维度调试,做出我们自己的网络来!!(详细教程~)
  • (solr系列:一)使用tomcat部署solr服务
  • (二)换源+apt-get基础配置+搜狗拼音
  • (附源码)php新闻发布平台 毕业设计 141646
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (九)信息融合方式简介
  • (算法)Game
  • (算法)N皇后问题
  • (转)程序员技术练级攻略
  • (转)机器学习的数学基础(1)--Dirichlet分布
  • (转载)hibernate缓存
  • ***测试-HTTP方法
  • .net refrector
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NET连接MongoDB数据库实例教程
  • .net通过类组装数据转换为json并且传递给对方接口
  • /proc/stat文件详解(翻译)
  • @cacheable 是否缓存成功_Spring Cache缓存注解
  • @TableId注解详细介绍 mybaits 实体类主键注解
  • [ 转载 ] SharePoint 资料
  • [1]从概念到实践:电商智能助手在AI Agent技术驱动下的落地实战案例深度剖析(AI Agent技术打造个性化、智能化的用户助手)
  • [100天算法】-实现 strStr()(day 52)