「杂文」身为 OIer 的我要在考试前 2 小时速通一点没学的《数据结构 C 语言 - 严蔚敏版》,我为什么会做这样的梦(雾)

写在前面

上课天天在最后排看马娘 live 导致的。

(2023.6.19 更新)最喜欢的 Hat on your head! 链接全挂了呃呃,补档一个第二喜欢的 Gaze on me。

还有五天考,简单翻阅一下。

对本书和本课的评价挂在最后。

面向 OIer 的个人向本书学习建议

  • 直接上手把以下比较陌生的章节的代码,按照给定形式实现一遍。如果所在高校有对应的实验课的话,写写实验题即可。

  • 将 OI 中并不常见的知识,或是写法与 OI 有所出入的部分进行一个习的学(以下使用(*)标出)。

  • 因为本书质量实在是欠佳,而且如果还在采用本书作为教材的高校大概教学水平也应该欠佳,为了照顾满头雾水的大部分学生,考试题目大概不会很难。

  • 然后随便考考应该就能 90+ 了(确信)。

一 绪论

呃呃呃。

太傻逼了。

要考这种傻逼东西我直接呃呃。

1.1

  • 写的太无聊了。
  • 而且上课直接跳了。

1.2 基本概念术语

就怕考这种傻逼东西。

  • 数据

    • 数据元素:作为整体进行考虑的基本单位。
    • 数据项:数据的不可分割的最小单位。
    • 数据对象:性质相同的数据元素的集合。
    • 数据结构:相互间存在一种或多种特定关系的数据元素的集合。
      • 集合
      • 线性结构:一对一
      • 树形结构:一对多
      • 图状结构、网状结构:多对多
  • 逻辑结构

  • 物理结构/存储结构:数据结构在计算机中的表示,包括元素的表示和关系的表示。

    • 数据元素的表示:

      • 元素/结点:一个表示一个数据元素的位串。
      • 数据域:由若干数据项组成的未传中对应于各个数据项的子位串。

      元素或结点可以看成数据元素在计算机中的映像。

    • 数据结构的存储结构被分为:顺序存储结构、链式存储结构、索引存储结构、哈希存储结构

      • 顺序映像 \(\rightarrow\) 顺序存储结构:借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系。
      • 非顺序映像 \(\rightarrow\) 链式存储结构:借助只是元素存储地址的指针表示数据元素之间的逻辑关系。
  • 数据类型

    • 非结构的原子类型:值不可分解
    • 结构类型:若干成分按照某种结构组成,可分解,成分可以是非结构的也可以是结构的。
  • 抽象数据类型(ADT)

    • 原子类型:值不可分解。(整数)

    • 结构类型:

      • 固定聚合类型:值由确定数目的成分按照某种结构构成。(复数,由两个有序实数组成)
      • 可变聚合类型:值的成分的数目不确定。(有序可变长度整数序列)
    • 抽象数据类型可表述为 \((D,S,P)\)\(D\) 位数据对象,\(S\)\(D\) 上的关系集,\(P\) 为对 \(D\) 的基本操作集。

    • 本书定义抽象数据类型的格式:

      ADT 抽象数据类型名 {
        数据对象:
        数据关系:
        基本操作:
      } 抽象数据类型名
      
      • 数据对象和数据关系的定义通过伪代码描述。

      • 基本操作的定义格式:

        基本操作名(参数表)  
          初始条件:
          操作结果:
        

        参数包括赋值参数(仅提供输入值)、引用参数(以 & 打头,提供输入值外还可返回操作结果)

    • 多形数据类型:值的成分不确定的数据类型。

1.3 抽象数据类型的表示与实现

  • 本书采用类 C 语言。

  • 预定义常量和类型。

  • 数据结构的表示用类型定义 typedef 描述。

  • 基本操作的算法用以下形式函数描述:

    函数类型 函数名(函数参数表) {
      //算法说明
      语句序列
    } //函数名
    
  • 以下常用语句及其表示略。

1.4 算法和算法分析

  • 算法:对特定问题求解步骤的一种描述,是指令的有限序列,每一条指令表示一个或多个操作。
    • 具有以下 5 个重要特性
    • 有穷性:步骤有穷,每一步时间有穷。
    • 确定性:每一条指令含义确切,输入相同输出相同。
    • 可行性:算法中描述的操作都是可以通过已经实现的基本运算执行有限次得到。
    • 输入
    • 输出
  • 算法的设计要求
    • 正确性
    • 可读性
    • 健壮性:可处理非法输入数据。
    • 效率与低存储量需求
  • 算法效率的度量
    • 时间复杂度
      • \(T(n) = O(f(n))\)
      • 语句的频度:该语句被重复执行的次数
    • 空间复杂度
      • \(S(n) = O(f(n))\)
      • 原地工作:额外空间相对于输入数据量来说为常数。

