Skip to content

本文并不是为初学者所编写的入门文章,而是为曾经系统学习过数据结构与算法,但发现自己走的太快,想要重新系统回顾一下的人准备的。

概述

学习数据结构 (data structure) 时,始终思考下面三点内容:

  • 数据的逻辑结构 (logical structure)
  • 数据的存储结构 (storage structure)
  • 数据的运算 (operation)

逻辑结构

数据的逻辑结构是为我们人类所服务的。当我们在说树、图、表、堆、栈等内容时,其实指的都是逻辑结构。 逻辑结构可以看作是从具体问题中抽象出来的数学模型,它与数据的存储方式无关。 逻辑结构大体上可分为三类:

  • 逻辑结构
    • 集合
    • 线性结构
    • 树形结构

存储结构

数据的存储结构是为计算机所服务的。计算机能理解的是存储结构,所以我们需要将逻辑结构映射为存储结构! 存储结构指的就是数据在计算机中的存储方式,通常是下面四种存储方式的组合:

  • 存储结构
    • 顺序存储结构 (sequential storage structure)
    • 链式存储结构 (linked storage structure)
    • 索引存储结构 (indexed storage structure)
    • 哈希存储结构 (hashed storage structure)

顺序存储结构:采用一组连续的存储单元存放所有的数据元素。 链式存储结构:每个数据元素用一个内存结点存储,每个结点是单独分配的,所有的结点地址不一定是连续的。 索引存储结构:除了存储数据元素外,还会建立一张索引表,来获取元素的存储地址。 哈希存储结构:又称散列存储结构,核心思想是是根据数据的内容,借助哈希/散列函数计算出一个值,这个值就是数据的存储地址。

运算

数据运算指的是对数据实施的操作。比如最基本的:

  • 排序
  • 遍历

不同的数据结构,还会有自己独特的运算,比如图的运算方式有 DFS 和 BFS

逻辑结构、存储结构和运算之间的关系

对于一种数据结构,其逻辑结构永远是唯一的,但它的存储结构可以是多种,而且在不同的存储结构中,同一种运算的实现方式也可能不同。

比如二叉树,当我们讨论二叉树时,几乎每个人在纸上画的二叉树都是差不多的,而且不管你是横着画还是竖着画,别人都是能理解的,因为我们所理解的是同一个逻辑结构。但是二叉树的具体实现却是各种各样,你可以使用顺序存储结构(比如数组),也可以使用链式存储结构(比如链表),甚至可以两个结合起来(链表的结点是数组)。而根据你的实现方式不同,你实现二叉树的运算(比如遍历等操作)时,也会不同。

这个在纸上画的方式、或者口述、或者和人交谈时你的肢体动作、或者你自己设计了一种字符串、这些都是数据结构的表示方式,而且表示的是数据的逻辑结构!

 define operation                       implement operation
       |                                         |
       |                                         |
       |                                         |
       v                  mapping                v
logical structure  -------------------->  storage structure

算法

定义:算法 (algorithm) 是对特定问题求解步骤的一种描述,它是指令的有限序列。

  • 算法的 5 个重要特性:
    • 有穷性
    • 确定性
    • 可行性
    • 有输入
    • 有输出

有穷性:必须在有穷时间内结束。

确定性:对于相同输入的输入,必须有相同的输出,不能有二义性。

  • 算法设计的目标
    • 正确性
    • 用户友好性
    • 可读性
    • 健壮性
    • 高效率
    • 低存储量

上面几个目标中,必须要记住的就是 健壮性。也就是要求该算法能够对不合理的数据进行检查!之所以强调这一点,而不强调其他几点,不是说其他的不重要,而是因为其他几点强调了也没啥用,因为那是能力问题。但 健壮性 不同,这是意识问题!只要你时刻记住健壮性,那么你在拿到数据时,就会先思考一下这个数据是否可以小于 0,是否可以是非数字类型。这也是为什么很多在线 IO 的答案中,即使题目明确了输入范围,但代码中仍然会判断一下数据是否在合理范围之内!

