二:树
对于大量的输入数据,链表的线性访问时间太慢,不宜食用。而树这种数据结构的大部分操作的运行时间平均为O(log N) 。书中设计到的数据结构叫做二叉查找数(binary search tree)。
树的基本知识
一颗树是一些点的集合。这个集可以是空集;若非空,则一棵树由称作根(root) 的节点 r 以及0个或多个非空的(子)树组成,这些子树中每一棵的根都由来自 r 的一条有向边连接。树的一种实现方法是在每一个节点除数据外还有一些指针,使得每一个子节点都有一个指针指向它。然而由于每个节点的儿子数变化很大,因此将每个节点的所有儿子都放在树节点的链表中。下面是节点的声明:
/* 树的节点声明 */
typedef struct TreeNode *PtrToNode;
typedef int ElementType;
struct TreeNode
{
ElementType Element;
PtrToNode FirstChild;
PtrToNode NextSibling;
}
树的遍历有先序遍历(preorder traversal) 和后序遍历(postorder trversal)。先序遍历中对节点的处理实在他的儿子节点被处理之前进行的,与此相反,后序遍历中对节点的处理是在子节点处理后进行。
二叉树(binary tree) 是一棵每个节点都有不多于两个子节点的树。二叉树的平均深度为O(N),声明如下:
/* 二叉树节点的声明 */
typedef struct TreeNode *Tree;
typedef int ElementType;
struct TreeNode
{
ElementType Element;
Tree Left;
Tree Right;
}
查找数ADT——二叉查找树
为简单起见,下面我们的例子中每个节点的关键字都是整数并且关键字都是互异的。使二叉树成为二叉查找树的性质是,对于树中每个节点X, 他的左子树中所有关键字值小于X 的关键字值,而它的右子树中所有关键字值大于X 的关键字值。下面列出了二叉查找数的重复定义和一些函数。
#ifndef _Tree_H
#define _Tree_H
struct TreeNode;
typedef struct TreeNode *Position;
typedef struct TreeNode *SearchTree;
typedef int ElementType;
SearchTree MakeEmpty(SearchTree T);
Position Find(ElementType X, SearchTree T);
Position FindMin(SearchTree T);
Position FindMax(SearchTree T);
SearchTree Insert(ElementType X,SearchTree T);
SearchTree Delete(ElementType X,SearchTree T);
ElementType Retrieve(Position P);
#endif /* _Tree_H */
/* place in the implementation file */
struct TreeNode
{
ElementType Element;
SearchTree Left;
SearchTree Right;
};
MakeEmpty
这个操作用于初始化。
SearchTree MakeEmpty(SearchTree T)
{
if(T!=NULL)
{
MakeEmpty(T->Left);
MakeEmpty(T->Right);
free(T);
}
return NULL;
}
Find
这个操作一般返回指向树T 中具有关键字 X 的节点的指针,如果这个节点不存在则返回NULL;
Position Find(ElementType X, SearchTree T)
{
if(T==NULL)
return T;
if(T->Element==X)
return T;
if(T->Element<X)
return Find(X,T->Right);
else
return Find(X,T->Left);
}
FindMin 和 FindMax
这两个函数分别返回树中的最大元素和最小元素的位置。分别用递归和非递归实现。
Position FindMin(SearchTree T)
{
if(T==NULL)
return T;
return FindMin(T->Left);
}
Position FindMax(SearchTree T)
{
if(T!=NULL)
while(T->Right!=NULL)
T=T->Right;
return T;
}
Insert
进行插入操作的函数是简单的,可以向Find函数那样查找,如果找到X,则什么都不做,否则将X 插入到遍历路径的最后一点上。
SearchTree Insert(ElementType X,SearchTree T)
{
Position p;
p=T;
while(p!=NULL) {
if(p->Element==X)
break;
else if(p->Element>X)
p=p->Left;
else
p=p->Right;
}
if(p==NULL) {
p=(Position)malloc(sizeof(TreeNode));
p->Element=X;
p->Left=NULL;
p->Right=NULL;
}
}
Delete
正如许多数据结构一样,最困难的是删除操作。删除一个节点时要考虑下面几种情形。如果节点是一个树叶,那么它可以直接删除;如果节点有一个儿子,则将该节点的父节点指针调整指向子节点后删除;最复杂的是处理有两个儿子的节点,一般的策略是用其右子树的最小的数据代替该节点的数据并递归的删除那个节点。如果删除的次数不多,通常使用的策略是惰性删除:当一个元素要被删除时,它仍留在书中,而只是做了一个被删除的标记。
SearchTree Delete(ElementType X,SearchTree T)
{
Position tmpcell;
if(T!=NULL) {
if(X<T->Element)
T->Left=Delete(X,T->Left);
else if(X>T->Element)
T->Right=Delete(X,T->Left);
else if(T->Left&&T->Right) {
tmpcell=FindMin(T->Right);
T->Element=tmpcell->Element;
T->Right=Delete(T->Element,T->Right);
}
else {
tmpcell=T;
if(T->Left==NULL)
T=T->Right;
else if(T->Right==NULL)
T=T->Left;
free(tmpcell);
}
}
}
AVL 树
AVL (Adelson- Velskii 和 Landis)树是带有平衡条件的二叉查找树。AVL 树的平衡条件是其每个节点的左子树和右子树的高度最多差1。因此,出去插入操作外,所有树操作都可以以时间O(log N) 执行。插入一个节点可能破坏AVL树的特性。事实上,我们可以通过对树的修正来维持平衡条件,称之为旋转(rotation)。
旋转
在插入之后,只有那些从插入点到根节点路径上的节点的平衡可能改变。我们,恶意从插入的节点上行找到一个节点重新平衡这棵树,并证明这一重新平衡保证了整个树的满足AVL 特性。设重新平衡的节点叫做a,a的两棵子树的高度差 2,可能有以下四种情况:1. 对a 的左儿子的左子树进行一次插入;2. 对a 的左儿子的右子树进行一次插入;3. 对a 的右儿子的左子树进行一次插入;4. 对a 的右儿子的左子树进行一次插入。情形1和4是对称的,2和3是对称的。第一种插入发生在”外边“,通过单旋转完成调整,第二种情况插入发生在”内部“,通过稍微复杂的双旋转处理。
#include <stdlib.h>
#ifndef _AvTree_H
#define _AvTree_H
struct AvlNode;
typedef struct AvlNode *Position;
typedef struct AvlNode *AvlTree;
typedef int ElementType;
AvlTree MakeEmpty( AvlTree T);
Position Find(ElementType X,AvlTree T);
Position FindMin(AvlTree T);
Position FindMax(AvlTree T);
AvlTree Insert(ElementType X,AvlTree T);
AvlTree Delete(ElementType X,AvlTree T);
ElementType Retrieve(Position P);
#endif /* _AvlTree_H */
/* Place in the implementation file */
struct AvlNode
{
ElementType Element;
AvlTree Left;
AvlTree Right;
int Height;
};
static int Height(Position P)
{
if(P==NULL)
return -1;
else
return P->Height;
}
AvlTree Insert(ElementType X,AvlTree T)
{
if(T==NULL) {
T=(AvlTree)malloc(sizeof(struct AvlNode));
T->Element=X;
T->Height=0;
T->Left=T->Right=NULL;
}
else if(X < T->Element)
{
T->Left=Insert(X,T->Left);
if(Height(T->Left)-Height(T->Right)==2)
if(X< T->Left->Element)
T=SingleRotateWithLeft(T);
else
T=DoubleRotateWithLeft(T);
}
else if(X>T->Element) {
T->Right=Insert(X,T->Right);
if(Height(T->Right)-Height(T->Left)==2)
if(X< T->Right->Element)
T=SingleRotateWithRight(T);
else
T=DoubleRotateWithRight(T);
}
T->Height=Max(Height(T->Left),Height(T->Right))+1;
return T;
}
static Position SingleRotateWithLeft(Position K2)
{
Position K1;
K1=K2->Left;
K2->Left=K1->Right;
K1->Right=K2;
K2->Height=Max(Height(K2->Left),Height(K2->Right))+1;
K1->Height=Max(Height(K1->Left),K2->Height)+1;
return K1;
}
static Position DoubleRotateWithLeft(Position K3)
{
K3->Left=SingleRotateWithRight(K3->Left);
return SingleRotateWithLeft(K3);
}
伸展树
伸展树保证从空树开始任意连续M 次对树的操作最多花费O(M log N) 时间。伸展树的基本想法是,当一个节点被访问后,他就要经过一系列AVL 树的旋转被放到根上。注意到,如果一个节点很深,那么在其路径上就存在许多节点也相对较深,通过重新构造可以使得所有这些节点的进一步访问时间变少。因此,如果节点过深,那么我们还要求重新构造应具有平衡这棵树的作用。
实施上述描述的构造的一种方法是执行单旋转,从下向上进行,在访问路径上的每一个节点和他们的父节点实施旋转。但是这种策略会存在一系列M 个操作共需要O(M*N) 的时间。
展开
展开(splaying)的思路类似于前面介绍的旋转想法,我们仍从底部向上沿着访问路径旋转。令X 是在访问路径上的一个节点,我们将在这个路径上实施旋转操作。如果X 的父节点是树根,那么我们只是旋转X 和树根,这就是最后一个旋转。否则,X 就是父亲P 和祖父G,存在两种情况,第一种是之字形,那么我们执行双旋转,如果出现另一种一字形:X 和P 都是左儿子,或者都是右儿子,做一字形旋转。
B-树
B-树是一种常用的查找树,它不是二叉树。阶为M 的B-树具有下列结构特性:树的根或者是一片树叶,或者其儿子数在2和M 之间;除根外,所有非树叶节点的儿子数在M/2+1和M之间;所有的树叶都在相同的深度上。所有数据都存储在树叶上。