二 线性表

2.1 线性表的类型定义

2.2 线性表的顺序表示和实现

  • 满足:

\[\begin{aligned} &\operatorname{location}(a_{i+1}) = \operatorname{location}(a_i) + \operatorname{length}\\ &\operatorname{location}(a_{i}) = \operatorname{location}(a_1) + (i-1)\times \operatorname{length} \end{aligned}\]

2.3 线性表的链式表示和实现

  • 特点

    • 用一组任意的储存单元存储线性表的数据元素。
    • 对于每个数据元素来说,除了存储器本身的信息之外,还需存储一个指示其直接后继的信息。
    • 数据域:存储数据元素信息的域。
    • 指针域:存储直接后继的信息的域。
  • 头结点:单链表的第一个结点之前附设的结点。

    • 信息域:可为空,可存长度等。
    • 指针域:指向线性表第一个元素结点的存储位置。
  • 循环链表:表中最后一个结点的指针域指向头结点。

    • 遍历中止条件变为后继是否为头结点。
  • 双向链表:两个指针域,一个指向前驱,一个指向后继。

    • 也可以构成双向循环链表。
  • 做题经验谈

    • 若某表最常用的操作是在最后一个结点之后插入一个结点或删除最后一二个结点,则采用带头结点的双循环链表省运算时间。(头结点的前驱即为最后一个结点)

三 栈和队列

3.1 栈

  • 略。

3.2 栈的应用举例

  • 数值转换。
  • 括号匹配。
  • 行编辑程序。
  • 迷宫求解(栈模拟 dfs)。
  • 中缀表达式转后缀表达式求值。
  • 想必大家早就写过了。

3.3 栈与递归的实现

  • 调用函数和被调用函数之间的链接及信息交换需通过栈来进行。
  • 递归的优点:程序结构简单清晰,易证明其正确性。
  • 递归的缺点:执行中占用内存较多且运行效率低。

3.4 队列

  • 双端队列ss
  • 链队列:用链表表示的队列。
    • 需要两个分别指示队头和队尾的指针才能唯一确定。
    • 空队列判断条件:头指针尾指针均指向头结点(空结点)。
  • 循环队列:顺序存储结构,令空间上的最后一个结点的下一个位置,指向空间上的第一个结点。
    • 头指针应指向队头结点,尾指针应指向队尾结点之后的位置。
    • 牺牲一个空间的写法:
      • 空队列判断条件:头指针等于尾指针。
      • 满队列判断条件:头指针在尾指针的下一位(此时牺牲了一个空间)。
    • 空间数为 \(n\) 的循环队列结点个数为:\((\) 最后一个元素位置 \(-\) 第一个元素位置位置 \(+ 1 + n)\bmod n\)
  • 做题记录
    • 在对链队列做出队操作时,可能改变 front 指针的值。(这里的 front 指针指头指针。如果设立了空的头结点则不会改变头指针的位置,头指针将永远指向头结点,否则头指针的位置将会改变为指向当前队列的第一个元素的位置)

四 串

4.1 串类型的定义

  • 由零个或多个字符组成有序序列。

4.2 串的表示和实现

  • 定长顺序存储表示
  • 堆分配存储表示
  • 串的块链存储表示:每个结点允许存放多个字符

