链表结构深度解析:从单向无头到双向循环的实现全指南

上篇博客实现动态顺序表时,我们会发现它存在许多弊端,如:

• 中间/头部的插⼊删除,时间复杂度为O(N)
• 增容需要申请新空间,拷⻉数据,释放旧空间。会有不⼩的消耗。
• 增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。

这些问题该怎么解决呢?

我们需要学习线性表的另一种实现方式--链表。


目录

1.链表的分类

2.单链表

2.1概念与结构

2.1.1结点

2.1.2链表的性质

2.2单链表的实现

2.2.1 SList.h

2.2.2 SList.c 

 3.双向链表

3.1.概念与结构

 3.2双向链表的实现

3.2.1List.h

 3.2.2 List.c

4.顺序表与链表比较 

 


1.链表的分类

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

链表可从三个维度分类组合:

  1. 单向与双向
    • 单向链表:结点仅含一个指向下一结点的指针;
    • 双向链表:结点含前驱、后继两个指针。
  2. 带头与不带头
    • 带头链表有专门头结点(不存数据),便于操作;
    • 不带头链表首个结点即数据结点。
  3. 循环与不循环
    • 循环链表:最后结点指针指向头结点(带头)或首结点(不带头);
    • 不循环链表:最后结点指针为 null(单向)或后继为 null 且首结点前驱为 null(双向)。

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

2.单链表

2.1概念与结构

概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
举个例子,这是一列火车:

淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
在链表⾥,每节“⻋厢”是什么样的呢?

2.1.1结点

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点”
结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望
plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。

2.1.2链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、结点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码,假设当前保存的结点为整型:
struct SListNode {int data; //结点数据struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数
据,也需要保存下⼀个结点的地址(直到下⼀个结点为空时保存的地址为空)。

2.2单链表的实现

注意:以下函数的一些参数有一些是二级指针,不是一级指针,使用时要格外注意。使用二级指针是因为要改变实参(使用一级指针的话,形参的变化影响不了实参)。

2.2.1 SList.h

#pragma once#include<stdio.h>
#include<assert.h>
#include<stdlib.h>typedef int SLTDataType;typedef struct SListNode
{SLTDataType data; //结点数据struct SListNode* next; //指针保存下⼀个结点的地址
}SLTNode;void SLTPrint(SLTNode * phead);//头部插⼊删除/尾部插⼊删除
void SLTPushBack(SLTNode * *pphead, SLTDataType x);
void SLTPushFront(SLTNode * *pphead, SLTDataType x);void SLTPopBack(SLTNode * *pphead);
void SLTPopFront(SLTNode * *pphead);//查找
SLTNode * SLTFind(SLTNode * phead, SLTDataType x);//在指定位置之前插⼊数据
void SLTInsert(SLTNode * *pphead, SLTNode * pos, SLTDataType x);//删除pos结点
void SLTErase(SLTNode * *pphead, SLTNode * pos);//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode * pos, SLTDataType x);//删除pos之后的结点
void SLTEraseAfter(SLTNode * pos);//销毁链表
void SListDestroy(SLTNode * *pphead);

2.2.2 SList.c 

2.2.2.1创建结点,打印链表,销毁链表

#define _CRT_SECURE_NO_WARNINGS#include "SList.h"// 测试函数:手动创建并遍历一个单链表,最后释放内存
void test1()
{// 手动创建4个节点并初始化数据(实际开发中建议封装成函数)SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));n1->data = 1;  // 节点1数据赋值为1SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));n2->data = 2;  // 节点2数据赋值为2SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));n3->data = 3;  // 节点3数据赋值为3SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));n4->data = 4;  // 节点4数据赋值为4// 手动连接节点形成链表:1 -> 2 -> 3 -> 4 -> NULLn1->next = n2;n2->next = n3;n3->next = n4;n4->next = NULL;  // 链表终止标志// 遍历链表并打印数据SLTNode* pcur = n1;  // 从链表头节点开始遍历while (pcur){printf("%d ", pcur->data);  // 打印当前节点数据pcur = pcur->next;          // 移动到下一个节点}printf("\n");  // 打印换行// 安全释放链表内存(防止内存泄漏)SLTNode* current = n1;  // 重新从头节点开始while (current){SLTNode* temp = current;  // 临时保存当前节点地址current = current->next;  // 先移动到下一个节点free(temp);               // 释放当前节点内存}// 注意:此时n1~n4已成为野指针,不可再访问
}int main()
{test1();  // 执行测试函数return 0;
}

封装函数:

1.创建结点:

SLTNode* CreateNode(int data)
{SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));if (node == NULL){perror("malloc");exit(EXIT_FAILURE);}node->data = data;node->next = NULL;return node;
}

