《数据结构与算法描述:c++实现》学习笔记:第四章
第四章 树
二叉查找树(binary search tree)。在很多应用程序中都有使用的两个库集合类set和map的实现基础。
本章目的:
- 了解树是如何用于实现几个流行的操作系统中的文件系统的。
- 了解树如何用来计算算术表达式的值。
- 指出如何利用树支持O(logN)平均时间进行的各种搜索操作,以及如何细化得到最坏情况时间界 O(logN)。
- 讨论并使用set和map类。
4.1 预备知识
树定义的一种自然的方式是递归的方法。一课树是一些结点的集合。这个集合可以是空集,若不是空集,则树由称作根的结点r以及零个或多个非空的子树T1,T2…Tk组成,这些子树中每一颗的根都被来自根r的一条有向的边所连接。
儿子:每一颗子树的根叫做根r的儿子。
父亲:r是每一颗子树的根的父亲。
兄弟:具有相同父亲的结点。
路径:结点n1到nk的路径定义为结点n1,n2,。。,nk的一个序列。
长:路径上的边的条数,为k-1.
深度:从根到ni的唯一路径的长。
高:ni到一片树叶的最长路径的长。(深度计算根节点到自己,高度计算自己到最下面)
一棵树是N个结点和N-1条边的集合。
4.1.1 树的实现
实现方法:将每个结点的所有儿子都放在树结点的链表中。
struct TreeNode
{
Object element;
TreeNode *firstChild;
TreeNode *nextChild;
};
4.1.2 树的遍历和应用
前序遍历:对结点的处理工作是在他的所有儿子结点被处理之前进行的。
后序遍历:对结点的处理工作是在他的所有儿子结点被处理之后进行的。
4.2 二叉树
二叉树(binary tree)一颗每个结点都不能有多于两个儿子的树。(儿子可能为空)
性质:平均二叉树的深度要比结点个数N小得多。为O(√N),对于特殊类型的二叉树,其深度的平均值是O(logN)。
4.2.1 实现
类似于双向链表的声明。
struct BinaryNode
{
Object element;
BinaryNode *left;
BinaryNode *right;
}
4.3 查找树ADT——二叉查找树
对于树中的每个结点X,它的左子树中的所有项的值小于X中的项,它的右子树中所有项的值大于x中的项。
平均深度为O(logN).
类模板中的contains、insert和remove的调用方法
4.3.1 contains
树T中有项为X的结点,contains操作返回true,否则返回false
bool contains(const Comparable & x, BinaryNode *t) const
{
if(t == NULL)
return false;
else if(x < t->element)
return contains(x,t->left);
else if(x > t->element)
return contains(x,t->right);
else
return true;
}
在这里使用尾递归,算法的表达式的简洁性是以速度的降低为代价的,使用的栈空间的量是O(logN)。
4.3.2 findMax和findMin
分别返回指向树中包含最大元和最小元的结点的指针。。findMin从根开始并且只要有左儿子就想左进行,终止点就是最小的元素。findMax相右进行。
//递归实现
BinaryNode * findMin(BinaryNode *t )const
{
if(t == NULL)
return NULL;
if(t->left == NULL)
return t;
return findMin(t->left);
}
//非递归实现
BinaryNode *findMax(BinaryNode *t) const
{
if(t!=NULL)
while(t->right != NULL)
t= t->right;
return t;
}
4.3.3 insert
先使用contains沿着树查找,如果找到X,什么都不用做。否则,将X插入到遍历的路径上的最后一点。
void insert(const Comparable & x,BinaryNode * &t)
{
if(t == NULL)
t = nex BinaryNode(x, NULL, NULL);
else if(x < t->element)
insert(x, t->left);
else if(x > t->element)
insert(x, t->right);
else;//do nothing
}
4.3.4 remove
一旦发现需要删除的结点,考虑几种情况
- 结点是一片树叶,可以立即被删除。
- 结点有一个儿子,该结点可以在其父结点调整它的链以绕过该节点然后被删除。
- 结点有两个儿子,用其右子树的最小的数据代替该结点的数据并递归地删除那个结点。
4.4 AVL树
AVL(Adelson-Velskii and Landis)树是带有平衡条件的二叉查找树。这个平衡条件必须要容易保持,而且必须保证树的深度是O(logN)。
一棵AVL树是其每个结点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1)
在高度为h的AVL树中,最少节点数S(h) = S(h-1) + S(h-2) + 1给出,对于h=0,S(h) = 1;h=1, S(h) = 2.
注意:在AVL树中,比较难的是插入操作,因为存在着破坏平衡性的危险,此时把必须重新平衡的结点叫做α。出现四种不平衡的情况。
- 对α的左儿子的左子树进行一次插入
- 对α的左儿子的右子树进行一次插入
- 对α的右儿子的左子树进行一次插入(2的镜像)
- 对α的右儿子的右子树进行一次插入(1的镜像)
第一种情况是插入发生在外边的情况(左-左和右-右),可以通过对树的一次单旋转完成调整。
第二种情况是插入发生在内部的情况(左-右和右-左),通过稍微复杂得双旋转完成调整。
4.4.1 单旋转
下图显示了单旋转如何调整情形1和情形4。
情形1:结点k2不满足AVL平衡性质(X是两层深度),左子树比右子树深两层。Y不可能与新X在同一层上,那样可在插入之前就失去了平衡,Y也不能和Z在同一层,那样k1就是在通向根的路径上破坏AVL平衡条件的第一个结点。所以,把X上移一层,Z下移一层。重新安排结点形成一颗等价的树。
4.4.2 双旋转
对于情形2和3,树Y太深,单旋转没有降低深度,所以通过双旋转完成。
4.5 伸展树
伸展树(splay tree),它保证从空树开始任意连续M次对树的操作最多花费O(MlogN)的时间。一般来说,当M次操作的序列总的最坏情形运行时间为O(Mf(N))时,我们就说它的摊还时间为O(f(N))。
伸展树的基本思想为,当一个节点被访问后,它就要经过一系列AVL树的旋转被推倒根上。另外,伸展树不要求保留高度或平衡信息,因此可以在某种程度上节省空间并简化代码。
4.5.2 伸展
伸展的方法类似于旋转。仍然从底向上沿着访问路径旋转。令X是在访问路径上的一个非根结点,在这个路径上实施旋转操作。如果X的父结点是树根,只要旋转X和树根,这就是沿着访问路径上的最后的旋转。否则,X就有父亲(P)和祖父(G),存在两种情况以及对称的情形要考虑。
第一种之字形,此时X是右儿子,P是左儿子。执行一次就像AVL双旋转那样的双旋转。
第二种一字型,X和P都是左儿子或者右儿子。在这种情况下,左边的树换成右边的。
4.6 树的遍历
递归方法进行树的遍历
void printTree(ostream & out = cout)const
{
if( isEmpty())
out<<"Empty tree"<<endl;
else
printTree(root,out);
}
/* Internal method to print a subtree rooted at t in sorted order*/
void printTree(BinaryNode *t, ostream & out )const
{
if(t != NULL)
{
printTree(t->left, out);
out<< t->element <<endl;
printTree(t->right, out);
}
}
中序遍历(inorder traversal)一般是先处理左子树,然后是当前的结点,最后处理右子树。其总的运行时间是O(N)。
后序遍历(postorder traversal)先处理两个子树然后才能处理当前结点,总的运行时间也是O(N)
/* Internal method to compute the height of a subtree rooted at t */
int height(Binary *t)
{
if(t== NULL)
return -1;
else
return 1 + max( height (t->left ), height(t->right));
}
前序遍历(preorder traversal)当前结点在其儿子结点之前处理。
层序遍历(level-order traversal)所有深度为d的结点要在深度为d+1的结点之前进行处理,用的较少。不是用递归实施,而是用到队列。
共同的思想:首先处理NULL的情形,
4.7 B树
阶为M的B树是一颗具有下列结构特性的树
(1)数据项存储在树叶上
(2)非叶结点存储直到M-1个键,以指示搜索的方向;键i代表子树i+1中最小的键。
(3)树的根或者是一片树叶,或者其儿子树在2和M之间
(4)除根外,所有非树叶结点的儿子树在M/2(向上取整)和M之间
(5)所有的树叶都在相同深度上并有L/2(向上取整)和L之间个数据项,稍后描述L的确定。
4.8 标准库中的set和map
4.8.1 set
set是一个排序后的容器,该容器不允许重复。包括begin, end, size, empty等方法。特有的操作是高效的插入、删除和执行基本查找。
插入insert
STL定义了一个名为pair的类模板,该类模板比struct多两个用来访问pair的两项的成员first和second。例如下面两个
pair<iterator, bool> insert (const Object & x);
pair<iterator, bool> insert (iterator hint, const Object & x);
单参数的insert执行如上,双参数insert允许对x将要插入的位置的线索的说明,此时插入很快,可以看为O(1).
删除erase
有几个版本的
int erase( const Object & x);
iterator erase( iterator itr);
iterator erase( iterator start, iterator end);
第一个单参数删除x,返回删除的元素的个数(0或者1)。第二个单参数的执行与在vector和list中一样,删除由iterator指定的位置的对象,返回指向下一个位置的元素。第三个双参数的执行与vector和list一样,从start开始从end结束的所有的项(不包括end)
查找操作
iterator find(const Object &x ) const;
4.8.2 map
map用来存储排序后的由键和值组成的项的集合。键必须唯一,但是多个键可以对应同一个值。因此,值不需要唯一。在map中的键保持逻辑排序后的顺序。支持begin, end, size, empty操作。也支持insert, find, erase
4.8.3 set和map的实现
底层实现是平衡二叉查找树。常常使用自顶向下红黑树。