探索链表的奇妙世界:从基础到高级应用

链表是计算机科学中一种基础且重要的数据结构,它如同一条由珠子串成的项链,每个珠子(节点)都包含着数据和指向下一个珠子的线索。

与数组相比,链表在插入和删除操作上更加灵活,无需预先分配固定大小的内存空间。

一、单链表:数据结构的入门之选

基本形态与特点

单链表是链表家族中最基础的成员,它由一系列节点组成,每个节点包含两部分:

数据域和指针域

数据域存储具体的数据,而指针域则指向下一个节点。

链表的起点由一个头指针(head)指向,最后一个节点的指针域指向 NULL,表示链表的结束。

特点总结

        单向访问:只能从头节点开始,沿着指针方向依次访问后续节点

        插入和删除操作效率高:O (1) 时间复杂度(前提是已知操作位置的前驱节点)

        动态内存分配:无需预先分配固定大小的内存

        随机访问效率低:访问第 n 个节点需要 O (n) 时间

示例代码:单链表的实现

#include <stdio.h>
#include <stdlib.h>// 定义单链表节点结构
typedef struct Node {int data;           // 数据域struct Node* next;  // 指针域,指向下一个节点
} Node;// 创建新节点
Node* createNode(int data) {Node* newNode = (Node*)malloc(sizeof(Node));if (newNode == NULL) {printf("内存分配失败\n");exit(1);}newNode->data = data;newNode->next = NULL;return newNode;
}// 在链表尾部插入节点
void append(Node** head_ref, int data) {Node* newNode = createNode(data);if (*head_ref == NULL) {*head_ref = newNode;return;}Node* last = *head_ref;while (last->next != NULL) {last = last->next;}last->next = newNode;
}// 打印链表
void printList(Node* node) {while (node != NULL) {printf("%d -> ", node->data);node = node->next;}printf("NULL\n");
}// 释放链表内存
void freeList(Node* head) {Node* temp;while (head != NULL) {temp = head;head = head->next;free(temp);}
}int main() {Node* head = NULL;append(&head, 1);append(&head, 2);append(&head, 3);printf("单链表内容: ");printList(head);freeList(head);return 0;
}

单链表示意图

头指针 head↓┌───────┐    ┌───────┐    ┌───────┐│ 1 | ──┼──→ │ 2 | ──┼──→ │ 3 | NULL└───────┘    └───────┘    └───────┘

二、双向链表:支持双向导航的灵活结构

基本形态与特点

双向链表在单链表的基础上进行了扩展,每个节点除了包含数据域和指向下一个节点的指针外,还增加了一个指向前驱节点的指针。

这种结构使得双向链表支持双向遍历,既可以从头节点向后访问,也可以从尾节点向前访问。

特点总结

                双向访问:支持从头节点和尾节点两个方向进行遍历

                插入和删除操作更加灵活:可以快速访问前驱节点

                每个节点占用更多内存:需要额外的指针域存储前驱节点信息

                操作复杂度增加:插入和删除操作需要维护两个指针

示例代码:双向链表的实现

#include <stdio.h>
#include <stdlib.h>// 定义双向链表节点结构
typedef struct DNode {int data;               // 数据域struct DNode* prev;     // 指向前驱节点的指针struct DNode* next;     // 指向后继节点的指针
} DNode;// 创建新节点
DNode* createDNode(int data) {DNode* newNode = (DNode*)malloc(sizeof(DNode));if (newNode == NULL) {printf("内存分配失败\n");exit(1);}newNode->data = data;newNode->prev = NULL;newNode->next = NULL;return newNode;
}// 在链表尾部插入节点
void appendDNode(DNode** head_ref, int data) {DNode* newNode = createDNode(data);if (*head_ref == NULL) {*head_ref = newNode;return;}DNode* last = *head_ref;while (last->next != NULL) {last = last->next;}last->next = newNode;newNode->prev = last;
}// 打印双向链表(正向)
void printForward(DNode* node) {while (node != NULL) {printf("%d <-> ", node->data);node = node->next;}printf("NULL\n");
}// 释放双向链表内存
void freeDList(DNode* head) {DNode* temp;while (head != NULL) {temp = head;head = head->next;free(temp);}
}int main() {DNode* head = NULL;appendDNode(&head, 1);appendDNode(&head, 2);appendDNode(&head, 3);printf("双向链表内容(正向): ");printForward(head);freeDList(head);return 0;
}

双向链表示意图

头指针 head↓┌───────┐    ┌───────┐    ┌───────┐
NULL←─| 1 | ──┼──→| 2 | ──┼──→| 3 |─→NULL└───────┘    └───────┘    └───────┘

三、循环链表:首尾相连的封闭结构