4.3 串的模式匹配算法(*)

  • 暴力 \(O(n^2)\) 略。

  • 以下介绍该书中描述的 KMP 算法与 OI 中常见写法的不同。

  • 规定

    • 下文中将在主串 \(s\) 中匹配模式串 \(t\)
    • 串的下标从 1 开始。
    • \(s[i:j]\) :字符串 \(s\) 的子串 \(s_i\cdots s_j\)
    • 真前/后缀:字符串 \(s\) 的真前缀定义为满足不等于它本身的 \(s\) 的前缀。同理就有了真后缀的定义:满足不等于它本身的 \(s\) 的后缀。
  • OI 中常见写法

    • 以博主撰写的 KMP 笔记为例:https://www.cnblogs.com/luckyblock/p/14245452.html

    • 定义 \(\operatorname{border}\):字符串 \(t\)\(\operatorname{border}\) 定义为,满足既是 \(t\) 的真前缀,又是 \(t\) 的真后缀的最长的字符串,如 \(\texttt{aabaa}\)\(\operatorname{border}\)\(\texttt{aa}\)

      \(\operatorname{fail}\):模式串 \(t\)\(\operatorname{fail}\) 是一个长度为 \(|t|\) 的整数数组,它又被称为 \(s\)失配指针\(\operatorname{fail}_i\) 表示前缀 \(t[1:i]\)\(\operatorname{border}\) 的长度,即:

      \[\operatorname{fail}_i = \max{\{ j \}},\, (j<i)\land(s[1,{\color{red}{j}}] = s[i-j+1, {\color{red}{i}}]) \]

      特别的,若不存在这样的 \(j\),则 \(\operatorname{fail}_i = 0\)。如 \(\texttt{abaabcac}\)\(\operatorname{fail} = \{0, 0, 1, 1, 2, 0, 1, 0\}\)

    • 加速匹配的原理为利用了当前已匹配的部分。若当前匹配到主串 \(s\) 的位置 \(i\),已匹配的长度为 \(j\),匹配模式串位置 \(j+1\) 失败,则利用 border 前后缀相等的特性,即可在失配时不必抛弃当前已匹配的部分,跳回到主串当前已匹配部分开头的下一个位置再从开头进行匹配。而是令已匹配长度 \(j = {\color{red}{\operatorname{fail}_j}}\) 再比较主串的当前位置 \(i\) 与模式串位置 \({\color{red}{j+1}}\) 是否相等,重复该过程直至匹配成功或 \(j=0\)

    • \(\operatorname{fail}\) 的方法是在模式串自身上运行 KMP 匹配算法,并将匹配到当前位时的最大匹配前缀的长度作为该位的 \(\operatorname{fail}\) 的值。

  • 本书中给出的写法:

    • 定义:\(\operatorname{next}\):模式串 \(t\)\(\operatorname{next}\) 是一个长度为 \(|t|\) 的整数数组,\(\operatorname{next}\) 代表模式串中第 \(i\) 个字符与主串中相应字符失配时,在模式串中需要重新和主串中该字符进行比较的字符的位置,有:/

      \[\operatorname{next}_i = \max\{j\}, ({\color{red}1<}j<i)\land(s[1,{\color{red}{j - 1}}] = s[i-j+1, {\color{red}{i-1}}]) \]

      特别的,规定 \(\operatorname{next}_1 = 0\),除此之外,若不存在这样的 \(j\),则 \(\operatorname{next}_i = 1\)。如 \(\texttt{abaabcac}\)\(\operatorname{next} = \{0, 1, 1, 2, 2, 3, 1, 2\}\)

      可以认为若 \(\operatorname{next}_i = k\)则前缀 \(t[1:i-1]\) \(\operatorname{border}\) 的长度为 \(k - 1\)

    • 加速匹配的原理类似。但是定义有所改变:设当前匹配到主串 \(s\) 的位置 \(i\)已匹配的长度\(j-1\) 待匹配的位置\(j\),且匹配模式串位置 \(j\) 失败,则失配时令待匹配位置 \(j = {\color{red}{\operatorname{next}_j}}\) 再比较主串的当前位置 \(i\) 与模式串位置 \(\color{red}j\) 是否相等,重复该过程直至匹配成功或 \(j=0\)

    • \(\operatorname{next}\) 的方法同样是在模式串自身上运行 KMP 匹配算法,但是将匹配到当前位时即将要匹配的位置作为该位的 \(\operatorname{next}\) 的值。

  • 两者除定义稍有不同外基本一致,复杂度显然均为 \(O(|s|+|t|)\) 级别,证明详见上述博文链接。

  • KMP 的改进算法:仅仅是根据模式串可能存在的连续相等的情况提出的一种常数优化,属于是空间换时间且效率提升一般,并且本书并没有给出形式化的定义,认为不考,略。

4.4 串操作应用举例

  • 略。

五 数组和广义表

5.1 数组的定义

  • 略。

5.2 数组的顺序表示和实现

  • \(n\) 维数组含有 \(\prod_{i=1}^{n} b_i\) 个数据元素,第 \(i\) 维的下标的取值范围为 \([0, b_i - 1]\)

  • \(n\) 维数组中,\(a_{j_1, j_2, \dots, j_n}\) 的存储地址满足:

    \[\operatorname{location}(a_{j_1, j_2, \dots, j_n}) = \operatorname{location}(a_{0, 0, \dots, 0}) + \left(\sum_{i=1}^{n-1}j_i\prod_{k=i+1}^{n}b_k + j_n\right)\times \operatorname{length} \]