2.打印链表

void SLTPrint(SLTNode* phead)
{assert(phead);SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}

3.销毁链表

void SListDestroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* current = *pphead;while (current){SLTNode* temp = current;current = current->next;free(temp);}*pphead = NULL;
}

 2.2.2.2尾部插入删除

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* new_node = CreateNode(x);if (*pphead == NULL){*pphead = new_node;}else{SLTNode* pcur = *pphead;while (pcur->next){pcur = pcur->next;}pcur->next = new_node;}
}void SLTPopBack(SLTNode** pphead)
{assert(pphead && *pphead);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}

2.2.2.3头部插入删除  

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* new_node = CreateNode(x);new_node->next = *pphead;*pphead = new_node;
}void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* tem = (*pphead)->next;free(*pphead);*pphead = tem;
}

2.2.2.3查找 

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* pcur = phead;while (pcur){if (pcur->data = x){return pcur;}pcur = pcur->next;}return NULL;
}

2.2.2.4在指定位置插入删除

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);if (*pphead == pos){SLTPushFront(pphead,x);}else{SLTNode* new_node = CreateNode(x);SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = new_node;new_node->next = pos;}
}void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* new_node = CreateNode(x);new_node->next = pos->next;pos->next = new_node;
}void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);if (*pphead == pos){SLTPopFront(pphead);}else{SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;}
}void SLTEraseAfter(SLTNode* pos)
{assert(pos);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}

 3.双向链表

3.1.概念与结构

带头双向循环链表是一种具有特定结构与特性的线性数据结构。其每个节点包含三部分:数据域(存储实际数据)、前驱指针(prev,指向前驱节点)与后继指针(next,指向后继节点),以此实现双向链接。链表存在一个头结点(通常为哨兵节点,不存储有效数据),尾节点的 next 指针指向头结点,头结点的 prev 指针指向尾节点,构成循环结构。

 

从结构细节看:

 
  • 头结点(head):作为链表起始点,next 指向首个数据节点(如 d1),prev 指向尾节点(如 d3)。
  • 数据节点(如 d1d2d3
    • d1 的 prev 指向头结点 headnext 指向 d2
    • d2 的 prev 指向 d1next 指向 d3
  • 尾节点(d3next 指向头结点 head,prev 指向 d2
typedef int LTDataType;typedef struct listNode {LTDataType data;struct listNode* next;struct listNode* prev;
}LTNode;

 3.2双向链表的实现

3.2.1List.h

#pragma once#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int LTDataType;typedef struct listNode {LTDataType data;struct listNode* next;struct listNode* prev;
}LTNode;LTNode* CreateNode(LTDataType x);//初始化
LTNode* LTInit2();
void LTInit1(LTNode** pphead);//打印链表
void LTPrint(LTNode* phead);//判断链表是否为空
bool LTEmpty(LTNode* phead);//尾插及尾删
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);//头插及头删
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, LTDataType x);//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);//删除pos结点
void LTErase(LTNode* pos);//销毁链表
void LTDestroy(LTNode* phead);

 3.2.2 List.c

3.2.2.1初始化链表,创建结点,打印链表,判断链表是否无有效数据

LTNode* CreateNode(LTDataType x)
{LTNode* new_node = (LTNode*)malloc(sizeof(LTNode));if (new_node == NULL){perror("malloc");exit(1);}new_node->data = x;new_node->prev = new_node->next = new_node;return new_node;
}void LTInit1(LTNode** pphead)
{assert(pphead);*pphead = CreateNode(-1);
}LTNode* LTInit2()
{LTNode* phead = CreateNode(-1);return phead;
}void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d ", pcur->data);pcur = pcur->next;}printf("\n");
}bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}

3.2.2.2尾插及尾删

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//phead->prev new_node pheadLTNode* new_node = CreateNode(x);new_node->next = phead;new_node->prev = phead->prev;phead->prev->next = new_node;phead->prev = new_node;
}void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//phead->prev->prev  phead->prev  pheadLTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);
}

尾插

 

尾删

 

3.2.2.3头插及头删

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* new_node = CreateNode(x);//phead new_node phead->nextnew_node->next = phead->next;new_node->prev = phead;phead->next->prev = new_node;phead->next = new_node;
}void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}

头插

头删

 3.2.2.4查找,在指定位置插入及删除

 

LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* new_node = CreateNode(x);new_node->next = pos->next;new_node->prev = pos;pos->next->prev = new_node;pos->next = new_node;
}void LTErase(LTNode* pos)
{assert(pos);assert(!(pos->prev == pos && pos->next == pos));pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);
}

在指定位置之后插入

 

删除指定位置结点

3.2.2.5销毁链表

 

void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* del = pcur;pcur = pcur->next;free(del);}free(phead);
}

4.顺序表与链表比较 

不同点顺序表链表(单链表)
存储空间上
物理上⼀定连续
逻辑上连续,但物理上不⼀定连续
随机访问
⽀持O(1)
不⽀持:O(N)
任意位置插⼊或者删除元素
可能需要搬移元素,效率低O(N)
只需修改指针指向
插⼊
动态顺序表,空间不够时需要扩容和空间浪费
没有容量的概念,按需申请释放,不存在空间浪费
应⽤场景
元素⾼效存储+频繁访问
任意位置⾼效插⼊和删除

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/80079.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

@PostConstruct @PreDestroy

PostConstruct 是 Java EE&#xff08;现 Jakarta EE&#xff09;中的一个注解&#xff0c;用于标记一个方法在对象初始化完成后立即执行。它在 Spring 框架、Java Web 应用等场景中广泛使用&#xff0c;主要用于资源初始化、依赖注入完成后的配置等操作。 1. 基本作用 执行时…

【ArcGIS微课1000例】0146:将多个文件夹下的影像移动到一个目标文件夹(以Landscan数据为例)

本文讲述将多个文件夹下的影像移动到一个目标文件夹,便于投影变换、裁剪等操作。 文章目录 一、数据准备二、解压操作三、批量移动四、查看效果五、ArcGIS操作一、数据准备 全球人口数据集Landscan2000-2023如下所示,每年数据位一个压缩包: 二、解压操作 首先将其解压,方…

专业级 GIF 制作工具深度解析:Gifski 与 GIPHY CAPTURE 的技术对比与实战指南

《Gifski 与 GIPHY CAPTURE&#xff1a;GIF 制作工具的深度对比与实战应用》 最近在尝试做一些培训文档&#xff0c;需要使用GIF图做动态效果&#xff0c;把工具选型过程给大家做一下分享。 先看一张对比表&#xff0c;具体如下&#xff1a; 场景 Windows macOS Linux 移…

selenium替代----playwright

安装 好处特点&#xff1a;这个东西不像selenium需要固定版本的驱动 pip config set global.index-url https://mirrors.aliyun.com/pypi/simplepip install --upgrade pippip install playwright playwright installplaywright install ffmpeg (处理音视频的)验证&#x…

Python代码编程基础

字符串 str.[]实现根据下标定位实现对元素的截取 for 循环可以实现遍历 while 循环可以在实现遍历的同时实现对某一下标数值的修改 字符串前加 r 可以实现对字符串的完整内容输出 字符串前加 f 可以实现对字符串内{}中包裹内容的格式化输出&#xff0c;仅在 v3.6 之后可用…

5月9号.

v-for: v-bind: v-if&v-show: v-model: v-on: Ajax: Axios: async&await: Vue生命周期: Maven: Maven坐标:

Spring 必会之微服务篇(1)

目录 引入 单体架构 集群和分布式架构 微服务架构 挑战 Spring Cloud 介绍 实现方案 Spring Cloud Alibaba 引入 单体架构 当我们刚开始学开发的时候&#xff0c;基本都是单体架构&#xff0c;就是把一个项目的所有业务的实现功能都打包在一个 war 包或者 Jar 包中。…

计算机的基本组成

#灵感# 记录下基础知识&#xff0c;此处专指计算机硬件方面&#xff0c;捎带记下芯片知识。 综述&#xff1a; 计算机硬件的基本组成包括运算器、控制器、存储器、输入设备和输出设备五大部分。其中&#xff0c;集成在一起的运算器和控制器称为 CPU&#xff08;处理器&#x…

【Python 列表(List)】

Python 中的列表&#xff08;List&#xff09;是最常用、最灵活的有序数据集合&#xff0c;支持动态增删改查操作。以下是列表的核心知识点&#xff1a; 一、基础特性 有序性&#xff1a;元素按插入顺序存储可变性&#xff1a;支持增删改操作允许重复&#xff1a;可存储重复元…

Qt 的原理及使用(1)——qt的背景及安装

