授课教师:邓俊辉
教师介绍:清华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]] ......