数据结构之单链表详解:从原理到C语言实现

news2024/11/30 10:35:22

一、 什么是单链表?

单链表(Singly Linked List)是一种线性数据结构,它的特点每个节点通过指针链接到下一个节点。不同于顺序表(数组),链表的每个元素(节点)并不存储在连续的内存空间中,而是由一个节点指向下一个节点,以形成链式结构

你可以把单链表想象成一列火车,每节车厢都装载着数据(元素),而每节车厢的尾部都连着下一节车厢,直至最后一节车厢指向空NULL,表示链表的结束。


二、 单链表的基本结构

在C语言中,单链表由节点构成。每个节点包含两部分:

  • 数据域:存储当前节点的数据信息。
  • 指针域:存储指向下一个节点的地址。

单链表的节点可以通过结构体来定义,如下所示:

#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;            // 数据域
    struct Node* next;   // 指针域,指向下一个节点
};
  • int data存储节点中的数据,可以是任何数据类型。
  • struct Node* next:指向下一个节点的指针,若当前节点是链表中的最后一个节点,则next的值为NULL

三、 单链表的基本操作

1. 创建节点

单链表的节点是通过动态内存分配创建的,在C语言中可以使用malloc来分配内存。以下是创建单链表节点的代码:

struct Node* createNode(int data) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node)); // 动态分配内存
    newNode->data = data;     // 设置节点的数据
    newNode->next = NULL;     // 初始状态下指向NULL
    return newNode;
}

代码解释

  • struct Node* createNode(int data)定义了一个函数,用于创建一个新节点。
  • malloc(sizeof(struct Node))动态分配内存,大小为struct Node的大小。
  • newNode->data = data为新节点赋值。
  • newNode->next = NULL新节点的next初始化为NULL,表示它暂时是链表的最后一个节点。
2. 插入节点

在进入插入节点这一内容前我们有一个非常非常非常重要的知识点需要理解:

为什么传参需要二级指针?为什么有的函数只要一级指针?

我们一起来仔细品味:

在处理单链表的某些操作时,比如插入或删除节点,尤其是修改链表头节点的操作中,我们通常需要使用二级指针。这是因为C语言中的参数传递是值传递,当我们在函数中传递一个指针时,实际上传递的是该指针的一个拷贝。如果我们想在函数内部修改链表的头节点(即head指针指向的地址),我们需要传递指向指针本身的指针,也就是二级指针

什么时候用一级指针?

对于不涉及修改链表头节点的操作,比如遍历链表或只修改链表中的某个节点的值时,只需要传递链表的头节点(即一级指针)即可,因为我们并不打算修改指针本身,只是通过指针访问链表的内容。

