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

数据结构:链表(Linked List)

目录

结构推导

回到最原始的问题 —— 我们如何存数据?

第二步:我们来看看数组的限制 

第三步:那我们该怎么做呢? 

第四步:我们推导链表的数据结构 

结构讲解

什么是链表?

什么是节点? 

如何创建节点?

 如何访问节点?


结构推导

🎯 目标:理解什么是链表(linked list),为什么我们需要它,以及它的结构从哪里来的。

回到最原始的问题 —— 我们如何存数据?

在 C 语言中,最基础的存数据方式是:

int a[100];

也就是 数组(array)。它非常常见,我们从一开始就用。

那我们先从数组的“第一性”特征说起:

数组的本质是:

一块连续的内存空间 + 索引访问

举个例子:

int a[5] = {10, 20, 30, 40, 50};

这个数组在内存里是这样的(假设从地址 1000 开始):

地址
100010
100420
100830
101240
101650

它的特性:

  • 地址连续

  • a[i] 的位置是:a + i × sizeof(int)

  • 可以快速通过索引访问:O(1)

详细内容可以参考:数据结构:数组(Array)_array[1..50]-CSDN博客


第二步:我们来看看数组的限制 

情况1:大小固定

如果你定义了:

int a[100];

你就固定只能存最多 100 个元素。如果只用了前 5 个元素,空间也浪费。

如果你事先不知道需要多少个元素,就很难选大小。

情况2:插入麻烦

假设你已经有数组:

int a[5] = {10, 20, 30, 40, 50};

现在你想在 第 2 个位置插入 99,就得把后面的都搬一格:

for (int i = 5; i > 1; i--) {a[i] = a[i - 1];
}
a[1] = 99;

这会导致O(n) 的时间复杂度。

情况3:删除也麻烦

类似的,如果你要删掉 a[2],也得把后面的全往前搬一格。

总结数组的问题:

问题描述
空间不灵活空间必须一开始就定下来
插入慢中间插入要整体移动元素
删除慢删除某项也要移动后面的元素
空间浪费有时只用一部分数组,但仍然占内存

第三步:那我们该怎么做呢? 

我们思考一下:

能不能只在用到数据时,才开辟空间?

能不能插入一个元素,而不影响其它元素的位置?

这就引出了一个新的思维模型:

✅ 使用“分散空间 + 指针连接”的模型

如果我们不强求内存连续,而是:

  • 每次添加一个节点时,就用 malloc 单独开一块空间

  • 每个节点都保存指向“下一个节点”的指针

我们就可以实现灵活的动态结构了。

这就是 链表(linked list) 的思想:

把所有的数据节点一个一个连起来,而不是排成连续数组


第四步:我们推导链表的数据结构 

我们要存一个元素,它需要什么?

  1. 数据本身,比如:一个整数

  2. 指向下一个节点的“线索”(指针)

所以我们设计一个节点结构:

struct Node {int data;           // 当前节点的数据struct Node* next;  // 指向下一个节点
};

那一个“链表”是什么?

一个链表是“若干个 Node 连起来”,只要有第一个节点(头结点)的地址,就能找到整个链表。

所以我们定义:

struct Node* head;  // 指向链表的起点

✅ 所以链表的基本单位是:节点 + 指针

[10 | *] --> [20 | *] --> [30 | NULL]
  • 每个方块是一个 Node

  • *next 指针

  • 最后一个节点的 next = NULL 表示链表结束

结论:为什么我们需要链表?

问题

链表怎么解决

数组大小固定

链表可以动态增加节点(malloc)

插入/删除慢

链表插入删除只需修改指针,不搬数据

空间浪费

用多少开多少,不多开


结构讲解

什么是链表?

在数组中,所有元素都在一块连续的内存空间中。你只能在一开始就分配好大小,插入/删除不方便。那么:

有没有一种数据结构,不要求数据连续存储,但依然能一个接一个地串起来?

 这就是链表的本质:

链表是由若干个节点组成的线性结构,每个节点都知道下一个节点在哪里,只需要记住第一个节点就能访问整个链表。

链表的核心思想是:

每个节点知道“谁在后面”,就像每一节车厢都知道后面那节车厢的位置。

整个链表就像是一列火车,每节车厢通过“挂钩”(指针)串联起来:

✅ 链表的特征:

  • 节点之间不要求连续内存

  • 每个节点都包含:

    • 本节点的数据

    • 指向下一个节点的指针

  • 通过“指针”把节点连接起来

 通常我们用一个指针 head 来指向第一个节点:

struct Node* head;

什么是节点? 

一个节点(Node)是链表的最基本组成单位,就是一个结构体,它包含:

  1. 一份数据(例如 int 类型)

  2. 一个指针,指向下一个节点

这就好比:

  • 火车的每节车厢(节点)都有:

    • 货物(数据)

    • 连接器(指针)去连下一节车厢

 用C语言表示:

