《数据结构》张明瑞 清华大学 计算机科学与技术专业 大二

 

授课教师:邓俊辉 

 

教师介绍:清华2014年度十大优秀老师!MOOC上数据结构公开课报名已超5万人!你还不来看

 

笔记简介:哈希表,红黑树,斐波那契二分查找,从基本到高端的数据结构全都有! 有重点,有插图,还有鄙人学习感悟,看完这笔记中的战斗机你还想啥!

 

20140603

————————————————————

ADT是抽象数据类型,为一组数据模型,加上一组操作,不涉及具体的储存方式,就像是用户使用的产品(黑箱),只考虑抽象层面

 

DS是数据接收,是根据特定语言实现ADT的算法,设计复杂度和各种具体机制。

 

(计算幂次方的方法:

pow(a,b)

we can devide b into binary type and pow(a,2^n) = pow(a,2^(n-1))

so we can do it in O(log2 n), each time rotate 1 time.

 

vector 向量数据结构,可以对不同数据类型操作,并且封装了许多操作,比如remove(r),sort(),search(e)无序去重,deduplicate(),uniquify()有序去重,traverse()遍历

 

search(n) 返回不大于n的元素的最后的rank

put(a,b) 将rank为a的数改为b

 

平均复杂度:根据数据结构个操作出现概率分布进行加权平均,每个操作作为独立事件,割裂了相关性与连贯性

 

分摊复杂度:将数据结构连续地实施足够多的操作,所需总体成本分摊至单次操作。对一系列操作整体考量,更加精确。

 

vector 插入算法:

从固定插入位置开始,先扩容,然后从rank = n downto 固定位置k

vector[n] = vector[n-1]逐个后移

最后执行插入。

 

vector 删除:

区间删除:

删除从lo 到 hi 的区间

 

则执行如下:

_elem[lo++] = elem[hi++]

即从前向后逐次移位

不能更改次序,因为如果要删除的区间与想向前移动的区间有重复区间,从后向前的话会覆盖掉重复区间的部分。

 

 

有序相向量的唯一化

低效版:

有序向量,重复的元素构成一个区间,因此每个区间保留一个即可。

于是可以从前向后依次检查来逐个删除相同元素

O(N^2)

 

高效版:

成批删除雷同元素,将他们视为一个整体。

i = j = 0

如果二者不相等,则_elem[++i] = _elem[++j],相当于忽略了所有重复元素(没有直接显示调用删除)。

 

_____________________________________

Fabonacci 查找法:

由于二分查找在大于、小于和等于的情况下比较次数不同,所以造成内部比较次数的不平均,因此为了平衡比较和查找次数,可以构建一个看起来不平衡的数列来平衡比较次数。这就是Fabonacci查找法

 

 

while(lo < hi)

{

     while (hi - ol < fib.get()) fib.previous();

     rank mi = lo + fib.get() - 1;

     if (e < A[mi]) hi = mi;

     else if (A[mi] < e) lo = mi + 1;

     else return mi;

}

return -1;

 

证明:运用递推*

 

————————————————————————

二分查找的改进版:

为了隐藏比较次数,只比较一次:

while(1 < hi - lo)

{

rank mi = (lo + hi) >>1;

( e < A[mi] ) ? hi = mi : lo = mi; (不再是 mi + 1)

}出口时 hi = lo + 1

return (e == A[lo]) ? lo : -1;

只是平均情况减少,但最好情况增加。

 

最优化版:如果不在其中,还可返回最近的小于此元素的坐标

 

 

 

冒泡排序改进:可以记录前面是否有逆序对,如果没有就证明不用再排序了

再改进:可以增加一个last,也就是最后一个逆序对的位置,然后以后就只用排开始到last位置的元素了。

 

 

————————————————————————

 

第三章:列表

双向列表:两个哨兵:header trailer(-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)!)  

推导过程详见:

http://blog.sina.com.cn/s/blog_497689ad01000azu.html

 

判断是否是一个栈混洗

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);

traverse(x->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]] ......

你可能感兴趣的:(数据结构与算法,前端,ViewUI)