1
? 并查集
? 静态搜索表
? 二叉搜索树
? AVL树
2
并查集 (Union-Find Sets)
? 并查集支持以下三种操作:
? Union (Root1,Root2) //合并操作
? Find (x) //搜索操作
? InitUFSets (s ) //初始化操作
? 对于并查集来说,每个集合用一棵树表示。
? 为此,采用 树的双亲表示 作为集合存储表示。
集合元素的编号从 0到 n-1。其中 n 是最大
元素个数。
3
? 在 双亲表示 中,第 i 个数组元素代表包含
集合元素 i 的树结点 。初始时,根结点的
双亲为 -1,表示集合中的元素个数。
? 在同一棵树上所有结点所代表的集合元素
在同一个子集合中。
? 为此,需要有两个映射:
? 集合元素到存放该元素名的树结点间的
对应;
? 集合名到表示该集合的树的根结点间的
对应。
4
? 设 S1= {0,6,7,8 },S2= { 1,4,9 },S3= { 2,3,
5 }
集合名 指针
0 S1
1 S2
2 S3
0 4 2
76 8 1 9 3 5
? 为简化讨论, 忽略实际的集合名, 仅用表
示集合的树的根来标识集合 。
5
? 初始时,InitUFSets(S) 构造一个森林,每棵
树只有一个结点,表示集合中各元素自成一
个子集合 S[i] = -1,i = 0,1,…,n-1。
? 用 Find(S,i) 寻找集合元素 i 的根 。
? 如果有两个集合元素 i 和 j:
Find(S,i) == Find(S,j)
表明这两个元素在同一个集合中,
? 如果两个集合元素 i 和 j 不在同一个集合
中,可用 Union(S,i,j) 将它们合并到一个集
合中 。
6
S1
下标
parent
集合 S1,S2 和 S3 的双亲表示
-4 4 -3 2 -3 2 0 0 0 4
0 1 2 3 4 5 6 7 8 9
0
76 8
4
1 9
2
3 5
S2 S3
7
S1 ? S2的可能的表示方法
下标
parent
集合 S1 ? S2和 S3的双亲表示
-7 4 -3 2 0 2 0 0 0 4
0 1 2 3 4 5 6 7 8 9
0
76 8 4
1 9
4
1 90
876
8
并查集的结构定义
const int SetSize = 50; //并查集元素个数
typedef struct { //并查集结构定义
int parent[SetSize]; //集合元素数组
} UFSets;
void InitUFSets (UFSets *S) { //集合初始化
for ( int i = 0; i < SetSize; i++ )
S->parent[i] = -1;
} //每一个自成一个单元素集合
9
?并查集操作的算法
? 查找
-5 0 1 2 3
0 1 2 3 4
Find (S,4)
Find (S,3) = 3
Find (S,2) =2
Find (S,1) = 1
Find (S,0) = 0
= -5 < 0 结束
10
int Find (UFSets * S,int x ) {
if ( S->parent [x] < 0 ) return x;
else return Find (S,S->parent [x] );
}
-5 0 1 2 3parent
Parent[4]
=3 Parent[3]
=2Parent[2]
=1
Parent[1]
=0
Parent[0]
=-5
0 1 2 3 4
11
void Union (UFSets *S,int Root1,int Root2) {
//求两个不相交集合 Root1与 Root2的并
S->parent[Root1] += S->parent[Root2];
S->parent[Root2] = Root1;
//将 Root2连接到 Root1下面
}
? Find和 Union操作性能不好。假设最初 n 个
元素构成 n 棵树组成的森林,S->parent[i]
= -1。做处理 Union(n-2,n-1),…,Union(1,
2),Union(0,1)后,将产生退化的树。
12
? 合并 -1-1 -1 -1 -10 2 3 4
-3
-5
0
3
2
1
3
3
4 1
3 3
22
0
2
3
1
4
Union(0,1)
-2
3
-4
1
4
2
1
2
3
4
Union(1,2)
Union(2,3)
Union(3,4)
13
? 执行一次 Union操作所需时间是 O(1),n-1
次 Union操作所需时间是 O(n)。
? 若再执行 Find(0),Find(1),…,Find(n-1),若
被搜索的元素为 i,完成 Find(i) 操作需要时
间为 O(i),完成 n 次搜索需要的总时间将达
到
? 改进的方法
? 按树的结点个数合并
? 按树的高度合并
? 压缩元素的路径长度
?
?
?
n
i
ni
1
2OO )()(
14
? 按树结点个数合并
?结点个数多的树的根结点作根
-1-1 -1 -1 -1
0
1
2 3 4
-1 -1
0
-7
2
5 6
-5 -2
2 2
3 3
2 0
1
3
4
5
6
2
3 3 0
2 0
56
2
31
4
Union(2,0)
15
void WeightedUnion (UFSets *S,
int Rt1,int Rt2 ) {
//按 Union的加权规则改进的算法
int temp = S->parent[Rt1] + S->parent[Rt2];
if ( S->parent[Rt2] < S->parent[Rt1] ) {
S->parent[Rt1] = Rt2; //Root2中结点多
S->parent[Rt2] = temp; //Root1指向 Root2
}
else {
S->parent[Rt2] = Rt1; //Root1中结点多
S->parent[Rt1] = temp; //Root2指向 Root1
}
}
16
? 按树高度合并
? 高度高的树的根结点作根
-0-0 -0 -0 -0
0
1
2 3 4
-0 -0
0
-2
2
5 6
-2 -1
2 2
3 3
2 0
1
3
4
5
6
2
3 3 0
2 0
56
2
31
4
Union(2,0)
17
Union操作的折叠规则
? 为进一步改进树的性能, 可以使用如下折叠
规则来, 压缩路径, 。 即,如果 j 是从 i 到
根 root 的 路 径 上 的 一 个 结 点, 且 S-
>parent[j] != root[j],则把 S->parent[j] 臵
为 root[i]。0 0
6 7 8 6 7 8
1 9 1
9
3 5
3
5
从 i = 5 压缩路径
18
int CollapsingFind (UFSets *S,int i ) {
//使用折叠规则的搜索算法
int j = i;
while ( S->parent[j] >= 0 )
j = S->parent[j];
//让 j 循双亲指针走到根
while ( i != j ) { //换 parent[i] 到 j
int temp = S->parent[i];
S->parent[i] = j; i = temp;
}
return j;
}
19
搜索 (Search)的概念
静态搜索表
? 所谓 搜索, 就是在数据集合中寻找满足某
? 种条件的数据对象 。
? 搜索的结果通常有两种可能:
? 搜索成功,即找到满足条件的数据对象
这时,作为结果,可报告该对象在结构中
的位臵,还可给出该对象中的具体信息。
? 搜索不成功,或搜索失败。作为结果,
应报告一些信息,如失败标志、位臵等。
20
? 通常称用于搜索的数据集合为 搜索结构,
它是由 同一数据类型的对象 (或记录 )组成。
? 在每个对象中有若干属性,其中有一个属
性,其值可唯一地标识这个对象。称为关
键码。 使用基于关键码的搜索,搜索结果
应是唯一的。 但在实际应用时,搜索条件
是多方面的,可以 使用基于属性的搜索方
法,但搜索结果可能不唯一。
21
? 实施搜索时有两种不同的环境。
? 静态环境,搜索结构在插入和删除等操
作的前后不发生改变。
? 静态搜索表
? 动态环境,为保持较高的搜索效率,搜
索结构在执行插入和删除等操作的前后
将自动进行调整,结构可能发生变化。
? 动态搜索表
22
#define MaxSize 100 //搜索表最大尺寸
typedef int DataType; //搜索数据的类型
typedef struct { //搜索表结点定义
DataType key; //关键码域
other; //其他数据域
} ListNode;
typedef struct dataList { //搜索表结点定义
ListNode data[MaxSize]; //数据存储数组
int n; //数组当前长度
}
静态搜索表结构的定义
23
? 衡量一个搜索算法的时间效率的标准是:
在搜索过程中关键码的 平均比较次数, 也
称为 平均搜索长度 ASL(Average Search
Length),通常它是 搜索结构中对象总数
n的函数 。
? 在静态搜索表中,利用数组元素的下标作
为数据对象的存放地址 。 搜索算法 根据给
定值 x,在数组中进行搜索 。 直到 找到 x在
数组中的位臵或可确定在数组中 找不到 x
为止 。
? 另外衡量一个搜索算法还要考虑算法所需
要的存储量和算法的复杂性等问题。
24
顺序搜索 (Sequential Search)
? 所谓顺序搜索,又称线性搜索,主要用于在
线性结构中进行搜索 。
? 设若表中有 n 个对象, 则顺序搜索从表的
先端 (或后端 ) 开始,顺序用各对象的关键
码与 给定值 x 进行比较,直到找到与其值相
等的对象,则搜索成功 ; 给出该对象在表中
的位臵 。
? 若整个表都已检测完仍未找到关键码与 x相
等的对象,则搜索失败 。 给出失败信息 。
25
int LinearSearch ( dataList A,DataType x ) {
//在数据表 A.data[1..n]中顺序搜索关键码为 x
//的数据对象,A.data[0].key 作为控制搜索自动
//结束的, 监视哨, 使用
A.data[0].key = x; int i = A.n;
//将 x送 0号位置设置监视哨
while ( A.data[i].key != x ) i-- ;
//从后向前顺序搜索
return i;
}
设臵“监视哨”的顺序搜索算法
26
顺序搜索的平均搜索长度
??
?
?
?
?
???
1
0
1
0
n
i
i
n
i
iis u c c pcpA S L ) 1 (,
)( 1
1
???? ?
?
inpAS L
n
i
is u c c
设搜索第 i 个元素的概率为 pi,搜索到第 i 个
元素所需比较次数为 ci,则搜索成功的平均
搜索长度,
在顺序搜索并设臵“监视哨”情形:
ci = n- i +1,i = n,n-1,?,1,因此
27
.)()(?
?
???????? n
i
s u c c
nnn
n
in
n
A S L
1 2
1
2
1111
在等概率情形,pi = 1/n,i = 1,2,?,n。
在搜索不成功情形,ASLunsucc = n+1.
28
顺序搜索的递归算法
int SequentSearch ( dataList A,dataType x,
int& loc ) {
//在数据表 A.data[0..n-1] 中搜索其关键码与
//给定值匹配的对象,函数返回其表中位置。
//参数 loc 是在表中开始搜索位置
if ( loc >= A.n ) return -1; //搜索失败
else if ( A.data[loc].key == x ) return loc;
//搜索成功
else return SequentSearch ( A,x,loc+1 );
//递归搜索
}
29
int SequentSearch ( dataList A,DataType x ) {
//在数据表 A.data[0..n-1] 中 顺序搜索关键码
为 x 的数据对象
for ( int i = 0; i < A.n; i++ )
if ( A.data[i].key == x ) return i; //成功
else if ( A.data[i].key > x ) break;
return -1;
//顺序搜索失败,返回失败信息
}
基于有序顺序表的顺序搜索算法
30
? 有序顺序表的顺序搜索
( 10,20,30,40,50,60 )
10
50
60
=
=
=
=
=
=
20
30
40
<
<
<
<
<
<
>
>
>
>
>
>
? ?
2
71
6
1 5
0
??? ?
?i
s u c c iA S L
? ?
7
27
61
7
1 5
0
??
?
?
?
?
?
???
?
?
?i
un s uc c
i
A S L
31
基于有序顺序表的折半搜索
? 设 n个对象存放在一个按其关键码从小到
大排好了序的有序顺序表中 。
? 折半搜索时,先求位于搜索区间正中的对象
的下标 mid,用其关键码与 给定值 x比较,
? A.data[mid].key == x,搜索成功 ;
? A.data[mid].key > x,把搜索区间缩小到
表的 前半部分, 继续折半搜索 ;
? A.data[mid].key < x,把搜索区间缩小到
表的 后半部分, 继续折半搜索 。
? 如果搜索区间已缩小到一个对象, 仍未找
到想要搜索的对象, 则搜索失败 。
32
搜索成功的例子
-1 0 1 3 4 6 8 10 126
0 1 2 3 4 5 6 7 8
搜索 low mid high6
6 8 10 12
5 6 7 8
low mid high
6 6
5
low mid high
6
33
搜索失败的例子
-1 0 1 3 4 6 8 10 125
0 1 2 3 4 5 6 7 8
搜索 low mid high5
6 8 10 12
5 6 7 8
low mid high
65
5
low mid high
5
34
int BinarySearch ( dataList A,DataType x,
int low,int high ) {
int mid = -1;
if ( low <= high ) {
mid = ( low + high ) / 2;
if ( A.data[mid].key < x )
mid = BinarySearch ( A,x,mid +1,high );
else if ( A.data[mid].key > x )
mid = BinarySearch ( A,x,low,mid -1 );
}
return mid;
}
基于有序顺序表的折半搜索递归算法
35
int BinarySearch ( dataList A,DataType x ) {
int high = A.n-1,low = 0,mid;
while ( low <= high ) {
mid = ( low + high ) / 2;
if ( A.data[mid].key < x )
low = mid + 1; //右缩搜索区间
else if ( A.data[mid].key > x )
high = mid - 1; //左缩搜索区间
else return mid; //搜索成功
}
return -1; //搜索失败
}
基于有序顺序表的折半搜索迭代算法
36
? 有序顺序表的折半搜索的判定树
( 10,20,30,40,50,60 )
10 50
=
=
==
=
=
30
<
<
<
<
<
<
>
>
> >
> >
20 40 60
ASLunsucc = (2*1+3*6)/7
= 20/7
ASLsucc = (1+2*2+
3*3)/6 = 14/6
37
折半搜索性能分析
? 若设 n = 2h-1,则描述折半搜索的判定树是
高度为 h-1 的满二叉树 。
2h = n+1,h = log2(n+1)。
? 第 0层结点有 1个,搜索第 0层结点要比较 1次 ;
第 1层结点有 2个,搜索第 1层结点要比较 2
次 ; …,第 i (0 ? i ? h) 层结点有 2i 个,搜索第
i 层结点要比较 i+1次, … 。
? 假定每个结点的搜索概率相等,即 pi = 1/n,
则搜索成功的平均搜索长度为
38
)221)(23
2211
11
122
1
1
0
1
??
?
?
?
?
????????
???????? ??
hh
n
i
i
n
0i
iis u c c
hh
(
n
C
n
CpA S L
?
121)(
221)( 232211 1221
????
???????????? ??
h
hh
h
hh?
11)(log11)(log
1
)1)(1 ) l o g((
1
1)21)((
1
22
2
?????
?
?
????????
nn
n
n
nnn
n
h
n
A S L
h
s u c c
可以用归纳法证明
这样
39
定义
二叉搜索树或者是一棵空树,或者是具有
下列性质的二叉树:
?每个结点都有一个作为搜索依据的关键
码 (key),所有结点的关键码互不相同。
?左子树 (如果存在 )上 所有结点的关键码
都小于根结点的关键码 。
?右子树 (如果存在 )上 所有结点的关键码
都大于根结点的关键码 。
?左子树和右子树也是二叉搜索树。
二叉搜索树 ( Binary Search Tree )
40
35
15 45
50402510
20 30
二叉搜索树例
? 结点左子树上
所有关键码小
于结点关键码
? 右子树上所有
关键码大于结
点关键码
? 注意:若从根结点到某个叶结点有一条路
径,路径左边的结点的关键码不一定小于
路径上的结点的关键码。
41
n 个结点的二叉搜索树的数目
【例】 3 个结点的二叉搜索树
5
1*2*3
4*5*6*
4
1C
13
1 3
3*2 ???
1
2
2
1
3
3 1 3
2
1
2
3
1
2
3
{123} {132} {213} {231} {312} {321}
42
二叉搜索树的结构定义
typedef char DataType; //树结点数据类型
typedef struct node { //搜索树结点定义
DataType data;
struct node *leftChild,*rightChild;
} BstNode;
typedef BstNode *BST; //二叉搜索树定义
如果对一棵二叉搜索树进行中序遍历,可
以按从小到大的顺序,将各结点关键码排列
起来,所以也称二叉搜索树为二叉排序树。
43
在二叉搜索树上进行搜索,是一个从根结
点开始,沿某一个分支逐层向下进行比较判
等的过程 。它可以是一个递归的过程。
二叉搜索树上的搜索
35
15 45
50402510
20 30
搜索 45
搜索成功
搜索 28
搜索失败
44
? 假设想要在二叉搜索树中搜索关键码为 x
的元素, 搜索过程从根结点开始 。
? 如果根指针为 NULL,则搜索不成功;否
则用给定值 x 与根结点的关键码进行比较:
? 如果给定值等于根结点的关键码, 则搜
索成功 。
? 如果给定值小于根结点的关键码, 则继
续递归搜索根结点的左子树;
? 否则 。 递归搜索根结点的右子树 。
45
? 搜索成功时检测指针停留在树中某个结点。
? 搜索不成功时检测指针停留在某个外结点
(失败结点)。
35
15 45
502510
20 30
搜索 22
搜索 45
46
BstNode * Find ( BstNode *ptr,DataType x ) {
//二叉搜索树的迭代的搜索算法
if ( ptr != NULL ) {
BstNode * p = ptr; //从根搜索
while ( p != NULL ) {
if ( p->data == x ) return p;
if ( p->data < x )
p = p->rightChild; //右子树
else p = p->leftChild; //左子树
}
}
return NULL; //搜索失败
}
47
35
15 45
50402510
20 30
28
插入新结点 28
二叉搜索树的插入
? 每次结点的插入,都要从根结点出发搜索
插入位臵,然后把
新结点作为叶结点
插入。
? 为了向二叉搜索树
中插入一个新元素,
必须先检查这个 元
素是否在树中已经
存在 。
48
? 在插入之前, 先使用搜索算法在树中检查
要插入元素有还是没有 。
? 搜索成功, 树中已有这个元素,不再插入 。
? 搜索不成功, 树中原来没有关键码等于
给定值的结点, 把新元素加到搜索操作
停止的地方 。
递归的二叉搜索树插入算法
void Insert ( DataType x,BstNode * & ptr) {
//将新元素 x插到以 *ptr为根的二叉搜索树中
49
if ( ptr == NULL ) { //空二叉树
ptr = new BstNode; //创建结点
if ( ptr == NULL )
{ cout <<,存储不足 " << endl; exit (1); }
ptr->data = x;
ptr->leftChild = ptr->rightChild = NULL;
}
else if ( x < ptr->data ) //在左子树插入
Insert ( x,ptr->leftChild );
else if ( x > ptr->data ) //在右子树插入
Insert ( x,ptr->rightChild );
}
50
输入数据,建立二叉搜索树的过程
输入数据 { 53,78,65,17,87,09,81,15 }
53 53
78
53
78
65
53
78
65
17
53
78
65 87
17
53
78
6509
17
87
53
78
65
81
17
8709
53
78
65
15
17
8709
81
51
void CreateBST ( BstNode *& T,
DataType Refvalue ) {
//输入数据,建立二叉搜索树 。 RefValue 是输入
//结束标志。这个值应取不可能在输入序列中
//出现的值,例如输入序列的值都是正整数时,
//取 RefValue为 0或负数。
dataType x; T = NULL; cin >> x;
while ( x != RefValue ) {
if ( Find (T,x) == NULL ) Insert ( x,T );
cin >> x;
}
}
52
同样 3 个数据 { 1,2,3 },输入顺序不同,
建立起来的二叉搜索树的形态也不同。这直
接影响到二叉搜索树的搜索性能。
如果输入序列选得不好,会建立起一棵单
支树,使得二叉搜索树的高度达到最大。
{2,1,3}
{1,2,3} {1,3,2} {2,3,1} {3,1,2} {3,2,1}
1
2
3
1
1 1
1
3
2 2
2
3
3
2
3
53
二叉搜索树的删除
? 在二叉搜索树中删除一个结点时, 必须将
因删除结点而断开的二叉链表重新链接起
来, 同时确保二叉搜索树的性质不会失去 。
? 为保证在删除后树的搜索性能不至于降低,
还 需要防止重新链接后树的高度增加 。
? 删除叶结点, 只需将其双亲结点指向它
的指针清零, 再释放它即可 。
? 被删结点缺右子树, 可以拿它的左子女
结点顶替它的位臵, 再释放它 。
54
? 被删结点缺左子树, 可以拿它的右子女
结点顶替它的位臵, 再释放它 。
? 被删结点左, 右子树都存在, 可以在它
的右子树中寻找中序下的第一个结点 (关
键码最小 ),用它的值填补到被删结点中,
再来处理这个结点的删除问题 。
53
78
65
17
8709
23
45
删除 45
缺右子树,用
左子女顶替
53
78
65
17
8709 23
55
88
53
78
88
17
9409 23
删除 78
缺左子树,用
右子女顶替
53
94
88
17
09 23
53
78
81
17
9409 45
删除 78
在右子树上找
中序下第一个
结点填补23
65
53
81
88
17
9409 45
23
65
56
二叉搜索树性能分析
? 对于有 n 个关键码的集合,其关键码有 n!
种不同排列,可构成不同二叉搜索树有
(棵 )
? 用树的搜索效率来评价这些二叉搜索树。
? 在树中增加失败结点 (外部结点 ),它们是搜
索失败时到达的结点,物理上实际不存在。
? 增加了外部结点的二叉搜索树称为扩充二
叉搜索树。
C n nn 21 1?
57
? 在扩充二叉搜索树中
? ○ 表示内部结点, 包含了关键码集合中
的某一个关键码;
? □ 表示外部结点, 代表各关键码间隔中
的不在关键码集合中的关键码 。
? 在每两个外部结点间必存在一个内部结点 。
58
? 例,已知关键码集合 { a1,a2,a3 } = { do,if,
to },对应搜索概率 p1,p2,p3,在各搜索不
成功间隔内搜索概率分别为 q0,q1,q2,q3。
可能的二叉搜索树如下所示。
do
if
to
do
if
to
q0 q1
p1 q2
p2 q3
p3
q0 q1 q2 q3
p1
p2
p3
(a) (b)
59
do
if
to
扩充二叉搜索树
q0
q1
p1
q2
p2
q3
p3
do
if
to
q0
q1
p1
q2
p2
q3
p3
(d)(c) do
if
to
q0
q1
p1
q2
p2 q3
p3
(e)
60
一棵扩充二叉搜索树的 搜索成功的平均搜
索长度 ASLsucc可以定义为 该树所有内部结点
上的权值 p[i]与搜索该结点时所需的 关键码
比较次数 c[i] (= l[i] + 1)乘积之和:
).][(*][ 1p
1
?? ?
?
i liAS L
n
i
s u c c
搜索不成功的平均搜索长度 ASLunsucc为树中
所有外部结点上权值 q[j]与到达外部结点所需
关键码比较次数 c'[j](= l'[j])乘积之和:
?
?
?
n
j
u n s u c c jljAS L
0
q ].[ *][
61
(1) 相等搜索概率的情形
设树中所有内, 外部结点的搜索概率都相等:
p[i] = 1/3,1 ? i ? 3
q[j] = 1/4,0? j ? 3
图 (a),ASLsucc = 1/3*3+1/3*2+1/3*1 = 6/3 = 2
ASLunsucc = 1/4*3*2+1/4*2+1/4*1 = 9/4
图 (b),ASLsucc = 1/3*1+1/3*2*2 = 5/3
ASLunsucc = 1/4*2*4 = 2
图 (c) ?图 (e),ASLsucc = 2,ASLunsucc = 9/4
图 (b)的情形所得的平均搜索长度最小。
62
一般把平均搜索长度达到最小的扩充二叉搜
索树称作最优二叉搜索树。
在 相等搜索概率的情形 下,所有内部、外部
结点的搜索概率都相等,视它们的权值都为 1。
同时,第 k 层有 2k 个结点,k = 0,1,?。则有
n 个内部结点的扩充二叉搜索树的内部路径长
度 I 至少等于序列
63
0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,…
的 前 n 项 的和。
因此,最优二叉搜索树的搜索成功的平均搜
索长度和搜索不成功的平均搜索长度分别为,
? ?? ?.?
?
??
n
i
s u c c iA S L
1
2 1l o g
? ?,?
?
??
?
1n
ni
u n s u c c iAS L
2
1
2l o g
(2) 不相等搜索概率的情形
设树中所有内、外部结点的搜索概率互不
相等。
64
p[1] = 0.5,p[2] = 0.1,p[3] = 0.05
q[0] = 0.15,q[1] = 0.1,q[2] = 0.05,q[3] = 0.05
do
if
to
do
if
to
q0=0.15
q1=0.1
p1=0.5 q2=0.05
p2=0.1
q3=0.05p3=0.05
q0=0.15 q1=0.1 q2=0.05
q3=
0.05
p1=0.5
p2=0.1
p3=0.05
(a) (b)
图 (a),ASLsucc = 0.5*3+0.1*2+0.05*1 = 1.75,
ASLunsucc = 0.15*3+0.1*3+0.05*2+0.05*1= 0.9。
ASL = ASLsucc + ASLunsucc = 2.65。
65
do
if
to
q0=
0.15
q1=0.1
p1=0.5
q2=0.05
p2=0.1
q3=0.05
p3=0.05
do
if
to
q0=0.15
q1=0.1
p1=0.5
q2=0.05
p2=0.1
q3=0.05p3=0.05
(d)(c)
图 (c), ASLsucc = 0.5*1+0.1*2+0.05*3 = 0.85,
ASLunsucc = 0.15*1+0.1*2+0.05*3+0.05*3 = 0.75.
ASL = 0.85+0.75 = 1.6
图 (b), ASL = 1.9; 图 (d), ASL = 2.15;
66
do
if
to
q0=0.15
q1=0.1
p1=0.5
q2=0.05
p2=0.1 q3=0.05
p3=0.05
(e)
图 (e), ASLsucc = 0.5*1
+0.05*2+0.1*3 = 0.9;
ASLunsucc = 0.15*1+
0.1*3+0.05*3+0.05*2
= 0.7;
ASL = 0.9+0.7 = 1.6.
由此可知,图 (c)和图 (e)的情形下树的平均搜
索长度达到最小,因此,图 (c)和图 (e)的情形
是最优二叉搜索树。
67
AVL树 高度平衡的二叉搜索树
AVL树的定义
一棵 AVL树或者是空树,或者是具有下列
性质的二叉搜索树,它的左子树和右子树都
是 AVL树,且左子树和右子树的高度之差的
绝对值不超过 1。
高度不平衡 高度平衡A
B
C
A
B
C
D
ED E
68
结点的平衡因子 balance (balance factor)
? 每个结点附加一个数字,给出该结点 右子
树的高度减去左子树的高度 所得的 高度差,
这个数字即为结点的平衡因子 balance
? AVL树任一结点平衡因子只能取 -1,0,1
? 如果一个结点的平衡因子的绝对值大于 1,
则这棵二叉搜索树就失去了平衡,不再是
AVL树。
? 如果一棵二叉搜索树是高度平衡的,且有 n
个结点,其高度可保持在 O(log2n),平均搜
索长度也可保持在 O(log2n)。
69
AVL树的结构定义
typedef int DataType; //结点数据类型
typedef struct node { //AVL树结点定义
DataType data; //结点数据域
int balance; //结点平衡因子域
struct node *leftChild,*rightChild;
//结点左、右子女指针域
} AVLNode;
typedef AVLNode * AVLTree; //AVL树
70
平衡化旋转
? 如果在一棵平衡的二叉搜索树中插入一个
新结点,造成了不平衡。此时必须调整树
的结构,使之平衡化。
? 平衡化旋转有两类:
? 单旋转 (左旋和右旋 )
? 双旋转 (左平衡和右平衡 )
? 每插入一个新结点时,AVL 树中相关结点
的平衡状态会发生改变。因此,在插入一
个新结点后,需要 从插入位臵沿通向根的
路径回溯, 检查各结点的平衡因子 。
71
? 如果在某一结点发现高度不平衡,停止回
溯。从发生不平衡的结点起,沿刚才回溯
的路径取直接下两层的结点。
? 如果这三个结点处于一条直线上,则采用
单旋转进行平衡化 。 单旋转可按其方向分
为左单旋转和右单旋转,其中一个是另一
个的镜像,其方向与不平衡的形状相关。
? 如果这三个结点处于一条折线上,则采用
双旋转进行平衡化 。 双旋转分为先左后右
和先右后左两类。
72
右单旋转 左单旋转 左右双旋转 右左双旋转
左单旋转 (RotateLeft )
h
h h
A
C
E
B
D
h
h
h+
1
B
A
C
ED
h h
h+
1
C
EA
B D
+1 +2
0 +1 0
0
73
右单旋转 左单旋转 左右双旋转 右左双旋转
左单旋转 (RotateLeft )
h
h h
A
C
E
B
D
h
h
h+
1
B
A
C
ED
h h
h+
1
C
EA
B D
+1 +2
0 +1 0
0
74
? 在子树 E中插入新结点, 该子树高度增 1导
致结点 A的平衡因子变成 +2,出现不平衡 。
? 沿插入路径检查三个结点 A,C和 E。 它们
处于方向为, \”的直线上, 需做左单旋转 。
? 以结点 C为旋转轴, 让结点 A反时针旋转 。
右单旋转 (RotateRight )
? 在左子树 D上插入新结点使其高度增 1,导
致结点 A的平衡因子增到 -2,造成不平衡。
? 为使树恢复平衡,从 A沿插入路径连续取 3
个结点 A,B和 D,它们处于一条方向为,/”
的直线上,需要做右单旋转。
75
h
h h
A
C
E
B
D h
h+
1
B
A
C
ED
h h
h+
1
CE
A
B
D
? 以结点 B为旋转轴,将结点 A顺时针旋转。
h
0
0
0
-1
-1
-2
先左后右双旋转 (RotationLeftRight)
? 在子树 F或 G中插入新结点, 该子树的高度
增 1。 结点 A的平衡因子变为 -2,发生了不
平衡 。
76
插入
00
-1 -2
+1
-1
h
h
A
C
ED
h-1h-1
h
h
A
h-1h
B C
ED
B
左单
旋转
F G F G
? 从结点 A起沿插入路径选取 3个结点 A,B
和 E,它们位于一条形如, ?” 的折线上,
因此需要进行先左后右的双旋转 。
? 以结点 E为旋转轴, 将结点 B反时针旋转 。
77
0
0
-2 0
0 +1
h
h
A
CE
D
-2
h-1
h
hh
A
h-1h
B C
E
D
B
右单
旋转
F
G FG
78
先右后左双旋转 (RotationRightLeft)
? 右左双旋转是左右双旋转的镜像 。
? 在子树 F或 G中插入新结点, 该子树高度增
1。 结点 A的平衡因子变为 2,发生了不平
衡 。
? 从 结点 A起沿插入路径选取 3个结点 A,C
和 D,它们位于一条形如, ?” 的折线上,
需要进行先右后左的双旋转 。
79
插入
右
单
旋
转
+1
0
0
0
-1
+1
0h
h
A
C
ED
h-1
B
F G
h-1
+2
0
0
0h
h
A
C
E
h
B
F G
h-1
D
? 首先做右单旋转:以结点 D为旋转轴, 将结
点 C顺时针旋转, 以 D代替原来 C的位臵 。
80
00
+2 0
-1 0
h
h
A
C
E
+2
h-1
h
hh
A
h-1 h
B C
E
D
B
左单
旋转
F
G
FG
D
? 再做左单旋转:以结点 D为旋转轴, 将结点
A反时针旋转, 恢复树的平衡 。
81
AVL树的插入
? 在向一棵本来是高度平衡的 AVL树中插入
一个新结点时, 如果树中某个结点的平衡
因子的绝对值 |balance| > 1,则出现了不平
衡, 需要做平衡化处理 。
? 算法 从一棵空树开始,通过输入一系列对
象关键码,逐步建立 AVL树 。在插入新结
点时 使用平衡旋转方法进行平衡化处理 。
82
1616
例,输入关键码序列为 { 16,3,7,11,9,26,18,
14,15 },插入和调整过程如下。
0
3
16
3
-1
0
7
0
1
-2
左右双旋 7
3 16
0 0
0
7
3
11
0
-1
1
7
3 16
16
11
9
0
-1
-2
右单旋 3
7
169
0 0
0
1
3
7
11
26
9 16
11
0
1
1
22
83
18 18
0
3
16
3
-1
0
16
0
2 右左双旋
7
3 9
0
0
0
18
26
11
-1
7
3 26
16
11
9
-1
左单旋
9
7
16
14
0
0
1
7
11
26
269
1
1
11
84
15
18
2
3
18
16
-2
左右双旋
7
3
0
0
0
11
7
14
9
-1
16
15
0
1
11
26 26
141
-2
9
从空树开始的建树过程
85
AVL树的高度
? 设在新结点插入前 AVL树的高度为 h,结
点个数为 n,则插入一个新结点的时间是
O(h)。 对于 AVL树来说, h多大?
? 设 Nh 是高度为 h 的 AVL树的最小结点数 。
根的 一棵子树的高度为 h-1,另一棵子树
的高度为 h-2,这两棵子树也是高度平衡
的 。 因此有
? N-1 = 0 (空树 )
? N0 = 1 (仅有根结点 )
? Nh = Nh-1 + Nh-2 +1,h > 0
86
? 可以证明,对于 h ? 0,有 Nh = Fh+3 -1 成立。
其中,斐波那契数列 F0 = 0,F1 = 1,
Fn = Fn-1 + Fn-2,n > 1时
? 有 n 个结点的 AVL树的高度不超过
1.44*log2(n+1)-1
? 在 AVL树删除一个结点并做平衡化旋转所
需时间为 O(log2n)。
? 二叉搜索树适合于组织在内存中的较小的
索引 (或目录 )。对于存放在外存中的较大的
文件系统,用二叉搜索树来组织索引不太
合适。
? 并查集
? 静态搜索表
? 二叉搜索树
? AVL树
2
并查集 (Union-Find Sets)
? 并查集支持以下三种操作:
? Union (Root1,Root2) //合并操作
? Find (x) //搜索操作
? InitUFSets (s ) //初始化操作
? 对于并查集来说,每个集合用一棵树表示。
? 为此,采用 树的双亲表示 作为集合存储表示。
集合元素的编号从 0到 n-1。其中 n 是最大
元素个数。
3
? 在 双亲表示 中,第 i 个数组元素代表包含
集合元素 i 的树结点 。初始时,根结点的
双亲为 -1,表示集合中的元素个数。
? 在同一棵树上所有结点所代表的集合元素
在同一个子集合中。
? 为此,需要有两个映射:
? 集合元素到存放该元素名的树结点间的
对应;
? 集合名到表示该集合的树的根结点间的
对应。
4
? 设 S1= {0,6,7,8 },S2= { 1,4,9 },S3= { 2,3,
5 }
集合名 指针
0 S1
1 S2
2 S3
0 4 2
76 8 1 9 3 5
? 为简化讨论, 忽略实际的集合名, 仅用表
示集合的树的根来标识集合 。
5
? 初始时,InitUFSets(S) 构造一个森林,每棵
树只有一个结点,表示集合中各元素自成一
个子集合 S[i] = -1,i = 0,1,…,n-1。
? 用 Find(S,i) 寻找集合元素 i 的根 。
? 如果有两个集合元素 i 和 j:
Find(S,i) == Find(S,j)
表明这两个元素在同一个集合中,
? 如果两个集合元素 i 和 j 不在同一个集合
中,可用 Union(S,i,j) 将它们合并到一个集
合中 。
6
S1
下标
parent
集合 S1,S2 和 S3 的双亲表示
-4 4 -3 2 -3 2 0 0 0 4
0 1 2 3 4 5 6 7 8 9
0
76 8
4
1 9
2
3 5
S2 S3
7
S1 ? S2的可能的表示方法
下标
parent
集合 S1 ? S2和 S3的双亲表示
-7 4 -3 2 0 2 0 0 0 4
0 1 2 3 4 5 6 7 8 9
0
76 8 4
1 9
4
1 90
876
8
并查集的结构定义
const int SetSize = 50; //并查集元素个数
typedef struct { //并查集结构定义
int parent[SetSize]; //集合元素数组
} UFSets;
void InitUFSets (UFSets *S) { //集合初始化
for ( int i = 0; i < SetSize; i++ )
S->parent[i] = -1;
} //每一个自成一个单元素集合
9
?并查集操作的算法
? 查找
-5 0 1 2 3
0 1 2 3 4
Find (S,4)
Find (S,3) = 3
Find (S,2) =2
Find (S,1) = 1
Find (S,0) = 0
= -5 < 0 结束
10
int Find (UFSets * S,int x ) {
if ( S->parent [x] < 0 ) return x;
else return Find (S,S->parent [x] );
}
-5 0 1 2 3parent
Parent[4]
=3 Parent[3]
=2Parent[2]
=1
Parent[1]
=0
Parent[0]
=-5
0 1 2 3 4
11
void Union (UFSets *S,int Root1,int Root2) {
//求两个不相交集合 Root1与 Root2的并
S->parent[Root1] += S->parent[Root2];
S->parent[Root2] = Root1;
//将 Root2连接到 Root1下面
}
? Find和 Union操作性能不好。假设最初 n 个
元素构成 n 棵树组成的森林,S->parent[i]
= -1。做处理 Union(n-2,n-1),…,Union(1,
2),Union(0,1)后,将产生退化的树。
12
? 合并 -1-1 -1 -1 -10 2 3 4
-3
-5
0
3
2
1
3
3
4 1
3 3
22
0
2
3
1
4
Union(0,1)
-2
3
-4
1
4
2
1
2
3
4
Union(1,2)
Union(2,3)
Union(3,4)
13
? 执行一次 Union操作所需时间是 O(1),n-1
次 Union操作所需时间是 O(n)。
? 若再执行 Find(0),Find(1),…,Find(n-1),若
被搜索的元素为 i,完成 Find(i) 操作需要时
间为 O(i),完成 n 次搜索需要的总时间将达
到
? 改进的方法
? 按树的结点个数合并
? 按树的高度合并
? 压缩元素的路径长度
?
?
?
n
i
ni
1
2OO )()(
14
? 按树结点个数合并
?结点个数多的树的根结点作根
-1-1 -1 -1 -1
0
1
2 3 4
-1 -1
0
-7
2
5 6
-5 -2
2 2
3 3
2 0
1
3
4
5
6
2
3 3 0
2 0
56
2
31
4
Union(2,0)
15
void WeightedUnion (UFSets *S,
int Rt1,int Rt2 ) {
//按 Union的加权规则改进的算法
int temp = S->parent[Rt1] + S->parent[Rt2];
if ( S->parent[Rt2] < S->parent[Rt1] ) {
S->parent[Rt1] = Rt2; //Root2中结点多
S->parent[Rt2] = temp; //Root1指向 Root2
}
else {
S->parent[Rt2] = Rt1; //Root1中结点多
S->parent[Rt1] = temp; //Root2指向 Root1
}
}
16
? 按树高度合并
? 高度高的树的根结点作根
-0-0 -0 -0 -0
0
1
2 3 4
-0 -0
0
-2
2
5 6
-2 -1
2 2
3 3
2 0
1
3
4
5
6
2
3 3 0
2 0
56
2
31
4
Union(2,0)
17
Union操作的折叠规则
? 为进一步改进树的性能, 可以使用如下折叠
规则来, 压缩路径, 。 即,如果 j 是从 i 到
根 root 的 路 径 上 的 一 个 结 点, 且 S-
>parent[j] != root[j],则把 S->parent[j] 臵
为 root[i]。0 0
6 7 8 6 7 8
1 9 1
9
3 5
3
5
从 i = 5 压缩路径
18
int CollapsingFind (UFSets *S,int i ) {
//使用折叠规则的搜索算法
int j = i;
while ( S->parent[j] >= 0 )
j = S->parent[j];
//让 j 循双亲指针走到根
while ( i != j ) { //换 parent[i] 到 j
int temp = S->parent[i];
S->parent[i] = j; i = temp;
}
return j;
}
19
搜索 (Search)的概念
静态搜索表
? 所谓 搜索, 就是在数据集合中寻找满足某
? 种条件的数据对象 。
? 搜索的结果通常有两种可能:
? 搜索成功,即找到满足条件的数据对象
这时,作为结果,可报告该对象在结构中
的位臵,还可给出该对象中的具体信息。
? 搜索不成功,或搜索失败。作为结果,
应报告一些信息,如失败标志、位臵等。
20
? 通常称用于搜索的数据集合为 搜索结构,
它是由 同一数据类型的对象 (或记录 )组成。
? 在每个对象中有若干属性,其中有一个属
性,其值可唯一地标识这个对象。称为关
键码。 使用基于关键码的搜索,搜索结果
应是唯一的。 但在实际应用时,搜索条件
是多方面的,可以 使用基于属性的搜索方
法,但搜索结果可能不唯一。
21
? 实施搜索时有两种不同的环境。
? 静态环境,搜索结构在插入和删除等操
作的前后不发生改变。
? 静态搜索表
? 动态环境,为保持较高的搜索效率,搜
索结构在执行插入和删除等操作的前后
将自动进行调整,结构可能发生变化。
? 动态搜索表
22
#define MaxSize 100 //搜索表最大尺寸
typedef int DataType; //搜索数据的类型
typedef struct { //搜索表结点定义
DataType key; //关键码域
other; //其他数据域
} ListNode;
typedef struct dataList { //搜索表结点定义
ListNode data[MaxSize]; //数据存储数组
int n; //数组当前长度
}
静态搜索表结构的定义
23
? 衡量一个搜索算法的时间效率的标准是:
在搜索过程中关键码的 平均比较次数, 也
称为 平均搜索长度 ASL(Average Search
Length),通常它是 搜索结构中对象总数
n的函数 。
? 在静态搜索表中,利用数组元素的下标作
为数据对象的存放地址 。 搜索算法 根据给
定值 x,在数组中进行搜索 。 直到 找到 x在
数组中的位臵或可确定在数组中 找不到 x
为止 。
? 另外衡量一个搜索算法还要考虑算法所需
要的存储量和算法的复杂性等问题。
24
顺序搜索 (Sequential Search)
? 所谓顺序搜索,又称线性搜索,主要用于在
线性结构中进行搜索 。
? 设若表中有 n 个对象, 则顺序搜索从表的
先端 (或后端 ) 开始,顺序用各对象的关键
码与 给定值 x 进行比较,直到找到与其值相
等的对象,则搜索成功 ; 给出该对象在表中
的位臵 。
? 若整个表都已检测完仍未找到关键码与 x相
等的对象,则搜索失败 。 给出失败信息 。
25
int LinearSearch ( dataList A,DataType x ) {
//在数据表 A.data[1..n]中顺序搜索关键码为 x
//的数据对象,A.data[0].key 作为控制搜索自动
//结束的, 监视哨, 使用
A.data[0].key = x; int i = A.n;
//将 x送 0号位置设置监视哨
while ( A.data[i].key != x ) i-- ;
//从后向前顺序搜索
return i;
}
设臵“监视哨”的顺序搜索算法
26
顺序搜索的平均搜索长度
??
?
?
?
?
???
1
0
1
0
n
i
i
n
i
iis u c c pcpA S L ) 1 (,
)( 1
1
???? ?
?
inpAS L
n
i
is u c c
设搜索第 i 个元素的概率为 pi,搜索到第 i 个
元素所需比较次数为 ci,则搜索成功的平均
搜索长度,
在顺序搜索并设臵“监视哨”情形:
ci = n- i +1,i = n,n-1,?,1,因此
27
.)()(?
?
???????? n
i
s u c c
nnn
n
in
n
A S L
1 2
1
2
1111
在等概率情形,pi = 1/n,i = 1,2,?,n。
在搜索不成功情形,ASLunsucc = n+1.
28
顺序搜索的递归算法
int SequentSearch ( dataList A,dataType x,
int& loc ) {
//在数据表 A.data[0..n-1] 中搜索其关键码与
//给定值匹配的对象,函数返回其表中位置。
//参数 loc 是在表中开始搜索位置
if ( loc >= A.n ) return -1; //搜索失败
else if ( A.data[loc].key == x ) return loc;
//搜索成功
else return SequentSearch ( A,x,loc+1 );
//递归搜索
}
29
int SequentSearch ( dataList A,DataType x ) {
//在数据表 A.data[0..n-1] 中 顺序搜索关键码
为 x 的数据对象
for ( int i = 0; i < A.n; i++ )
if ( A.data[i].key == x ) return i; //成功
else if ( A.data[i].key > x ) break;
return -1;
//顺序搜索失败,返回失败信息
}
基于有序顺序表的顺序搜索算法
30
? 有序顺序表的顺序搜索
( 10,20,30,40,50,60 )
10
50
60
=
=
=
=
=
=
20
30
40
<
<
<
<
<
<
>
>
>
>
>
>
? ?
2
71
6
1 5
0
??? ?
?i
s u c c iA S L
? ?
7
27
61
7
1 5
0
??
?
?
?
?
?
???
?
?
?i
un s uc c
i
A S L
31
基于有序顺序表的折半搜索
? 设 n个对象存放在一个按其关键码从小到
大排好了序的有序顺序表中 。
? 折半搜索时,先求位于搜索区间正中的对象
的下标 mid,用其关键码与 给定值 x比较,
? A.data[mid].key == x,搜索成功 ;
? A.data[mid].key > x,把搜索区间缩小到
表的 前半部分, 继续折半搜索 ;
? A.data[mid].key < x,把搜索区间缩小到
表的 后半部分, 继续折半搜索 。
? 如果搜索区间已缩小到一个对象, 仍未找
到想要搜索的对象, 则搜索失败 。
32
搜索成功的例子
-1 0 1 3 4 6 8 10 126
0 1 2 3 4 5 6 7 8
搜索 low mid high6
6 8 10 12
5 6 7 8
low mid high
6 6
5
low mid high
6
33
搜索失败的例子
-1 0 1 3 4 6 8 10 125
0 1 2 3 4 5 6 7 8
搜索 low mid high5
6 8 10 12
5 6 7 8
low mid high
65
5
low mid high
5
34
int BinarySearch ( dataList A,DataType x,
int low,int high ) {
int mid = -1;
if ( low <= high ) {
mid = ( low + high ) / 2;
if ( A.data[mid].key < x )
mid = BinarySearch ( A,x,mid +1,high );
else if ( A.data[mid].key > x )
mid = BinarySearch ( A,x,low,mid -1 );
}
return mid;
}
基于有序顺序表的折半搜索递归算法
35
int BinarySearch ( dataList A,DataType x ) {
int high = A.n-1,low = 0,mid;
while ( low <= high ) {
mid = ( low + high ) / 2;
if ( A.data[mid].key < x )
low = mid + 1; //右缩搜索区间
else if ( A.data[mid].key > x )
high = mid - 1; //左缩搜索区间
else return mid; //搜索成功
}
return -1; //搜索失败
}
基于有序顺序表的折半搜索迭代算法
36
? 有序顺序表的折半搜索的判定树
( 10,20,30,40,50,60 )
10 50
=
=
==
=
=
30
<
<
<
<
<
<
>
>
> >
> >
20 40 60
ASLunsucc = (2*1+3*6)/7
= 20/7
ASLsucc = (1+2*2+
3*3)/6 = 14/6
37
折半搜索性能分析
? 若设 n = 2h-1,则描述折半搜索的判定树是
高度为 h-1 的满二叉树 。
2h = n+1,h = log2(n+1)。
? 第 0层结点有 1个,搜索第 0层结点要比较 1次 ;
第 1层结点有 2个,搜索第 1层结点要比较 2
次 ; …,第 i (0 ? i ? h) 层结点有 2i 个,搜索第
i 层结点要比较 i+1次, … 。
? 假定每个结点的搜索概率相等,即 pi = 1/n,
则搜索成功的平均搜索长度为
38
)221)(23
2211
11
122
1
1
0
1
??
?
?
?
?
????????
???????? ??
hh
n
i
i
n
0i
iis u c c
hh
(
n
C
n
CpA S L
?
121)(
221)( 232211 1221
????
???????????? ??
h
hh
h
hh?
11)(log11)(log
1
)1)(1 ) l o g((
1
1)21)((
1
22
2
?????
?
?
????????
nn
n
n
nnn
n
h
n
A S L
h
s u c c
可以用归纳法证明
这样
39
定义
二叉搜索树或者是一棵空树,或者是具有
下列性质的二叉树:
?每个结点都有一个作为搜索依据的关键
码 (key),所有结点的关键码互不相同。
?左子树 (如果存在 )上 所有结点的关键码
都小于根结点的关键码 。
?右子树 (如果存在 )上 所有结点的关键码
都大于根结点的关键码 。
?左子树和右子树也是二叉搜索树。
二叉搜索树 ( Binary Search Tree )
40
35
15 45
50402510
20 30
二叉搜索树例
? 结点左子树上
所有关键码小
于结点关键码
? 右子树上所有
关键码大于结
点关键码
? 注意:若从根结点到某个叶结点有一条路
径,路径左边的结点的关键码不一定小于
路径上的结点的关键码。
41
n 个结点的二叉搜索树的数目
【例】 3 个结点的二叉搜索树
5
1*2*3
4*5*6*
4
1C
13
1 3
3*2 ???
1
2
2
1
3
3 1 3
2
1
2
3
1
2
3
{123} {132} {213} {231} {312} {321}
42
二叉搜索树的结构定义
typedef char DataType; //树结点数据类型
typedef struct node { //搜索树结点定义
DataType data;
struct node *leftChild,*rightChild;
} BstNode;
typedef BstNode *BST; //二叉搜索树定义
如果对一棵二叉搜索树进行中序遍历,可
以按从小到大的顺序,将各结点关键码排列
起来,所以也称二叉搜索树为二叉排序树。
43
在二叉搜索树上进行搜索,是一个从根结
点开始,沿某一个分支逐层向下进行比较判
等的过程 。它可以是一个递归的过程。
二叉搜索树上的搜索
35
15 45
50402510
20 30
搜索 45
搜索成功
搜索 28
搜索失败
44
? 假设想要在二叉搜索树中搜索关键码为 x
的元素, 搜索过程从根结点开始 。
? 如果根指针为 NULL,则搜索不成功;否
则用给定值 x 与根结点的关键码进行比较:
? 如果给定值等于根结点的关键码, 则搜
索成功 。
? 如果给定值小于根结点的关键码, 则继
续递归搜索根结点的左子树;
? 否则 。 递归搜索根结点的右子树 。
45
? 搜索成功时检测指针停留在树中某个结点。
? 搜索不成功时检测指针停留在某个外结点
(失败结点)。
35
15 45
502510
20 30
搜索 22
搜索 45
46
BstNode * Find ( BstNode *ptr,DataType x ) {
//二叉搜索树的迭代的搜索算法
if ( ptr != NULL ) {
BstNode * p = ptr; //从根搜索
while ( p != NULL ) {
if ( p->data == x ) return p;
if ( p->data < x )
p = p->rightChild; //右子树
else p = p->leftChild; //左子树
}
}
return NULL; //搜索失败
}
47
35
15 45
50402510
20 30
28
插入新结点 28
二叉搜索树的插入
? 每次结点的插入,都要从根结点出发搜索
插入位臵,然后把
新结点作为叶结点
插入。
? 为了向二叉搜索树
中插入一个新元素,
必须先检查这个 元
素是否在树中已经
存在 。
48
? 在插入之前, 先使用搜索算法在树中检查
要插入元素有还是没有 。
? 搜索成功, 树中已有这个元素,不再插入 。
? 搜索不成功, 树中原来没有关键码等于
给定值的结点, 把新元素加到搜索操作
停止的地方 。
递归的二叉搜索树插入算法
void Insert ( DataType x,BstNode * & ptr) {
//将新元素 x插到以 *ptr为根的二叉搜索树中
49
if ( ptr == NULL ) { //空二叉树
ptr = new BstNode; //创建结点
if ( ptr == NULL )
{ cout <<,存储不足 " << endl; exit (1); }
ptr->data = x;
ptr->leftChild = ptr->rightChild = NULL;
}
else if ( x < ptr->data ) //在左子树插入
Insert ( x,ptr->leftChild );
else if ( x > ptr->data ) //在右子树插入
Insert ( x,ptr->rightChild );
}
50
输入数据,建立二叉搜索树的过程
输入数据 { 53,78,65,17,87,09,81,15 }
53 53
78
53
78
65
53
78
65
17
53
78
65 87
17
53
78
6509
17
87
53
78
65
81
17
8709
53
78
65
15
17
8709
81
51
void CreateBST ( BstNode *& T,
DataType Refvalue ) {
//输入数据,建立二叉搜索树 。 RefValue 是输入
//结束标志。这个值应取不可能在输入序列中
//出现的值,例如输入序列的值都是正整数时,
//取 RefValue为 0或负数。
dataType x; T = NULL; cin >> x;
while ( x != RefValue ) {
if ( Find (T,x) == NULL ) Insert ( x,T );
cin >> x;
}
}
52
同样 3 个数据 { 1,2,3 },输入顺序不同,
建立起来的二叉搜索树的形态也不同。这直
接影响到二叉搜索树的搜索性能。
如果输入序列选得不好,会建立起一棵单
支树,使得二叉搜索树的高度达到最大。
{2,1,3}
{1,2,3} {1,3,2} {2,3,1} {3,1,2} {3,2,1}
1
2
3
1
1 1
1
3
2 2
2
3
3
2
3
53
二叉搜索树的删除
? 在二叉搜索树中删除一个结点时, 必须将
因删除结点而断开的二叉链表重新链接起
来, 同时确保二叉搜索树的性质不会失去 。
? 为保证在删除后树的搜索性能不至于降低,
还 需要防止重新链接后树的高度增加 。
? 删除叶结点, 只需将其双亲结点指向它
的指针清零, 再释放它即可 。
? 被删结点缺右子树, 可以拿它的左子女
结点顶替它的位臵, 再释放它 。
54
? 被删结点缺左子树, 可以拿它的右子女
结点顶替它的位臵, 再释放它 。
? 被删结点左, 右子树都存在, 可以在它
的右子树中寻找中序下的第一个结点 (关
键码最小 ),用它的值填补到被删结点中,
再来处理这个结点的删除问题 。
53
78
65
17
8709
23
45
删除 45
缺右子树,用
左子女顶替
53
78
65
17
8709 23
55
88
53
78
88
17
9409 23
删除 78
缺左子树,用
右子女顶替
53
94
88
17
09 23
53
78
81
17
9409 45
删除 78
在右子树上找
中序下第一个
结点填补23
65
53
81
88
17
9409 45
23
65
56
二叉搜索树性能分析
? 对于有 n 个关键码的集合,其关键码有 n!
种不同排列,可构成不同二叉搜索树有
(棵 )
? 用树的搜索效率来评价这些二叉搜索树。
? 在树中增加失败结点 (外部结点 ),它们是搜
索失败时到达的结点,物理上实际不存在。
? 增加了外部结点的二叉搜索树称为扩充二
叉搜索树。
C n nn 21 1?
57
? 在扩充二叉搜索树中
? ○ 表示内部结点, 包含了关键码集合中
的某一个关键码;
? □ 表示外部结点, 代表各关键码间隔中
的不在关键码集合中的关键码 。
? 在每两个外部结点间必存在一个内部结点 。
58
? 例,已知关键码集合 { a1,a2,a3 } = { do,if,
to },对应搜索概率 p1,p2,p3,在各搜索不
成功间隔内搜索概率分别为 q0,q1,q2,q3。
可能的二叉搜索树如下所示。
do
if
to
do
if
to
q0 q1
p1 q2
p2 q3
p3
q0 q1 q2 q3
p1
p2
p3
(a) (b)
59
do
if
to
扩充二叉搜索树
q0
q1
p1
q2
p2
q3
p3
do
if
to
q0
q1
p1
q2
p2
q3
p3
(d)(c) do
if
to
q0
q1
p1
q2
p2 q3
p3
(e)
60
一棵扩充二叉搜索树的 搜索成功的平均搜
索长度 ASLsucc可以定义为 该树所有内部结点
上的权值 p[i]与搜索该结点时所需的 关键码
比较次数 c[i] (= l[i] + 1)乘积之和:
).][(*][ 1p
1
?? ?
?
i liAS L
n
i
s u c c
搜索不成功的平均搜索长度 ASLunsucc为树中
所有外部结点上权值 q[j]与到达外部结点所需
关键码比较次数 c'[j](= l'[j])乘积之和:
?
?
?
n
j
u n s u c c jljAS L
0
q ].[ *][
61
(1) 相等搜索概率的情形
设树中所有内, 外部结点的搜索概率都相等:
p[i] = 1/3,1 ? i ? 3
q[j] = 1/4,0? j ? 3
图 (a),ASLsucc = 1/3*3+1/3*2+1/3*1 = 6/3 = 2
ASLunsucc = 1/4*3*2+1/4*2+1/4*1 = 9/4
图 (b),ASLsucc = 1/3*1+1/3*2*2 = 5/3
ASLunsucc = 1/4*2*4 = 2
图 (c) ?图 (e),ASLsucc = 2,ASLunsucc = 9/4
图 (b)的情形所得的平均搜索长度最小。
62
一般把平均搜索长度达到最小的扩充二叉搜
索树称作最优二叉搜索树。
在 相等搜索概率的情形 下,所有内部、外部
结点的搜索概率都相等,视它们的权值都为 1。
同时,第 k 层有 2k 个结点,k = 0,1,?。则有
n 个内部结点的扩充二叉搜索树的内部路径长
度 I 至少等于序列
63
0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,…
的 前 n 项 的和。
因此,最优二叉搜索树的搜索成功的平均搜
索长度和搜索不成功的平均搜索长度分别为,
? ?? ?.?
?
??
n
i
s u c c iA S L
1
2 1l o g
? ?,?
?
??
?
1n
ni
u n s u c c iAS L
2
1
2l o g
(2) 不相等搜索概率的情形
设树中所有内、外部结点的搜索概率互不
相等。
64
p[1] = 0.5,p[2] = 0.1,p[3] = 0.05
q[0] = 0.15,q[1] = 0.1,q[2] = 0.05,q[3] = 0.05
do
if
to
do
if
to
q0=0.15
q1=0.1
p1=0.5 q2=0.05
p2=0.1
q3=0.05p3=0.05
q0=0.15 q1=0.1 q2=0.05
q3=
0.05
p1=0.5
p2=0.1
p3=0.05
(a) (b)
图 (a),ASLsucc = 0.5*3+0.1*2+0.05*1 = 1.75,
ASLunsucc = 0.15*3+0.1*3+0.05*2+0.05*1= 0.9。
ASL = ASLsucc + ASLunsucc = 2.65。
65
do
if
to
q0=
0.15
q1=0.1
p1=0.5
q2=0.05
p2=0.1
q3=0.05
p3=0.05
do
if
to
q0=0.15
q1=0.1
p1=0.5
q2=0.05
p2=0.1
q3=0.05p3=0.05
(d)(c)
图 (c), ASLsucc = 0.5*1+0.1*2+0.05*3 = 0.85,
ASLunsucc = 0.15*1+0.1*2+0.05*3+0.05*3 = 0.75.
ASL = 0.85+0.75 = 1.6
图 (b), ASL = 1.9; 图 (d), ASL = 2.15;
66
do
if
to
q0=0.15
q1=0.1
p1=0.5
q2=0.05
p2=0.1 q3=0.05
p3=0.05
(e)
图 (e), ASLsucc = 0.5*1
+0.05*2+0.1*3 = 0.9;
ASLunsucc = 0.15*1+
0.1*3+0.05*3+0.05*2
= 0.7;
ASL = 0.9+0.7 = 1.6.
由此可知,图 (c)和图 (e)的情形下树的平均搜
索长度达到最小,因此,图 (c)和图 (e)的情形
是最优二叉搜索树。
67
AVL树 高度平衡的二叉搜索树
AVL树的定义
一棵 AVL树或者是空树,或者是具有下列
性质的二叉搜索树,它的左子树和右子树都
是 AVL树,且左子树和右子树的高度之差的
绝对值不超过 1。
高度不平衡 高度平衡A
B
C
A
B
C
D
ED E
68
结点的平衡因子 balance (balance factor)
? 每个结点附加一个数字,给出该结点 右子
树的高度减去左子树的高度 所得的 高度差,
这个数字即为结点的平衡因子 balance
? AVL树任一结点平衡因子只能取 -1,0,1
? 如果一个结点的平衡因子的绝对值大于 1,
则这棵二叉搜索树就失去了平衡,不再是
AVL树。
? 如果一棵二叉搜索树是高度平衡的,且有 n
个结点,其高度可保持在 O(log2n),平均搜
索长度也可保持在 O(log2n)。
69
AVL树的结构定义
typedef int DataType; //结点数据类型
typedef struct node { //AVL树结点定义
DataType data; //结点数据域
int balance; //结点平衡因子域
struct node *leftChild,*rightChild;
//结点左、右子女指针域
} AVLNode;
typedef AVLNode * AVLTree; //AVL树
70
平衡化旋转
? 如果在一棵平衡的二叉搜索树中插入一个
新结点,造成了不平衡。此时必须调整树
的结构,使之平衡化。
? 平衡化旋转有两类:
? 单旋转 (左旋和右旋 )
? 双旋转 (左平衡和右平衡 )
? 每插入一个新结点时,AVL 树中相关结点
的平衡状态会发生改变。因此,在插入一
个新结点后,需要 从插入位臵沿通向根的
路径回溯, 检查各结点的平衡因子 。
71
? 如果在某一结点发现高度不平衡,停止回
溯。从发生不平衡的结点起,沿刚才回溯
的路径取直接下两层的结点。
? 如果这三个结点处于一条直线上,则采用
单旋转进行平衡化 。 单旋转可按其方向分
为左单旋转和右单旋转,其中一个是另一
个的镜像,其方向与不平衡的形状相关。
? 如果这三个结点处于一条折线上,则采用
双旋转进行平衡化 。 双旋转分为先左后右
和先右后左两类。
72
右单旋转 左单旋转 左右双旋转 右左双旋转
左单旋转 (RotateLeft )
h
h h
A
C
E
B
D
h
h
h+
1
B
A
C
ED
h h
h+
1
C
EA
B D
+1 +2
0 +1 0
0
73
右单旋转 左单旋转 左右双旋转 右左双旋转
左单旋转 (RotateLeft )
h
h h
A
C
E
B
D
h
h
h+
1
B
A
C
ED
h h
h+
1
C
EA
B D
+1 +2
0 +1 0
0
74
? 在子树 E中插入新结点, 该子树高度增 1导
致结点 A的平衡因子变成 +2,出现不平衡 。
? 沿插入路径检查三个结点 A,C和 E。 它们
处于方向为, \”的直线上, 需做左单旋转 。
? 以结点 C为旋转轴, 让结点 A反时针旋转 。
右单旋转 (RotateRight )
? 在左子树 D上插入新结点使其高度增 1,导
致结点 A的平衡因子增到 -2,造成不平衡。
? 为使树恢复平衡,从 A沿插入路径连续取 3
个结点 A,B和 D,它们处于一条方向为,/”
的直线上,需要做右单旋转。
75
h
h h
A
C
E
B
D h
h+
1
B
A
C
ED
h h
h+
1
CE
A
B
D
? 以结点 B为旋转轴,将结点 A顺时针旋转。
h
0
0
0
-1
-1
-2
先左后右双旋转 (RotationLeftRight)
? 在子树 F或 G中插入新结点, 该子树的高度
增 1。 结点 A的平衡因子变为 -2,发生了不
平衡 。
76
插入
00
-1 -2
+1
-1
h
h
A
C
ED
h-1h-1
h
h
A
h-1h
B C
ED
B
左单
旋转
F G F G
? 从结点 A起沿插入路径选取 3个结点 A,B
和 E,它们位于一条形如, ?” 的折线上,
因此需要进行先左后右的双旋转 。
? 以结点 E为旋转轴, 将结点 B反时针旋转 。
77
0
0
-2 0
0 +1
h
h
A
CE
D
-2
h-1
h
hh
A
h-1h
B C
E
D
B
右单
旋转
F
G FG
78
先右后左双旋转 (RotationRightLeft)
? 右左双旋转是左右双旋转的镜像 。
? 在子树 F或 G中插入新结点, 该子树高度增
1。 结点 A的平衡因子变为 2,发生了不平
衡 。
? 从 结点 A起沿插入路径选取 3个结点 A,C
和 D,它们位于一条形如, ?” 的折线上,
需要进行先右后左的双旋转 。
79
插入
右
单
旋
转
+1
0
0
0
-1
+1
0h
h
A
C
ED
h-1
B
F G
h-1
+2
0
0
0h
h
A
C
E
h
B
F G
h-1
D
? 首先做右单旋转:以结点 D为旋转轴, 将结
点 C顺时针旋转, 以 D代替原来 C的位臵 。
80
00
+2 0
-1 0
h
h
A
C
E
+2
h-1
h
hh
A
h-1 h
B C
E
D
B
左单
旋转
F
G
FG
D
? 再做左单旋转:以结点 D为旋转轴, 将结点
A反时针旋转, 恢复树的平衡 。
81
AVL树的插入
? 在向一棵本来是高度平衡的 AVL树中插入
一个新结点时, 如果树中某个结点的平衡
因子的绝对值 |balance| > 1,则出现了不平
衡, 需要做平衡化处理 。
? 算法 从一棵空树开始,通过输入一系列对
象关键码,逐步建立 AVL树 。在插入新结
点时 使用平衡旋转方法进行平衡化处理 。
82
1616
例,输入关键码序列为 { 16,3,7,11,9,26,18,
14,15 },插入和调整过程如下。
0
3
16
3
-1
0
7
0
1
-2
左右双旋 7
3 16
0 0
0
7
3
11
0
-1
1
7
3 16
16
11
9
0
-1
-2
右单旋 3
7
169
0 0
0
1
3
7
11
26
9 16
11
0
1
1
22
83
18 18
0
3
16
3
-1
0
16
0
2 右左双旋
7
3 9
0
0
0
18
26
11
-1
7
3 26
16
11
9
-1
左单旋
9
7
16
14
0
0
1
7
11
26
269
1
1
11
84
15
18
2
3
18
16
-2
左右双旋
7
3
0
0
0
11
7
14
9
-1
16
15
0
1
11
26 26
141
-2
9
从空树开始的建树过程
85
AVL树的高度
? 设在新结点插入前 AVL树的高度为 h,结
点个数为 n,则插入一个新结点的时间是
O(h)。 对于 AVL树来说, h多大?
? 设 Nh 是高度为 h 的 AVL树的最小结点数 。
根的 一棵子树的高度为 h-1,另一棵子树
的高度为 h-2,这两棵子树也是高度平衡
的 。 因此有
? N-1 = 0 (空树 )
? N0 = 1 (仅有根结点 )
? Nh = Nh-1 + Nh-2 +1,h > 0
86
? 可以证明,对于 h ? 0,有 Nh = Fh+3 -1 成立。
其中,斐波那契数列 F0 = 0,F1 = 1,
Fn = Fn-1 + Fn-2,n > 1时
? 有 n 个结点的 AVL树的高度不超过
1.44*log2(n+1)-1
? 在 AVL树删除一个结点并做平衡化旋转所
需时间为 O(log2n)。
? 二叉搜索树适合于组织在内存中的较小的
索引 (或目录 )。对于存放在外存中的较大的
文件系统,用二叉搜索树来组织索引不太
合适。