基本形态与特点

循环链表是一种特殊的链表结构,它的最后一个节点的指针域不再指向 NULL,而是指向链表的头节点,形成一个闭合的环。

循环链表可以是单循环链表(只使用一个方向的指针)或双循环链表(使用双向指针)。

特点总结

                首尾相连:可以从任意节点开始遍历整个链表

                没有明显的头节点和尾节点:需要特别注意循环终止条件

                适合实现环形缓冲区、轮询调度等场景

                插入和删除操作需要考虑维护循环特性

示例代码:单循环链表的实现

#include <stdio.h>
#include <stdlib.h>// 定义循环链表节点结构
typedef struct CNode {int data;struct CNode* next;
} CNode;// 创建新节点
CNode* createCNode(int data) {CNode* newNode = (CNode*)malloc(sizeof(CNode));if (newNode == NULL) {printf("内存分配失败\n");exit(1);}newNode->data = data;newNode->next = newNode;  // 初始时指向自身return newNode;
}// 在链表尾部插入节点
void appendCNode(CNode** head_ref, int data) {CNode* newNode = createCNode(data);if (*head_ref == NULL) {*head_ref = newNode;return;}CNode* last = (*head_ref)->next;  // 找到尾节点while (last->next != *head_ref) {last = last->next;}last->next = newNode;newNode->next = *head_ref;
}// 打印循环链表
void printCList(CNode* head) {if (head == NULL) return;CNode* temp = head;do {printf("%d -> ", temp->data);temp = temp->next;} while (temp != head);printf("(回到头节点)\n");
}// 释放循环链表内存
void freeCList(CNode* head) {if (head == NULL) return;CNode* temp = head->next;while (temp != head) {CNode* next = temp->next;free(temp);temp = next;}free(head);
}int main() {CNode* head = NULL;appendCNode(&head, 1);appendCNode(&head, 2);appendCNode(&head, 3);printf("循环链表内容: ");printCList(head);freeCList(head);return 0;
}

单循环链表示意图

   ┌───────┐    ┌───────┐    ┌───────┐│ 1 | ──┼──→ │ 2 | ──┼──→ │ 3 | ──┼──┐└───────┘    └───────┘    └───────┘  │↑                                  │└──────────────────────────────────┘

四、双向循环链表:最灵活的链表结构

基本形态与特点

双向循环链表结合了双向链表和循环链表的特点,每个节点既包含指向前驱节点的指针,也包含指向后继节点的指针,并且链表的首尾节点相连形成一个环。

这种结构提供了最大的灵活性,但也需要更多的内存和更复杂的操作。

特点总结

双向遍历:可以从任意节点开始向前或向后遍历首尾相连:

没有明显的头节点和尾节点插入和删除操作

需要维护四个指针(前驱和后继节点的指针)

适合需要频繁双向遍历和循环操作的场景

示例代码:双向循环链表的实现

#include <stdio.h>
#include <stdlib.h>// 定义双向循环链表节点结构
typedef struct DCNode {int data;struct DCNode* prev;struct DCNode* next;
} DCNode;// 创建新节点
DCNode* createDCNode(int data) {DCNode* newNode = (DCNode*)malloc(sizeof(DCNode));if (newNode == NULL) {printf("内存分配失败\n");exit(1);}newNode->data = data;newNode->prev = newNode;newNode->next = newNode;return newNode;
}// 在链表尾部插入节点
void appendDCNode(DCNode** head_ref, int data) {DCNode* newNode = createDCNode(data);if (*head_ref == NULL) {*head_ref = newNode;return;}DCNode* last = (*head_ref)->prev;  // 找到尾节点last->next = newNode;newNode->prev = last;newNode->next = *head_ref;(*head_ref)->prev = newNode;
}// 打印双向循环链表(正向)
void printDCListForward(DCNode* head) {if (head == NULL) return;DCNode* temp = head;do {printf("%d <-> ", temp->data);temp = temp->next;} while (temp != head);printf("(回到头节点)\n");
}// 释放双向循环链表内存
void freeDCList(DCNode* head) {if (head == NULL) return;DCNode* temp = head->next;while (temp != head) {DCNode* next = temp->next;free(temp);temp = next;}free(head);
}int main() {DCNode* head = NULL;appendDCNode(&head, 1);appendDCNode(&head, 2);appendDCNode(&head, 3);printf("双向循环链表内容(正向): ");printDCListForward(head);freeDCList(head);return 0;
}

双向循环链表示意图

   ┌───────┐    ┌───────┐    ┌───────┐│ 1 | ──┼──→ │ 2 | ──┼──→ │ 3 | ──┼──┐└───────┘    └───────┘    └───────┘  │↑  ↑                                ││  └────────────────────────────────┘│                                     │└─────────────────────────────────────┘

