平衡树系列 「1」 从二叉查找树谈到平衡树
本蒟蒻也说不清什么时候会更新。。已更完
#0 pre
树是一种抽象的数据结构,其拥有强大的生命力。
本质上树是图的特例,但是树的特殊性质使它处理起比图方便得多。
而二叉树又是树的一种特例了,于是透过二叉树,我们拥有了丰富多彩的数据结构们。二叉树存图少见,但是用于数据结构则常见得很。为什么呢?二叉树有着友好的性质如下:
1> 二叉,只需记录左右儿子
2> 方便旋转的实现(以后会讲)
3> h(满二叉树)=O(log n)
因此利于数据的储存。
随机查找访问树内的节点只需要O(h(_TREE))的时间复杂度,减少了不必要的访问。
#1 什么是二叉查找树
二叉查找树本质是二分查找的推广。
比如我们有以下序列:
[ 2 4 6 8 10 12 14 16 ]
我们要查找的是6.怎么办?
[ 2 4 6 8 10 12 14 16 ]
l ^ r //与中点8比较,较小,r=mid-1
[ 2 4 6 8 10 12 14 16 ]
l ^ r //较大
[ 2 4 | 6 8 10 12 14 16 ]
l^r //较大
[ 2 4 6 8 10 12 14 16 ]
l^r //找到
为了确定范围,我们比较了三次。然后第4次比较说明6在序列中,且Seq[2]=6。
我们可以将匹配到每个元素的路径记录下来,用Trie树储存(额。。不懂没关系。。),像这样:
8
|-4
| |-6
| | \-8
| \-2
\-12
|-14
| \-16
\-10
//这个不符合顺序。。2,6及14,10与其子树位置应该是颠倒的。
那么为什么要有这么个结构呢?以后会讲到它比bi-search先进的地方。
#2 动态的二分查找
二叉查找树练习一(BST A)
题目描述
维护一个数据结构,使其可以: 1. 初始化为一个长度为n的有序序列(升序)(n<500001) 2. m次插入一个数(m<10001) 3. 查询数列的第k大数
//其实可以用块状数组艹过去。。
数组的插入是O(n)的.
1. 1 2 3 4 5 6 7 8 10 | 2
2. 1 2_3 4 5 6 7 8 10
^2 //bi-search O(log n)
3. 1 2 _ [ 3 4 5 6 7 8 10 ] //(move backward and there is one empty place) O(n)
4. 1 2 2 3 4 5 6 7 8 10
> time cost: O(n)
块状数组(lev 2)
1. [1 2 3] [4 5 6] [7 8 10] | 2
2. [1 2 3] [4 5 6] [7 8 10] ^chosen block for 2 //O(log sqrt n)
3. [1 2_3] [4 5 6] [7 8 10]
^chosen position for 2 //O(log sqrt n)
4. [1 2 _ [3]] [4 5 6] [7 8 10] //moving backward O(sqrt n)
5. [1 2 2 3] [4 5 6] [7 8 10] //insert
6. [ ] //check length =4 <=2sqrt(n),not mixing
7* mixing: O(sqrt n)
> time cost: O(sqrt n)
BST [ 4 ] | [ 2 ][ 7 ] | 9 [ 1 ][ 3 ][ 6 ][ 8 ] | [5] [10] | [ 4 ] [ 2 ][ 7 ] [ 1 ][ 3 ][ 6 ][ 8 ] [5] [10] <- find that 9<10 //finding O(h(n)「=log n expected for random data」)
[] <- a null node created * yellow backgrounded nodes are the nodes that are accessed [ 4 ] [ 2 ][ 7 ] [ 1 ][ 3 ][6 ][ 8 ] [5] [ 10 ] <- insert to its left child [9]
time cost: O(log n) expected for random data.
为什么要强调对于随机数据的期望呢?这些操作都不是严格O(log n)的。。在顺序数据下完全可以退化为O(n). //感受到了世界对我深深的恶意。。
。。比如,有顺序的数据。。自己脑补一下那个场面。。我们的萌萌哒二叉树就变成一个链表了啊囧。。
怎么预防呢?于是我们讲到了平衡树。但是在讲平衡树之前,有必要将普通(虽然写裸BST的比平衡树的要少啊囧)BST讲完。
#3 删除节点
我们的萌萌哒二叉树。。呸,BST,如何删除节点呢?这个要情况讨论了。。
Case 1: 没有子树.
- 删除即可.
- 维护父亲的标记(size域之类)
复杂度:O(1「deletion」+log n 「维护标记」)
Case 2: 有一棵子树.
- 删除这个节点,将它的子树拖过来
- 维护标记...
复杂度:O(1「deletion」+log n 「维护标记」)
Case 3: 完蛋两棵子树都有怎么办
- 找到它在序列里的后继,交换位置后,删除之
- 维护标记...
复杂度:O(1「deletion」+log n 「维护标记」)
Case 1 和 Case 2是显然的。让我们证明Case 3的复杂度及不会使树的高度增加。(假设原先BST平衡即O(h(n))=O(log n))
首先给出BST的性质,这是显然的.
1. max{left(n))<=val(n)
2. min(right(n))>=val(n)
那么由节点N有两棵子树可知其有右子树.则它的后继在其右子树中.
记其后继为Succ(N),则Succ(N)最多只有右子树.
证明:反证法易得.
由Case 1及Case 2说明删除(不维护标记)是O(1)的
由实际删除的是Succ(N)且Succ(N)的删除是Case 1 或 Case 2可得高度不增
获取Succ(N)的办法://无普遍性。
01| K=right(N)
02| while(left(K)) K=left(K)
03| return K
#4 Prev和Succ函数
正如delete操作,Prev和Succ是两个很重要的函数。
我无法证明这两个函数的时间复杂度是O(log n)的。渐进上界是O(log n)易得,但是其均摊复杂度我就不知道了囧。。果然是学渣。。
这两个函数可以用O(n)的时间遍历这个序列中的所有节点(当然,按顺序.)
让我们看看后继怎么遍历的。
[ 8 ] [ 4 ] [ 12 ] [ 2 ] [ 6 ] [ 10 ] [ 14 ] [1] [3] [5] [7] [9] [11] [13][15] [ 8 ] [ 4 ] [ 12 ] [ 2 ] [ 6 ] [ 10 ] [ 14 ] [1] [3] [5] [7] [9] [11] [13][15] [ 8 ] [ 4 ] [ 12 ] [ 2 ] [ 6 ] [ 10 ] [ 14 ] [1] [3] [5] [7] [9] [11] [13][15] [ 8 ] [ 4 ] [ 12 ] [ 2 ] [ 6 ] [ 10 ] [ 14 ] [1] [3] [5] [7] [9] [11] [13][15] ....
稍微动一下脑子,可以想出它的规则:
1. 如果该节点有右子树,那么它的后继一定在它的右子树中,且是最小的一个。正确性显然,得到的算法就是:
01 走向右儿子
02 一直向左儿子走直到没有左儿子
03 //正确性显然
2. 如果该节点没有右儿子
01 找到第一个大于它的祖先
02 //正确性显然。
求前导就反过来。
#5 select 和 rank
select,即按第k大寻找一个节点。一般返回指针。 rank,即返回节点是第k大。 (当然大小没有唯一的标准,是基于比较的) select 和 rank 的实现需要一个size域,可以当作一种标记,记录以这个节点子树中节点的个数。# select start select k: a=tree.root while k: s=a.lc.size if s>=k a=a.lc; elif s==k+1 break; else k-=s+1, a=a.rc; ret a # select end # rank start rank p: a=tree.root k=1 # find p dynamically # note: only searches for the lower bound. For easier implementation while a: if a.val == p.val: k+=a.lc.size, break if a.val < p.val: a=a.lc else: k+=a.rc.size+1 a=a.rc ret k
#6 旋转
[ 2 ]
[1] [3]
或
[ 1 ]
[ 2 ]
[ 3 ]
考虑一棵链树.
[ a ]
[ b ]
[c] [d]
此中,c<b<d<a.
我们可以机智地将它变换形态变成:
[ b ]
[ c ][ a ]
[ d ]
注意这样是不会有冲突的.而且我们改变了树的形状.我们把这个操作称为`对a的右旋操作'
刚好和它对称的,则有`对a的左旋操作'.具体操作脑补一下即可.
#7 谈到平衡树为止
下一篇: 平衡树系列 「2」 平衡树漫谈
#Section tail
模板将会写好的。这里挖个坑撒。