超牛《数据结构》笔记 张明瑞 清华大学 计算机科学与技术专业 大二Word文档下载推荐.docx
- 文档编号:19532435
- 上传时间:2023-01-07
- 格式:DOCX
- 页数:39
- 大小:3.39MB
超牛《数据结构》笔记 张明瑞 清华大学 计算机科学与技术专业 大二Word文档下载推荐.docx
《超牛《数据结构》笔记 张明瑞 清华大学 计算机科学与技术专业 大二Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《超牛《数据结构》笔记 张明瑞 清华大学 计算机科学与技术专业 大二Word文档下载推荐.docx(39页珍藏版)》请在冰豆网上搜索。
这就是Fabonacci查找法
while(lo<
hi)
{
while(hi-ol<
fib.get())fib.previous();
rankmi=lo+fib.get()-1;
if(e<
A[mi])hi=mi;
elseif(A[mi]<
e)lo=mi+1;
elsereturnmi;
}
return-1;
证明:
运用递推*
————————————————————————
二分查找的改进版:
为了隐藏比较次数,只比较一次:
while(1<
hi-lo)
rankmi=(lo+hi)>
>
1;
(e<
A[mi])?
hi=mi:
lo=mi;
(不再是mi+1)
}出口时hi=lo+1
return(e==A[lo])?
lo:
-1;
只是平均情况减少,但最好情况增加。
最优化版:
如果不在其中,还可返回最近的小于此元素的坐标
冒泡排序改进:
可以记录前面是否有逆序对,如果没有就证明不用再排序了
再改进:
可以增加一个last,也就是最后一个逆序对的位置,然后以后就只用排开始到last位置的元素了。
第三章:
列表
双向列表:
两个哨兵:
headertrailer(-1和n)
列表的查找:
没有高效方法,只能按位置依次向后找。
列表的排序:
选择排序
一直找当前最大的。
以及
插入排序
一次找一个,然后进行对比排序。
逆序对个数:
可以决定插入排序的复杂度
设想,当一个元素前面有n个比他大的元素
则在插入排序的时候,会从后向前比较n次。
一次这时候设总逆序对为I,复杂度就为O(I+N)
____________________________________
第四章:
队列与栈
应用:
进制转换
括号匹配
栈混洗:
将A栈中元素移动到B栈中,通过中间栈S,规定:
只能移动到S再全部移动到B
那么有多少种方法呢?
打个比方,1先入栈,然后又入了一些,则此时让一些栈,则当1出栈时,假设前面已经进入了k个元素,则后面n-k个只能排列在B的后n-k个位置上
所以这样推算,也就得到了递推关系:
S(N)=∑S(K-1)S(N-K)
这样一个递推数叫做Catalan数,其结果为(2n)!
/(n!
(n+1)!
)
推导过程详见:
判断是否是一个栈混洗
O(n)的算法:
直接借助A,B,S模拟混洗过程,运用贪心的原则,如果每次S.pop()时候已经空了或者需要弹出的元素不在S最顶端,则是非法的。
栈混洗与括号的联系:
一个栈混洗的过程可以表示为一个合法的括号表达式:
每次push看作是(,pop看作是)
由此可以预见:
有多少种栈混洗结果,n对括号可以构成的表达式(合法)也就有多少种。
中缀表达式求值:
将数字压入栈,并且当遇到运算符时,先判断之前是否有运算符的优先级比当前运算符更大,如果是则首先进行上一个运算符的运算,然后递推进行判断再前一个运算符(运算级相同也要运算),直到不成立,就接着压栈。
直到最后到头(压尽元素)。
引入两个栈。
优先级操作符的比较:
制表
遇到小于则入栈,遇到大于则计算(栈顶元素<
>
=当前元素)
》》》》》》》》》》》》》》》》》》
部分习题笔记:
1.归并排序的算法复杂度证明:
O(nlogn)
2.归并排序的优化:
对于两段已经排好顺序并且合起来也已经有序的情况,我们只需要增加一条语句:
if(a[mi-1]<
=a[mi])merge(lo,mi,hi);
3.链表访问的优化:
由于对数据结构的操作往往都限定于一个较小的子集,所以可以将每次查找的链表元素移到首元素。
在这种情况下,经常被访问的元素将集中在前端,大大提高访问效率。
第四章树
1树的表示方法:
可以将各个节点组成数组结构,包含孩子节点数据集与父节点的标号,如果有某个节点孩子节点,那么此节点里面的孩子节点数据集(可以为列表或者向量)就存储又大到小的孩子。
再存储此节点的父亲节点。
这时候向下查找与孩子的数目线性相关,向上查找与深度有关。
改进:
每个节点主需要记录两个引用:
纵向的firstchild以及横向的nextsibling(相邻兄弟),这时候储存结构的规整性大大增加。
2二叉树:
真二叉树:
每个节点都是2个度或者0个度(如果不够则在下面补满2个孩子)。
更加规整(实际操作其实是假想的,并不存在)。
如何用二叉树来描述多叉树
将长子作为左节点,次子作为右结点。
二叉树的表示:
binode类:
表示树的结点
父亲、左右孩子、高度、颜色(红黑树)、npl(左式堆)
父亲、孩子均为引用。
树形结构最重要的就是遍历
定义一个树类,里面有树根(节点类)、树的高度、规模、判空函数、各种遍历方法、子树的删除插入与分离等。
前序遍历:
。
VLR顺序
中序:
LVR
后序:
LRV
即V(父亲)的顺序在哪里就是什么序遍历
先序遍历:
visit(x->
data);
traverse(x->
lchild,visit);
rchild,visit);
O(N)复杂度
但是递归在运行栈中占用空间很大。
所以非常有必要从递归改为迭代。
注意:
尾递归
化解为迭代:
利用栈的方法,既然需要先处理左边,就先把右边压入栈
新的构思:
先便利左侧链,在自下而上遍历右子树。
每次访问左孩子,然后将右孩子一次压入栈。
想法:
其实也可以先储存A,B表示当前层的两个节点,对A(左)节点判断有无孩子,如果有,则访问,并且A=LA,B=RA;
反之访问B结点,并且A=LB,B=RB;
如果都没有,B为父节点的右兄弟节点,然后再次寻找。
(自编)
对于中序遍历:
同样构建一个栈,存储右子树,
不同的是,左子树一直遍历,同时一直压入栈,直到尽头,取节点值
这时候再在返回后再pop弹出栈顶节点的值,然后将此节点右结点变为活跃节点继续进行左子树遍历。
对于后序遍历:
首先将根压栈(因为根没有右结点),然后依次:
如果有左节点,就把右结点压栈,再将左节点,如果没有,就把右结点压栈,直到为空;
然后对当前节点进行pop,如果pop!
=父节点,那么就代表pop的是右结点,就向右结点下方继续遍历,如果是,就直接pop然后输出;
我的方法:
先向左搜索到头,然后一路push,最后的直接输出,将当前节点改为父节点的右结点,在一路push,最后push后子节点已经为空,返回。
输出。
然后再将top(此时出来的为父节点)的节点与刚刚输出的节点比较,如果相同,证明父节点的所有子节点已经遍历完了,就再pop输出父节点,然后继续push;
如果不相同,证明刚刚输出的是左端,右结点还没有遍历,遍历右结点。
直到栈空。
树的重构:
中序遍历+先序/后序遍历任意一种便可以忠实还原原来的树形结构。
(分而治之,找到左右子树)
证法:
归纳假设
对于真二叉树,可以使用先序+后序还原。
(分而治之)
测试题:
并查集:
第六章:
图
第一部分图的表示
邻接矩阵
邻接表:
每个顶点有一个链表,链表储存了他的相邻接点(出度)
也可用数组的方式来表示,一个数组A[]表示点,A[1]表示A的第一个相邻接点,一个数组B[]表示A[]中每个点的临界点个数,比如B[N]表示A[N]的临界点个数,然后提取A[N]相邻接点信息时可以这样做:
IF(B[I++]!
=-1)
A[B[I]]......
第二部分:
BFS广度优先搜索
用一个队列来维护,与二叉树层次遍历相似,先入队的先出队操作,对其进行相邻边搜索,并且对未标记的邻接点标记。
*BFS搜索从S->
A点的路径是最短路径(等权图下)
第三部分:
DFS深度优先搜索
任意找一个点,随机找一个邻接点然后访问,随后递归地进行访问(即随机访问目前点的下一个相邻点),知道访问不能(没有相邻点或者所有相邻点都已被访问,做了标记),然后一步一步退回,直到当前节点还有其他相邻点未被访问,那么继续向内扩展。
第七章:
二叉搜索树(BST)
优点:
既是列表的列表(二叉树),又体现了向量的优点。
循关键码访问:
对数据项的访问通过关键码,关键码之间可以比较大小(将词条中比较操作重载)。
有序性:
对一个节点,左子树每个节点都不比他关键码大,对称地,右边子树每个元素不都比他的关键码小。
对BST做中序遍历,必然是单调非降序列。
只需要考察所有节点的垂直投影:
只要垂直投影单调非降,则满足BST
接口:
可以直接从bintree派生而来
只需要派生查找、插入、删除三个接口。
BST的查找:
查找类似于二分查找,对于关键码,如果大于则走右孩子,小于则走左孩子,直到找到或者没找到。
此时返回一个节点,并且将此节点的父节点也存起来,方便后面的插入操作。
o(logn)
BST的插入:
BST的删除:
1.最简单的情况:
没有左子树或者右子树
此时可以直接将左节点或者右结点与要删除的节点交换,对于没有左右结点的节点,可以通过配置哨兵的方式来同样以此实现。
2.复杂的情况:
双结点
对于双节点,可以先找到这个节点的直接后继(即按照中序遍历向后第一个节点),它的直接后继一定是比它大的最小元素,因此可以与之进行交换,交换后待删除结点一定是没有节点或者只有右结点的情况,就可以转到情况1进行操作。
反思:
注意不同结构、不同思路之间的融会贯通以及转换。
反思2:
任何复杂的问题都是由简到繁进行解答的,而且繁琐的方面一般都是由简单的解法组合变换而成的。
因此要从简单入手,逐步解决。
平衡性与等价性测量:
对于n个互异的数,一共有catelan(N)种情况,其高度平均为根号N
如何达到理想平衡(高度最低)?
兄弟子树高度越接近,高度越低。
引理:
由n个结点组成的二叉树,其高度不低于log2N(理想平衡)
高度渐进地不超过logn——适度平衡,渐进意义上的理想高度——平衡二叉搜索树(BBST)。
等价BST:
不同的二叉搜索树可以对应相同的中序遍历序列。
如何等价?
两个原则:
上下可变,左右不乱。
等价变换:
第一类:
ZIG(顺时针)
第二类:
ZAG(逆时针)
相反操作
AVL树
对于每个节点左右子树高度不相差1的树称为~
平衡因子:
对于一个节点,它的左子树减去右子树的高度。
高度为h的AVL树,至少包含
S(h)=fib(h+3)-1个节点
递推
S(h)=
S(h-1)+S(h-2)+1
S(h)+1=[S(h-1)+1]+[S(h-2)+1]
以上是fabonacci式
左子树-右子树高度
定义理想平衡以及AVL平衡
由BST派生接口:
查找
重写:
插入与删除
插入与删除操作:
对于插入,最多有O(logn)个节点发生变化。
而对于删除,最多有O
(1)节点发生变化。
如何实现?
zig-zag旋转
首先:
遇到了插入后平衡因子绝对值大于1的情况。
这时候进行逆时针旋转,找哪一个点呢?
我们知道,插入引起的失衡是通过引起祖先一代一代失衡而使得整树失衡的过程,所以只要根绝在源头,也就是最先失衡的(最低级的)祖先,把它调整为平衡,整个树就又恢复平衡了。
因此需要将它与引起它失衡的子节点交换。
例如此图:
P和V交换
具体操作:
用一个临时引用rc指向p,然后将p的左儿子交给g,之后gp交换,p替代g成为祖先。
这种交换成为zigzig
对应zagzag为顺时针交换。
对于zigzag,也就是之字形交换(还有zagzig)
麻烦一些。
具体方法:
先zig旋转
再次zag旋转
也就是说,先平衡小的,再平衡大的。
如果左子树高度太大,便使用zig
如果右子树高度太大,便使用zag平衡。
操作:
先插入(BST),然后依次向上查找祖先,找到第一个失衡祖先,进行旋转调整。
删除算法:
依然进行旋转。
此时的情况是,祖孙三代都是左或右孩子比较多。
当旋转过后,如果T2存在孩子,这时调整后的新树高度不变
但是如果T2不存在孩子,则调整后子树的高度减少1,可能会造成失衡。
因此这时候可能最多需要做O(logn)次调整。
当三代节点更多孩子的个数不是朝一个方向排列,也就是按照之字形排列,
那么就需要先转换为一边倒的类型I,在进行节点旋转。
但此时高度减少了1.
——————————————————————
真正的操作,其实大可不必按照理解的思路进行旋转
可以类比一个魔方,如果是组装工人,不需要根据规则旋转,而是直接组装。
将需要旋转的祖孙三代,gpv
按照中序遍历重新命名a<
b<
c
然后对于gpv的孩子们(不超过四个)
按照中序遍历次序,从新命名
T0<
T1<
T2<
T3
于是可以重新组装
3+4重构。
对于重命名的规则,可以分四种情况分别列举。
至此,删除与插入操作(包括子操作旋转操作)迎刃而解。
AVL树有什么缺点呢?
需要单独记录平衡因子,需要改造元素结构并且额外封装。
单次动态调整之后,全书的拓扑结构变化量可高达Ω(logn)
数据结构(下)
第八章
高级搜索树
一伸展树
对于一些元素,很可能要经常对其进行访问,或者对与其相邻的一些元素经常访问,因此可以进行优化。
(与链表进行比较:
某些链表的元素会被集中访问,因此每次访问一个元素可以将之移动到最前端,可以提高访问效率。
回到BST,可以将某一段时间内经常访问的元素移动到树根的位置。
(利用zig与zag旋转)
但这样会遇到只有一个狭长分支的最坏情况。
怎么优化呢?
通过zigzag旋转(AVL树的双旋调整)
通过zig-zag或者zag-zag
(相当于不直接旋转所寻找的那个节点在的一级,而是从他的父节点下手旋转)
当需要zigzig或者zagzag(也就是在分支在一侧的情况)
旋转与avl树相同。
代码实现:
splay函数:
分情况:
1,当前孩子为右孩子还是做孩子
2,当前孩子的父亲为右孩子还是左孩子
分情况进行zig、zag旋转。
旋转细节:
同avl,可以直接进行拆开重接,而非生搬硬套模拟旋转过程
注:
还需要考虑没有祖父节点的情况(即只需要直接旋转)
查找函数:
查找此节点存在否,若存在,则返回splay后的节点,否则splay与要查找值相似的节点并返回其接口。
插入:
首先调用search(即首先将相似节点splay到了根节点,然后进行插入)
因此可以直接在根节点处进行插入。
依然是调用了search(),所以根节点必为树根
因此直接在树根处进行删除,然后选择左右任意子树中最小的节点作为根节点。
B树:
每个节点未必只有两个分差
底层节点的深度相同
更宽、更矮。
超级节点的概念:
多个关键码位于一个节点。
例如:
为什么如此(这样与上图不是等价吗?
针对外部查找大大提高了输入输出(I/O)效率
每次访问一个关键码可以读出一组数据,相比于一个节点一个数据,这样的效率提高了很多。
例如有1G的数据,单次查找一个数据,
如果二叉树,则需要log(2,10^9)=30次,而如果以关键节点有256个关键码的B树,只需要查找log(256,10^9)<
4次。
B树的阶:
阶数=路数
B树的外部节点(也就是叶子节点的下一个节点(空节点))深度相同,且即B树的高度。
B树的命名:
内部节点:
不超过m-1个关键码
不超过m个分支
内部节点的分支数:
对于树根:
2<
=n+1
其余节点:
(取上界)m/2<
B树每个节点可以视为线性序列
那么怎样表示呢?
我们可以用向量来表示
比如:
第一个向量存放n个关键码
第二个存放相对应的n+1个引用。
含有N个关键码的M阶B树,最大高度为
h<
=1+log(m/2)[(n+1)/2]=O(logN)
提示:
内部节点尽可能瘦(节点数位m/2上界)而根节点可以只分两支。
最小高度:
h>
=log(n)(N+1)=
Ω(logmN)
B树的插入算法:
先查找,然后返回一个最近不大于插入值的关键码位置
然后关键码+1,分支也对应加一。
如果违反了约定,即插入后关键码数目上溢,则需要处理。
(分裂)
此时从此组关键码中位数分裂,并将中位数关键码向上追寻,并且插入至左上两关键码的中间。
如图:
如果父节点也上溢,那么再次操作。
当然,根节点也需要有相应的上溢处理。
如果根节点也发生上溢,则也取出中位数,使之成为新的根节点。
导致B树增高(此时为两个分支)
—————————————————————————
B树的删除
首先进行查找。
如果找到,如果它不是叶节点,则在他的右子树中一直向左找到其直接后继,然后互换位置,进行删除。
此时可能会发生关键码数目下溢,需要进行旋转。
旋转:
如果在其左边、右边的兄弟子树有足够多的关键码,则可以进行旋转
但是如果没有兄弟满足有足够多的关键码(或者不存在),则需要双方合并(此时左右兄弟至少存在其一)将上方的父节点合并下来。
父节点也可能发生下溢,如法炮制进行处理。
一种可以记录历史版本的树
例如,红色线表示相邻版本可以保留的数据
蓝线表示更新的位置。
红黑树:
1树根必为黑色
2所有外部节点也必为黑色
3其余节点:
红色节点只能有黑色孩子。
(红父红子必为黑)
4从任何一个外部节点到根的黑色节点数目相等(黑深度)
提升变换:
将所有红色孩子提升至与父节点同一级的高度
结果不可能有两个红色节点相邻
经过提升:
可以发现,所有底层节点的深度相同。
这也是为什么要求黑深度相等的原因。
本质:
相当于一棵B树的拉伸
(2,4)树==红黑树
每一种提升后的红黑节点组合都相当于一个4阶B树
每一个节点最多拥有四个分支。
红黑树的高度:
特指黑高度。
先将带插入值插入到确定位置,然后染为红色,此时第三条性质即其父亲必为黑色可能不满足。
如何修复?
考察其父亲兄弟节点即插入节点叔父节点的颜色。
当叔父节点为黑色:
有四种情况:
zigzagzigzigzagzagzagzig
只考虑前两种情况
先收缩一下。
这时调整只需要重新染色。
当叔父节点为红色:
向上合并,形成非法的上溢(B树)
进行上溢调整分裂。
找到居中关键码,上移
删除操作:
当删除节点与它的替代者至少有一个是红色的时候,可以直接删除(+重新染色)
但当两个节点都是黑色的时候,删除可能会破坏第4条性质(黑深度)
调整算法:
联系到B树:
合并后相当于下溢缺陷
因此与B树的调整算法相似。
分为四种情况:
1.当删除节点兄弟节点为黑,并且其兄弟节点至少有一个红孩子
调整为:
此时s继承P的颜色
实质:
观察B树的旋转
2.当兄弟节点为黑,并且它的两个孩子也为黑的情况:
(1)如果父节点P为红色:
此时无法旋转,只能合并。
最终变为红黑树:
此时不会再次发生下溢(因为p的颜色为红色)
(2)如果父亲节点P为黑色:
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 数据结构 超牛数据结构笔记 张明瑞 清华大学 计算机科学与技术专业 大二 笔记 计算机科学 技术 专业