数据结构复习
绪论
数据元素
- 数据的基本单位
- 可以由若干数据项组成
- 数据项是具有独立含义的最小标识单位
数据的逻辑结构分类
- 线性结构
- 非线性结构:层次结构和群结构
- 无结构
数据的储存结构分类
- 顺序储存
- 链接储存
抽象数据类型三大特征
- 信息隐蔽
- 数据封装
- 使用与实现相分离
算法
-
算法 + 数据结构 = 程序
-
特征:输入、输出、确定性、有穷性、有效性
-
算法设计基本方法:穷举型、迭代法、递推法、递归法、动态规划法
-
语句频度: T ( n ) T(n) T(n);渐进时间复杂度: O ( n ) O(n) O(n).
线性表
特点
- 除第一个元素外,其他每一个元素有一个且仅有一个直接前趋。
- 除最后一个元素外,其他每一个元素有且仅有一个直接后继。
- 有逻辑上的顺序性,有唯一的首元素和尾元素。
- 每一表元素都是原子数据,不允许“表中套表”
顺序表
- 特点:元素的逻辑顺序和物理顺序一致。
- 查找成功的平均比较次数: A C N = n + 1 2 ACN = \frac{n+1}{2} ACN=2n+1; 查找不成功: n n n
- 插入时平均移动元素个数: A M N = n 2 AMN = \frac{n}{2} AMN=2n.
- 删除时平均移动元素个数: A M N = n − 1 2 AMN = \frac{n-1}{2} AMN=2n−1.
链表
- 分类:单链表、循环链表、双向链表
- 链表中第一个元素结点成为首元结点,最后一个结点成为尾结点。首元结点不是头结点。
- 结点的逻辑顺序与物理顺序可以不一致。
- 头结点位于表的最前端,本身不带数据,仅标志表头。
单链表
-
建立单链表
- 前插法:元素链接顺序与输入顺序相反。
- 后插法:元素链接顺序与输入顺序一致。后插法注意结尾要加上
rear->link = NULL;
-
插入元素
- 在首结点前插入元素:
newnode->link = first;
first = newnode;
- 在表中或表尾插入元素:
newnode->link = p->link;
p->link = newnode;
- 删除元素
- 删除首结点
q = first;
first = first->link;
free(q);
- 删除表中或表尾元素
//p是要删掉的结点的前一个结点
q = p->link;
p->link = q->link;
free(q);
- 头结点
- 头结点位于表的最前端,本身不带数据。
- 同一空表与非空表的操作。
- 简化链表操作实现。
循环链表
-
判断单链表为空的条件是:
first->link = first;
-
遍历循环单链表:
for(p = first->link; p != first; p = p->link)
-
对于带尾指针的循环链表:
- 在表尾插入节点:O(1).
- 在表尾删除结点要寻找前驱:O(n).
- 在表头插入结点:O(1).
- 在表头删除节点:O(1).
双向链表
- 寻找前驱节点和后继节点的时间复杂度都是O(1).
顺序表和链表额比较
- 空间分配:
- 顺序表的储存空间可以是静态分配的,也可以是动态分配的。
- 链表的储存空间是动态分配的。
- 储存密度:
- 数据存储量/所占存储总量。
- 顺序表的存储密度=1,链表密度 < 1。
- 存取方式:
- 顺序表可以随机存取,也可以顺序存取
- 链表只能顺序存取。
一元多项式
- n 阶一元多项式有 n+1 项。
- 用静态数组表示,但是不适用于稀疏多项式。
- 对于稀疏多项式,保留非零系数项以及它的指数。
- 可以用多项式的链表表示,每个节点三个域:coef, exp, link.
静态链表
-
为每一个元素附加链接指针。
-
不允许修改物理位置,通过修改链接来修改逻辑顺序。
-
每个节点存放 data 和 link。用数组定义,运算过程储存空间大小不会发生变化。
-
链表的头结点在 A[0],链接指针用数组下标表示。A[0].link 给出连边第一个节点的位置。
-
静态链表结点的 link 就是下一个元素的下标,没有的话就是-1。
-
判断是否非空:
A.elem[0].link == -1;
-
A.avail:当前可分配空间的首地址(下标)。
栈和队列
栈
概念
- 只允许在一端插入和删除的线性表。允许插入和删除的一段成为栈顶,另一端称为栈底。
- 特点:后进先出 (Last In First Out)。
顺序栈
- 初始化:
s.top = -1;
- 判断栈是否为空:
s.top == -1;
- 判断栈满:
s.top == s.maxSize - 1;
- push元素:
s.elem[++s.top] = x;
- pop元素:
x = s.elem[s.top]; s.top--;
双栈共享一个栈空间
- 一个数组空间 V[maxSize],栈顶指针数组 t[2],栈底指针数组 b[2].
- 初始:
t[0] = b[0] = -1; t[1] = b[1] = maxSize;
- 栈满条件:
t[0] + 1 == t[1];
- 栈空条件:
t[0] == b[0];
或t[1] == b[1];
链式栈
- 栈顶在头指针。
- 顺序栈有栈满问题,链式栈无栈满问题。这里的链式栈不带头结点。
- 判断栈空:
s == NULL;
p->data = x; p->link = S;
S = p;
- pop操作:
LinkNode* p = S;
x = p->data;
S = p->link;
栈的混洗
- 进栈元素编号为 1, 2, 3, …, n 时,出栈序列有 1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^{n} n+11C2nn
队列
特征
- 只允许在一段删除,另一端插入的线性表。允许删除的一端叫队头 (front),允许插入的一端叫队尾 (rear)。
- 先进先出 (First In First Out).
- 队列和栈的共性在于他们都是限制了存取位置的线性表;区别在于存取位置有所不同。
- 队满时再进队出现的溢出往往是假溢出。
进队出队方案
- **(课本方案)**先加元素再动指针:队尾指针指向实际队尾的后一位置,队头指针指示实际队头的位置。
- 先动指针再加元素:队尾指针指示实际队尾的位置;队头指针指示实际队头的前一位置。
循环队列
-
进队和出队时指针都是顺时针前进。
-
队头指针进1:
front = (front + 1) % queSize;
-
队尾指针进1:
rear = (rear + 1) % queSize;
-
队列初始化:
front = rear = 0;
-
队空条件:
front == rear;
-
队满条件:
(rear + 1) % queSize == front;
链式队列
-
不带头结点,队头在链头,队尾在链尾。
-
队空条件:
Q.front = NULL;
-
置空队列:
Q.front = Q.rear = NULL;
算术表达式三种形式
- 中缀表示
- 前缀表示
- 后缀表示
算术表达式求值
- 使用中缀表达式计算时,需要同时使用两个栈辅助求值。一个叫操作符栈OPTR( operator ),一个叫操作数栈OPND (operand).
- 使用后缀表达式求值,则需要使用一个栈;
递归
定义
- 对象是递归的
- 过程是递归的
需要用递归的情况
- 定义是递归的:阶乘,斐波那契
- 数据结构是递归的:单链表
- 问题的解法是递归的:汉诺塔
递归过程
- 主程序第一次调用递归的过程成为外部调用,递归自己调用递归成为内部调用,返回调用过程的地址不同
- 每次调用必须记下返回上层什么地方的地址。
- 递归工作栈。
- 尾递归和单项递归可以直接用迭代实现其非递归过程,其他情形必须借助栈实现非递归过程。
- 尾递归只有一个递归语句;单项递归可能有多个分支,可以利用之前保存的结果(斐波那契数列)。
双端队列
- 可以用数学归纳法证明,全进全出后可能的出队顺序有 n ! n! n! 种。
- 输入受限的双端队列,输出受限的双端队列。
优先队列
- 数字越小,优先级越高。
- 每次在优先队列中插入新元素时,新元素总是插入在队尾。
- 删除元素时,先从队列中查找权值最小的元素删除,再把队列中最后的元素填补到被删元素的位置。
字符串
字符串相关概念
- 空串的长度为0,空白串是由空白字符组成。
- 子串:串中任意个连续字符组成的子序列
- 主串:包含子串的串。
- 子串在主串中的位置:通常将子串在主串中首次出现时,该子串首字符对应的主串中的下标(从0开始)。
- 特别地,空串是任意串的子串,任意串是其自身的子串
- 通常在程序中使用的串可分为两种:串变量和串常量。
在C中常用的字符串操作
- 单个字符串的输入函数 gets (str)
- 字符串输出函数 puts (str)
- 字符串求长度函数 strlen (str)
- 字符串连接函数 strcat (str1, str2)
- 字符串比较函数 strcmp (str1, str2)
储存
- 定长顺序存储表示:顺序串。
- 堆分配存储表示:存储空间是动态分配的。
- 块链存储表示:单链表
- 链表的每个结点可以存储 1 个字符,称其“块的大小”为 1;也可以存储 n 个字符,称其“块的大小”为 n。
- 存储密度:该串串值占用存储空间的大小 / 为该串分配存储空间的大小。
- 节点大小为4时,存储利用率高,操作复杂;结点大小为1时,存储利用率低,操作简单,可直接存取字符。
- 块链存储一般带头结点,设置头尾指针。
字符串模式匹配
- 定义:在字符串中寻找子串(第一个字符)在串中的位置
- 模式串:子串;
- 目标串:主串。
- 两种字符串匹配算法:BF算法(朴素的模式匹配算法),KMP(无回溯的模式匹配算法)
BF
- 若设 n 为目标 T 的长度,m 为模式 P 的长度
- 匹配算法最多比较 n-m+1趟
- 若每趟比较都比较到模式 P 尾部才出现不等,要做 m 次比较,则在最坏情况下,总比较次数 (n-m+1)*m。
- 在多数场合下 m 远小于 n,因此,算法的运行时间为O(n*m)。
- 低效的原因在于每趟重新比较时,目标 T 的检测指针要回退。
多维数组和广义表
数组
- 一维数组的数组元素为不可再分割的单元素时,是线性结构;但它的数组元素是数组时,是多维数组,是非线性结构。
- 数组是相同类型的数据元素的集合,一维数组的每个数组元素是一个序对,由下标(index)和值(value)组成。
多维数组
- 多维数组的特点是每一个数据元素可以有多个直接前驱和多个直接后继。
- 数组元素的下标一般具有固定的下界和上界
- 静态定义的数组,在编译时静态分配存储空间,一旦数组空间用完则不能扩充
- 动态定义的数组,动态分配语句 malloc 分配存储空间和初始化,在撤销数组时通过 free 语句动态释放。
特殊矩阵的压缩存储
- 对称矩阵
- 若 i < j i<j i<j,数组元素 A [ i ] [ j ] A[i][j] A[i][j] 在矩阵的上三角部分, 在数组 B 中没有存放,可以找它的对称元素 $A[j][i] = j *( j +1 ) / 2 + i $
- 开始看清时 i ≥ j i \ge j i≥j 还是 i < j i < j i<j.
- 三对角矩阵
- 总共有 3 n − 2 3n-2 3n−2 个非零元素。
- 在一维数组 B 中 A [ i ] [ j ] A[i][j] A[i][j] 在第 i i i 行,它前面有 3 ∗ i − 1 3*i-1 3∗i−1 个非零元素, 在本行中第 j 列前面有 j − i + 1 j-i+1 j−i+1 个,所以元素 A [ i ] [ j ] A[i][j] A[i][j] 在 B 中位置为 k = 2 ∗ i + j k = 2*i + j k=2∗i+j。
- w对角矩阵
- 又称为带状矩阵
稀疏矩阵
-
设矩阵 A 中有 s 个非零元素。令 e = s/(m*n),称 e 为矩阵的稀疏因子。
-
e ≤ 0.05 e≤0.05 e≤0.05 时称之为稀疏矩阵。
-
每一个三元组 ( i , j , a i j ) (i, j, a_{ij}) (i,j,aij) 唯一确定了矩阵A的一个非零元素。因此,稀疏矩阵可由表示非零元素的一系列三元组及其行列数唯一确定。
稀疏矩阵的存储
- 顺序存储表示
- 带行指针数组的二元数组
- 链接表示(采用十字链表)
- 快速转置算法
- 普通的转置方法:对于每一个列,在全体元素中找符合的,时间复杂度为O(col*terms)
- 快速转置方法:确定每一个列在转置后的矩阵中的初始位置。首先通过对元素的循环确定位置。再对列循环计算初始位置。最后对元素循环。时间复杂度为O(max(col,terms))
- 十字链表处注意第 i’ 行和第 i 列使用的是同一个头结点。所以头结点一共n个
- 十字链表元素具有6个部分。判断是否为元素的head,行数,列数,向下的指针域,向右的指针域,还有自身的数据值。
广义表
- L S ( a 1 , a 2 , a 3 , … , a n ) LS (a_1, a_2, a_3, …, a_n) LS(a1,a2,a3,…,an)
- 表元素,可以是表(称为子表),可以是单元素(称为原子,不可再分)
- n > 0时,表的第一个表元素称为广义表 的表头(head),除此之外,其它表元素组成的表称为广义表的表尾(tail)
广义表的存储表示
- 头尾表示法
- 有两种节点:表结点,单元素节点
- 每个节点有一个标志域tag. tag = 0 表示该结点时单元素结点,tag = 1 表示该结点是表结点。
- 空表没有节点,指向空表的头指针为空。
- hlink指针指向表头元素结点,tlink指向表尾(该表尾肯定还是表)
- 扩展线性链表表示
- 两种结点:原子结点和表结点
- tag = 0,value 存放元素的值,tlink 存放指向同一层下一表元素结点的指针; 表结点的标志 tag = 1,hlink 存放指向子表的指针,tlink指向表尾(该表尾肯定还是表)
- 优点:每个广义表都有一个起到“头结点”作用的表结点,即使空表,也有一个表结点。
- 缺点:表头元素结点插入或删除时较为困难
- 层次表示
- 原子结点、子表结点和头结点(非表头元素结点)
- 结点类型 tag:
= 0, 表头结点;= 1, 原子结点;= 2, 子表结点 - 信息 info:tag = 0 时, 存放引用计数(ref);tag = 1 时, 存放数据值(value);tag = 2 时, 存放指向子表头结点的指针(hlink)
- 尾指针tlink:tag = 0 时, 指向该表第一个结点;tag ≠ \ne = 0 时, 指向同一层下一个结点
树与二叉树
树和森林概念
- 两种树:自由树( n > 0 n>0 n>0),有根树( n ≥ 0 n\ge0 n≥0). n = 0 n=0 n=0,T成为空树;否则为非空树。
- r 是一个特定的称为根 (root) 的结点,它只有直接后继,但没有直接前驱
- 树是分层结构,又是递归结构。每棵子树的根结点有且仅有一个直接前趋,但可以有 0 个或多个直接后继。
- 结点的度(degree):结点所拥有的子树棵数。叶结点(leaf):度为0的结点,又称终端结点
- 兄弟(sibling):同一双亲的子女互称为兄弟
- 子孙(descendant):某一结点的子女,以及这些子女的子女都是该结点的子孙。
- 结点的深度(depth):结点所处层次,即从根到该结点的路径上的分支数加一。根结点在第1层。
- 结点的深度和结点的高度是不同的。结点的深度即结点所处层次,是从根向下逐层计算的;结点的高度是从下向上逐层计算的:叶结点的高度为1, 其他结点的高度是取它的所有子女结点最大高度加一。树的深度与高度相等。
- 树的度(degree):树中结点的度的最大值。
- 有序树(ordered tree):树中根结点的各棵子树是有次序的
- 删去一棵非空树的根结点,树就变成森林(不排除空的森林);反之,若增加一个根结点,让森林中每一棵树的根结点都变成它的子女,森林就成为一棵树。
二叉树概念
- 若二叉树的层次从 1 开始, 则在二叉树的第 i i i 层最多有 2 i − 1 2^{i-1} 2i−1 个结点。( i ≥ 1 i≥1 i≥1)
- 高度为 h 的二叉树最多有 2 h − 1 2^{h -1} 2h−1个结点。(h≥1)
- 空树的高度为0,只有根节点的树的高度为1.
- 对任何一棵二叉树, 如果其叶结点有 n 0 n_0 n0 个, 度为2的非叶结点有 n 2 n_2 n2 个, 则有: n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
- 完全二叉树:第 h 层从右向左连续缺若干结点
- 具有 n (n≥0) 个结点的完全二叉树的高度为: ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1) \rceil ⌈log2(n+1)⌉
- 设完全二叉树中叶结点有 n 0 n_0 n0 个, 则该二叉树总的结点数为 n = 2 n 0 n = 2n_0 n=2n0, 或 n = 2 n 0 – 1 n = 2n_0 –1 n=2n0–1。
- 若完全二叉树的结点数为奇数,没有度为 1 的结点;为偶数,有一个度为1的结点。
二叉树的存储
- 顺序存储表示
- 二叉链表表示
- 使用二叉链表,找子女的时间复杂度为O(1),找双亲的时间复杂度为 O ( l o g 2 i ) ~ O ( i ) O(log_2i)~O(i) O(log2i)~O(i),其中, i i i 是该结点编号。
- 三叉链表表示
- 使用三叉链表,找子女、双亲的时间都是O(1)。
二叉树的遍历
-
交换二叉树所有分支的左右子女:可以先序遍历,也可以后序遍历,但不可以中序遍历
-
用括号的方式输出二叉树:
- 无论先序、中序、后序遍历,都可以归属为一种单一的设计模式,叫做欧拉巡回遍历,基于它可以很容易地写出通用的非递归算法。
- 二叉树的层次序遍历
二叉树的计数
- 有二叉树的先序序列和中序序列可以唯一地确定一棵二叉树
- 如果先序序列固定不变,给出不同的中序序列,可得到不同的二叉树: C 2 n n n + 1 \frac{C_{2n}^{n}}{n+1} n+1C2nn
线索二叉树
- 又称为穿线树
- ltag (或rtag) = 0,表示相应指针指示左子女(或右子女结点);
- ltag (或rtag) = 1, 表示相应指针为前趋(或后继)线索。
树与森林
树的存储表示
- 双亲表示:使用数组存放,根结点双亲为-1
- 子女链表表示:子女依次链接的是同一级的子女。没有子女的话指向子女的指针为空
- 子女指针表示:每个结点指向子女指针数量为树的度
- 广义表表示:子表的头结点是子树的根。所以根结点应当在括号之外。这里的表头指的是根结点的第一个孩子
- 子女-兄弟链表表示。一个指针指向第一个子女,一个指向下一个兄弟(判断叶子节点:一个结点的lchild为空)。寻找指定结点 *p 的第一个子女、下一个兄弟的操作很简单,时间复杂度为O(1),但寻找双亲操作则比较复杂。
树的遍历
- 深度优先遍历:先根次序遍历,后根次序遍历
//先根次序遍历
print(u);
for(u->v):
visit(v);
//后根次序遍历
for(u->v):
visit(v);
print(u);
- 广度优先遍历
树与二叉树的应用
哈夫曼树
概念
- 树的路径长度是各结点到根结点的路径长度之和 PL.
- 带权路径长度:二叉树的带权路径长度是各叶结点所带权值 w i w_i wi 与该结点到根的路径长度 l i l_i li 的乘积的和。 W P L = ∑ i = 0 n − 1 w i ∗ l i WPL=\sum\limits_{i=0}^{n-1}w_i*l_i WPL=i=0∑n−1wi∗li
- 带权路径长度达到最小的二叉树即为哈夫曼树。
- 在哈夫曼树中,权值大的结点离根最近。
- Huffman树的叶结点又称为外结点,分支结点又称为内结点。
- Huffman树是严格二叉树。有 n 个外结点,就有 n-1 个内结点,表示需要构造 n-1 次二叉树。树中总结点数为2n-1。
- 构造出来的Huffman树可能不惟一,因为选根节点的时候没有规定谁是左子树,谁是右子树。
- 哈夫曼的最小带权路径是唯一的。
应用
- 最佳判定树:利用Huffman树,可以在构造判定树(决策树)时让平均判定(比较)次数达到最小。
堆
- 小根堆: K i ≤ K 2 i + 1 K_i \le K_{2i+1} Ki≤K2i+1 && $K_i \le K_{2i+2} $
- 大根堆: K i ≥ K 2 i + 1 K_i \ge K_{2i+1} Ki≥K2i+1 && K i ≥ K 2 i + 2 K_i \ge K_{2i+2} Ki≥K2i+2.
- 左儿子:2i+1;右儿子:2i+2;父结点:(i-1)/2;
- 小根堆的建立
void creatMinHeap ( minHeap& H, HElemType arr[], int n ) {
//将一个数组从局部到整体,自下向上调整为小根堆
for ( int i = 0; i < n; i++ ) H.elem[i] = arr[i]; //复制
H.curSize = n;
for ( i = (H.curSize-2)/2; i >= 0; i-- )
//自底向上逐步扩大小根堆
siftDown ( H, i, H.curSize-1 );
}
- 向下筛选
void siftDown ( minHeap& H, int i, int m ) {
//从结点i开始到m为止, 自上向下比较, 将一个集合局
//部调整为小根堆
HElemType temp = H.elem[i];
for ( int j = 2*i+1; j <= m; j = 2*j+1 ) {
if ( j < m && H.elem[j] > H.elem[j+1] ) j++;
if ( temp <= H.elem[j] ) break; //小则不做调整
else { H.elem[i] = H.elem[j]; i = j; } //小者上移
}
H.elem[i] = temp; //回放temp中暂存的元素
}
3.插入算法
bool Insert ( minHeap& H, HElemType x ) {
//将x插入到小根堆中并从新调整形成新的小根堆
if ( H.curSize == heapSize ) return false; //堆满,返回插入不成功信息
H.elem[H.curSize] = x; //插入到最后
siftUp ( H, H.curSize ); //从下向上调整
H.curSize++; //堆计数加1
return true;
}
- 向上筛选
void siftUp ( minHeap& H, int start ) {
//从结点start开始到结点0为止, 自下向上比较, 将集
//合重新调整为堆。
HElemType temp = H.elem[start];
int j = start, i = (j-1)/2;
while ( j > 0 ) { //沿双亲路径向上直达根
if ( H.elem[i] <= temp ) break; //双亲值小
else { H.elem[j] = H.elem[i]; j = i; i = (i-1)/2; }
} //双亲的值下降,j与i的位置上升
H.elem[j] = temp; //回送
}
- 删除算法
bool Remove ( minHeap& H, HElemType& x ) {
//从小根堆中删除堆顶元素并通过引用参数 x 返回
if ( H.curSize == 0 ) return false; //堆空, 返回
x = H.elem[0]; //返回最小元素
H.elem[0] = H.elem[H.curSize-1]; //最后元素填补到根结点
H.curSize--;
siftDown ( H, 0, H.curSize-1 ); //从新调整为堆
return true;
}
二叉查找树
性质
-
每个结点都有一个作为查找依据的关键码(key),所有结点的关键码互不相同。
-
左子树(如果非空)上所有结点的关键码都小于根结点的关键码。
-
右子树(如果非空)上所有结点的关键码都大于根结点的关键码。
-
左子树和右子树也是二叉查找树。
-
如果对一棵二叉查找树进行中序遍历,可以按从小到大的顺序将各结点关键码排列起来。
-
可用判定树描述查找过程。内结点是树中原有结点,外结点是失败结点,代表树中没有的数据
-
查找的关键码比较次数最多不超过树的高度。
插入算法
- 都要从根结点出发查找插入位置,然后把新结点作为叶结点插入。
- 为了向二叉查找树中插入一个新元素,必须先检查这个元素是否在树中已经存在。
删除算法
- 被删结点的右子树为空,可以拿它的左子女结点顶替它的位置,再释放它。
- 被删结点的左子树为空,可以拿它的右子女结点顶替它的位置,再释放它。
- 被删结点的左、右子树都不为空,可以在它,的右子树中寻找中序下的第一个结点。用它的值填补到被删结点中,再来处理这个结点的删除问题。
- 对于有 n 个关键码的集合,其关键码有 n! 种不同排列,可构成不同二叉查找树有 C 2 n n n + 1 . \frac{C_{2n}^{n}}{n+1}. n+1C2nn.
查找
- 查找成功平均查找长度 A S L s u c c = ∑ i = 0 n − 1 p [ i ] ∗ l [ i ] ASL_{succ} = \sum\limits_{i=0}^{n-1}p[i]*l[i] ASLsucc=i=0∑n−1p[i]∗l[i],其中 p [ i ] p[i] p[i] 是查找概率, l [ i ] l[i] l[i] 是该结点所在层数(也叫关键码比较次数)。
- 查找成功平均查找长度 A S L u n s u c c = ∑ i = 0 n − 1 p [ i ] ∗ ( l [ i ] − 1 ) . ASL_{unsucc} = \sum\limits_{i=0}^{n-1}p[i]*(l[i]-1). ASLunsucc=i=0∑n−1p[i]∗(l[i]−1).
- 一般把平均查找长度达到最小的判定树称作最优二叉查找树。
- 在相等查找概率的情形下,最优二叉查找树的上面 h-1(h是高度)层都是满的,只有第 h 层不满。如果结点集中在该层的左边,则它是完全二叉树;如果结点散落在该层各个地方,则有人称之为理想平衡树。
AVL
定义
- 空树
- 左右子树都是AVL
- 左右子树高度差不超过1.
平衡因子
- 每个结点附加一个数字,给出该结点左子树的高度减去右子树的高度所得的高度差,这个数字即为结点的平衡因子 bf(balance factor)。
- AVL树任一结点平衡因子只能取 -1, 0, 1。
平衡化旋转
- 单旋转(LL旋转和RR旋转)
- 双旋转(LR旋转和RL旋转)
- 在插入一个新结点后,需要从插入位置沿通向根的路径回溯,检查各结点的平衡因子。如果在某一结点发现高度不平衡,停止回溯。从发生不平衡的结点起,沿刚才回溯的路径取直接下两层的结点。
- 如果这三个结点处于一条直线上,则采用单旋转进行平衡化。
- 如果这三个结点处于一条折线上,则采用双旋转进行平衡化。
AVL树的高度
- N h = F h + 2 − 1 N_h=F_{h+2}-1 Nh=Fh+2−1, h ≥ 1 h \ge 1 h≥1.
- 如果高度 h 固定,最少节点数为 N h N_h Nh,最多节点数为 2 h − 1 2^h-1 2h−1.
- 若节点数n固定,最小高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1) \rceil ⌈log2(n+1)⌉,最大高度不超过 1.44 ∗ l o g 2 ( n + 2 ) 1.44*log_2(n+2) 1.44∗log2(n+2)
并查集
支持以下三种操作
- 合并操作 Union
- 查找操作 Find
- 初始化 init
改进方法
- 按照树的结点个数合并
- 按照树的高度合并(即按秩合并)
- 压缩元素的路径长度
八皇后问题
- 采用的是递归和回溯的方法
图
概念
- 对于n个结点的完全图,如果是有向图,则有 n ∗ ( n − 1 ) n *(n-1) n∗(n−1) 条边;若是无向图,则有 n ∗ ( n + 1 ) 2 \frac{n*(n+1)}{2} 2n∗(n+1)
- 路径:路径上顶点互不重复。
- 强连通:从任意顶点出发可以到达任意顶点。
存储
- 邻接矩阵
- 有向图中,统计第 i 行1的个数可得顶点 i 的出度,统计第 j 列1的个数可得顶点 j 的入度
- 无向图中,统计第 i 行1的个数可得顶点 i 的度。
- 若一个图有n个点,e条边,则无向图邻接矩阵有2e个非零元素,而有向图有e个非零元素。
- 邻接矩阵适用于稠密图。
- 对于一个图,邻接矩阵表示是唯一的。
- 邻接表
- 出边表、入边表(逆邻接表)
- 同一个顶点发出的边链接在同一个边链表之中。
- 对于一个图,由于各边链入顺序不同,邻接表表示是不唯一的。它适用于稀疏图。
- 一个图有n个顶点,e条边,用邻接表储存。对于无向图,需要n个顶点结点,2e个边结点;对于有向图,需要n个顶点结点,e个边节点。
- 邻接多重表
- 使用邻接多重表,每一条边仅被表示一次
- 无向图边结点的结构:其中, mark 是处理标记; vertex1和vertex2是该边两顶点位置。Path1 指向下一条依附 vertex1的边;path2 指向下一条依附 vertex2 的边。
- 顶点结构:data 存放与该顶点相关的信息,Firstout 是指示第一条依附该顶点的边的指针。
- 有向图边结点:mark 是处理标记;vertex1 和 vertex2指明该有向边始顶点和终顶点的位置。nextout指向同一顶点发出的下一条边的边结点;nextin 指向进入同一顶点的下一条边的边结点。
- 顶点结点的结构:数据成员 data 存放与该顶点相关的信息,指针 Firstout 指示以该顶点为始顶点的出边表的第一条边,Firstin 指示以该顶点为终顶点的入边表的第一条边。
图的遍历
- 又叫“周游”。
- 为了避免重复访问,可以设置一个标志顶点是否被访问过的辅助数组 visited[].
- 图的遍历分类:深度优先遍历,广度优先遍历;
- 深度优先生成树;广度优先生成树
- 对强连通遍历得深度优先生成树;对非强连通图遍历得深度优先生成森林。
Kosaraju算法
- 首先对图进行一次DFS,在回退时记录结点回溯的顺序
- 把图所有的有向边逆转
- 按照回溯节点编号从大到小的顺序再进行一次DFS,得到的深度优先生成森林(树)记为强连通分量的划分。
双连通分量
- 关节点:删去某个点可以将原来的图分割成两个或两个以上的连通分量
- 没有关节点的连通图叫做双连通图
最小生成树
- 用不同的遍历方法,可能得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树
特点
- 必须使用带权图的n-1条边来连接网络中的n个结点
- 不能使用产生回路的边
- 各边上的权值总和达到最小
Kruskal算法和Prim算法
- Kruskal算法的结点结构:v1, v2, key;Prim算法是采用邻接矩阵作为带权图的存储表示。
- Kruskal算法通过选边来构造最小生成树;Prim算法通过选点来构造最小生成树
- Prim算法仅适用于边稠密的带权图;Kruskal算法对于边稀疏和边稠密的带权图都适用
- Prim算法: O ( n 2 ) O(n^2) O(n2)
- Kruskal算法: O ( e ∗ l o g 2 e ) O(e*log_2e) O(e∗log2e)
- 其他求最小生成树的算法:破圈法,dijkstra算法
最短路径
问题解法
-
边上权值非负的单源最短路径问题:Dijkstra算法(邻接矩阵 O ( n 2 ) O(n^2) O(n2))
-
边上权值任意的单源最短路径问题:Bellman和Ford算法
-
所有顶点之间的最短路径:Floyd算法
-
非带权图的最短路径算法:广度优先搜索(时间复杂度:邻接表 O ( n + e ) O(n+e) O(n+e),邻接矩阵 O ( n 2 ) O(n^2) O(n2);空间复杂度 O ( n ) O(n) O(n)).
活动网络
AOV网络 (Activity on vertices)
- AOV网络不能出现有向回路,即有向环;对于一个给定的AOV网络,必须先判断是否存在有向环。
- 检测有向环的一种方法是对AOV网络构造它的拓扑有序序列
- 如果通过拓扑排序可以把所有顶点排在一个拓扑序列之中,则网络必然不会出现有向环。
AOE (Activity on edges)
- 用边上权值表示活动持续时间,用顶点表示事件
- 完成整个工程所需时间取决于从源点到汇点的最长路径的长度。这条路径叫做关键路径。
- 事件 v i v_i vi 最早可能开始时间 V e [ i ] Ve[i] Ve[i]:从源点 v 0 v_0 v0 到顶点 v i v_i vi的最长路径长度。
- 事件 v i v_i vi 的最迟允许开始时间 V l [ i ] Vl[i] Vl[i]:等于 V e [ n − 1 ] Ve[n-1] Ve[n−1] 减去从 v i v_i vi 到 v n − 1 v_{n-1} vn−1 的最长路径长度。
- 活动 a k a_k ak 最早开始时间 A e [ k ] Ae[k] Ae[k]:设活动 a k a_k ak 在有向边 < v i , v j > <v_i, v_j> <vi,vj> 上,则 A e [ k ] Ae[k] Ae[k] 是从源点 v 0 v_0 v0 到顶点 v i v_i vi 的最长路径长度。即 A e [ k ] = V e [ i ] Ae[k] = Ve[i] Ae[k]=Ve[i].
- 活动 a k a_k ak 最迟允许开始时间 A l [ k ] Al[k] Al[k]: A l [ k ] = V l [ j ] − d u r ( < v i , v j > ) Al[k] = Vl[j]-dur(<v_i,v_j>) Al[k]=Vl[j]−dur(<vi,vj>),其中 d u r ( < v i , v j > ) dur(<v_i,v_j>) dur(<vi,vj>) 是完成 a k a_k ak 所需的时间。
- 松弛时间: A l [ k ] − A e [ k ] Al[k]-Ae[k] Al[k]−Ae[k]. A l [ k ] = A e [ k ] Al[k]=Ae[k] Al[k]=Ae[k] 的 a k a_k ak 叫做关键活动,没有时间余量。
查找
线性表和链表
顺序表
- A S L s u c c = n + 1 2 ASL_{succ} = \frac{n+1}{2} ASLsucc=2n+1.
- A S L u n s u c c = n + 1 ASL_{unsucc}=n+1 ASLunsucc=n+1
折半查找
- 若设 n = 2 h − 1 n=2^h-1 n=2h−1,则判定树是高度为 h h h 的满二叉树。
- n < 2 h − 1 n < 2^h-1 n<2h−1 时,它对应的判定树是一棵理想平衡树,其高度是 h = ⌈ l o g 2 ( n + 1 ) ⌉ h=\lceil log_2(n+1)\rceil h=⌈log2(n+1)⌉.
跳表
- O ( l o g 2 n ) O(log_2n) O(log2n).
索引结构和B树
- 当数据元素很多,可采用索引方法来实现存储和查找
- 线性索引:稠密索引,稀疏索引
B树
- 是一个m阶查找树,要么是空树,要么满足:
- 除失败节点,所有结点最多有 m 棵子树
- 根节点至少有 2 个子女
- 除根节点和失败节点外,所有节点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉ 个子女
- 所有失败节点都位于同一层,是虚结点。指向这些结点的指针为空。
- m阶B树: n ≤ m h − 1 n \le m^h -1 n≤mh−1.
B树的插入
- 除失败节点外,每个节点的关键码个数在 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] . [\lceil m/2\rceil - 1, m-1]. [⌈m/2⌉−1,m−1].
B+树
- 所有关键码都存放在叶节点中,上层的非叶结点的关键码是其子树中最大关键码的复写
- 叶结点包含了全部关键码及指向相应数据记录存放地址的指针,且叶结点本身按关键码从小到大顺序链接
- 每个结点最多有 m 棵子树
散列
- 使每个关键码与结构中唯一存储位置相对应
- 转换函数叫散列函数,表或结构称作散列表。
- 同义词:产生冲突的散列地址相同的不同关键码为同义词。
- 散列表允许有m个地址时,其值域需要在 0 到 m - 1 之间
- 散列函数:直接定址法,除留余数法,数字分析法
- 处理冲突:
- 开地址法:线性探测、二次探测、双散列法
- 链地址法
- 链地址法优于开地址法;除留余数法优于其他类型的散列函数
排序
概述
总体变化趋势
- 有序区增长:插入排序、选择排序、起泡排序、堆排序、归并排序
- 有序程度增长:快速排序、希尔排序、基数排序
- KCN:排序码比较次数;RMN:记录移动次数
十种排序
-
锦标赛排序
- 稳定
- 时间复杂度: O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n),空间复杂度: O ( n ) O(n) O(n).
-
基数排序
- 最高位优先:桶排序或箱排序
- 最低位优先:分配排序
-
归并排序
- 运行时间不依赖待排序元素序列的初始排列
-
快速排序
子树
2. 根节点至少有 2 个子女
3. 除根节点和失败节点外,所有节点至少有
⌈
m
/
2
⌉
\lceil m/2 \rceil
⌈m/2⌉ 个子女
4. 所有失败节点都位于同一层,是虚结点。指向这些结点的指针为空。
- m阶B树: n ≤ m h − 1 n \le m^h -1 n≤mh−1.
B树的插入
- 除失败节点外,每个节点的关键码个数在 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] . [\lceil m/2\rceil - 1, m-1]. [⌈m/2⌉−1,m−1].
B+树
- 所有关键码都存放在叶节点中,上层的非叶结点的关键码是其子树中最大关键码的复写
- 叶结点包含了全部关键码及指向相应数据记录存放地址的指针,且叶结点本身按关键码从小到大顺序链接
- 每个结点最多有 m 棵子树
散列
- 使每个关键码与结构中唯一存储位置相对应
- 转换函数叫散列函数,表或结构称作散列表。
- 同义词:产生冲突的散列地址相同的不同关键码为同义词。
- 散列表允许有m个地址时,其值域需要在 0 到 m - 1 之间
- 散列函数:直接定址法,除留余数法,数字分析法
- 处理冲突:
- 开地址法:线性探测、二次探测、双散列法
- 链地址法
- 链地址法优于开地址法;除留余数法优于其他类型的散列函数
排序
概述
总体变化趋势
- 有序区增长:插入排序、选择排序、起泡排序、堆排序、归并排序
- 有序程度增长:快速排序、希尔排序、基数排序
- KCN:排序码比较次数;RMN:记录移动次数
十种排序
[外链图片转存中…(img-TknNtTYo-1610800753098)]
-
锦标赛排序
- 稳定
- 时间复杂度: O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n),空间复杂度: O ( n ) O(n) O(n).
-
基数排序
- 最高位优先:桶排序或箱排序
- 最低位优先:分配排序
-
归并排序
- 运行时间不依赖待排序元素序列的初始排列
-
快速排序
- 代排元素是乱序时,排序效率最高;待排元素有序时,排序效率最低。