void printList(struct Node* head) {
    struct Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

这里我们只需要传递链表的头节点head,通过它来遍历链表。由于我们不会修改链表的结构(比如不更改头节点),因此不需要使用二级指针。

什么时候用二级指针?

当我们需要修改链表的结构特别是修改链表的头节点时,传递二级指针是必须的。例如,插入新节点到链表头部,或者删除链表的头节点。如果我们只传递一个一级指针,函数内部对头节点的修改不会影响到外部的链表结构。

void insertAtHead(struct Node** head, int data) {
    struct Node* newNode = createNode(data);  // 创建新节点
    newNode->next = *head;  // 新节点的next指向当前的头节点
    *head = newNode;        // 头节点更新为新节点
}

在这里,head是二级指针,即struct Node** head*head指向链表的头节点,通过修改*head,我们就能改变链表的头节点,使其指向新插入的节点。这样在函数外部,链表的头节点也会正确更新。

如果我们只传递一级指针,即struct Node* head,在函数内部修改head的值并不会影响外部的链表结构,因为函数接收到的是头节点指针的拷贝,修改的只是拷贝的内容,而不是原始指针。

理解了上面的问题我们就能继续单链表的插入操作了:

单链表中有三种常见的插入操作:

  1. 头部插入:在链表的最前面插入节点。
  2. 尾部插入:在链表的最后面插入节点。
  3. 中间插入:在指定位置插入节点。

我们先来看看头插:

头部插入节点
void insertAtHead(struct Node** head, int data) {
    struct Node* newNode = createNode(data);  // 创建新节点
    newNode->next = *head;  // 新节点的next指向当前的头节点
    *head = newNode;        // 头节点更新为新节点
}
  • void insertAtHead(struct Node** head, int data)定义了一个函数,接受链表头节点的指针和要插入的数据。
  • newNode->next = *head将新节点的next指向当前的头节点。
  • *head = newNode将链表的头节点更新为新节点。
尾部插入节点
void insertAtTail(struct Node** head, int data) {
    struct Node* newNode = createNode(data);  // 创建新节点
    if (*head == NULL) {
        *head = newNode;  // 如果链表为空,新节点为头节点
    } else {
        struct Node* temp = *head;
        while (temp->next != NULL) {
            temp = temp->next;  // 找到链表的最后一个节点
        }
        temp->next = newNode;  // 最后一个节点的next指向新节点
    }
}
  • 如果链表为空,直接将新节点作为头节点。
  • 否则,遍历链表直到找到最后一个节点,将新节点插入到尾部。
中间插入节点
void insertAtPosition(struct Node** head, int data, int pos) {
    struct Node* newNode = createNode(data);
    if (pos == 1) {
        newNode->next = *head;  // 如果位置是1,则插入头部
        *head = newNode;
    } else {
        struct Node* temp = *head;
        for (int i = 1; i < pos - 1 && temp != NULL; i++) {
            temp = temp->next;  // 找到指定位置的前一个节点
        }
        if (temp != NULL) {
            newNode->next = temp->next;  // 插入新节点
            temp->next = newNode;
        }
    }
}
  • 如果插入位置是1,直接头部插入。
  • 否则,遍历链表找到指定位置,将新节点插入到链表中。
3. 删除节点

单链表中删除节点时,需要找到要删除节点的前一个节点,将它的next指向被删除节点的下一个节点。

void deleteNode(struct Node** head, int key) {
    struct Node* temp = *head;
    struct Node* prev = NULL;
    
    // 如果要删除的是头节点
    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }
    
    // 查找要删除的节点
    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }
    
    // 节点不存在
    if (temp == NULL) return;
    
    prev->next = temp->next;  // 前一个节点指向要删除节点的下一个节点
    free(temp);  // 释放要删除的节点
}
  • 首先检查要删除的是否是头节点。
  • 如果不是,遍历链表找到要删除的节点,并调整指针,使链表重新连接。
  • 释放被删除节点的内存。
4. 查找节点

查找某个节点时,只需要遍历链表,找到与目标值匹配的节点即可。

int search(struct Node* head, int key) {
    struct Node* current = head;
    while (current != NULL) {
        if (current->data == key) {
            return 1;  // 找到节点
        }
        current = current->next;
    }
    return 0;  // 未找到
}
  • 遍历链表,逐个比较节点的数据域,直到找到匹配的节点。
  • 如果找到,返回1;如果未找到,返回0。
5. 遍历链表

遍历链表时,从头节点开始,逐个访问每个节点,直到链表结束(即nextNULL)。

void printList(struct Node* head) {
    struct Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}
  • 从头节点开始,依次输出每个节点的数据,直到到达链表末尾(nextNULL)。

4. 单链表的优缺点与应用场景

恭喜你,看到这,有关单链表的基本操作和细节我们就学完啦~

同时我们也要知道单链表与顺序表(数组)相比有一些显著的优缺点和应用场景

优点:
  • 动态大小:链表不需要预先分配内存,可以根据需要动态增加节点,避免了顺序表大小固定的限制。
  • 插入和删除效率高:在链表中插入和删除节点的时间复杂度为O(1),只需调整指针,而不需要像数组那样移动元素。
缺点:
  • 随机访问效率低:链表不支持随机访问,查找某个节点的时间复杂度为O(n),需要从头遍历到目标位置。
  • 额外内存消耗:每个节点都需要额外存储一个指针(next),相较于数组,链表的内存开销更大。

