一棵树是由N个节点和N-1条边的集合

从节点n(1)到n(k)的路径(path)定义为节点n(1),n(2),...,n(k)的一个序列,且对1<=i<k,节点n(i)是n(i+1)的父亲。这个路径的长为改路径上的边的条数,即k-1

一棵树的内部路径长(internal path length)定义为所有节点的深度和

假设所有的树出现的机会均等,则树所有节点的平均深度为O(log N)

节点的深度(depth)为根到该节点唯一路径的长;节点的高(height)是从该节点到树叶节点最长路径的长。一棵树的高等于它根的高,深度等于最深的树叶节点的深度;一棵树的深度和高是相等的

如果存在从n(1)到n(2)的一条路经,那么n(1)是n(2)的一个祖先(ancestor),n(2)是n(1)的一个后裔(descendant)。如果n(1) != n(2),则n(1)是n(2)的真祖先(proper ancestor),n(2)是n(1)的真后裔(proper descendant)


先序遍历(preorder traversal):对一个节点的处理是在它的诸儿子节点被处理之前进行的

后序遍历(postorder traversal):对一个节点的处理是在它的诸儿子节点被处理后进行的

中序遍历(inorder traversal):先处理左子树,再处理该节点,最后处理右子树


普通树的实现可以通过让一个节点有两个指针,一个child指针指向它的第一个儿子,一个sibling指针指向它的兄弟


注意,由于链表的每个节点只有一个指针(这个指针指向下一个节点),即便是双向链表的表头仍然可以与普通的链表节点结构相同,所以设置表头是有意义的。然而对于树来说,无法设置树头节点,因为无论是child指针,sibling指针,left指针还是right指针都是有特殊含义的,所以即便设置了树头节点也要进行特殊处理,从而就失去了设置树头节点的意义(参考链表,栈和队列)。所以,树不宜设置头节点,而这就要求每一个函数操作都必须返回一个指向节点的指针,好对作为参数传入函数的值进行更改



二叉树

二叉树是一棵树,其中每个节点都不能有多于两个的儿子

二叉树的一个性质是平均二叉树的深度要比N小很多,这个平均深度为O(√n)。对于二叉查找树(binary search tree),其深度的平均值是O(log N)

二叉查找树中每个节点的关键字都大于左子树的关键字,并且每个节点的关键字都小于右子树的关键字

在二叉查找树的情况下,对于任意单个运算不再保证O(log N)的时间界,但可以证明任意连续M次操作在最坏情况下花费的时间为O(M log N)


在删除二叉查找树的一个节点时,如果该节点只有一个儿子则可以用这个子节点代替它;如果有两个子节点,则通常找出右子树的最小节点,用这个节点取代被删除的节点,然后在右子树中将这个最小节点删除即可。由于这个最小节点肯定没有左子树,所以它的删除比较简单。




AVL树

AVL树是带有平衡条件的二叉查找树,它的每个节点的左子树和右子树的高度差不能超过1(空树的高度定义为-1),每个节点中都保存着这个节点的高度信息。

在高度为h的AVL树中,最少节点数S(h)由S(h) = S(h-1) + S(h-2) + 1给出,对S(0) = 1, S(1) = 2

除去插入操作外,所有树操作都可以以时间O(log N)执行(假设懒惰删除)。进行插入操作时,需要更新通往根路径上那些节点的所有平衡信息;插入操作的难度在于可能会破坏AVL的平衡性。如果发生这种情况,需要在插入后进行处理,这种修正称为旋转(rotation)。旋转结束后,得到的新AVL树和插入前的原树高度相同

在插入后,只有那些从插入节点到根的路径上的节点的平衡可能会被破坏,因此沿着这条路径上行到根并跟新平衡信息时,可以找到一个节点,它的新平衡破坏了AVL条件。把这个必须平衡的节点叫做a,则不平衡可能有以下四种情况:

1. 对a的左儿子的左子树进行插入

2. 对a的左儿子的右子树进行插入

3. 对a的右儿子的左子树进行插入

4. 对a的右儿子的右子树进行插入

其中,1、4是插入发生在“外边”,这种情况可以通过一次单旋转完成调整;2、3的插入发生在“内部”,要通过双旋转进行处理


其实对所需旋转的判断和具体方法可以这样总结:

