写在前面
本文主要分为三个部分。
第一部分介绍了二叉搜索树的基本性质。
第二部分全面详细地讲述了二叉搜索树的各种基本操作。包括WALK/遍历、SEARCH/查找、MINIMUM/最小关键字、MAXIMUM/最大关键字、SUCCESSOR/后继、PREDECESSOR/前驱、INSERT/插入、DELETE/删除等。主要参考《算法导论》(中文第3版)中有关二叉搜索树的相关介绍说明。
对于每一种基本操作,都至少分三个重点进行讲解。它们分别是基本操作过程及原理(包含伪代码及C++实现)、时间复杂度分析以及举例分析(配图)。力求让每位读者可以直观地理解。
第三部分则是完整的代码实现及实例分析。
本文断断续续写了几天,各部分的举例分析也十分明了,为了讲得更清楚一些,所有的配图都是自行制作的。请尊重劳动成果,供人供己查阅,如有错误之处,欢迎指正。
原创文章,转载请注明出处。http://blog.csdn.net/qq_21396469/article/details/78419609
一、二叉搜索树简介与基本性质
1、定义
二叉搜索树(BST)又称二叉查找树或二叉排序树。一棵二叉搜索树是以二叉树来组织的,可以使用一个链表数据结构来表示,其中每一个结点就是一个对象。一般地,除了key和卫星数据(文末附注1)之外,每个结点还包含属性lchild、rchild和parent,分别指向结点的左孩子、右孩子和双亲(父结点)。如果某个孩子结点或父结点不存在,则相应属性的值为空(NIL)。根结点是树中唯一父指针为NIL的结点,而叶子结点的孩子结点指针也为NIL。
2、基本性质
根据《算法导论》(中文第3版)的相关介绍,二叉搜索树中的关键字总是以满足二叉搜索树性质的方式来存储:
设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key≤x.key。如果y是x右子树中的一个结点,那么y.key≥x.key。
在二叉搜索树中:
① 若任意结点的左子树不空,则左子树上所有结点的值均不大于它的根结点的值;
② 若任意结点的右子树不空,则右子树上所有结点的值均不小于它的根结点的值;
③ 任意结点的左、右子树也分别为二叉搜索树。
一棵典型的二叉搜索树如下:
二、二叉搜索树的基本操作与代码实现
1、二叉搜索树的结点
正如前面所说,每个二叉搜索树的结点,包含关键字key、左孩子指针lchild、右孩子指针rchild以及父结点指针parent。在C++实现中,我们定义一个结点类BSTNode来表示一个结点,并初始化结点的关键字等于0,左右孩子指针和父结点指针为NIL。/* 二叉搜索树节点 */
class BSTNode
{
private:
double key; // 关键字
BSTNode *lchild; // 左孩子
BSTNode *rchild; // 右孩子
BSTNode *parent; // 父节点
friend class BSTree;
public:
BSTNode(double k = 0.0, BSTNode *l = NULL, BSTNode *r = NULL, BSTNode *p = NULL) :key(k), lchild(l), rchild(r), parent(p){}
};
2、二叉搜索树的基本操作
对于一棵二叉搜索树来说,它支持许多动态集合操作,包括WALK(遍历)、SEARCH(查找)、MINIMUM(最小关键字)、MAXIMUM(最大关键字)、SUCCESSOR(后继)、PREDECESSOR(前驱)、INSERT(插入)、DELETE(删除)等。下面将依次讲解这些操作的具体过程及实现。
2.1 WALK(遍历)
2.1.1 中序遍历/INORDER-TREE-WALK
二叉搜索树的性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序遍历(inorder tree walk)算法。对于中序遍历来说,输出的子树根的关键字位于其左子树的关键字值和右子树的关键字值之间。其伪代码如下:
// 中序遍历
void inOrder_Tree_Walk(BSTNode *x)
{
if (x != NULL)
{
inOrder_Tree_Walk(x->lchild);
cout << x->key << " ";
inOrder_Tree_Walk(x->rchild);
}
}
得益于二叉搜索树的性质,当使用中序遍历来访问一棵二叉搜索树上的所有结点时,最后得到的访问序列恰好是所有结点关键字的升序序列。
2.1.2 先序遍历/PREORDER-TREE-WALK
跟中序遍历类似,先序遍历(preorder tree walk)算法也是通过递归来实现的。区别在于先序遍历输出的子树根的关键字在其左右子树的关键字值之前。我们同样可以写出先序遍历的伪代码:
同样,根据上述伪代码,我们可以很容易地写出先序遍历二叉搜索树的实现。
// 先序遍历
void preOrder_Tree_Walk(BSTNode *x)
{
if (x != NULL)
{
cout << x->key << " ";
preOrder_Tree_Walk(x->lchild);
preOrder_Tree_Walk(x->rchild);
}
}
2.1.3 后序遍历/POSTORDER-TREE-WALK
跟中序遍历和先序遍历类似,后序遍历(postorder tree walk)算法也是通过递归来实现的。区别在于后序遍历输出的子树根的关键字在其左右子树的关键字值之后。同样地,我们可以写出后序遍历的伪代码:
根据上述伪代码,我们可以很容易地写出后序遍历二叉搜索树的实现。
// 后序遍历
void postOrder_Tree_Walk(BSTNode *x)
{
if (x != NULL)
{
postOrder_Tree_Walk(x->lchild);
postOrder_Tree_Walk(x->rchild);
cout << x->key << " ";
}
}
2.1.4 遍历的时间复杂度
遍历一棵有n个结点的二叉搜索树需要耗费θ(n)(文末附注2)的时间,因为初次调用之后,对于树中的每个节点这个过程恰好要自己调用两次:一次是它的左孩子,一次是它的右孩子。
根据《算法导论》(中文第3版)定理12.1:
如果x是一棵具有n个结点子树的根,那么调用INORDER-TREE-WALK(x)需要θ(n)的时间。
类似地,PREORDER-TREE-WALK(x)和POSTORDER-TREE-WALK(x)也只需要θ(n)的时间。
2.1.5 遍历访问序列举例
基于上述中序遍历、先序遍历、后序遍历的算法,对于下图典型的二叉搜索树来说,其三种遍历所得到的关键字访问序列分别为:
① 中序遍历:2、5、5(叶子)、6、7、8
② 先序遍历:6、5、2、5(叶子)、7、8
③ 后序遍历:2、5(叶子)、5、8、7、6
这里也验证了我们前面提到的一句话:使用中序遍历来访问一棵二叉搜索树上的所有结点时,最后得到的访问序列恰好是所有结点关键字的升序序列。
2.2 查找(SEARCH)
2.2.1 查找过程
在二叉搜索树中查找一个具有给定关键字key的结点,需要输入一个指向树根的指针x和一个关键字k,如果这个结点存在,则TREE-SEARCH返回一个指向关键字为k的结点的指针;否则返回NIL。
具体查找过程为:
① 从树根开始查找,并沿着这棵树中的一条简单路径向下进行;
② 若树为空树,则查找失败,返回NIL;
③ 对于遇到的每个结点x,若关键字k等于结点x的关键字,查找终止,返回指向结点x的指针;
④ 若关键字k小于结点x的关键字,则查找在x的左子树中继续(根据二叉搜索树的性质,k此时不可能在右子树中);
⑤ 对称地,若关键字k大于结点x的关键字,则查找在x的右子树中继续(k此时不可能在左子树中);