五、链表与数组的对比

特性链表数组
内存分配动态分配,无需预先指定大小静态分配,需要预先指定大小
插入 / 删除效率O (1)(已知位置)O (n)(需要移动元素)
随机访问效率O(n)O(1)
内存利用率可能有碎片(每个节点需要额外指针)连续内存,无碎片
适用场景频繁插入删除,数据量不确定频繁随机访问,数据量固定

总结

链表作为一种基础的数据结构,在计算机科学中有着广泛的应用。通过本文的介绍,我们了解了链表的四种主要类型:

        单链表:最简单的链表形式,支持单向遍历

        双向链表:增加了前驱指针,支持双向遍历

        循环链表:首尾相连,适合环形结构的应用

        双向循环链表:结合了双向链表和循环链表的特点,提供最大的灵活性

每种链表结构都有其独特的形态、特点和适用场景。

在实际编程中,我们可以根据具体需求选择合适的链表类型。

链表的核心优势在于动态内存分配和高效的插入删除操作,这使得它在许多场景下比数组更加适用。

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

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

相关文章

黑马点评双拦截器和Threadlocal实现原理

文章目录 双拦截器ThreadLocal实现原理 双拦截器 实现登录状态刷新的原因&#xff1a; ​ 防止用户会话过期&#xff1a;通过动态刷新Token有效期&#xff0c;确保活跃用户不会因固定过期时间而被强制登出 ​ 提升用户体验&#xff1a;用户无需频繁重新登录&#xff0c;只要…

Windows 中动态库.dll 的 .lib 文件有什么作用?

在 Windows 平台开发中, 动态链接库(Dynamic Link Library, DLL)。与之相关的还有一个常让人困惑的文件——.lib 文件。那么,这个 .lib 文件到底有什么作用呢? 一、什么是 .lib 文件? .lib 文件是 静态导入库(Import Library) 文件,它通常与动态链接库(DLL)一起生成…

细说STM32单片机FreeRTOS消息缓冲区及其应用实例

目录 一、消息缓冲区功能概述 二、消息缓冲区操作相关函数 1、相关函数概述 2、部分函数详解 &#xff08;1&#xff09;创建消息缓冲区 &#xff08;2&#xff09;写入消息 &#xff08;3&#xff09;读取消息 &#xff08;4&#xff09;消息缓冲区状态查询 三、消息…

【缓存】JAVA本地缓存推荐Caffeine和Guava

&#x1f31f; 引言 在软件开发过程中&#xff0c;缓存是提升系统性能的常用手段。对于基础场景&#xff0c;直接使用 Java集合框架&#xff08;如Map/Set/List&#xff09;即可满足需求。然而&#xff0c;当面对更复杂的缓存场景时&#xff1a; 需要支持多种过期策略&#x…

IDA插件 MIPSROP的安装和使用方法

前言 笔者的IDA版本为9.0&#xff0c;刚开始根据一些博客描述以为将mipsrop.py拷贝到IDA的plugins目录即可&#xff0c;可操作后发现事情好像没这么简单&#xff0c;复制进去后就发现没有博客中所说的 MIPS ROP Finder &#xff0c;笔者在网上搜索了很多博客后在 https://bbs.…

(1)转置后,行列式的值不变 (2)将行列式的任意两行互换位置后,行列式改变符号

以下是对原始内容在不改变内容本身的前提下进行的格式优化&#xff0c;以提升可读性和逻辑清晰度&#xff1a; ✅ 行列式的几何意义 行列式&#xff08;determinant&#xff09;是线性代数中一个非常重要的概念&#xff0c;它的几何含义可以从以下几个方面理解&#xff1a; &a…

最大似然估计(Maximum Likelihood Estimation, MLE)详解

一、定义 最大似然估计 是一种参数估计方法&#xff0c;其核心思想是&#xff1a; 选择能使观测数据出现概率最大的参数值作为估计值。 具体来说&#xff0c;假设数据 D x 1 , x 2 , … , x n D{x_1,x_2,…,x_n} Dx1​,x2​,…,xn​独立且服从某个概率分布 P ( x ∣ θ ) P(…

用go从零构建写一个RPC(3)--异步调用+多路复用实现

在前两个版本中&#xff0c;我们实现了基础的客户端-服务端通信、连接池、序列化等关键模块。为了进一步提升吞吐量和并发性能&#xff0c;本版本新增了 异步发送机制 和 多路复用支持&#xff0c;旨在减少资源消耗、提升连接利用率。 代码地址&#xff1a;https://github.com/…

FFmpeg 安装包全攻略:gpl、lgpl、shared、master 区别详解

