[408] NOTES on DataStructure
1 绪论
- 快慢指针
Type foo(Node *head) {
Node *slow, *fast;
slow = fast = head;
while (fast->next) {
slow = slow->next;
fast = fast->next;
if (fast->next) fast = fast->next;
}
...
}
2 线性表
-
顺序表
- 插入元素平均移动次数:sum p(n-i+1) = n/2
- i位置插入元素,后移的元素个数:n-i+1
- 删除元素平均移动次数:sum p(n-i) = (n-1)/2
- i位置删除元素,前移的元素个数:n-i
- 顺序查找比较次数:sum p*i = (n+1)/2
-
链表
- 头插法:生成的链表结点次序与输入数据的顺序相反,可用于逆置链表
- 循环单链表判空:头指针的指针指向自身
- 静态链表:借助数组实现,next==-1标识链表结束
-
常见考法
- 将两个有n个元素的有序表归并的最少比较次数、最多比较次数?
- 最少:固定一张表不改变,另一张表移动完:n-1
- 最多:交替移动,2n-1
- 将两个有n个元素的有序表归并的最少比较次数、最多比较次数?
3 栈和队列、压缩存储
-
栈:
- n个元素入栈,出栈元素的排列个数:卡特兰数(C(n, 2n) / (1+n))
- 应用:
- 括号匹配
- 表达式求值
- 求解二叉树不同形状个数
- 一定的先序+中序遍历可确定一棵二叉树
- 先序:根左右,看作入栈顺序
- 中序:左根右,看作出栈顺序
- 给定N个元素的先序,相当于给定入栈顺序,出栈顺序为卡塔兰数
- 卡塔兰数:C(N, 2N) / (N+1)
-
队列:
- 链队
- 出队操作:可能头、尾指针都要修改(出队到空,尾指针指向NULL)
- 队空:front == NULL,rear == NULL
- 非空情况:rear指向链尾
- 循环队列:牺牲一个位置用于判断是队空还是队满
- 队空:front == rear
- 队满:(rear+1) % N == front
- 元素个数:(rear - front + N) % N
- 双端队列
- 队列输出顺序:1234为双端队列输入
- 输入受限、不能输出受限:4132
- 不能输入受限、输出受限:4213
- 不能输入受限、不能输出受限:4231
- 队列输出顺序:1234为双端队列输入
- 链队
-
对称矩阵:只存放主对角线和下三角区的元素
即各行存放个数:1+2+3+…+n = n(n+1)/2 -
三角矩阵:只存放主对角线和上/下三角区的元素以及另一三角区的常量
即各行存放个数:1+2+3+…+n+1(常量) = n(n+1)/2 + 1 -
三对角矩阵:非零系数在三条对角线上(主对角线、高、低对角线)
4 树与二叉树
-
二叉树
- 二叉树与度为2的树:
- 度为2的树至少有3个结点、二叉树可以为空或只有1个孩子
- 二叉树无论孩子个数都有左右之分
- 满二叉树
- 高度为h,结点数为2^h-1个的二叉树
- 若二叉树采用顺序存储,则要按照满二叉树的结点数分配存储空间
- 完全二叉树
- 每个结点都与相同高度的满二叉树编号一一对应的二叉树
- 叶子结点可以出现在最后一层以及倒数第二层
- 度为1的结点至多1个,而且只有左孩子
- 二叉排序树 BST:中序序列为有序序列
- 删除结点
- 叶结点直接删除
- 只有一棵子树,让该子树代替被删除结点
- 有两棵子树,让其中序第一个子女替换,转换为删除那个结点
- 查找效率:O(logN)
- 删除结点
- 霍夫曼树:带权路径最短的二叉树
- 带权路径:各个叶子结点(高度*权值)之和
- 对N个结点编码
- 新建了N-1个结点
- 共2N-1个结点
- 不存在度为1的结点
- 沿着霍夫曼树的边得到的是前缀编码
- 扩展到N叉树情况:补虚拟“0“叶子结点,补齐N整数倍个结点
- 平衡二叉树:由二叉排序树发展而来
不平衡情况调整:- 单旋转:LL旋转、RR旋转
- 双旋转:LR旋转、RL旋转
注意被调至根处的结点原先子树相对左右位置不变
- 线索二叉树
按某一遍历顺序的到序列,对每一结点:- 无左子树,左指针指向前驱结点
- 无右子树,右指针指向后继结点
- 无前驱或后继则指向NULL
- 二叉树与度为2的树:
-
二叉树遍历与树
- 二叉树遍历:确定唯一的二叉树
- 先序和中序
- 后序和中序
- 层次遍历和中序
- 层次遍历和后序
- 先序与后序不能确定唯一的树,但能确定结点之间的祖先关系:
对两个结点X、Y,若先序中相对顺序为XY,后序中为YX,则X是Y祖先
- 树的后根遍历对应森林的中序(也有称后序)遍历、二叉树的中序遍历
- 树转换为二叉树
- 每个结点左指针指向第一个孩子,右结点指向右兄弟
- 图示即,兄弟之间连线,每个结点只保留和第一个孩子的连线
- 森林转换为二叉树
- 先把每棵树转换为二叉树
- 每棵树根结点视为兄弟关系,以第一棵树根结点为根
- 二叉树转换为森林
- 断开根结点与右子树连接,得到两棵二叉树
- 第一棵为原森林第一棵树,接下来递归进行第一步
- 二叉树遍历:确定唯一的二叉树
-
常用结论
- 树的度是结点数减1
- n个结点的二叉链表有n+1个空链域
- n个结点的二叉线索树有n+1个线索数
- 非空二叉树叶子结点数为双分支(度为2)结点数加1
- n个结点能构成的二叉树种类:卡特兰数 C(n, 2n)/(1+n)
- 先序和后序遍历正好相反的二叉树一定是高度等于结点数的二叉树
- 叶子结点的先后顺序在各个遍历方式中都一样
- 叶子结点不一定在最底层
- 高度为h的平衡二叉树最少的结点数有规律,记为Nh:
- N0 = 0,N1 = 1
- Nh = Nh-1+Nh-2+1
- 相同表述:非叶结点平衡因子均为1
- 每个非叶子结点平衡因子均为0的平衡二叉树一定是满二叉树
- 二叉排序树插入时候没有平衡操作,发展成平衡二叉树才进行平衡
- 中序有序的AVL树注意区分中序是降序还是升序
- 降序:树中最大元素一定没有左子树
- 升序:树中最小元素一定没有左子树
- 二叉排序树、平衡二叉树,删除结点x后再插入x
- 二叉排序树
- x为叶子结点,树结构不变
- x为非叶子结点,树结构改变
- 平衡二叉树:无论x是不是叶结点,都可能改变或不变
- 二叉排序树
5 图
-
简单图、路径、回路
- 简单图:不存在重复边或顶点到自身的边
- 简单路径:顶点不重复出现的路径
- 简单回路:除第一个顶点和最后一个顶点,其他顶点无回路
-
强连通图
- 有向图概念
- 每对顶点之间都有到对方的路径
- 强连通分量:极大强连通子图,再添加顶点就不连通
-
生成树
- 包含全部顶点的极小连通子图,再添加边就出现环路
- 顶点数为n的图,生成树有n-1条边
- 由此,顶点数为n的图,有大于n-1条边则一定有环
-
带权图:边带权值,也称为“网”
-
稠密图、稀疏图:边很少的图称为稀疏图,衡量标准:|E|< |V|log|V|
-
存储方法
- 邻接矩阵
- 容易确定两个顶点是否有边
- 确定边数需要逐个位置检测
- 适合稠密图存储
- 表示唯一
- 拓展:若记到自己的边以及无法到达的边值为0,矩阵A
- A^m = B,B中有非0元素Bij = k,k ≠ 0
- 非0元素含义:从i到j长度为m的路径有k条
- 邻接表
- 容易确定任意顶点的所有邻边
- 方便计算有向图顶点出度
- 计算有向图顶点入度需要遍历检测
- 表示不唯一
- 十字链表:用于有向图
- 邻接多重表:用于无向图
- 邻接矩阵
-
图遍历
- BFS
- 时间复杂度
- 邻接表:O(V + E)
- 邻接矩阵:O(V^2)
- 可用于最短路径问题
- 类似于二叉树的层次遍历
- 时间复杂度
- DFS
- 时间复杂度
- 邻接表:O(V + E)
- 邻接矩阵:O(V^2)
- 连通图DFS经过的边形成一棵生成树
- 类似于二叉树的先序遍历
- 时间复杂度
- 连通性
- 无向图:一次遍历访问到全部结点则连通
- 有向图:从初始点能访问到所有结点则连通
- BFS
-
最小生成树:权值之和最小的生成树
- 不一定唯一、但权值之和为一
- Prim算法:距离已有树最近顶点加入生成树
- 时间复杂度:O(V^2)
- 适合求解稠密图的情况
- Kruskal算法:选择未选的、权值最小的、连接不同分量的边加入生成树
- 判断是否连接不同分量:并查集
- 时间复杂度:O(log E)
- 适合求解稀疏图的情况
- 最小生成树唯一的条件
- 所有权值均不相等
- 有相等权值的边,但在构造时权值相等的边都被加入最小生成树
-
最短路径
- Dijkstra:单源最短路径
- 辅助数组:
- dist:记录源点到各个顶点当前最短路径长度,无路径为无穷大
- path:从源点到各个顶点最短路径的前驱结点
- 流程:
- 初始化
- 选取当前dist中值最小、未访问的顶点
- 根据选取的结点更新dist、path
- 重复n-1次2~3步
- 时间复杂度:O(V^2)
- 辅助数组:
- Floyd:各顶点之间最短路径
- 流程:
- 初始化:邻接矩阵中,有边则对应位置值为边权值
- 逐步尝试加入顶点,若使得路径长度减小则更新
- 时间复杂度:O(V^3)
- 适用于带权无向图
- 流程:
- Dijkstra:单源最短路径
-
关键路径
- 关键路径:源点到汇点的最大路径长度的路径
- 关键活动:关键路径上的顶点(活动)
- 计算步骤
- 计算ve
- 源点ve=0
- 其他结点取最晚(max)的到达时间
- 计算vl
- 汇点vl=ve
- 其他结点取最早(min)的发生时间
- 计算e:e = 该活动(边)起点的ve
- 计算l:l = 该活动(边)终点的vl-活动时间(边权)
- 找到l与e相等的顶点
- 计算ve
- 不能任意缩短关键活动,因为缩短到一定程度可能不再是关键活动
- 可能存在多条关键路径,加快同时在所有关键路径上的活动才能缩短时间
-
常见问题
- N个顶点无向图,任何情况都连通(边怎么变化都连通):(N-1)*(N-2)/2+1
只需去除一个结点后保证是完全连通子图,再添加一条边将剩下结点连接上 - 有向图邻接表存储,删除一个结点时间复杂度:O(V + E)
- 删除出边只需要删除该结点的邻接表项
- 删除入边需要遍历所有边
- 生成树与连通子图、连通分量
- 极大连通子图 = 连通分量
- 连通无向图中:即该图本身
- 不联通无向图中:各个连通分量
- 极小连通子图 = 生成树
- 极大连通子图 = 连通分量
- N个顶点无向图,任何情况都连通(边怎么变化都连通):(N-1)*(N-2)/2+1
6 查找
-
平均查找长度:衡量查找算法效率最主要的指标
- 查找长度:需要比较的关键字次数
- 平均查找长度 ASL = sum p*c
-
顺序查找
- 成功ASL = sum pi * (n-i+1) = (n+1)/2
- 失败ASL = n
- 有序表失败ASL = n/2 + n / (n+1)
-
折半查找
- 成功ASL = log2(n+1) - 1
- 通过判定树进行分析
- 计算每个关键字查找成功的长度
- 计算查找长度总和
- 成功平均ASL = 长度总和 / 关键字数
- 判定树的特征与合法性
- 判定树是一棵平衡二叉排序树,中序序列有序
- 折半查找过程必须统一使用向上取整或向下取整
- 同时出现两种取整方法都的二叉排序树不是合法判定树
- 判断时,由上至下递归进行判断
- 参考:折半查找判定树
-
B树
- m阶B树:所有结点孩子个数最大值为m,m叉平衡查找树
- 至多有m棵子树,m-1个关键字
- 根结点若非终端结点,则至少有2棵子树
- 所有非叶结点至少有m/2棵子树(向上取整),至少有m/2-1个关键字
- 叶子结点在同一层次,都不带有信息
- 结点关键字数 = 结点孩子数 - 1
- m阶B+树:
- 结点的索引项只含有对应子树的最大关键字,不存储记录
- 结点关键字数 = 结点孩子数 - 1
- 叶结点包含全部关键字
- 存在一个指针,指向值最小的叶子节点,所有叶子连接成一个链表
- 高度为2的m阶B树,最少的关键字个数:m个
根结点达到m-1个关键字时,再加入一个关键字就分裂出第二层。
- m阶B树:所有结点孩子个数最大值为m,m叉平衡查找树
-
散列表
- 装填因子:关键字个数 / 表长;装填因子越大,ASL越大
- 冲突处理:
- 开放定址法
- 线性探测:容易出现聚集,冲突后向“前”查找空闲单元
- 二次探测:逐次尝试±i^2
- 拉链法:冲突记录存储在一个链表中,不出现聚集现象
- 开放定址法
- 计算ASL:如H = key mod m
- 列出存储结构(表)
- 查找成功:根据key来查
- 列出每个key的比较次数
- ASL = (比较次数和) / key个数
- 查找不成功:根据地址0~m-1来查
- 列出每个地址0~m-1查找到空的比较次数(包含与空位置比较)
- ASL = (比较次数和) / 地址个数m
- 提高散列表查询效率
- 减小装填因子
- 设计冲突少的散列函数
- 处理冲突时避免产生聚集现象
- 若使用开放定址法,不能直接删除元素,只能标记为删除状态,否则会导致搜索路径被中断。
7 排序
-
稳定排序
- 直接插入排序
- 折半插入排序
- 冒泡排序
- 归并排序
- 基数排序
-
不稳定排序(快“些”选一堆,“些”谐音“希”)
- 希尔排序
- 快速排序
- 简单选择排序
- 堆排序
-
外部排序:归并排序
最佳归并树:霍夫曼树思想
分析:两个有序表的合并最坏情况比较次数是m+n-1,依赖于表长,利用霍夫曼树最短带权路径的特性,能最小化比较次数。 -
复杂度分析
- 时间复杂度
- O(n^2)
- 直接插入排序
- 折半插入排序
- 冒泡排序
- 选择排序
- O(nlogn)(快“些”归“队”,“些”谐音“希”,“队”谐音“堆”)
- 快速排序
- 希尔排序
- 归并排序
- 堆排序
- O(d(n+r)):基数排序(如930,d位数=3,r基数=10)
- O(n^2)
- 空间复杂度
- 快速排序:O(logn)
- 归并排序:O(n)
- 基数排序:O®
- 其他:O(1)
- 时间复杂度
-
堆排序
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 确定大量数据中前少数部分序列
- 建堆调整:自右向左,自下而上进行检测,发生调换时后递归进行下去直到被调换结点满足堆的条件
- 删除结点:将最下层、最右边一个结点填补删去的位置,再调整
-
排序原理与常见问题
- 每一趟排序确保一个关键字到达最终位置
- 交换类(冒泡、快速)
- 选择类(选择、堆)
- 关键字比较次数与原始序列无关:选择排序、折半插入排序
- 排序趟数与原始序列有关:交换类
- 元素移动次数与原始序列无关:基数排序(无论如何都进行收集)
- 已经递增原始序列排序:直接插入最省时,快速排序最费时(达到最坏)
- 确定大量数据中前少数部分序列:堆排序
- 顺序存储更换为链式存储,希尔排序、堆排序效率降低,这两个排序算法利用了顺序存储的随机访问特性
- 第n趟快速排序后,定有n个元素,左边元素都小于它,右边元素都大与它
- 快速排序的递归次数与初始序列有关,与划分后分区的处理顺序无关
- 希尔排序组内采用直接插入排序
- 升序链表归并为降序链表:头插法,复杂度O(max(m, n))
- 注意外部排序最佳归并树是霍夫曼树,m路归并,补虚拟“0“叶子结点,补齐最底层有m个叶子结点。
- 每一趟排序确保一个关键字到达最终位置
to be continue
Karl 2020/12/24