假设有问题的节点是A,插入发生在A的儿子Y的子树。如果插入元素的大小在A和Y之间,则需要进行双旋转;否则进行单旋转。对节点所记录的高度的更新在最后(判断、执行旋转之后)进行,设为其两个子树的较大高度+1即可(注意单旋转后要更新被交换的两个结点的高度信息)。

单旋转:在A的位置用Y取代A。如果A>Y(左旋转,Y是A的左儿子),则将A作为Y的右儿子,然后将Y原来的右儿子设为A的左儿子;如果A<Y(右旋转,Y是A的右儿子),则将A作为Y的左儿子,然后将Y原来的左儿子设为A的右儿子

双旋转:进行两次单旋转即可。假设插入发生在Y的儿子K所在的子树,则先对Y进行一次单旋转(方向取决于K是Y的左儿子还是右儿子),再对A进行一次单旋转(方向取决于Y是A的左儿子还是右儿子)即可

使用代码进行实现时,使用递归的方法比较简单



伸展树(splay tree)

保证从空树开始任意连续M次对树的操作最多花费O(M log N)时间。伸展树的基本想法是,当一个节点被访问后,他就要经过一系列AVL树的旋转被放到根上。如果一个节点很深,那么在其路径上就存在许多的节点也相对较深,通过重新构造可以是对所有这些节点的进一步访问所花费的时间变少(当一个节点被访问后,它很有可能不久又会再被访问)。伸展树不要求保留高度或平衡信息

当M次操作序列总的最坏情形运行时间为O(MF(N))时,就说它摊还(amortized)运行时间为O(F(N))。一颗伸展树每次操作的摊还代价是O(log N)。如果任意特定操作可以有最坏时间界O(N),而我们仍然要求一个O(log N)的摊还时间界,那么一个节点只要被访问就必须被移动。


展开:展开操作不仅将访问的节点移动到根处,而且还能将访问路径上大部分节点的深度大致减少一半。令X是访问路径上的非根节点,将在这个路径上进行旋转操作。如果X的父节点是树根,那么只要旋转X和树根即可;否则,X就有父亲(P)和祖父(G)。第一种情况是之字形(zig-zag),即X和P一个是左儿子一个是右儿子,这时需要像AVL树那样进行双旋转;第二种情况是一字形(zig-zig),X和P都是左/右儿子,这时应进行如下变换:


当访问路径太长而超出正常查找时间时,旋转对未来的操作有益;而当访问耗时很少时,这些旋转则不那么有益甚至有害



B树

B数是一个查找树,所有的数据都存储在树叶上,节点值的区分左边是闭区间右边是开区间(<= x <)。阶为M的B树具有以下结构特性:

1. 树的根或者是一片树叶,或者其儿子数在2到M之间

2. 除根外,所有非树叶节点的儿子数在M/2到M之间

3. 所有的树叶都在相同的深度上

在每一个内部节点上都含有指向该结点各儿子的指针P1,P2,...,PM和分别代表在子树P2,P3,...,PM中发现的最小关键字的值k1,k2,...,k M-1(如果指针是NULL则对应的k是未定义的)对于每一个节点,其子树P1中所有的关键字都小于子树P2的关键字,以此类推。


对B树进行进行插入时,要向上一直寻找到一个儿子节点数小于M的节点,且只有在访问路径上的那些内部节点才有可能发生变化。最简单的方法是分裂这个路径中所有儿子数为M的节点

删除节点时,如果删除后只剩一个关键字,可以选择和它的兄弟节点合并。如果节点的父亲失去了一个儿子,则需要向上检查到顶部;如果根节点失去了它的第二个儿子,则这个根也要删除,于是树就减少了一层


对于一般的M阶B树,当插入一个关键字时,唯一的困难发生在接收该关键字的节点已经具有M个关键字的时候,则需要将这个节点平均分成两个,这使得父节点多出一个儿子,所以必须检查这个节点是否可以被父节点接受;如果父节点已经具有M个儿子,那么这个父节点也要进行分裂。我们要重复这个过程直到找到一个父节点具有少于M个儿子。如果需要分裂根节点,则要创建一个新的根,这个根有两个儿子

分裂时,新的父节点应使用该节点右子树中最小的元素值


B数的深度最多是log M/2 N(以M/2为底的N的对数)。对于路径上的每个节点,使用折半查找可以以O(log M)的时间确定选择哪个分支,对于插入和删除运算,最坏情形的运行时间为O(M logM N) = O((M/log M) log N),不过一次查找只花费O(log N)。从运行时间考虑,M的最好(合法的)选择是3或4


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值