这些 FFmpeg 安装包有很多版本和变种&#xff0c;主要区别在于以下几个方面&#xff1a; ✅ 一、从名称中看出的关键参数&#xff1a; 1. 版本号 master&#xff1a;开发版&#xff0c;最新功能&#xff0c;但可能不稳定。n6.1 / n7.1&#xff1a;正式版本&#xff0c;更稳定…

深度学习实战:从图像分类到文本生成的完整案例解析

1 图像分类案例 1.1 CIFAR10数据集介绍 cifar数据是torchvision第三方包提供的数据集 训练集5w 测试集1w y标签 10个类别 10分类问题 一张图形状 (32, 32, 3) import torch import torch.nn as nn from torchvision.datasets import CIFAR10 from torchvision.transforms i…

Android 添加系统服务的完整流程

[应用程序] (应用进程)│↓ 调用简单API [SoundManager] │ ├─ 代理模式门面模式&#xff08;应用进程&#xff09;│ ├─ 缓存数据 ←─ 装饰器模式&#xff08;应用进程&#xff09;│ └─ 转换异常 ←─ 适配器模式&#xff08;应用进程&#xff09;│↓ 通过Bind…

wan2.1代码笔记

GPU内存不够&#xff0c;可以先运行umt5&#xff0c;然后再运行wanpipeline&#xff0c;参考FLUX.1代码笔记&#xff0c;或者使用ComfyUI。 下面使用随机数代替umt5 embedding。 import torch from diffusers.utils import export_to_video from diffusers import Autoencoder…

环境搭建与工具配置

3.1 本地环境搭建 3.1.1 WAMP环境搭建漏洞靶场&#xff08;一、二&#xff09; WAMP&#xff08;Windows Apache MySQL PHP&#xff09;是搭建本地Web漏洞靶场的基础环境。 安装步骤&#xff1a; Apache&#xff1a;下载并安装最新版Apache HTTP Server&#xff0c;配置监…

STM32F446主时钟失效时DAC输出异常现象解析与解决方案

—### 现象概述 在STM32F446微控制器应用中&#xff0c;若主时钟&#xff08;HSE&#xff09;的晶体信号对地短路&#xff0c;但DAC&#xff08;数模转换器&#xff09;仍能输出变化信号&#xff0c;这一现象看似矛盾&#xff0c;实则与系统时钟切换机制密切相关。本文将从硬件…

React 如何封装一个可复用的 Ant Design 组件

文章目录 前言一、为什么需要封装组件&#xff1f;二、 仿antd组件的Button按钮三、封装一个可复用的表格组件 (实战)1. 明确需求2. 设计组件 API3. 实现组件代码4. 使用组件 三、封装组件的最佳实践四、进阶优化 总结 前言 作为一名前端开发工程师&#xff0c;在日常项目中&a…

STC89C52RC/LE52RC

STC89C52RC 芯片手册原理图扩展版原理图 功能示例LED灯LED灯的常亮效果LED灯的闪烁LED灯的跑马灯效果&#xff1a;从左到右&#xff0c;从右到左 数码管静态数码管数码管计数mian.cApp.cApp.hCom.cCom.hDir.cDir.hInt.cInt.hMid.cMid.h 模板mian.cApp.cApp.hCom.cCom.hDir.cDir…

踩坑记录:RecyclerView 局部刷新notifyItemChanged多次调用只触发一次 onBindViewHolder 的原因

1. 问题背景 在做项目的时候&#xff0c;RecyclerView需要使用局部刷新&#xff0c;使用 notifyItemChanged(position, payload) 实现局部刷新&#xff0c;但发现调用多次只执行了一次&#xff0c;第二个刷新不生效。 2. 错误示例&#xff08;只处理 payloads.get(0)&#xff…

OpenLayers 加载鹰眼控件

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 地图控件是一些用来与地图进行简单交互的工具&#xff0c;地图库预先封装好&#xff0c;可以供开发者直接使用。OpenLayers具有大部分常用的控件&#x…

WPF···

设置启动页 默认最后一个窗口关闭,程序退出,可以设置 修改窗体的icon图标 修改项目exe图标 双击项目名会看到代码 其他 在A窗体点击按钮打开B窗体,在B窗体设置WindowStartupLocation=“CenterOwner” 在A窗体的代码设置 B.Owner = this; B.Show(); B窗体生成在A窗体中间…

github公开项目爬取

import requestsdef search_github_repositories(keyword, tokenNone, languageNone, max_results1000):"""通过 GitHub API 搜索仓库&#xff0c;支持分页获取所有结果&#xff08;最多 1000 条&#xff09;:param keyword: 搜索关键词:param token: GitHub To…