struct Node {int data;           // 节点中存的数据struct Node* next;  // 指向下一个节点的指针
};
成员含义
int data存储数据,比如整数 10
Node* next保存下一个节点的地址,如果是最后一个就为 NULL

如何创建节点?

为什么我们不能直接写:

struct Node node1;

1. 这句代码在 栈(stack)上 分配了一个 Node 类型的变量。它的特点是:

  • 分配在栈上(函数调用结束后会被自动释放)

  • 内存大小在编译时就已经确定

  • 生命周期由作用域决定,作用域结束就失效

🔍 2. 链表是动态结构,不能预知节点个数

链表的本质是:

  • 你不知道总共有多少个节点(比如根据用户输入构建)

  • 每一个节点都需要“现用现开”,而不是提前一次性分配

所以我们需要在“运行时”动态分配空间: 

struct Node* newNode = (struct Node*) malloc(sizeof(struct Node));

这行代码的含义是:

  1. malloc(sizeof(struct Node))堆(heap)上动态分配一块内存,大小正好是一个 Node

  2. 返回值是 void*,强制类型转换成 struct Node*

  3. newNode 是一个指针,指向新建的节点

再从链表的设计需求来倒推

  • 节点数是动态变化的(例如用户输入10个节点)

  • 节点之间是用指针连接的

  • 每个节点可能在任何时刻被插入或删除

  • 节点需要长期存在,甚至跨越函数作用域

 所以:必须用动态内存分配,也就是 malloc()。struct Node node1; 是不能满足上述需求的


 如何访问节点?

假设我们已经创建了多个节点,并链接起来:

struct Node* head = (struct Node*) malloc(sizeof(struct Node));
head->data = 10;struct Node* second = (struct Node*) malloc(sizeof(struct Node));
second->data = 20;head->next = second;
second->next = NULL;

访问链表节点的本质逻辑是沿着 next 指针走,这就叫做遍历链表(traverse the linked list)。

举个例子,假设你有一个链表,要访问节点内容:

head → [10 | * ] → [20 | * ] → [30 | NULL]

你想访问第二个节点的值 20:

你不能说 head[1],因为它不是数组。

你只能说:

head->next->data

这个表达式的意义是:

  1. head 是第一个节点的地址

  2. head->next 是第二个节点的地址

  3. head->next->data 就是第二个节点的值

访问第二个节点的数据: 

printf("%d\n", head->next->data);  // 输出 20

遍历就是:

head 开始,沿着每个节点的 next 指针走,直到你走到 NULL(链表结尾)

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

这段代码会访问每个节点的 data,直到遇到 NULL。 

http://www.dtcms.com/a/309402.html

相关文章:

  • ModeSeq论文阅读
  • 使用 Vive Tracker 替代 T265 实现位姿获取(基于 Ubuntu + SteamVR)
  • Cloud Storage:一款高效便捷的云端存储解决方案
  • xcode swift项目运行、连接真机运行报错,引入文件夹失败
  • iOS 抓不到包怎么办?全流程排查思路与替代引导
  • Spring Boot 文件上传限制配置全攻略:Java 配法 vs YAML 配法
  • webpack面试题及详细答案80题(41-60)
  • k8s之DevicePlugin
  • 第13届蓝桥杯Python青少组_省赛_中/高级组_2022年4月17日真题
  • C语言---位运算符的分类与用法(按位与、按位或 |、按位异或^、按位取反~、左移<<、右移>>)
  • 【前端】CSS Flexbox布局示例介绍
  • 应用药品注册证识别技术,为医药行业的合规、高效与创新发展提供核心驱动力
  • 数据结构:算法复杂度与空间复杂度
  • 《协作画布的深层架构:React与TypeScript构建多人实时绘图应用的核心逻辑》
  • 提升文档管理:推荐一键Docker部署的全文索引搜索引擎工具
  • Ubuntu 24.04.2 LTS 安装mysql8.0.36保姆级教程(从安装到远程连接)
  • ZKmall开源商城微服务架构电商平台:服务注册与配置中心设计
  • 如何创建一个飞书应用获取自己的飞书AppID和AppSecret?
  • Spring之【循环引用】
  • 第三阶段—8天Python从入门到精通【itheima】-140节(pysqark实战——基础准备)
  • 江协科技STM32 12-2 BKP备份寄存器RTC实时时钟
  • 二分查找(基础)
  • 启动中国蚁剑
  • 东芝时钟控制的双极步进电机驱动器TB67S209FTG
  • 关于皮带机流水线的控制思想解析
  • Sklearn 机器学习 文本数据 TF-IDF实现文本向量化
  • Linux 内存管理之 Rmap 反向映射
  • 每天一点跑步运动小知识
  • 使用gcc代替v语言的tcc编译器提高编译后二进制文件执行速度
  • 分布在背侧海马体CA1区域的位置细胞(place cells)对NLP中的深层语义分析的积极影响和启示