平衡树系列 「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 旋转

BST拥有好的性质。但是,我们很悲剧地发现,它不能自己维护平衡。这是其致命的缺点。 很显然,对于一个相同的序列,有不同的构造BST的办法,比如1 2 3
[  2  ]
[1] [3]

[ 1 ]
[ 2 ]
[ 3 ]
第一种情况较为理想,这样的高度是 (log n)级别的。显然,如果我们先插入 [2]再插入 [1],[3]就可以了。但是如果顺序的数据,就会退化为第二种情况,高度 O(n),且由于树的特殊性,我们无法二分查找。 我们需要一种操作,使得一棵BST可以(有效地)改变形态且不变大小关系。
考虑一棵链树.
[ a ]
[ b ]
[c] [d]
此中,c<b<d<a.
我们可以机智地将它变换形态变成:
[ b ]
[ c ][ a ]
[ d ]
注意这样是不会有冲突的.而且我们改变了树的形状.我们把这个操作称为`对a的右旋操作'
刚好和它对称的,则有`对a的左旋操作'.具体操作脑补一下即可.

#7 谈到平衡树为止

于是我们讲到了平衡树。平衡树本质上是一个二元组(maintain,T)将maintain函数应用在树T上。 当然平衡树种类很多。。有些平衡树是不用旋转的。但是基本上是用旋转的。
下一篇: 平衡树系列 「2」 平衡树漫谈

#Section tail

模板将会写好的。这里挖个坑撒。

posted @ 2014-11-28 00:01  zball  阅读(390)  评论(2编辑  收藏  举报