二叉搜索树是一种特殊的二叉树,对于其中的任意一个节点z,如果其存在左子树,则其左子树存储的关键字值最大不超过z所存储的关键字值;如果其存在右子树,则其右子树存储的关键字值最小不小于z所存储的关键字值。对于相同的关键字集合,可以有不同的二叉搜索数构型。如下图所示都是二叉搜索树:
二叉搜索数拥有许多特殊的性质,下面就以其性质与一些属性为主线,实现一个基本的二叉搜索树。首先,定义一个头文件如下:
#ifndef __BinarySearchTree_H_
#define __BinarySearchTree_H_
#include <iostream>
using namespace std;
template<class Type>
struct tagTreeNode
{
Type Data;
tagTreeNode* Parent;
tagTreeNode* LeftChild;
tagTreeNode* RightChild;
tagTreeNode() : Data(Type()) , Parent(NULL) , LeftChild(NULL) , RightChild(NULL) {}
};
template<class Type>
class BinarySearchTree
{
public:
typedef tagTreeNode<Type> TreeNode;
typedef tagTreeNode<Type>* PTreeNode;
public:
BinarySearchTree();
~BinarySearchTree();
void AddNode(Type data);
bool Delete(Type data);
bool Delete(PTreeNode pNode);
PTreeNode Find(Type data);
bool HasNode(Type data);
void ResetTree();
void PreOrderTreeWalk();
void InOrderTreeWalk();
void PostOrderTreeWalk();
PTreeNode TreeMaxmum();
PTreeNode TreeMinmum();
PTreeNode TreeMaxmum(PTreeNode pNode);
PTreeNode TreeMinmum(PTreeNode pNode);
PTreeNode TreePredecessor(PTreeNode pNode); //前驱
PTreeNode TreeSuccessor(PTreeNode pNode); //后继
private:
void Destory();
void _Destory(PTreeNode pNode);
void _PreOrderTreeWalk(PTreeNode pNode);
void _InOrderTreeWalk(PTreeNode pNode);
void _PostOrderTreeWalk(PTreeNode pNode);
void TransPlant(PTreeNode src , PTreeNode dst);
private:
PTreeNode m_Root;
};
#endif __BinarySearchTree_H_
该头文件使用了简单的模板,先定义了一个结构体tagTreeNode来表示树的节点,其有四个数据,一个存储关键字,其余三个指向本结构体指针分别表示父结点、左孩子结点、右孩子结点。如果指向不存在则为NULL(VS2008没有C++11中的空指针)。定义了一个类BinarySearchTree来表示二叉搜索树,该类只定义了一个指向根结点的结构体指针。
1、插入与查找:设x是二叉搜索树中的一个结点,如果y是x左子树中的一个结点,那么y.data ≤ x.data 。如果y是x右子树中的一个 结点,那么 y.data ≥ x.data。
根据这个性质,我们可以构造一个二叉搜索树,定义向二叉搜索树中增加结点如下:
template<class Type>
void BinarySearchTree<Type>::AddNode(Type data)
{
cout<<"Add node "<<data<<endl;
PTreeNode pNewNode = new TreeNode;
pNewNode->Data = data;
PTreeNode pParent = NULL;
PTreeNode pNode = m_Root;
while(pNode)
{
pParent = pNode;
if(pNode->Data > data)
pNode = pNode->LeftChild;
else
pNode = pNode->RightChild;
}
if(pParent == NULL)
{
pNewNode->Parent = NULL;
m_Root = pNewNode;
}
else
{
pNewNode->Parent = pParent;
if(pParent->Data > data)
pParent->LeftChild = pNewNode;
else
pParent->RightChild = pNewNode;
}
}
我们先把要增加的结点生成出来,再放到适当的位置。第一个while循环查找位置:如果当前结点的值比要插入结点的值大则继续在其左子树中查找,否则在右子树中查找,直到符合该性质的空尾,保留可以插入的位置的父结点以便插入。
同理,根据这个性质,我们可以很容易的在一棵二叉搜索树中查找一个值、一个结点是否存在或实现定位。
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::Find(Type data)
{
PTreeNode pNode = m_Root;
while(pNode && pNode->Data != data)
{
if(pNode->Data > data)
pNode = pNode->LeftChild;
else
pNode = pNode->RightChild;
}
return pNode;
}
template<class Type>
bool BinarySearchTree<Type>::HasNode(Type data)
{
PTreeNode pNode = Find(data);
if(pNode != NULL)
{
cout<<"Has Node "<<data<<endl;
return true;
}
else
{
cout<<"Do Not Have Node "<<data<<endl;
return false;
}
}
当然,可以更简单的实现递归版的查找功能,但是循环版的在性能上来说更好一点。
2、遍历:如果x是一颗有n个结点子树的根,那么中序遍历该树需要Θ(n)时间。
我们这里不去证明该定理,而是去实现中序、先序、后序遍历。谈到二叉树,就必须了解它的三种遍历方式。所谓中序遍历即:先遍历某个结点,再去遍历它的左子树的结点,然后去遍历它的右子树的结点。其实这三种遍历区别在于哪个时候访问当前结点的值。
由于这种遍历的方式,让人很自然就想到用递归去遍历,这里也是这样实现的:
template<class Type>
void BinarySearchTree<Type>::InOrderTreeWalk()
{
_InOrderTreeWalk(m_Root);
cout<<endl;
}
template<class Type>
void BinarySearchTree<Type>::_InOrderTreeWalk(PTreeNode pNode)
{
if(pNode == NULL)
return;
_InOrderTreeWalk(pNode->LeftChild);
cout<<pNode->Data<<" ";
_InOrderTreeWalk(pNode->RightChild);
}
第一个函数只是传递树根调用第二个函数。第二个函数是中序遍历的递归实现:如果当前结点为空,则立即返回,这是跳出递归的必须条件。先遍历左子树,如果其左子树不为空,则又会用同样的方法递归遍历其左子树,如果做子树为空,则会遇到跳出条件理解返回。访问当前结点的值,再遍历其右子树。同理,先序遍历和有序遍历基本相同:
template<class Type>
void BinarySearchTree<Type>::PreOrderTreeWalk()
{
_PreOrderTreeWalk(m_Root);
cout<<endl;
}
template<class Type>
void BinarySearchTree<Type>::PostOrderTreeWalk()
{
_PostOrderTreeWalk(m_Root);
cout<<endl;
}
template<class Type>
void BinarySearchTree<Type>::_PreOrderTreeWalk(PTreeNode pNode)
{
if(pNode == NULL)
return;
cout<<pNode->Data<<" ";
_PreOrderTreeWalk(pNode->LeftChild);
_PreOrderTreeWalk(pNode->RightChild);
}
template<class Type>
void BinarySearchTree<Type>::_PostOrderTreeWalk(PTreeNode pNode)
{
if(pNode == NULL)
return;
_PostOrderTreeWalk(pNode->LeftChild);
_PostOrderTreeWalk(pNode->RightChild);
cout<<pNode->Data<<" ";
}
3、搜索:对于一棵高度为h的二叉搜索树,要查找最大/最小关键值元素总能再O(h)时间内完成。
所谓最大关键值元素即在某一棵不为空的二叉搜索棵树中,其存储的值不比树中其他存储的值小。我们可以根据数的特性,一直搜索一个数的右子树来实现,即如果以z为根的二叉搜索数,如果其存在右子树,则最大关键值元素一定在其右子树中。同理,如果该树存在左子树,则其最小关键值元素一点存在其左子树中。如果不存在子树,则为其本身。
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::TreeMaxmum()
{
return TreeMaxmum(m_Root);
}
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::TreeMinmum()
{
return TreeMinmum(m_Root);
}
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::TreeMaxmum(PTreeNode pNode)
{
while(pNode && pNode->RightChild)
pNode = pNode->RightChild;
return pNode;
}
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::TreeMinmum(PTreeNode pNode)
{
while(pNode && pNode->LeftChild)
pNode = pNode->LeftChild;
return pNode;
}
4、前驱和后继:
要查找前驱,则根据二叉搜索树的性质,可以分为两种情形:
a、如果结点x的左子树非空,则其前驱为其左子树的最大关键值结点。
b、如果结点x的左子树为空,则其前驱为沿该结点向上,第一个不为左孩子的结点的父结点。当然,这种情形前驱可能不存在。
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::TreePredecessor(PTreeNode pNode)
{
if(pNode->LeftChild)
return TreeMaxmum(pNode->LeftChild);
PTreeNode pParent = pNode->Parent;
while(pParent && pNode == pParent->LeftChild)
{
pNode = pParent;
pParent = pNode->Parent;
}
return pParent;
}
如上面的第一幅图,值为2的结点一直往上查找第一个不为左孩子的结点,但是不存在,所有其前驱不存在。值为8的结点向上查找这样的结点,找到了值为7的结点,所有值为7的结点即为其前驱。第二棵二叉搜索树中,
深度为5的值为5的结点一直向上查找,找到7为第一个不为左孩子的结点,且其父结点存在,则其父结点(深度为2的值为5的结点)即为要找的前驱。
要查找后继,我们有如下策略:
a、如果结点存在右孩子,则其后继为其右孩子为根节点的子树的最小关键字结点。
b、如果结点不存在有孩子,则其后继为沿父结点往上查找,第一个不为右孩子的结点的父结点。也可能不存在。
template<class Type>
tagTreeNode<Type>* BinarySearchTree<Type>::TreeSuccessor(PTreeNode pNode)
{
if(pNode->RightChild)
return TreeMinmum(pNode->RightChild);
PTreeNode pParent = pNode->Parent;
while(pParent && pNode == pParent->RightChild)
{
pNode = pParent;
pParent = pNode->Parent;
}
return pParent;
}
5、删除结点
删除结点比较复杂,可以分以下情形:
a、如果z没有左孩子(包括有右孩子和没有孩子结点的情形),那么用其右孩子来替换z,这个右孩子可以是NULL,也可以不是。
b、如果z仅有一个孩子且为其左孩子,则用其左孩子来替换z。
c、如果z同时存在两个孩子,则需要查找其后继y,做如下操作:
1) 如果y是z的右孩子,则用y替换z,仅保留y的右孩子。
2) 用y的右孩子替换y,然后用y替换z。
如上图为各种删除的情形,其中实现为父子关系,虚线表示中间可能间隔多个结点相连。程序实现如下
template<class Type>
bool BinarySearchTree<Type>::Delete(PTreeNode pNode)
{
if(! pNode)
return false;
if(NULL == pNode->LeftChild) // two case:1.left and right child both are NULL. 2.left child is NULL,and right child is not NULL
{
TransPlant(pNode , pNode->RightChild); //make his right child or NULL to replace his position
delete pNode;
return true;
}
else if(NULL == pNode->RightChild) //left child is not NULL and right child is NULL
{
TransPlant(pNode , pNode->LeftChild); //make his left child to replace his position
delete pNode;
return true;
}
else //both children are not NULL
{
PTreeNode y = TreeMinmum(pNode->RightChild);
if(y->Parent != pNode)
{
TransPlant(y , y->RightChild);
y->RightChild = pNode->RightChild;
y->RightChild->Parent = y;
}
TransPlant(pNode , y);
y->LeftChild = pNode->LeftChild;
y->LeftChild->Parent = y;
delete pNode;
return true;
}
}
template<class Type>
void BinarySearchTree<Type>::TransPlant(PTreeNode src , PTreeNode dst)
{
if(! src->Parent)
m_Root = dst;
else if(src == src->Parent->LeftChild)
src->Parent->LeftChild = dst;
else
src->Parent->RightChild = dst;
dst->Parent = src->Parent;
}
这里专门写了个函数TransPlant来处理用dst结点替换src结点的操作。
其他一些函数实现:
template<class Type>
BinarySearchTree<Type>::BinarySearchTree():
m_Root(NULL)
{
}
template<class Type>
BinarySearchTree<Type>::~BinarySearchTree()
{
Destory();
}
template<class Type>
void BinarySearchTree<Type>::ResetTree()
{
Destory();
m_Root = NULL;
}
template<class Type>
void BinarySearchTree<Type>::Destory()
{
_Destory(m_Root);
}
template<class Type>
void BinarySearchTree<Type>::_Destory(PTreeNode pNode)
{
if(pNode == NULL)
return;
if(pNode->LeftChild)
_Destory(pNode->LeftChild);
if(pNode->RightChild)
_Destory(pNode->RightChild);
cout<<"delete node "<<pNode->Data<<endl;
delete pNode;
}