单链表非常适合那些需要频繁插入和删除操作的场景,比如:

  • 实现动态队列和栈:链表可以轻松实现动态的队列和栈,适合那些数据量不固定的场景。
  • 内存有限的嵌入式系统:由于链表的动态内存分配特性,适合在一些内存有限的嵌入式系统中使用。

说在最后

单链表是学习指针和动态内存管理的重要基础。通过理解单链表的结构和操作,我相信你能够掌握如何高效管理内存,并为后续学习双向链表、循环链表等更复杂的数据结构打下坚实的基础。

在学习单链表的过程中,建议你多动手编写代码,自己尝试实现插入、删除、查找等操作,深入理解指针的使用和内存管理技巧。

最后如果觉得有收获的话记得点赞加收藏哦~~

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

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

相关文章

【简单版】通过 Window.performance 实现前端页面(性能)监控

1 背景 前端监控系统告警xx接口fetchError 问题&#xff1a;前端监控系统没有更多的错误信息&#xff0c;查询该fetch请求对应的接口日志返回200状态码、无请求异常记录&#xff0c;且后台能查到通过该fetch请求成功发送的数据。那是前端页面的错误还是前端监控系统的问题&…

yjs机器学习常见算法01——KNN(1)(K—近邻算法)

1.K—近邻算法 的含义&#xff1a; 简单来说就是通过你的邻居的“类别”&#xff0c;来推测你的“类别” 定义&#xff1a;如果一个样本在特征空间中的k个最相似&#xff08;即特征空间中最临近&#xff09;的样本中大多数属于某一类别&#xff0c;则该样本也属于这个类别。 2.…

【Python爬虫系列】_028.Python玩Redis

课 程 推 荐我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈虚 拟 环 境 搭 建 :👉👉 Python项目虚拟环境(超详细讲解) 👈👈PyQt5 系 列 教 程:👉👉 Python GUI(PyQt5)教程合集 👈👈

Redis原理篇之网络模型

Redis原理篇之网络模型 文章目录 Redis原理篇之网络模型1 用户空间和内核空间2 阻塞IO3 非阻塞IO4 IO多路复用4.1 IO多路复用-select4.2 IO多路复用-poll4.3 IO多路复用-epoll4.4 总结 5 信号驱动IO6 异步IO7 同步和异步8 Redis网络模型8.1 Redis是单线程吗&#xff1f;为什么要…

基于Opencv中的DNN模块实现图像/视频的风格迁移

一、DNN模块的介绍 1、简介 OpenCV中的DNN&#xff08;Deep Neural Network&#xff09;模块是一个功能强大的组件&#xff0c;它支持深度学习网络模型的加载和推理。虽然DNN模块不提供模型的训练功能&#xff0c;但它可以与主流的深度学习框架&#xff08;如TensorFlow、Caf…

tigeR免疫治疗数据分析工具学习和整理

tigeR整合了多个肿瘤的数据集&#xff0c;用于探索生物标志物和构建预测免疫治疗反应模型。 该工具内置了 11 个黑色素瘤数据集、3 个肺癌数据集、2 个肾癌数据集、1 个胃癌数据集、1 个低级别胶质瘤数据集、1 个胶质母细胞瘤数据集和 1 个头颈鳞状细胞癌数据集的 1060 例具有…

网络资源模板--Android Studio 实现简易新闻App

目录 一、项目演示 二、项目测试环境 三、项目详情 四、完整的项目源码 一、项目演示 网络资源模板--基于Android studio 实现的简易新闻App 二、项目测试环境 三、项目详情 登录页 用户输入&#xff1a; 提供账号和密码输入框&#xff0c;用户可以输入登录信息。支持“记…

2022年10月自考《操作系统概论》02323试题

目录 一.选择题 二.填空题 三.简答题 四.综合体 一.选择题 1.以下各种操作系统中&#xff0c;对可靠性要求最高的是 &#xff08;书中&#xff09;P25页 A.分时操作系统 B.实时操作系统 C.多道批处理系统 D.单道批处理系统 2.一个进程正常执行完毕时&#xff0c;需要对其…

简述光密度仪日常中的用途及光密度测量方法