算法复杂度

  • 算法复杂度
    • 时间复杂度 time complexity
    • 空间复杂度 space complexity

这里只提一下空间复杂度。一个算法的存储量包括三个方面:

  • 输入数据所占空间
  • 程序本身所占空间
  • 临时变量所占空间

在分析算法的空间复杂度时,通常只考虑临时变量所占空间。注意函数形参不计入临时变量。

线性表

linear list

常见的存储结构:

  • 顺序存储结构 —— 顺序表 (sequential list)
  • 链式存储结构 —— 链表 (linked list)

线性表的基本运算:

  • 创建(申请空间)
  • 初始化(值)
  • 销毁(释放空间)
  • 获取长度
  • 判断是否为空表
  • 打印输出
  • 获取第 i 个数据元素值
  • 查找某个数据元素
  • 插入数据元素到指定位置
  • 删除数据元素
  • 排序

常见的逻辑结构(通常是使用链式存储结构):

  • 单链表
  • 双链表
  • 循环链表

链表相关术语:

  • 头指针 (head pointer)
  • 首指针 (first pointer)
  • 尾指针 (tail pointer)

通常每个链表都带有一个头结点,头指针就指向头结点。而链表的第一个数据元素是在首结点中,首指针就指向首结点。

当然,这说的是通常情况,如果你要自己实现一个头结点和首结点是同一个结点的链表,也可以!

栈 (stack) 是一种操作受到限制的线性表。它最重要的特性就是 后进先出 (LIFO, Last In First Out)

常用存储结构为

  • 顺序存储结构
  • 链式存储结构

相关术语:

  • 栈顶 (top)
  • 栈底 (bottom)
  • 进栈 (push)
  • 出栈 (pop)

栈的基本运算

  • 初始化(空栈)
  • 销毁(释放空间)
  • 判断栈是否为空
  • 进栈
  • 出栈
  • 获取栈顶元素(不出栈)

队列

队列 (queue) 也是一种操作受到限制的线性表。它最重要的特性就是 先进先出 (FIFO, First In First Out)

常用存储结构(思考一下如何使用两个栈实现队列)

  • 顺序存储结构
  • 链式存储结构

基本运算

  • 初始化(空队列)
  • 销毁(释放空间)
  • 判断队列是否为空
  • 入队(插入队尾)
  • 出队

常见逻辑结构类型:

  • 队列
  • 循环队列
  • 双端队列

相关术语:

  • 队尾 (rear)
  • 队首 (front)
  • 入队 (enqueue)
  • 出队 (dequeue)
  • 假溢出 (false overflow)

假溢出:在循环队列中,由于判断队列是否已满的条件设置不合理而导致队列已满,但实际上还有空位置。

串 (string) 是由零个或多个字符组成的有限序列。

串的存储结构通常为顺序存储结构。当然,你也可以使用链式存储结构。

基本运算

  • 生成串
  • 销毁串
  • 复制串
  • 判断串是否相等
  • 获取串的长度
  • 拼接串
  • 提取子串
  • 插入子串
  • 删除子串
  • 替换子串
  • 输出串

相关术语:

  • 空串
  • 子串 (substring)
  • 子序列 (subsequence)

子串指的是串中一段任意长度的 连续 字符所组成的序列。

子序列指的是从串中按顺序提取出的任意长度的可 非连续 字符所组成的序列。

常见算法

  • BF (Brute-Force) 暴力算法
  • KMP 算法

KMP 指的是三位大佬:D. E. Knuth、J. H. Morris 和 V. R. Pratt。

字符串的存储方式

