本文涉及更多的是概念,代码部分请参考之前写过的 2 篇博客
本文主要是基础的数据结构和算法概念,可能部分地方会涉及更高级的算法和算法,具体内容以后会单独写的。此外一些性质还会不断补充,也希望可以得到您的指点,谢谢。
数据结构
程序 = 数据结构 + 算法
数据结构基本概念
- 数据的逻辑结构:反映数据元素之间的关系的数据元素集合的表示。数据的逻辑结构包括集合、线形结构、树形结构和图形结构四种。
- 数据的存储结构:数据的逻辑结构在计算机存储空间种的存放形式称为数据的存储结构。常用的存储结构有顺序、链接、索引等存储结构。
在数据结构中,没有前件的结点称为根结点,没有后件的结点成为终端结点
数据结构的基本操作
插入和删除是对数据结构的两种基本操作。此外还有查找、分类、合并、分解、复制和修改等。
线性结构和非线性结构
根据数据结构中各数据元素之间前后件关系的复杂程度,一般将数据结构分为两大类型:线性结构和非线性结构。
- 线性结构:有且只有一个根结点;每个结点最多有一个前件,最多只有一个后件。
- 非线性结构: 如果一个数据结构不是线性结构,称之为非线性结构。
本文涉及一下内容:
- 四种线性结构的存储结构:顺序表、链表、索引、散列
- 两种常见的线性逻辑结构:队列、栈
- 非线性逻辑结构:循环队列、双向队列、双向循环队列、树、图
存储结构
顺序表
顺序表是线性表的顺序存储结构,指的是用一组地址连续的存储单元依次存储线性表的数据元素。
顺序表具备如下两个基本特征:
- 顺序表中的所有元素所占的存储空间是连续的;
- 顺序表中各数据元素在存储空间中是按逻辑顺序依次存放的。
假设顺序表的每个元素需占用 $K$ 个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置。则顺序表中第 $i+1$ 个数据元素的存储位置 $LOC(a_i+1)$ 和第 $i$ 个数据元素的存储位置 $LOC(a_i)$ 之间满足下列关系为:
$LOC(a_{i+1}) = LOC(a_i)+K$$LOC(a_i) = LOC(a_1)+(i-1)*K$
其中,$LOC(a_1)$是顺序表的第一个数据元素 $a_1$ 的存储位置,通常称做顺序表的起始位置或基地址。顺序存储结构也称随机存取结构。
顺序表常见操作(括号中为算法平均时间复杂度,没有写明的具体复杂度依赖不同算法和运算规则):
插入($O(n)$)、删除($O(n)$)、查找、排序、分解、合并、复制($O(n)$)、逆转($O(n)$)
链表
链表指线性表的链式存储结构。一组任意的存储单元存储线性表的数据元素,因此,为了表示每个数据元素 $a_i$ 与其直接后继数据元素 $a_{i+1}$ 之间的逻辑关系,对数据元素 $a_i$ 来说,除了存储其本身的信息(数据域)之外,还需存储一个变量指示其直接后继的信息(指针域)。这两部分信息组成数据元素 $a_i$ 的存储映象,称为结点。$N$ 个结点链结成一个链表。该链表就是传统的单向链表。
有时,我们在单链表的第一个结点之前附设一个结点,称之为头结点,它指向表中第一个结点。头结点的数据域可 以不存储任何信息,也可存储如线性表的长度等类的附加信息,头结点的指针域存储指向第一个结点的指针。在单链表中,取得第 I 个数据元素必须从头指针出发寻找,因此,链表是非随机存取的存储结构。
以上提到的链表指针域只包括一个指针,指向下一个数据的地址,如果我们将链表最后一个结点指针域的指针指向链表的头结点地址,就构成了一个环状的存储结构,我们称作循环链表。
当然我们可以给每个结点的指针域再添加一个指针,使其指向前一个数据结点的地址,这样就构成了双向链表,而将头结点的前一个结点指向尾结点,同时将尾结点的下一个结点指向头结点就构成了双向循环链表。
如果链表的尾结点的指针域指向了该链表之前的任意一个结点,我们称该链表为有环链表。环形链表就是其中一个特例
顺序表常见操作(括号中为算法平均时间复杂度,没有写明的具体复杂度依赖不同算法和运算规则):
插入($O(n)$)、删除($O(n)$)、查找、排序、分解、合并、复制($O(n)$)、逆转($O(n)$)
索引
索引存储除建立存储结点信息外,还建立附加的索引表来标识结点的地址。索引表由若干索引项组成。
对于索引的理解最好的例子就是《新华字典》,它建立的2套索引表(拼音、部首)。字典的正文就是从“啊”到“做”的每个字的解释,有上千页,就是是数据。而前面的拼音/部首就是索引表,索引表告诉你某个读音/部首在第几页,这就好比是指向数据地址的指针。而索引表可以有一级的也可以是多级的,比如字典中的部首索引就是两级的。
索引存储结构是用结点的索引号来确定结点存储地址,其优点是检索速度快,缺点是增加了附加的索引表,会占用较多的存储空间。
散列
散列存储,又称哈希(hash)存储,是一种力图将数据元素的存储位置(预留连续存储区域)与关键码之间建立确定对应关系的查找技术。散列法存储的基本思想是由结点的关键码值决定结点的存储地址。散列技术除了可以用于存储外,还可以用于查找。
散列以数据中每个元素的关键字 $K$ 为自变量,通过散列函数 $H(k)$ 计算出函数值,以该函数值作为一块连续存储空间的的单元地址,将该元素存储到函数值对应的单元中。由于该函数值唯一,所以查找时间复杂度为 $O(1)$
线性逻辑结构
线性表
线性表满足以下特征:
- 有且只有一个根结点 $a_1$,它无前件;
- 有且只有一个终端结点 $a_n$,它无后件;
- 除根结点与终端结点外,其他所有结点有且只有一个前件,也有且只有一个后件。线性表中结点的个数 $n$ 称 为线性表的长度。当 $n=0$ 时称为空表。
栈
栈实际上也是一个线性表,只不过是一种特殊的线性表。栈是只能在表的一端进行插入和删除运算的线性表,通常称插入、删除这一端为栈顶(TOP),另一端为栈底(BOTTOM)。当表中没有元素时称为栈空。 栈顶元素总是后被插入(入栈)的元素,从而也是最先被移除(出栈)的元素;栈底元素总是最先被插入的元素,从而也是最后才能被移除的元素。所以栈是个 后进先出(LIFO) 的数据结构
栈的基本运算有三种:入栈、出栈与读栈顶,时间复杂度都是$O(1)$
队列
队列是只允许在一端删除,在另一端插入的顺序表,允许删除的一端叫做队头,用对头指针 $front$ 指向对头元素的下一个元素,允许插入的一端叫做队尾,用队尾指针 $rear$ 指向队列中的队尾元素,因此,从排头指针 $front$ 指向的下一个位置直到队尾指针 $rear$ 指向的位置之间所有的元素均为队列中的元素。
队列的修改是 先进先出(FIFO) 。往队尾插入一个元素称为入队运算。从对头删除一个元素称为退队运算。
队列主要有两种基本运算:入队运算和退队运算,复杂度都是$O(1)$
循环队列
在实际应用中,队列的顺序存储结构一般采用循环队列的形式。所谓循环队列,就是将队列存储空间的最后一个 位置绕到第一个位置,形成逻辑上的环状空间。在实际使用循环队列时,为了能区分队满还是队列空,通常需要增加一个标志 $S$。
循环队列主要有两种基本运算:入队运算和退队运算,复杂度都是$O(1)$
- 入队运算
指在循环队列的队尾加入一个新元素,首先 $rear=rear+1$, 当 $rear=m+1$ 时,置 $rear=1$,然后将新元素插入到队尾指针 指向的位置。当 $S=1, rear=front$,说明队列已满,不能进行入队运算,称为“上溢”。
- 退队运算
指在循环队列的排头位置退出一个元素并赋给指定的变量。首先 $front=front+1$, 并当 $front=m+1$ 时,置 $front=1$, 然后 将排头指针指向的元素赋给指定的变量。当循环队列为空 $S=0$,不能进行退队运算,这种情况成为“下溢”。
非线性逻辑结构
树
树是一种简单的非线性结构。树型结构具有以下特点:
- 每个结点只有一个前件,称为父结点,没有前件的结点只有一个,称为树的根结点。
- 每一个结点可以有多个后件结点,称为该结点的子结点。没有后件的结点称为叶子结点
- 一个结点所拥有的后件个数称为结点的度
- 树的最大层次称为树的深度。
二叉树
二叉树是一种树型结构,通常采用链式存储结构,满足以下特性:
- 它的特点是每个结点至多只有二棵子树(即二叉树中不存在度大于 2 的结点);
- 二叉树的子树有左右之分,其次序不能任意颠倒。
二叉树的基本性质
- 在二叉树的第 $i$ 层上至多有 $2i-1$ 个结点
- 深度为 $k$ 的二叉树至多有 $2k-1$ 个结点($k \geq 1$)
- 在任意一个二叉树中,度为 0 的结点总是比度为 2 的结点多一个
- 具有 $N$ 个结点的二叉树,其深度至少为 $\lfloor log_2 N \rfloor+1$
- 霍夫曼树的带权路径长度 $len = 2n+1$; $n$ 为所以叶子权重和。
二叉树的遍历
就是遵从某种次序,访问二叉树中的所有结点,使得每个结点仅被访问一次。分为以下几种:
- 前序遍历(DLR): 首先访问根结点,然后遍历左子树,最后遍历右子树。
- 中序遍历(LDR): 首先遍历左子树,然后根结点,最后右子树
- 后序遍历(LRD): 首先遍历左子树,然后遍历右子树,最后访问根结点。
此外图的遍历也可以用在树上,包括:
- 广度优先遍历(层序遍历): 从根结点开开始逐层向下,从左到右遍历。
- 深度优先遍历: 从根结点出发沿左子树遍历到叶子结点再逐层向上向遍历右子树。
除此之外还有很多有特点的特殊二叉树:
- 满二叉树:除最后一层以外,每一层上的所有结点都有两个子结点。
- 在满二叉树的第 $K$ 层上有 $2^{K-1}$ 个结点,且深度为 $M$ 的满二叉树有 $2M-1$ 个结点
- 完全二叉树:除最后一层以外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。
- 具有 $N$ 个结点的完全二叉树的深度为 $\lfloor log_2 N \rfloor+1$
- 完全二叉树总结点数为 $N$,则叶子结点数为$\lceil N/2 \rceil$
堆
最常见的完全二叉树就是 堆 了。堆满足以下条件
- 堆中某个结点的值总是不大于或不小于其父结点的值
- 堆总是一棵完全二叉树
将根结点最大的堆叫做 最大堆 或 大根堆 ,根结点最小的堆叫做 最小堆 或 小根堆 。
堆具有以下基本操作:
- 插入: 向堆中插入一个新元素
- 获取: 获取当前堆顶元素的值
- 删除: 删除堆顶元素
- 包含: 判断堆中是否存在某个元素
哈希表
常用的哈希函数
- 直接寻址法: $H(k)$ 是一个线性函数,如果该位置已经有值就向下寻找到第一个空的地方作为散列地址
- 平方取中法: 取 $k$ 平方以后值的中间几位作为散列地址
- 数字分析法: 对于比较规律的 $k$ 值,找出其差异较大的部分作为散列地址
- 折叠法: 将 $k$ 分成很多部分,然后做模二和作为散列地址
- 留余数法: $H(k)=k % p, p \leq m$,其中 $p$ 为素数,$m$ 为表的长度
- 随机数法: 取键字的随机值作为散列地址,关键字长度时使用
实现映射的函数是哈希函数,简单的 hash 可能会发生碰撞(不同输入得到相同输出),为了防止碰撞,考虑以下方法:
- 链地址法(拉链法): 当发生碰撞时,将发生碰撞的数据元素连接到同一个单链表中,而新元素插入到链表的前端
- 线性探针法: 线性探针法地址增量 $d \in D_1 = \{1,2,3, ... , m-1\}$, $i$ 为探测次数,$m$ 为表的长度,该方法遇到冲突地址会依次探测下一个地址($d = d_i + 1$)直到有空的地址,若找不到空地址,则溢出。
平均查找长度
- 线性探针法平均长度推算(其中 $m$ 为表中数据长度,$n$ 为表长度):
$ASL_s=\frac{\sum_{i=1}^m d_i}{m}$,查找成功$ASL_u=\frac{\sum_{i=1}^n d_i}{n}$,查找不成功
注:线性探针法查找成功时 $d_i$ 为每次放入元素时的地址增量,不成功时 $d_i$ 为在表长度内依次查找每个元素到下一个空地址的地址增量(索引在表长度内循环)
- 链地址法平均长度推算(其中 $k$ 为最长链长度,其中 $m$ 为表中数据长度,$n$ 为表长度):
$ASL_s=\frac{\sum_{i=1}^k ({当前级指针数量} \times {当前级数})}{m}$,查找成功$ASL_u=\frac{\sum_{i=1}^n {当前个位置链长度}}{n}$,查找不成功
哈希表相关特性
- 线性探针法容易“聚集”,影响查找效率,而链地址法不会
- 链地址法适应表长不确定情况
- $装填因子(\alpha)=\frac{哈希表中的记录数}{哈希表的长度}$
图
图有两种定义:
- 二元组的定义:图 $G$ 是一个有序二元组 $(V,E)$,其中 $V$ 称为顶集(Vertices Set),$E$ 称为边集(Edges set),$E$ 与 $V$ 不相交。它们亦可写成 $V(G)$ 和 $E(G)$ 。$E$ 的元素都是二元组,用 $(x,y)$ 表示,其中 $x,y \in V$
- 三元组的定义: 图 $G$ 是指一个三元组$(V,E,I)$,其中 $V$ 称为顶集,$E$ 称为边集,$E$ 与 $V$ 不相交;$I$ 称为关联函数,$I$ 将 $E$ 中的每一个元素映射到$V \times V$。如果 $e$ 被映射到 $(u,v)$,那么称边 $e$ 连接顶点 $u,v$,而 $u,v$ 则称作 $e$ 的端点,$u,v$ 此时关于 $e$ 相邻。同时,若两条边 $i,j$ 有一个公共顶点 $u$,则称 $i,j$ 关于 $u$ 相邻。
图的分类
图有不同的分类规则,具体如下:
分类1
- 有向图: 如果图中顶点之间关系不仅仅是连通与不连通,而且区分两边的顶点的出入(存在出边和入边),则为有向图。
- 无向图: 如果图中顶点之间关系仅仅是连通与不连通,而不区分两边顶点的出入(不存在出边和入边),则为无向图。
单图
分类2
- 有环图: 单向遍历回可以到已遍历的点,比如有环链表
- 无环图: 单向遍历不能回到已遍历的点,比如树
分类3
- 带权图: 图的具有边带有关于该边信息的权值,比如地图中两点间距离
- 无权图: 图的每个边都不具有有关于该边信息的权值,其仅表示是否连通
其他
- 单图: 一个图如果任意两顶点之间只有一条边且边集中不含环,则称为单图
图的表示采用邻接矩阵和类似树的形式(顶点指针域是个指针数组)的形式,其具有以下特点:
- 无向图的邻接矩阵是对称矩阵
- 带权图的矩阵中元素为全职,无权图中用0/1分别表示不连通/连通
- 主对角线有不为零元素,该图一定有环
图的遍历
- 广度优先遍历: 广度优先遍历是连通图的一种遍历策略。因为它的思想是从一个顶点 $V_0$ 开始,辐射状地优先遍历其周围较广的区域。
- 深度优先遍历:
图的相关性质:
- $N$ 个顶点的连通图中边的条数至少为 $N-1$
- $N$ 个顶点的强连通图的边数至少有 $N$
- 广度优先遍历用来找无权图最短路径(有权图其实也行,多点东西呗)
简单数据结构的增删改查
操作 | 添加 | 删除 | 查找 | 使用条件 |
---|---|---|---|---|
数组 | $O(n)$ | $O(n)$ | $O(n)$ | 数定下标 |
链表 | $O(1)$ | $O(n)$ | $O(n)$ | 两端修改 |
变长数组 | $O(1)$ | $O(n)$ | $O(n)$ | 数不定下标 |
栈 | $O(1)$ | $O(1)$ | - | LIFO |
队列 | $O(1)$ | $O(1)$ | - | FIFO |
哈希表 | $O(1)$ | $O(1)$ | $O(1)$ | key操作,无序 |
树字典 | $O(log_2 n)$ | $O(log_2 n)$ | $O(log_2 n)$ | key操作,有序 |
哈希集合 | $O(1)$ | $O(1)$ | $O(1)$ | 唯一值,无序 |
树集合 | $O(log_2 n)$ | $O(log_2 n)$ | $O(log_2 n)$ | 唯一值,有序 |
算法
算法基本概念
- 算法的基本特征:可行性,确定性,有穷性
- 算法的基本要素:算法中对数据的运算和操作、算法的控制结构。
- 算法设计的基本方法:穷举法、动态规划、贪心法、回溯法、递推法、递归法、分治法、散列法,分支限界法。
- 算法设计的要求:正确性、可读性、健壮性、效率与低存储量需求
- 算法的基本结构:顺序、循环、选择
算法复杂度
- 算法的时间复杂度:指执行算法所需要的计算工作量(不代表算法实际需要时间)
- 算法的空间复杂度:执行这个算法所需要的额外内存空间(代表算法实际需要的空间)
复杂度表示方法: 使用大写 $O$ 表示:$O(n)$表示时间复杂度时指 $n$ 个数据处理完成使用 $n$ 个单位的时间;表示空间复杂度时指 $n$ 个数据处理完成使用了 $n$ 个单位的辅助空间。
字符串算法
字符串算法除了增删改查以外,还有很多匹配算法,比如最耳熟能详的 KMP 算法(不属于基础部分),这里整理一些相关算法的性质:
- 一个长为 n 的字符串有 $n(n+1)/2+1$ 个子串
- n的字符串,其中的字符各不相同,则S中的互异的非平凡子串有 $\frac{1}{2}n^2+\frac{1}{2}n-1$ 个
排序算法
排序算法实际上可以分为内排序和外排序:
- 内排序:在排序过程中,所有元素调到内存中进行的排序,称为内排序。内排序是排序的基础。内排序效率用比较次数来衡量。按所用策略不同,内排序又可分为插入排序、选择排序、堆排序、归并排序、冒泡排序、快速排序、希尔排序及基数排序等等。
- 外排序:在数据量大的情况下,只能分块排序,但块与块间不能保证有序。外排序用读/写外存的次数来衡量其效率。
排序算法时间复杂度
排序算法分为以下几类:
- 插入类排序:插入排序、希尔排序
- 交换类排序:冒泡排序、快速排序
- 选择类排序:选择排序、堆排序
- 归并排序
- 基数排序
算法 | 时间复杂度(最好) | 时间复杂度(最好) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 稳定 |
希尔排序 | $O(n^{1.3})$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 不稳定 |
选择排序 | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | 不稳定 |
堆排序 | $O(nlog_2 n)$ | $O(nlog_2 n)$ | $O(nlog_2 n)$ | $O(1)$ | 不稳定 |
冒泡排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 稳定 |
快速排序 | $O(nlog_2 n)$ | $O(nlog_2 n)$ | $O(n^2)$ | $O(nlog_2 n)$ | 不稳定 |
归并排序 | $O(nlog_2 n)$ | $O(nlog_2 n)$ | $O(nlog_2 n)$ | $O(n)$ | 稳定 |
基数排序 | $O(d(r+n))$ | $O(d(n+rd))$ | $O(d(r+n))$ | $O(n+rd)$ | 稳定 |
注:
- 基数排序的复杂度中,$r$ 代表关键字基数,$d$ 代表长度,$n$ 代表关键字个数
- 排序算法的稳定性指在原序列中,$r_i=r_j$,且 $r_i$ 在 $r_j$ 之前,而在排序后的序列中,$r_i$ 仍在 $r_j$ 之前,则称这种排序算法是稳定的;否则称为不稳定的。
查找算法
查找算法时间复杂度
算法 | 查找(最坏) | 插入(最坏) | 删除(最坏) | 查找(最好) | 插入(最好) | 删除(最好) | 是否要求有序 |
---|---|---|---|---|---|---|---|
顺序结构 | N | N | N | $\frac{N}{2}$ | N | $\frac{N}{2}$ | No |
二分算法 | logN | N | N | logN | $\frac{N}{2}$ | $\frac{N}{2}$ | Yes |
二叉查找树(BST) | N | N | N | 1.39logN | 1.39logN | $\sqrt{N}$ | Yes |
2-3树 | clogN | clogN | clogN | clogN | clogN | clogN | Yes |
红黑树 | 2logN | 2logN | 2logN | logN | logN | logN | Yes |
哈希散列查找 | logN | logN | logN | 3~5 | 3~5 | 3~5 | No |
哈希探针查找 | logN | logN | logN | 3~5 | 3~5 | 3~5 | No |
平均查找长度(ASL) = 查找表中第 $i$ 个元素概率($P_i$) $\times$ 找到第 $i$ 个元素时已经比较的次数($C_i$)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。