5.3 矩阵的压缩储存

  • 对称/三角矩阵只存上/下三角,按照某个顺序原则将其压缩储存到一维数组中。
  • 稀疏矩阵:只储存非零元。
    • 稀疏因子:\(n\times m\) 的矩阵有 \(t\) 个元素非零:\(\delta = \frac{t}{n\times m}\),通常认为 \(\delta\le 0.05\) 时矩阵稀疏。
    • 三元组顺序表实现:\((i, j, a_{i, j})\)
      • 实现矩阵转置。(咕咕咕)
      • 快速转置。
    • 行逻辑链接的顺序表实现。
    • 十字链表实现。

5.4 及之后

  • 上课直接没讲。
  • 略。

六 树和二叉树

常识!

6.1 树的定义和基本术语

  • 子树
  • 结点:包含一个数据元素及若干指向其子树的分支。
  • 结点的度:结点拥有的子树的个数。
  • 叶子/终端结点:度为 0 的结点。
  • 非终端结点/分支结点
  • 树的度:树内各结点的度的最大值。
  • 双亲/父亲
  • 孩子
  • 兄弟
  • 堂兄弟
  • 树的深度:树中结点的最大层次。
  • 有序树、无序树
  • 森林

6.2 二叉树

  • 显然的结论:

    • \(i\) 层上至多有 \(2^{i-1}\) 个结点。
    • 深度为 \(k\) 的二叉树至多有 \(2^{k}-1\) 个结点。
    • 二叉树叶结点数为 \(n_0\),度为 2 结点数为 \(n_2\),则 \(n_0 = n_2 + 1\)
  • 满二叉树:深度为 \(k\) 且有 \(2^k - 1\) 个结点的二叉树。

  • 完全二叉树:深度为 \(k\) 且至少有 \(2^{k-1}\) 个结点的二叉树。

    • 叶结点只可能出现在 \(k\)\(k-1\) 层。
    • \(n\) 个结点的完全二叉树深度为 \(\left\lfloor\log_2 n\right\rfloor + 1\)
    • 对于第 \(i\) 个结点,其父亲为 \(\left\lfloor\frac{i}{2}\right\rfloor\),左儿子为 \(2\times i\),右儿子为 \(2\times i+1\)(如果存在)。

6.3 遍历二叉树和线索二叉树

  • 先序遍历:根左右。

  • 中序遍历:左根右。

  • 后序遍历:左右根。

  • 线索二叉树

    • 分为先序、中序、后序三种。多次先序、中序、后序遍历二叉树时的常数优化。
    • 如果某个结点的左儿子为空,则将其左儿子指向其前驱结点;如果某个结点的右儿子为空,则将其右儿子指向其后继结点。在第一次遍历时顺便维护即可。
    • 先序线索遍历二叉树:可快速得到后继(从该结点向子树中递归),无法快速得到前驱(前驱即父结点,该结点及其子树中无指向其父结点的指针)。
    • 中序遍历线索二叉树:可快速得到前驱、后继。
    • 后序遍历线索二叉树:可快速得到前驱,无法快速得到后继。

6.4 树和森林

  • 双亲表示法

  • 孩子表示法

  • 孩子兄弟表示法

  • 森林转化为二叉树 \(F = \{T_1, T_2, \dots, T_m\}\rightarrow B\)

    • \(F\) 为空则 \(B\) 为空。
    • 否则 \(B\) 的根为 \(F\) 中第一棵树 \(T_1\) 的树根,左子树为去掉 \(T_1\) 的根结点后 \(T_1\) 形成的子树森林构成的二叉树,右子树为 \(\{T_2, T_3, \dots, T_m\}\) 构成的二叉树。
  • 二叉树转化为森林 \(B \rightarrow F = \{T_1, T_2, \dots, T_m\}\)

    • \(B\) 为空,则 \(T\) 为空。
    • 否则 \(F\) 中第一颗树的根 \(T_1\) 根为 \(B\) 的根,\(T_1\) 根的子树森林为 \(B\) 的左子树转换形成的森林,\(\{T_2, T_3, \dots, T_m\}\)\(B\) 的右子树转换形成的森林。
  • 遍历森林:按照树的顺序进行单棵树的遍历。

6.5 树和等价关系

  • 并查集。
  • 上课没讲。