光密度仪在日常中的用途 光密度仪在众多领域发挥着重要作用。在医疗领域&#xff0c;它常用于检测生物样本中的物质浓度&#xff0c;如血液中特定成分的含量测定。在化学分析中&#xff0c;可精确测量溶液的浓度&#xff0c;为实验和研究提供准确数据。在工业生产中&#xff0…

go+bootstrap实现简单的注册登录和管理

概述 使用&#xff0c;gomysql实现了用户的登录&#xff0c;注册&#xff0c;和管理的简单功能&#xff0c;不同用户根据不同权限显示不同的内容 实战要求&#xff1a; 1、用户可以注册、登录&#xff1b; 2、登录后可以查看所有的注册的用户&#xff1b; 3、管理员操作对用…

PHP(一)从入门到放弃

参考文献&#xff1a;https://www.php.net/manual/zh/introduction.php PHP 是什么&#xff1f; PHP&#xff08;“PHP: Hypertext Preprocessor”&#xff0c;超文本预处理器的字母缩写&#xff09;是一种被广泛应用的开放源代码的多用途脚本语言&#xff0c;它可嵌入到 HTML…

Qt/C++编写的mqtt调试助手使用说明

一、使用说明 第一步&#xff0c;选择协议前缀&#xff0c;可选mqtt://、mqtts://、ws://、wss://四种&#xff0c;带s结尾的是走ssl通信&#xff0c;ws表示走websocket通信。一般选默认的mqtt://就好。第二步&#xff0c;填写服务所在主机地址&#xff0c;可以是IP地址也可以…

使用LSPatch+PlusNE修改手机软件

一、问题概述 国内使用一些软件&#xff0c;即使科学上网&#xff0c;打开都是网络错误&#xff0c;更换节点同样如此。 二、软件下载 通过官网或者正规商店(如Google play)下载并且安装。 是的&#xff0c;先要下载一个无法使用的版本&#xff0c;后续对其进行修改。 三、下…

代码随想录(七) —— 二叉树部分

1. 二叉树的四种遍历方式的理解 前序遍历&#xff0c;中序遍历&#xff0c;后序遍历&#xff1b;层次遍历 结合另一篇博客&#xff0c;关于灵神的题单刷题 二叉树刷题记录-CSDN博客 理解&#xff1a; 在二叉树类型题目中&#xff0c;遍历顺序的选择需要根据具体问题来确定…

算法笔记day04

目录 1. 在字符串中找出连续最长的数字串 2.岛屿数量 3.拼三角 1. 在字符串中找出连续最长的数字串 字符串中找出连续最长的数字串_牛客题霸_牛客网 (nowcoder.com) 算法思路&#xff1a; 这是一道简单的双指针题目&#xff0c;首先用i遍历数组&#xff0c;当遍历到数字的时…

春日技术辅导:Spring Boot课程答疑

3系统分析 3.1可行性分析 通过对本课程答疑系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本课程答疑系统采用JAVA作为开发语言&#xff0c;Spring Boot框…

数据驱动,漫途能耗管理系统打造高效节能新生态!

在我国能源消耗结构中&#xff0c;工业企业所占能耗比例相对较大。为实现碳达峰、碳中和目标&#xff0c;工厂需强化能效管理&#xff0c;减少能耗与成本。高效的能耗管理系统通过数据采集与分析&#xff0c;能实时监控工厂能源使用及报警情况&#xff0c;为节能提供数据。构建…

JVM 调优篇10 使用arthas排优

一 Arthas的作用 1.1 作用 1. 这个类从哪个 jar 包加载的&#xff1f;为什么会报各种类相关的 Exception&#xff1f; 2.是否有一个全局视角来查看系统的运行状况&#xff1f; 3. 有什么办法可以监控到JVM的实时运行状态&#xff1f; 4. 怎么快速定位应用的热点&#x…

TensorFlow详细配置

Anaconda 的安装路径配置系统环境变量 1 windows path配置 2 conda info C:\Users\Administrator>conda info active environment : None user config file : C:\Users\Administrator\.condarc populated config files : C:\Users\Administrator\.condarc …

【含文档】基于Springboot+Vue的高校科研信息管理系统(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…