原文链接:408-theory-Data-Structure
线性表 Linear List
结构
顺序表 Sequential List
链表 Linked List
带头结点和不带头结点两种实现,带头结点保证插入删除的一致性
单链表 Singly Linked List
双向链表 Doubly Linked List
循环链表 Circular Linked List
循环单链表 Circular Singly Linked List
循环双链表 Circular Doubly Linked List
基本函数
- ListInit(&L);初始化
- ListLength(L);计算表长
- ListEmpty(L);判断表空
- ListPrint(L);输出表
- LocateElem(L,e);按值查找
- GetElem(L,i);下标查找
- ListInsert(&L,i,e);插入
- ListDelete(&L,i,&e);删除
- ListDestroy(&L);摧毁
Sequential List
1 | // seqlist |
LinkedList
1 | # include <stdio.h> |
实例
约瑟夫环
栈 Stack
结构
顺序栈 Sequential Stack
链栈 Linked Stack
共享栈 Shared Stack
基本函数
- StackInit(&S);初始化
- StackEmpty(S);判断栈空
- StackPush(S,e);入栈
- StackPop(S,&e);出栈
- StackGetTop(S,&e);获取栈顶元素
- StackDestroy(&S);摧毁
SequentialStack
1 | # include <stdio.h> |
LinkedStack
1 | # include <stdio.h> |
实例
函数式计算
迷宫问题
DFS深度优先搜索中的应用
队列 Queue
结构
顺序实现 Sequential Queue
由于队列本身只存在首尾的元素操作,因此常用顺序结构实现
假溢出和循环队列
队空和队满判断条件
受限制队列
输入受限制队列
输出受限制队列
基本函数
- QueueInit(&S);初始化
- QueueEmpty(S);判断队空
- QueueFront(S,&e);获取队头元素
- Queuerear(S,&e);获取队尾元素
- EnQueue(S,e);入队
- DeQueue(S,&e);出队
- QueueDestroy(&S);摧毁
SequentialQueue
1 | # include <stdio.h> |
LinkedQueue
1 | # include <stdio.h> |
实例
函数式计算
BFS广度优先搜索中的应用
数组 Array
结构
矩阵压缩
下标、索引的计算问题
一般问题中经常会出现索引和下标同时出现的情况,在计算机中索引从0开始,
下标访问元素为An,实际索引地址为A[n-1]。解决方法为全部采用下标计算,最后在下标访问地址减1。
对角矩阵
三角矩阵
三对角矩阵
稀疏矩阵
基本函数
实例
Numpy高效矩阵库的实现
串
结构
存储格式
- 静态存储:静态为使用一个连续数组存储,存在上限。
- 动态存储:动态为使用malloc和free实现在堆区的存储。
- 块链存储:在每个块内连续纯粹,块之间使用链存储。
模式匹配算法
暴力匹配 O(mn)
KMP O(m+n)
子串,前后缀,最长匹配前后缀、PM(partical match)数组、Next数组、Nextval数组
实现思路:通过模式串的最长匹配前后缀长度,如果i字符出现失配,可以让i位前的子串的前缀字符直接移到后缀位置,因为是匹配的,也就是跳过中间的无效字符:
基本函数
- StringAssign(&T,chars);初始化
- StringDestroy(&S);摧毁
- StringLength(S);摧毁
- Stringcopy(&S2,S1);S1赋值到S2
- StringCompare(S1,S2);比较
- SubString(&sub,S1,start,len);获取子串
- StringConcat(&S3,S1,S2);合并
- Index(S,P) 判断串S中首次出现P的索引
String
1 | # include <stdio.h> |
实例
树
概念
树
二叉树
满二叉树
完全二叉树
二叉排序树
平衡二叉树
根结点 叶结点 非终端结点 度 双亲 左右孩子 兄弟 叔侄 子孙 祖先
二叉树与度为2的有序树不同,二叉树允许为空,但树为空时度不为2
结构
顺序存储
链式存储
1 |
|
二叉树
先序 中序 后序遍历
线索二叉树
先序 中序 后序线索二叉树进行先序 中序 后序遍历
先序不易找前驱,后序不易找后继
因为按照左孩子前驱右孩子后继,先序时结点的前驱是父结点,但是包含左孩子的结点不指向父结点。但是容易找后继,只需要先序遍历该结点子树找到前驱为该结点的子结点即可。后序找后继也是同理
1 |
|
非递归形式实现上述遍历
森林
双亲、孩子、孩子兄弟表示法
树、森林转二叉树
树、森林的遍历
树 | 森林 | 二叉树 |
---|---|---|
先根 | 先序 | 先序 |
后根 | 中序 | 中序 |
实例
哈夫曼编码
并查集
图
概念
有向图 无向图 完全图
简单图 多重图
子图
连通 连通图 连通子图 连通分量:极大连通子图
强连通图 强连通分量
生成树 生成森林
入度 出度 边权 网
路径 路径长度 回路
简单路径 简单回路
距离
结构
邻接矩阵存储(有向无向)
邻接表存储(有向无向)
十字链表存储(有向)
邻接多重表存储(无向)
遍历
可以通过图的遍历判断图的联通性
广度优先搜索BFS
队列实现
BFS可实现求权长度相同网络的单源最短路径问题
方法 | 时间 | 空间 |
---|---|---|
邻接表 | O(v+e) | O(v) |
邻接矩阵 | O(v2) | O(v) |
深度优先搜索DFS
栈实现
方法 | 时间 | 空间 |
---|---|---|
邻接表 | O(v+e) | O(v) |
邻接矩阵 | O(v2) | O(v) |
实例
最小生成树Minimum-Spanning Tree
prim算法:选择一个结点,每次添加一个可加入的最短距离结点直到所有结点都添加
kruskal算法:每次选取最短路边加入其结点,如果这两个结点已经在同一集合中跳过该边
最短路径
dijkstra算法
先从选定结点开始构造一个距离数组和路径数组,距离初始化为该结点到其它结点的直接距离。每次选择当前集合与其它点之间最短距离结点,并通过该新结点更新与其它结点的最短距离,如果$dis[i]>dis[j]+e(j,i)$就更新
floyd算法
通过最短距离矩阵直接计算所有结点之间的最短距离。
有向无环图实现公共子表达式
拓扑排序
AOV网 结点网
AOE网 边网
六参数法 求解关键路径和最早最晚开始完成时间
查找
概念
查找表 静态查找 动态查找 关键字 平均查找长度ASL 平均查找成功长度 平均查找失败长度
顺序查找
顺序查找
折半查找
分块查找
ASL=(b+1)/2+(s+1)/2 b为块数 s为块中元素个数
当b=sqrt(n)时最优
1 |
|
树形查找
二叉排序树
建树 插入 删除
删除时两种情况
- 叶结点-直接删除
- 非终端结点,只有一棵子树-子树的根结点替代当前结点
- 非终端结点,两棵子树-找到其直接后继或前驱替换,并将直接后继或前驱删除
1 |
|
平衡二叉树AVL
左右子树高度差绝对值为平衡因子,任意结点平衡因子不大于1的树为平衡二叉树。
特点:高效查询、低效插入和删除,查找稳定
插入
从插入处自底向上访问父结点,计算平衡因子直到平衡因子大于1,包括以下4种情况:
- 最高处为LL结点-右旋
- 最高处为RR结点-左旋
- 最高处为RL结点-先右旋后左旋
- 最高处为LR结点-先左旋后右旋
删除
- 删除叶结点时先直接删除
- 删除非叶结点找到前驱或后继替代然后直接删除
- 在删除的结点的父结点处向上判断,若结点树高-1则需回溯到祖父结点判断调整
查找
平均查找长度O(logn)
1 |
|
红黑树
红黑树减弱了插入删除的调整条件
主要满足5个性质:
- 结点红或黑
- 根结点黑
- 叶结点黑(叶结点为NULL结点)
- 红结点不相邻(红结点父结点和子结点都是黑)
- 结点到任意叶结点的简单路径包含黑结点数相同 (黑高,结点最大的黑高作为树的黑高)
特点:
- 根结点到叶结点最长路径不大于最短路径2倍
- 红黑树高 $h\leq2log_2^{n+1}$ 由于树的结点比黑结点多得到
- 插入的都为红结点
插入
- 空树插入- 染黑
- 父结点为黑- 直接插入
- 父结点为红,叔红- 父、叔结点变黑,爷结点变红,继续向上修改直到根结点
- 父结点为红,叔黑(4种情况):(1)父子LL:父结点与爷结点颜色交换,再右旋;(2)父子RR:父结点与爷结点颜色交换,再左旋;(3)父子LR:父子左旋后,新父结点与爷结点颜色交换,再右旋;(4)父子RL:父子右旋后,新父结点与爷结点颜色交换,再左旋;
删除
按照AVL树的删除方式转为其前驱后继删除,找到次底层结点
(删除结点无子树)
- 删除结点为红-直接删除
- 删除结点为黑,兄弟结点无子树(兄弟无子树表示两个都是叶结点为子树,那由红黑树定义兄弟也为黑)- 删除结点,兄弟变红,父变黑
- 删除结点为黑,兄弟结点有一个子树且为LL RR形式- 删除结点,兄弟结点子树、父结点变黑,LL右旋,RR左旋
- 删除结点为黑,兄弟结点有一个子树且为LR RL形式- 删除结点,兄弟结点和子树颜色交换,LR左旋,RL右旋,同3情况,继续按照3.中操作(兄弟结点子树、父结点变黑,LL右旋,RR左旋)
- 删除结点为黑,兄弟结点有两个子树,兄弟结点黑- 删除结点,兄弟结点和父结点颜色交换
- 删除结点为黑,兄弟结点有两个子树,兄弟结点红- 兄弟结点和父结点颜色交换,被删为L左旋,被删为R右旋,删除结点,当前被删结点的兄弟结点和父结点颜色交换
(删除结点一个子树)
- 删除结点为黑,将子树涂黑放到删除结点处
(删除结点两个子树)
- 找到前驱或后继替换,转为无子树和1子树情况
查找
与二叉排序树相同
B树
m阶B树为所有结点平衡因子为0的m路平衡查找,满足:
- 每个结点最多m棵子树,最少⌈m/2⌉棵子树,即关键字范围[⌈m/2⌉-1,m-1]
- 根结点若存在子树,则至少2个子树
- 非叶结点关键字升序
- 叶结点都在同一层,且不包含信息
特点:
- 叶结点数(查找失败)为关键字总数n+1
- n个关键字的m阶B树高度范围:$[\log_m^{n+1},\log_{⌈m/2⌉}^{(n+1)/2}+1]$
关键字个数应该小于等于所有结点都有m-1个关键字总数
$n\leq(m-1)(\sum_{i=0}^{h-1}{m^i})$ => $h\geq\log_m^{n+1}$
由于B树根结点最少2个子树,可用:关键字数+1大于等于最少叶结点数(查找失败)
$n+1\geq2(⌈m/2⌉)^{h-1}$ => $h\leq\log_{⌈m/2⌉}^{(n+1)/2}+1$ - B树结点数不包括叶子结点
查找
查找关键字,如果相等则返回,不相等找到对应区间子树继续查找,直到为叶结点表示查找失败
插入
插入位置会搜索到非叶结点底层
- 插入前关键字数小于m-1-直接插入
- 插入前关键字数等于m-1-从⌈m/2⌉处分裂出左右树,将⌈m/2⌉插入到父结点关键字,若父结点溢出继续向上分裂。直到到根结点,根结点分裂后会导致树高+1
删除
B树的删除操作不唯一,当删除的不是最底层结点时,将其替换为前驱或后继,一直替换到非叶结点最底层,转换为非叶结点底层关键字删除操作
- 当结点关键字数≥⌈m/2⌉,直接删除
- 当结点关键字数==⌈m/2⌉-1,兄弟关键字数≥⌈m/2⌉,借一个实现平衡
- 当结点关键字数==⌈m/2⌉-1,兄弟关键字数==⌈m/2⌉-1,将这两个兄弟结点和对应的父结点关键字合并,并并删除该父关键字,同时修正索引结点,直到根结点
B+树
m阶B+树满足:
- 关键字范围[⌈m/2⌉,m]
- 根结点若存在子树,则至少2个子树,其它结点至少⌈m/2⌉+1棵子树
- 非叶结点关键字是其子树中最大(或最小)的关键字,一般选右子树最小(下面默认)
- 叶结点包含全部关键字和数据指针,所有叶结点关键字顺序排序且相邻叶结点是链接的
查找
查找关键字,一直查找到叶结点,叶结点没有查找失败
插入
插入位置为叶结点
- 插入前关键字数< m- 直接插入
- 插入前关键字数=m,索引父结点< m- 从⌈m/2⌉+1处分裂出左右树(⌈m/2⌉+1在左表示关键字最大,在右表示关键字最小),同时将⌈m/2⌉+1插入到父结点关键字
- 插入前关键字数=m,索引父结点= m- 从⌈m/2⌉+1处分裂出左右树,同时将⌈m/2⌉+1插入到父结点关键字,由于父结点溢出继续向上分裂。直到到根结点,根结点分裂后会导致树高+1
- 旋转:B+树为了减少结构操作,当插入前关键字数=m,但兄弟结点有空,将索引的关键字放到兄弟结点,腾出一个位置插入。
删除
- 叶结点关键字数> ⌈m/2⌉,直接删除
- 叶结点关键字数= ⌈m/2⌉,兄弟关键字数> ⌈m/2⌉,借一个关键字过来,同时索引的关键字要更新
- 叶结点关键字数= ⌈m/2⌉,兄弟关键字数= ⌈m/2⌉,将这两个兄弟结点和对应的父结点关键字合并,并并删除该父关键字,同时修正索引结点,直到根结点
数列查找
散列表查找
散列表是通过关键字直接获取元素地址进行查找
散列函数 散列表 冲突 二次堆积 查找成功 查找失败
散列函数方法
- 直接定址 addr=a*key+1 唯一不冲突
- 平方取中
- 数值分析
- 除留余数
- 随机数
- 折叠法
冲突处理
开放地址法
当冲突时使用定义的冲突处理计算出下一个地址,直到不冲突$Hi=(H(key)+di)%m$ 其中di为增量序列,可以选择1.线性探测法 2.平方探测法 3.双散列法链表法
散列表不直接存储数据,使用链表,同义词会形成一个链表
性能分析
查找成功ASL
查找失败ASL
装填因子α=表中数据数/散列表长度,α越小越不容易冲突
排序
内部排序:数据在内存中
插入排序
直接插入排序
折半插入排序
希尔排序
交换排序
冒泡排序
快速排序
选择排序
简单选择
堆排序
归并排序
基数排序
外部排序
归并排序
二路归并(增加输入缓冲区数量即多路归并)
- 将数据划分r块(保证至少内存能存放3块)
- 进行r/2次二路归并排序,剩余r/2块: 每次读取两块(并对每块内部排序),依次选择两块中最小值放入缓冲区,缓冲区满则写入数据.当存在内存块读取完,则直接写入剩下块数据.此时完成一次归并
- 第二次归并块数减半,但块长加倍,单块无法全部读入.进行r/4次二路归并排序: 每次先读取两块的部分数据,缓冲区满了则写入,内存块读取完
了从硬盘块继续读入,知道全部读取完,完成排序. - 一直归并直到硬盘中只剩一块,完成外部排序.
多路归并败者树
针对多路归并算法,提出的败者树:每个内存块排序后的第一个数据作为树的叶子结点,比较两个子树结点大小,大的失败,记录到父结点,小的继续向上比较.一直到根结点,平均比较logk次(k路归并)
选择置换排序
- 从硬盘中读取数据,找到最小值输出,标记MIN.
- 继续读入数据,找到最小值.若最小值>MIN:输出,并更新MIN为当前最小值.如果最小值< MIN,标记当前最小值不可输出.选择次小值比较输出.
- 当缓冲区全是不可输出时,得到的输出序列即为一个归并区
- 得到所有归并区,归并后完成排序
1 | #include"iostream" |