6.6 赫夫曼树及其应用(*)

  • 路径长度:路径上的分支数目。

  • 树的路径长度:从树根到每个结点的路径长度之和。

  • 带权路径长度:根结点到该结点路径长度与结点权值乘积。

  • 树的带权路径长度 \(\operatorname{WPL}\):树中所有叶子结点的带权路径长度之和。

  • 赫夫曼树/最优二叉树:一棵有 \(n\) 个带权叶子结点的二叉树,且在所有树的形态中带权路径长度 \(\operatorname{WPL}\) 最短的二叉树。

  • 赫夫曼算法贪心地构造赫夫曼树

    • 给定 \(n\) 个叶子结点的权值 \(w_1, w_2\dots w_n\)
    • 首先利用给定条件构成 \(n\) 棵二叉树的集合 \(F = \{T_1, T_2, \dots, T_n\}\),其中每棵二叉树仅有根结点且权值为对应权值。
    • 每次选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,新的二叉树根结点权值为选择的两棵树根结点权值之和,在 \(F\) 中删除被选择的两棵树并将新的二叉树加入 \(F\) 中。
    • 重复上述过程直至只含一棵树。
  • 赫夫曼编码

    • 前缀编码:任一个编码都不是另一个编码的前缀。
    • 出现次数为权值 \(w_i\),编码长度为 \(l\),构造出赫夫曼树。从根向叶结点移动,路径上向左儿子移动代表 0,向右儿子移动代表 1,构成的串即赫夫曼编码。

6.7 回溯法与树的遍历

  • 就是搜索。
  • 略。

6.8 树的计数

  • 这东西为什么会出现在这本书上???

七 图

7.1 图的定义和术语

  • 顶点
  • \(<v, w>\):从 \(v\)\(w\),有向边。
  • 弧尾/初始点 \(v\)
  • 弧头/终端点 \(w\)
  • \((v, w)\):无向边。
  • 完全图:\(\frac{n(n-1)}{2}\) 条有向边。
  • 有向完全图:\(n(n-1)\) 条有向边。
  • 稀疏图
  • 稠密图
  • 权:与边或者弧相关的数。
  • 网:带权图。
  • 邻接点:有无向边相连的点。
  • 依附:边依附于其两个端点。
  • 相关联:边和其两个端点相关联。
  • 度:和某个顶点相关联的边的数目。
  • 入度:以顶点为头的弧的数目。
  • 出度:以顶点为尾的弧的数目。
  • 路径
  • 回路/环
  • 简单路径:无环。
  • 连通:无向图,中两点间有路径。
  • 连通图:无向图中,任意两点间均有路径。
  • 连通分量:无向图中的极大连通子图。
  • 强连通图:有向图中,任意两点间均有路径。
  • 生成树:连通图的包含图中全部顶点的极小连通子图,边数为 \(n-1\)
  • 有向树:恰有一个顶点的入度为 0,其余顶点入度均为 1 的有向图。
  • 生成森林:含有图中所有顶点,由若干棵有向树组成。

7.2 图的存储结构

  • 邻接矩阵

  • 邻接表

  • 十字链表

  • 临接多重表

  • 做题记录

    • 有向图的邻接表和逆邻接表中表结点的个数一定相等。(逆邻接表相比于邻接表只是把弧头和弧尾的次序换了一下,总结点数均等于弧的数量)

7.3 图的遍历

  • DFS
  • BFS
  • 时间复杂度:邻接矩阵 \(O(n^2)\),邻接表 \(O(n + e)\)

7.4 图的连通性

  • MST
    • prim:参考 OI。
    • kruscal:参考 OI。