如果你使用过 c 语言,那么你应该和发现 c 语言中不存在“字符串”,其他语言所提供的字符串,其实是它内部帮你实现了“字符串”这一数据结构。所以这里可以进一步讨论一下串的存储方式。一般来说,一个字节(8 位)可以表示一个字符(假设只存储 ASCII 码)。而计算机内存是 按字编址,即以‘字’作为存储单位,一个存储单位就是一个‘字’大小。而一个‘字’具体表示多少个字节,则取决与机器。比如 64 位操作系统上,一个‘字’通常是 64 位(8 个字节)。

那么, 这个时候使用顺序存储串时,你就可以有多种选择了:是选择一个‘字’存储一个字符,还是选择一个‘字’存储多个字符(即一个字节存储一个字符)呢?很明显,前者的占用空间大,但处理起来方便,比如 c 语言你可以直接使用 char 来存储一个 ASCII 字符。而后者的占用空间小,但处理起来很麻烦,比如 c 语言中你需要使用 char char / unsigned char 来存储一个 ASCII 字符。

之所以在这里提及这个,是因为很多“神奇”的算法都涉及到这种思维,比如各种使用位运算的神奇操作。

计算机架构中,一个 word (字) 被定义一个指定位长的数据单位,当计算机处理器和内存直接进行寻址时,就会用到这个数据单位。专业点说,word 代表的是计算机中数据总线的宽度。通常,一个 word 的长度会是 8 的倍数,因为一个字节通常是 8 位。1

递归

数组

数组太过常见了,常见到我不知道应该如何来介绍它。因为不同编程语言中有对于的数组,对于没有学过 c 语言的人,或者只学习过 js 语言的人来说,可能在看到线性表和数组这两个内容居然分开讲时,就已经蒙圈了。

在 c 语言中,数组一旦指定大小,就不能改变了,如果非要改变,就得重新申请新的空间,然后复制过去。而在 js 语言中,基本不需要考虑这种情况。但考虑到申请的空间,需要重新申请空间,这一需求在算法设计中算是常见的,所以这里指的数组是永远较多限制的数组结构。

数组的基本运算:

  • 初始化
  • 销毁
  • 读操作
  • 写操作

由于数组使用非常频繁,所以各种高级语言中都有很多有关数组的 api,这些 api 都可以尝试自己实现一下。

数组涉及的相关术语,主要是和矩阵相关:

  • 按行优先存储
  • 按列优先存储
  • 对称矩阵 (Symmetric matrix)
  • 上三角矩阵 (upper triangular matrix)
  • 下三角矩阵 (lower triangular matrix)
  • 对角矩阵 (diagonal matrix)
  • 稀疏矩阵 (sparse matrix)
  • 三元组表 (list of 3-tuples)

对称矩阵:矩阵内元素满足 aij = aji

上三角矩阵和下三角矩阵,是根据主对角线(对角线指的是方向是 \)进行划分的。上三角矩阵指的是矩阵的左下 (◣) 部分的所有元素均为常数。下三角矩阵指的是右上 (◥) 部分的所有元素是常量。

对角矩阵,类似与上三角矩阵和下三角矩阵的组合,要求主对角线的上面和下面都有相同条非零元素构成的次对角线。

稀疏矩阵:当矩阵中的非零元素非常少时,就称为稀疏矩阵。这种时候,如果还用二维数组来存储就有点浪费空间了。我们可以用三元组 (i, j, aij) 来表示这些非零元素。

广义表

