1. 简介

在本教程中,我们将介绍几种常见的数据结构,包括:

  • 数组(Arrays)
  • 向量(Vectors)
  • 链表(Linked Lists)
  • 树(Trees)
  • 图(Graphs)
  • 栈(Stacks)

这些数据结构在编程中非常基础且实用。我们不会聚焦于某种特定的编程语言,而是从通用的角度进行讲解,帮助读者理解它们的用途和适用场景。

2. 数组(Arrays)

数组是计算机科学中最基础的数据结构之一,它是一组连续的内存区域,每个元素都有固定的大小。

我们可以为每个元素分配一个索引(从 0 开始),通过索引快速访问数组中的任意元素。数组的访问方式通常是基于偏移量计算的,例如要访问第 n 个元素,只需将 n 乘以单个元素的大小,再加上数组的起始地址即可。

下图展示了一个一维数组中访问第 4 个元素的过程:

ArrayVector

在编程语言中,我们通常使用 V[4]V(4) 的方式来访问数组中的第 4 个元素。

2.1. 矩阵(Matrices)

矩阵是数组的一种特殊形式,它表示一个二维的数据集合。我们可以将矩阵理解为“数组的数组”。

下图展示了矩阵的结构:

ArrayOfArrays3

第一个维度用于访问“主数组”中的子数组,第二个维度用于访问子数组中的具体元素。

例如,如果我们有一个 2D 数组 M,其中每个子数组包含 5 个元素,我们可以通过 M[1][2] 来访问第一行第二列的元素:

ArrayAsMatrix

在内存中,矩阵是按行优先或列优先的方式线性存储的。例如,访问 M[1][2] 实际上是通过以下方式计算偏移量的:

address = matrixStart + (rowSize * rowIndex + columnIndex) * elementSize

2.2. 数组与矩阵的用途

数组是编程中最基本的数据结构之一,几乎可以用来表示任何一组有序或无序的数据集合。例如:

✅ 一组数字、字符串、城市名、任务列表等都可以用数组表示
✅ 每个数组元素可以是一个基本类型,也可以是一个复杂对象(如员工信息)
✅ 表格数据(如 Excel 表格)本质上就是二维数组
✅ 机器学习中的数据集、图像像素、线性代数运算(如矩阵乘法)都依赖数组结构

3. 图(Graphs)

图是一种非常有用的数据结构,起源于图论。图由节点(Nodes)边(Edges)组成,节点之间通过边连接。

图中的节点和边都可以拥有属性,这些属性在图论中被称为“颜色”(color)。边可以是有向的(directed)或无向的(undirected)。

下图展示了一个包含 4 个节点的图,其中两个节点有标签“Jill”和“Jack”,边的类型包括有向边、无向边以及双向边:

GraphConcepts

图可以用来表示很多现实世界的问题,例如:

✅ 网页之间的链接关系(有向图)
✅ 社交网络中的好友关系(无向图)
✅ 地图中的路径规划(带权图)
✅ AI 中的状态转移(搜索空间)

3.1. 树(Trees)

树是图的一种特殊形式,它是一个无环的连通图。也就是说,树中任意两个节点之间只有一条唯一的路径。

下图展示了同一个树结构的两种不同画法:

GraphAsTree1

树中有一个特殊的节点叫做“根节点”(root),其余节点通过边连接到根节点,形成层次结构。每个节点可以有多个子节点,但只有一个父节点。

3.2. 图的表示方法

图可以用多种方式表示,常见的包括:

1. 基于节点的表示

每个节点包含其连接的边信息。例如:

节点 1 的信息:
- 颜色:1
- 边信息:
  - 边 0:指向节点 2
  - 边 1:指向节点 3
  - 边 2:指向节点 4

2. 基于边的表示

每条边包含两个节点的引用,以及边的属性(如权重、方向)。

边 1 -> 2 的信息:
- 类型:有向边
- 颜色:无
- 节点 1 和节点 2 的引用

3. 邻接矩阵(Adjacency Matrix)