7.5 有向无环图及其应用(*)

  • 拓扑排序

  • 关键路径

    • 对于一张仅有一个 0 入度点(设为 1),仅有 1 个 0 出度点(设为 \(n\))的 DAG:

    • 结点为事件,有向边为活动。

    • 定义 \(f_u\) 表示事件 \(u\) 的最早发生时间,钦定 \(f_1 = 0\),且有:

      \[f_{v} = \max_{(u, v) \in E} \{ f_u + w_{u, v} \} \]

      即从 1 到达结点 \(u\)最长路

    • 定义 \(g_u\) 表示事件 \(u\) 的最迟发生时间,钦定 \(g_n = f_n\),且有:

      \[g_u = \min_{(u, v)\in E}\{ g_v - w_{u, v}\} \]

      即在反图上,边权取负值时,钦定 \(g_n = f_n\) 前提下从 \(n\) 到其他点的最长路

    • 若某条有向边 \((u, v)\) 满足 \(f_u + w_{i, j}= g_v\),则这条边代表的活动是一个关键活动。

  • 做题记录

    • 图的拓扑序列唯一,但其弧数不一定\(n-1\)。(这题翻车了太呃呃了。猴子都能举出来的例子:一条长链加上边 \(1\rightarrow n\)

7.6 最短路径

  • Dijkstra:参考 OI。
  • Floyd:最短路 \(\operatorname{dis}(u, v)\)\(\operatorname{p}(u, v, w)\) 如果 \(w\)\(u\rightarrow v\) 最短路上的结点,则 \(\operatorname{p}(u, v, w) = 1\),否则为 \(0\)(初始化后,使用中转点 \(k\) 更新时检查 \(w\) 是否位于 \(u\rightarrow k\)\(k\rightarrow v\) 上即可)。

八 动态内存管理

  • 没讲。

九 查找

  • 查找表:由同一类型的数据元素构成的集合。
    • 静态查找表
    • 动态查找表
  • 关键字:数据元素中某个数据项的值
    • 主关键字:可唯一标识一个数据元素
    • 次关键字
  • 查找:根据给你个的某个值在查找表中确定一个其关键字等于给定值的数据元素

9.1 静态查找表

  • 顺序表的查找
    • 顺序查找:从最后一个数据元素开始逐个比较。
    • 必定成功,且每个数据元素查找概率相等条件下平均查找长度 \(ASL = \frac{n+1}{2}\)
    • 成功不成功等概率,且每个数据元素查找概率相等条件平均查找长度 \(ASL = \frac{3(n+1)}{4}\)。且
  • 有序表的查找
    • 折半查找:就是二分。
    • 必定成功,且每个数据元素查找概率相等条件下平均查找长度 \(ASL = \frac{n+1}{n}\log_2(n+1) - 1\approx \log_2(n+1) - 1\)
  • 静态树表
  • 索引顺序表
    • 原表先排序,然后分为若干段,记录每段的最大值和每段位置在原表中最靠前的元素的位置。
    • 查找时先查被查找元素大小在哪个范围,再从记录的位置开始在原表中顺序查找。
    • 每块含有 \(s\) 个数据元素,假定每个数据元素查找概率相同,使用折半查找查所在块时,平均查找长度:\(ASL \approx \log_2(\frac{n}{s} + 1) + \frac{s}{2}\)

9.2 动态查找表

  • 二叉排序树:左子树上所有结点的值小于根结点的值,右子树上所有结点的值均大于根结点的值。

  • 平衡二叉树 \(\operatorname{AVL}\):左右子树深度之差绝对值不超过 1。

    • 平衡因子 \(\operatorname{BF}\):左子树深度减去右子树深度。
  • 做题记录

    • 给出不同的输入序列建造二叉排序树,可能得到相同的二叉排序树(2 1 3 与 2 3 1,均可能得到以 2 为根的树)。

9.3 哈希表(*)

  • 哈希函数

  • 冲突

  • 哈希表:根据设定的哈希函数和处理冲突的方法将一组关键字映射到一个有限的连续的地址集上,并以关键字在地址集中的映射作为记录在表中的存储位置。

  • 哈希造表/散列

  • 哈希地址/散列地址

  • 构造哈希函数的方法

    • 直接定址法:\(\operatorname{H}(key) = a \times key + b\)
    • 数字分析法
    • 平方取中法
    • 折叠法
    • 除留余数法 \(\operatorname{H}(key) = key \bmod p\)
    • 随机数法
  • 处理冲突的方法:为冲突的哈希地址再找到另一个空哈希地址,构造过程中若继续冲突则继续构造,直至不发生冲突。

    • 开放定址法:

      \[\operatorname{H}_i = (\operatorname{H}(key) + d_i)\bmod m (i\in [1, k], k\le m-1) \]

      • 线性探测再散列:\(d_i = 1, 2, \dots, m-1\)
      • 二次探测再散列:\(d_i = 1^2, -1^2, 2^2, -2^2, \dots, \pm k^2\)
      • 伪随机探测再散列:\(d_i = \operatorname{random}\)
    • 再哈希法

      \[\operatorname{H}_i = \operatorname{RH}_i(key) (i\in [1, k]) \]

      \(\operatorname{RH}\) 为不同的哈希函数。

    • 链地址法:将所有哈希地址相同的数据元素存储在同一线性链表中。

    • 建立公共溢出区:所有冲突的全部填入溢出表。

  • 哈希表的查找及其分析

    • 开放定址法:不断允许哈希函数并进行查找,直到查找到对应的值,或是地址空。
    • 哈希表的装填因子:\(\alpha = \frac{\operatorname{number\ of\ elements}}{\operatorname{length\ of\ hashtable}}\)
    • 线性探测再散列查找成功的平均查找长度:\(S \approx \frac{1}{2}\left(1 + \frac{1}{1-\alpha}\right)\),不成功:\(U \approx \frac{1}{2}\left(1 + \frac{1}{(1-\alpha)^2}\right)\)
    • 二次探测再散列、随机探测再散列查找成功的平均查找长度:\(S \approx -\frac{1}{\alpha}\ln{(1-\alpha)}\),不成功:\(U \approx \frac{1}{1-\alpha}\)
    • 链地址法查找成功的平均查找长度:\(S \approx 1+\frac{\alpha}{2}\),不成功:\(U \approx \alpha + e^{-\alpha}\)
    • 对于一个给定的哈希表分析查找成功的平均查找长度:
      • 对于哈希中的每一个值进行一次查找,记录查找成功的查找次数求和后除以值的个数,即为平均查找长度。

十 内部排序

10.1 概述

  • 稳定排序:关键字相同的元素在排序后相对位置不变。

  • 不稳定排序:关键字相同的元素在排序后相对位置可能改变。

  • 内部排序:排序记录仅放在计算机随机存储器中进行的排序过程。

  • 外部排序:数据量过大,在排序过程中需要对外存进行访问。

10.2 插入排序

  • 直接插入排序:将一个记录直接插入到已排好序的有序表中。
    • 通过顺序查找求得该记录应当插入的位置,令被插入位置之后的元素后移,然后将该记录插入。
    • 时间复杂度 \(O(n^2)\) 级别。
    • 已经基本有序的表排序时效率较高。
  • 折半插入排序
    • 将顺序查找位置过程改为折半查找。
    • 时间复杂度仍为 \(O(n^2)\) 级别。
  • 2-路插入排序
    • 同时维护两个待插入的有序表。
    • 将第一个元素看做参考值,如果将要插入的元素大于该值则插入第一个有序表,否则插入第二个有序表。
    • 之后再将两个表首尾相接即可。
    • 时间复杂度仍为 \(O(n^2)\) 级别。
  • 表插入排序
    • 用链表维护有序表,使得插入变为 \(O(1)\) 级别,但是查找插入位置必须遍历链表。
    • 而且不支持随机查找。
    • 时间复杂度仍为 \(O(n^2)\) 级别。
    • 重排有序链表使地址与关键字均有序,复杂度 \(O(n)\) 级别。
  • 希尔排序:分为若干轮,每轮将整个待排记录序列分割成若干子序列,分别进行直接插入排序,然后在下一轮扩大分割的子序列的长度再进行……直至在最后一轮,扩大到整个序列,对整个序列进行一次直接插入排序。
    • 利用了直接插入排序对基本有序的表排序时效率较高的特性。
    • 分割子序列的方法:确定增量值 \(\operatorname{d}\),然后子序列分为:

      \[\begin{aligned} &\left\{ a_1, a_{\operatorname{d} + 1}, a_{\operatorname{2\times d} + 1}, \dots\right\}\\ &\left\{ a_2, a_{\operatorname{d} + 2}, a_{\operatorname{2\times d} + 2}, \dots\right\}\\ &\dots\\ &\left\{ a_d, a_{\operatorname{2\times d}}, a_{\operatorname{3\times d}}, \dots\right\} \end{aligned}\]

    • 注意应使增量序列中的值均互质,且最后一轮希尔排序的增量值 \(\operatorname{d} = 1\)

10.3 快速排序

  • 起泡排序
    • 什么 b 名字
  • 快速排序
    • 对于待排序的序列,首先进行划分:任取一个记录作为枢纽(或支点)(通常取第一个记录),将关键字比它小的放在该元素左侧,大的放在右侧。
    • 然后以该枢纽所在位置为界,再分别对左右两侧进行快速排序。
    • 平均复杂度为 \(O(kn\log n)\) 级别,\(k\) 为常数。
    • 若原序列有序则退化为冒泡排序。

10.4 选择排序(*)

  • 简单选择排序:共进行 \(n-1\) 轮,第 \(i\) 轮选择 \([i, n]\) 中最小的元素并将其与此时第 \(i\) 个元素交换位置。
  • 树形选择排序:线 段 树 排 序
    • 首先对整个序列建线段树,维护区间最小值。
    • 然后取出序列最小值作为有序表的第一个元素,然后单点修改该位置为无穷。
    • 不断重复上述过程直至排序完成。
    • 时空复杂度均为 \(O(n\log n)\)
  • 堆排序
    • 我不会手写二叉堆,所以这里着重记录一下。
    • 当某长度为 \(n\) 的序列 \(a\),满足 \(a_i\le a_{2\times i} \land a_{i}\le a_{2\times i + 1}\) 时,称之为小顶堆(小根堆),若满足 \(a_i\ge a_{2\times i} \land a_{i}\ge a_{2\times i + 1}\) 时,则称之为大顶堆(大根堆)。
    • 以下以大根堆为例。
    • 取出堆顶元素并维护堆的形态:先将堆顶元素与堆的最后一个元素(第 \(n\) 个元素)交换位置,然后向下调整:在该结点的儿子中,找到权值最大的结点与该结点交换,重复此过程直到到达底层。完成该过程后堆的大小减一。
    • 插入元素:将被插入元素插入到位置 \(n+1\),然后向上调整,如果该结点的权值大于它父亲的权值则将两者交换,重复此过程直到不满足或者到根。完成该过程后堆的大小加一。
    • 将序列升序排序:使用大顶堆:首先建堆,然后重复上述取出堆顶元素并维护堆形态的过程,不断将当前最大的元素调整到最后即可。
    • 时间复杂度为 \(O(n\log n)\),空间复杂度为 \(O(n)\)

10.5 归并排序

  • 2-路归并排序。
    • 大家都会写。
    • 与快速排序、堆排序相比,是一种稳定的排序方法。

10.6 基数排序(*)

  • 基数排序是一种将多关键字排序转化为若干次单关键字排序的思想,并非一种具体的排序方法,单关键字排序过程需要由其他排序方法实现。

  • 基数排序的两种多关键字的转化方法:

    • 设有 \(k\) 个关键字:
    • 最高位优先(MSD):需要进行划分。
      首先按照第一关键字进行排序,然后将原序列按照第一关键字是否相同划分为若干子序列,再对每个子序列按照第二关键字排序,再将每个子序列按照第二关键字是否相同进行划分……重复上述过程直至无法划分或考虑了全部的关键字。
    • 最低位优先(LSD):需要使用稳定的排序方法。
      首先按照第 \(k\) 关键字使用一种稳定的排序方法进行排序,然后按照第 \(k-1\) 关键字使用一种稳定的排序方法进行排序……直至考虑了全部的关键字。
  • 链式基数排序

    • 就是他妈的把整数/字符串比较拆成从高到低位比较。
    • 使用最低位优先的基数排序,按照优先级递增顺序进行排序,设每一位仅有 \(r\) 种取值,则建立 \(r\) 个链表代表该位为 \(0\sim 9\) 的元素,遍历序列时将每个元素链接到对应链表之后。
    • 遍历完成后将所有链表按照顺序链接起来即完成对于该关键字的排序。
    • 总复杂度为 \(O(d(n + rd))\),其中 \(d\) 为待排序数的最高位数,\(r\) 为每一位的取值范围。

10.7 各种内部排序方式的比较讨论

  • 有如下表所示:

    排序方法 平均时间复杂度 时间复杂度上界 辅助储存空间复杂度
    简单排序 \(O(n^2)\) \(O(n^2)\) \(O(1)\)
    快速排序 \(O(n\log n)\) \(O(n^2)\) \(O(n)\)
    堆排序 \(O(n\log n)\) \(O(n\log n)\) \(O(1)\)
    归并排序 \(O(n\log n)\) \(O(n\log n)\) \(O(n)\)
    基数排序 \(O(d(n + rd))\) \(O(d(n + rd))\) \(O(rd)\)

    “简单排序”包括除希尔排序之外的所有插入排序、起泡排序和简单选择排序。

  • 平均时间性能:快速排序最佳但最坏情况下不如堆排序和归并排序。对于后两者,在 \(n\) 较大时归并排序优于堆排序,但所需的辅助存储量最多。

  • 上表简单排序中直接插入排序为最简单,序列基本有序或 \(n\) 较小时表现最佳,因此常将它和其他的排序方法(如快速排序、归并排序等)结合使用。

  • 基数排序的时间复杂度也可写成 \(O(d\times n)\)。因此,最适用于 \(n\) 值很大而关键字较小的序列。若关键字也很大,而序列中大多数记录的“最高位关键字”均不同,则亦可先按“最高位关键字”不同将序列分成若干“小”的子序列,而后进行直接插入排序。

  • 从方法的稳定性来比较:基数排序是稳定的内排方法,所有时间复杂度为 \(O(n^2)\) 的简单排序法也是稳定的,归并排序也是稳定的。快速排序、堆排序和希尔排序等时间性能较好的排序方法都是不稳定的。一般来说,排序过程中的“比较”是在“相邻的两个记录关键字”间进行的排序方法是稳定的。

  • 基于比较的排序算法时间复杂度上界的下界\(O(n\log n)\)

写在最后

  • 没时间了懒得评价了,翻知乎吧!
  • 养了十把小栗帽就是跑不下打比,怎么会是呢。

写在写在最后之后

  • (2023.6.20 20:14交卷四小时后)考得很傻逼,应该能拿个看得过去的分数。
  • (出分后)陈教真好:
image.png
posted @ 2023-06-16 01:31  Luckyblock  阅读(69)  评论(3编辑  收藏  举报