广义表 (generalized) 是线性表的推广,就像多位数组是一维数组的推广一样。广义表中的元素数量是任意的,元素的类型可以是原子类型(不可再分隔的数据类型,也可以是另一个广义表。js 中的数组其实就相当于广义表。

广义表的存储结构通常是链式存储,因为很难为一个广义表分配固定大小的存储空间。

基本运算:

  • 创建
  • 销毁
  • 求广义表的长度
  • 求广义表的深度
  • 打印输出

树的常见逻辑表示方法

  • 树形表示法 (tree representation)
  • 括号表示法 (bracket representation)
  • 文氏图表示法 (venn diagram representation)
  • 凹入表示法 (concave representation)

树的遍历:

  • 先根遍历 (preOrder transversal)
  • 后根遍历 (postOrder transversal)
  • 层次遍历 (level transversal)

常见术语

  • 结点的度 (degree of node)
  • 树的度 (degree of tree)
  • m 次树 (m-tree)
  • 分支结点 (branch node)
  • 叶子结点 (leaf node)
  • 路径 (path)
  • 路径长度 (path length)
  • 子结点 (child node)
  • 父结点 (parent node)
  • 兄弟结点 (sibling)
  • 子孙结点 (descendant)
  • 祖先结点 (ancestor)
  • 结点层次 (level)
  • 结点深度 (depth)
  • 树的高度 (height)
  • 树的深度 (depth)
  • 有序树 (ordered tree)
  • 无序树 (unordered tree)
  • 森林 (forest)

结点的度:结点的子树个数

树的度:树的所有结点中,最大的结点的度

m 次树:度为 m 的树

分支结点:度不为 0 的结点

路径:一个结点序列

路径的长度:路径上的分支的数量(即结点序列 - 1)

有序树:树中的每个结点的子树都是按照一定次序从左到右安排的,且相对次序是不能随意改变的。无特别说明时,树指的都是有序树。

二叉树

二叉树 (binary tree) 是严格区分左右子树的。

基本分类:

  • 满二叉树 (full binary tree)
  • 完全二叉树 (complete binary tree)

常见存储结构:

  • 顺序存储结构
  • 链式存储结构

基本运算

  • 构造二叉树
  • 销毁(释放空间)
  • 查找特定结点
  • 获取左子结点
  • 获取右子结点
  • 获取高度
  • 先序遍历 (preOrder transversal)
  • 中序遍历 (inOrder transversal)
  • 后序遍历 (postOrder transversal)
  • 层序遍历 (level transversal)
  • 以括号表示法打印输出(序列化)

线索二叉树

相关术语:

  • 前驱结点 (predecessor node)
  • 后驱结点 (successor node)
  • 线索二叉树 (threaded binary tree)

先序/中序/后续遍历过程中,一个结点的前驱结点指的就是遍历当前结点前所在结点;一个结点的后驱结点就是遍历当前结点后所在结点。

当某个结点的左指针为空时,让其指向该结点的前驱结点;

当每个结点的右指针为空时,让其指向该结点的后驱结点;

线索二叉树指的通常是中序遍历中的前驱结点和后驱结点。

中序线索二叉树图示例 - 来自知乎中序线索二叉树图示例 - 来自知乎

哈夫曼树

相关术语:

  • 权 (weight)
  • 带权路径长度 (WPL, Weighted Path Length)
  • 哈夫曼树 (Huffman tree)

权:结点所携带的有意义的数值。

带权路径长度:根结点到当前结点之间的路径长度和该结点上的权的乘积。

哈夫曼树:,又称最优二叉树,表示带权路径长度最小的二叉树。

相关算法:

  • 构造哈夫曼树
  • 哈夫曼编码

构造哈夫曼的算法很简单:每次从还未被选取的结点中,选取两个数值最小的结点,然后合成一个新结点。重复此步骤,直到所有结点都被选取过。

哈夫曼编码:将需要编码的字符作为结点,其权值等于该字符在电文中出现的次数,然后构造出哈夫曼树。规定哈夫曼树的左分支为 0,右分支为 1。那么各个字符所到根结点的路径,就是该字符的编码。

并查集

常见存储方式有:

  • 邻接矩阵 (adjacency matrix)
  • 邻接表 (adjacency list)

图的基本运算:

  • 创建
  • 销毁
  • 遍历
  • 打印输出

常见算法:

  • 深度优先遍历 (DFS, Depth First Search)
  • 广度优先遍历 (BFS, Breadth First Search)

生成树和最小生成树

一个连通图的极小连通子图就是 生成树 。所以生成树中的边的数量一定等于顶点数量 - 1。也就是说生成树中不会存在环,或者说如果在生成树上再添加任意一条边,那么就一定会出现一个环。

如果连通图中的每条边都有权值,那么所有生成树中,权值之和最小的就是 最小生成树

常见算法:

  • 普里姆算法 (Prim)
  • 克鲁斯卡尔算法 (Kruskal)

最短路径

常见算法

  • Dijkstra 算法
  • 弗洛伊德算法 (Floyd)

拓扑排序

有向图 V 中的顶点序列 v1,v2, ..., vn 如果满足:若 vi 到 vj 有路径,则 vi 在 vj 的前面。那么这个序列就称为拓扑序列。

在有向图中寻找一个拓扑序列的过程称为拓扑排序 (topological sort)

AOE 网

相关术语:

  • 源点 (source)
  • 汇点 (converge)
  • 关键路径 (critical path)
  • 关键活动 (key activity)
  • AOE 网 (activity on edge network)

用一个有向无环图描述工程的预计进度,顶点表示事件,有向边表示活动,边的权表示完成活动所需要的时间,或者说活动持续时间。入度为 0 的顶点表示开始事件(开工),出度为 0 的点表示结束事件(完工),这样的一个带权的有向无环图称为 AOE 网。

源点:入度为 0 的顶点,也就是开始事件(开工)

汇点:出度为 0 的顶点,也就是结束事件(完工)

关键路径:AOE 网中,从源点到汇点的所有路径中,路径长度最大的路径

关键活动:关键路径上的顶点

查找算法

常见算法

  • 顺序查找 (sequential search)
  • 折半查找 (binary search)
  • 分块查找 (block search)

相关术语

  • 动态查找表 (dynamic search table)
  • 静态查找表 (static search table)
  • 内查找 (internal search)
  • 外查找 (external search)
  • 平均查找长度 (ASL, Average Search Length)
  • 比较树 (comparison tree) / 判定树 (decision tree)

动态查找表:查找的同时进行增删改操作

静态查找表:查找时不涉及增删改操作

内查找:整个查过过程都只在内存中

外查找:查找过程中需要访问外存

判定树/比较树:折半查找的过程可以使用二叉树表示,把当前查找区间的中间位置上的元素作为二叉树的根,根的左子树是左子表所构成的二叉树,右子树同理。

二叉搜索树

二叉搜索树 (BST, binary search tree) 又称二叉排序树。重要性质: BST 的中序遍历结果是一个递增有序序列

基本运算

  • 创建
  • 插入
  • 查找
  • 删除

平衡二叉树

平衡二叉树通常指的是 AVL 树。平衡二叉树在 BST 的基础上增加了树形的约束,即每个结点都是平衡的。

基本运算

  • LL 型调整
  • RR 型调整
  • LR 型调整
  • RL 型调整
  • 创建
  • 插入
  • 查找
  • 删除

B_ 树

外查找

B+ 树

外查找

哈希表查找

常见术语:

  • 哈希表 (hash address)
  • 哈希函数 (hash function)
  • 哈希地址 (hash address)
  • 哈希冲突 (hash collisions)
  • 同义词 (synonym)
  • 装填因子 (load factor)

哈希函数构造方法

  • 直接定址法
  • 除留余数法
  • 数字分析法

哈希冲突解决方案

  • 线性探测法 (linear probing)
  • 平方探测法 (square probing)
  • 拉链法 (chaining)

排序

内排序

  • 插入排序
    • 直接插入排序
    • 折半插入排序
    • 希尔排序 / 分组插入排序
  • 交换排序
    • 冒泡排序
    • 快速排序
  • 选择排序
    • 简单选择排序
    • 堆排序
  • 归并排序
  • 基数排序

外排序

  • 磁盘排序
  • 磁带排序

生成初始归并段 多路平衡归并 最佳归并树