邻接矩阵是一个 n x n 的矩阵,用于表示图中节点之间的连接关系。其中:

  • A[i][j] = 0:表示节点 i 和节点 j 之间没有边
  • A[i][j] = 非零值:表示存在一条从 i 到 j 的有向边
  • A[i][j] = 非零值 且 A[j][i] = 非零值:表示节点 i 和 j 之间存在无向边

下图是一个图及其对应的邻接矩阵:

AdjacencyMatrix-1

3.3. 树与图的用途

树和图在实际开发中应用广泛:

✅ 语义网(Semantic Web)中使用图来表示实体之间的关系(RDF)
✅ AI 中的搜索算法(如 BFS、DFS)使用图来表示状态空间
✅ 游戏 AI(如井字棋)中使用树来表示所有可能的走法
✅ 函数式编程中的表达式解析可以使用树结构
✅ 化学分子结构可以用图表示,节点为原子,边为化学键

例如,下图展示了一个简单的 RDF 图:

test-ontology-visualization

下图展示了一个井字棋游戏的状态树:

TikTakToe1-1

4. 链表(Linked Lists)

链表是一种动态数据结构,由一系列节点组成,每个节点包含数据和一个指向下一个节点的指针。

链表的基本结构如下:

LinkedListBasic

链表有三种常见形式:

  • 单向链表(Singly Linked List):每个节点只指向下一个节点
  • 双向链表(Doubly Linked List):每个节点同时指向前一个和后一个节点
  • 循环链表(Circular Linked List):最后一个节点指向第一个节点

4.1. 链表表示数组与树

链表非常灵活,可以用来表示数组和树结构:

例如,一个整数数组 [0, 1, 2, 3, 4, 5] 可以用链表表示如下:

LinkedListArray

同样,树结构也可以用链表来表示:

LinkedListGraph

4.2. 链表的用途

链表在插入和删除操作上具有优势,因为不需要像数组那样移动大量元素:

✅ 插入操作的时间复杂度为 O(1)(已知插入位置)
✅ 删除操作的时间复杂度也为 O(1)
✅ 适合频繁插入/删除的场景(如缓存、LRU 算法)

例如,插入一个元素到数组和链表中的操作对比:

数组插入(O(n)):

InsertVector

链表插入(O(1)):

LinkedListInsert

5. 栈与队列(Stacks and Queues)

栈和队列是用于控制数据进出顺序的结构。

5.1. 栈(Stack)

栈是一种后进先出(LIFO, Last In First Out)的数据结构。每次插入(push)元素都放在栈顶,每次弹出(pop)元素也从栈顶取出。

例如,压入 5 个元素后,最后一个压入的元素最先被弹出:

StackOperations

5.2. 队列(Queue)

队列是一种先进先出(FIFO, First In First Out)的数据结构。每次插入(enqueue)元素放在队尾,每次取出(dequeue)元素从队头取出。

例如,将 5 个球依次入队后,第一个入队的球最先出队:

QueueOperations1

5.3. 栈与队列的用途

栈的常见用途:

✅ 实现“撤销”功能(Undo)
✅ 递归调用中的上下文保存(调用栈)

队列的常见用途:

✅ 任务调度(如打印任务排队)
✅ 广度优先搜索(BFS)中的节点访问顺序
✅ 消息队列系统(如 RabbitMQ、Kafka)

例如,递归函数调用时,栈用于保存函数上下文:

RecursiveCall

6. 总结

本文介绍了几种常见的数据结构,包括数组、矩阵、图、树、链表、栈和队列。它们各有特点,适用于不同的场景:

数据结构 插入复杂度 查找复杂度 适用场景
数组 O(n) O(1) 静态数据、快速访问
链表 O(1) O(n) 频繁插入/删除
O(1) O(1) 撤销操作、递归
队列 O(1) O(1) 任务调度、BFS
视结构而定 视结构而定 状态空间、社交网络
O(log n) O(log n) 文件系统、搜索算法

选择合适的数据结构对程序的性能和可维护性至关重要。在实际开发中,应根据具体需求选择合适的数据结构,并结合时间复杂度和空间复杂度进行权衡。


原始标题:Common and Useful Data Structures