1. Qt 背景介绍 1.1 什么是 Qt Qt 是⼀个 跨平台的 C 图形⽤⼾界⾯应⽤程序框架 。它为应⽤程序开发者提供了建⽴艺术级图形 界⾯所需的所有功能。它是完全⾯向对象的&#xff0c;很容易扩展。Qt 为开发者提供了⼀种基于组件的开发模 式&#xff0c;开发者可以通过简单的拖拽…

多分类问题softmax传递函数+交叉熵损失

在多分类问题中&#xff0c;Softmax 函数通常与交叉熵损失函数结合使用。 Softmax 函数 Softmax 函数是一种常用的激活函数&#xff0c;主要用于多分类问题中。它将一个实数向量转换为概率分布&#xff0c;使得每个元素的值在 0 到 1 之间&#xff0c;且所有元素的和为 1。 …

数智读书笔记系列032《统一星型模型--一种敏捷灵活的数据仓库和分析设计方法》

引言 在当今数字化时代,数据仓库作为企业数据管理的核心基础设施,承担着整合、存储和提供企业数据的关键角色。随着商业环境的快速变化和业务需求的日益复杂,数据仓库的设计方法也在不断演进,以适应新的挑战和要求。 背景与意义 数据仓库领域长期存在着两种主流方法论之…

RT-Thread 深入系列 Part 1:RT-Thread 全景总览

摘要&#xff1a; 本文将从 RTOS 演进、RT-Thread 的版本分支、内核架构、核心特性、社区与生态、以及典型产品应用等多维度&#xff0c;全面呈现 RT-Thread 的全景图。 关键词&#xff1a;RT-Thread、RTOS、微内核、组件化、软件包管理、SMP 1. RTOS 演进与 RT-Thread 定位 2…

[docker基础一]docker简介

目录 一 消除恐惧 1) 什么是虚拟化&#xff0c;容器化 2)案例 3)为什么需要虚拟化&#xff0c;容器化 二 虚拟化实现方式 1)应用程序执行环境分层 2)虚拟化常见类别 3)常见虚拟化实现 一&#xff09;主机虚拟化(虚拟机)实现 二&#xff09;容器虚拟化实现 一 消除恐…

PostgreSQL 的 pg_advisory_lock 函数

PostgreSQL 的 pg_advisory_lock 函数 pg_advisory_lock 是 PostgreSQL 提供的一种应用级锁机制&#xff0c;它不锁定具体的数据库对象&#xff08;如表或行&#xff09;&#xff0c;而是通过数字键值来协调应用间的并发控制。 锁的基本概念 PostgreSQL 提供两种咨询锁(advi…

SGLang 实战介绍 (张量并行 / Qwen3 30B MoE 架构部署)

一、技术背景 随着大语言模型&#xff08;LLM&#xff09;的飞速发展&#xff0c;如何更高效、更灵活地驾驭这些强大的模型生成我们期望的内容&#xff0c;成为了开发者们面临的重要课题。传统的通过拼接字符串、管理复杂的状态和调用 API 的方式&#xff0c;在处理复杂任务时…

微服务中 本地启动 springboot 无法找到nacos配置 启动报错

1. 此处的环境变量需要匹配nacos中yml配置文件名的后缀 对于粗心的小伙伴在切换【测试】【开发】环境的nacos使用时会因为这里导致项目总是无法启动成功

Lua从字符串动态构建函数

在 Lua 中&#xff0c;你可以通过 load 或 loadstring&#xff08;Lua 5.1&#xff09;函数从字符串动态构建函数。以下是一个示例&#xff1a; 示例 1&#xff1a;基本动态函数构建 -- 动态构建一个函数 local funcStr "return function(a, b) return a b end"-…

【Python】‌Python单元测试框架unittest总结

1. 本期主题&#xff1a;Python单元测试框架unittest详解 unittest是Python内置的单元测试框架&#xff0c;遵循Java JUnit的"测试驱动开发"&#xff08;TDD&#xff09;理念&#xff0c;通过继承TestCase类实现测试用例的模块化组织。本文聚焦于独立测试脚本的编写…

【Python 实战】---- 使用Python批量将 .ncm 格式的音频文件转换为 .mp3 格式

1. 前言 .ncm 格式是网易云音乐专属的加密音频格式,用于保护版权。这种格式无法直接播放,需要解密后才能转换为常见的音频格式。本文将介绍如何使用 Python 批量将 .ncm 格式的音频文件转换为 .mp3 格式。 2. 安装 ncmdump ncmdump 是一个专门用于解密 .ncm 文件的工具。它…