6. 二叉搜索树
由于一般的二叉树没有二叉搜索树那样的特性(对每一个结点,其左子树中的数据值都小于结点本身的数据值,而右子树中的数据值都大于或等于该结点的数据值),所以也就没有了像二叉搜索树那样的成员函数(如:插入结点、删除结点、搜索),这使得大部分关于二叉树的考察都是集中在二叉搜索树中的。
【注】如果不作特别说明的话,一般情况下对二叉树和二叉搜索树是不作区分的。
另外,二叉树的应用还有堆、AVL树。
下面重点讲述二叉搜索树:
【思想】二叉搜索树的递归过程其实就是一个扫描过程,在复制树(copyTree)和清除树(_clearTree)中用到的是后序扫描(LRN);而在打印树(_printTree)中用到的是(右)中序扫描(RNL);而在搜索树(_findTree)中用的是前序扫描(NLR)。之所以扫描方式不同,是为了更方便得完成各自的任务。
#include <stdafx.h>
#include <iostream>
using namespace std;
struct TreeNode
{
int data;
TreeNode *left;
TreeNode *right;
//由于下面定义了非默认构造函数,于是编译器不再自动生成默认构造函数,所以这里一定要自定义默认构造函数。
//而对每个参数设置默认值等同于定义了默认构造函数,从而达到一举两得的作用。
TreeNode(const int _data=0, TreeNode *_left=NULL, TreeNode *_right=NULL):data(_data), left(_left), right(_right){}
};
class BinSTree
{
public:
TreeNode *root; //root设为public数据成员是为了方便调用
private:
TreeNode *copyTree(TreeNode *node);//用在复制构造函数和赋值操作符中
//【注】以下两个私有成员函数为了和相应的公有成员函数相区别,分别在各自前面加了一个下划线,但是如果不加的话其实也
//正确,因为这属于函数重载(参数列表是不同的)
void _clearTree(TreeNode *node);//在clearTree和析构函数中会被用到
void _printTree(TreeNode *node, int level);//只能用在printTree函数中
TreeNode *_findNode(TreeNode *node, const int item)const;//只能用在findNode中
void _insertNode(TreeNode *&node, TreeNode *newNode);//只能用在insertNode中,二叉树搜索排序法必需的函数
public:
BinSTree():root(NULL){}//必须对root进行初始化,因为TreeNode为自定义类型,编译器是不会自动进行初始化的。
//由于二叉搜索树需要进行深度复制,故还得定义复制构造函数、析构函数和赋值操作符,但是鉴于这三个函数对二叉搜索树的
//构建、插入结点、删除结点、打印没有影响,故可以省略。
BinSTree(const BinSTree &tree);
~BinSTree(){clearTree();}
BinSTree & operator=(const BinSTree &rhs);
TreeNode *findNode(const int item)const;
void insertNode(const int item); //二叉树搜索排序法必需的函数
void deleteNode(const int item);
//printTree与clearTree对一般二叉树也适用。
void printTree();
void printTreeNLR();//前序非递归遍历
void printTreeLNR();//中序非递归遍历
void printTreeLRN();//后序非递归遍历
//在clearTree中调用_clearTree后,要将root清为NULL,否则root将成为悬浮指针。
void clearTree();
};
//复制树,私有成员函数
//方法一:调用TreeNode的构造函数
TreeNode *BinSTree::copyTree(TreeNode *node)
{
if(NULL == node)
return NULL; //递归终止条件
TreeNode *newNode = new TreeNode(node->data, copyTree(node->left), copyTree(node->right));
return newNode;
}
//方法二:直接对TreeNode的成员进行复制操作
//用在复制构造函数和赋值操作符中。遍历顺序为:LRN(后序遍历),当然也可以采用NLR(前序遍历)(只需将语句3置于1之前即可)
TreeNode *BinSTree::copyTree(TreeNode *node)
{
if(NULL == node)
return NULL; //递归终止条件
TreeNode *newNode = new TreeNode;
newNode->left = copyTree(node->left); //语句1
newNode->right = copyTree(node->right); //语句2
newNode->data = node->data; //语句3
return newNode;
}
//删除树,私有成员函数
//用在析构函数、赋值操作符以及clearTree中,为了进行递归操作,需要在_clearTree中加一个参数表示当前子树的根结点
void BinSTree::_clearTree(TreeNode *node)
{
//【思想】先删除左子树后删除右子树,然后再删除当前结点,故为后序遍历。
//按LRN(后序)的扫描顺序进行扫描并删除结点的。具体地,先从左子树的最下层依次向上进行删除,再从右子树的最下层依 //次向上进行删除。
if(NULL == node)
return; //递归终止条件
_clearTree(node->left); //清除左子树
_clearTree(node->right); //清除右子树
delete node;
}
//打印树,私有成员函数
//只能用在printTree函数中
//在打印二叉搜索树时,如果成员函数printTree没有参数,则很难使用递归来进行打印编程,因此,在该函数中添加了每次打印时的//子树的根结点node以及其所处的层次/深度(这通过level个空格来控制)。
//【思想】先打印右子树,再打印当前结点,然后在打印左子树,故采用RNL遍历顺序(右中序遍历)。
void BinSTree::_printTree(TreeNode *node, int level)
{
//递归终止条件
if(NULL == node)
return;
//递归步骤
//采用RNL的顺序进行横向打印
_printTree(node->right, level+1);//打印右子树的结点
for(int i=0; i!=level; ++i)
cout << " ";//第level层的结点前加level个空格,以表明层次性
cout << node->data << endl;//打印当前结点
_printTree(node->left, level+1);//打印左子树的结点
}
//打印二叉树/二叉搜索树
void BinSTree::printTree()
{
_printTree(root, 0);
}
//前序非递归遍历
void BinSTree::printTreeNLR()
{
stack<TreeNode *> s;
TreeNode *currNode=root;
while((NULL!=currNode) || !s.empty())
{
if(NULL != currNode)
{
cout << currNode->data << " ";
s.push(currNode);
currNode = currNode->left; //访问完当前结点后再指向左孩子结点
}
else
{
currNode = s.top();
s.pop();
currNode = currNode->right; //如果没有左孩子则指向右孩子
}
}
}
//中序非递归遍历
void BinSTree::printTreeLNR()
{
stack <TreeNode *> s;
TreeNode *currNode=root;
while((NULL!=currNode) || !s.empty())
{
if(NULL != currNode)
{
s.push(currNode);
currNode = currNode->left;
}
else
{
currNode = s.top();
cout << currNode->data << " ";//与前序遍历相比,仅仅在访问N结点的顺序上有了变化
s.pop();
currNode = currNode->right;
}
}
}
//后序非递归遍历
void BinSTree::printTreeLRN()
{
stack<TreeNode *> s;
TreeNode *preNode=NULL, *currNode=root;
/*preNode表示之前访问过的结点,这是因为后序遍历的间断性导致如果没有标签变量记录之前访问过的结点的话,将导致重复对右子树进行访问*/
while ((NULL!=currNode) || !s.empty())
{
if (NULL != currNode)
{
s.push(currNode);
currNode = currNode->left;
}
else
{
currNode = s.top();//这时不能进行出栈操作,因为有可能还有右子树
if (currNode->right!=NULL && preNode!=currNode->right)
//存在右子树且还没有访问过,则currNode指向右子树,然后重复进行右子树中各结点的访问
currNode=currNode->right;
else/*不存在右子树或右子树已经被访问过,则对该结点进行访问,并更新preNode以及对s进行出栈操作,然后将
currNode置为NULL,从而使得下一步向上继续访问*/
{
cout << currNode->data << " ";
preNode=s.top();
s.pop();
currNode=NULL;
//下一步应该访问父结点,故将currNode设为NULL,使得下一次循环时程序执行上一层的访问操作
}
}
}
}
//复制构造函数
BinSTree::BinSTree(const BinSTree &tree)
{
root = copyTree(tree.root);
}
//清除树并将根结点清为空指针
void BinSTree::clearTree()//要将清为NULL的node返回给调用函数
{
_clearTree(root);
//调用_clearTree后,要将root清为NULL,否则root将成为悬浮指针,
//从而影响到下一步的插入等操作。
root = NULL;
}
//赋值操作符
BinSTree & BinSTree::operator=(const BinSTree &rhs)
{
//避免自我复制
if(this == &rhs)
return *this;
clearTree();//清除当前树
root = copyTree(rhs.root);
return *this;
}
//搜索结点
//(1)迭代/循环方法(也即非递归方法)
TreeNode *BinSTree::findNode(const int item)const
{
TreeNode *currPtr = root;
while(currPtr != NULL)
{
if(item == currPtr->data)
break;
if(item < currPtr->data)
currPtr = currPtr->left;
else
currPtr = currPtr->right;
}
return currPtr;
//当currPtr!=NULL时循环结束,表明已经找到值为item的结点,否则,表明未找到。
}
//(2)递归方法
//私有成员函数
//只能用在findNode函数中,用来作findNode的递归部分。由于是递归,故还需要专门控制递归的参数(node)
TreeNode *BinSTree::_findNode(TreeNode *node, const int item)const
{
if(NULL == node)//终止条件
return NULL;
//递归步骤:用的是前序扫描(NLR)
if(item == node->data)
return node;
if(item < node->data)
return _findNode(node->left, item);//注意不要落掉return
else
return _findNode(node->right, item);//注意不要落掉return
}
//搜索结点
//递归方法
TreeNode *BinSTree::findNode(const int item)const
{
return _findNode(root, item);
}
//插入结点
【思想】插入结点一定是在叶子结点上插入的(空树除外)。这是由二叉搜索树的性质决定的。可见,插入结点要比删除结点简单得多。
//(1)非递归/迭代方法
void BinSTree::insertNode(const int item)
{
TreeNode *currPtr=root, *parentPtr=NULL, *newNode;
while(currPtr != NULL)
{
parentPtr = currPtr;
if(item < currPtr->data) // 二叉搜索树的左子结点一定小于父结点
currPtr = currPtr->left;
else
currPtr = currPtr->right; // 二叉搜索树的右子结点一定不小于(大于或等于)父结点
}
// 循环结束后,currPtr==NULL,而parentPtr一定为叶子结点
newNode = new TreeNode;
newNode->data = item;
newNode->left = NULL;
newNode->right = NULL;
if(NULL == parentPtr)
root = newNode;// 特殊情况
else if(item < parentPtr->data)
parentPtr->left = newNode;
else
parentPtr->right = newNode;
}
//(2)递归方法
//私有成员函数
//只能用在insertNode中,遍历顺序同样为NLR
void BinSTree::_insertNode(TreeNode *&node, TreeNode *newNode)
{
if(NULL == node) //插入结点一定是在叶子结点上插入的,所以当出现node为NULL时则意味着递归终止条件的满足
node = newNode; //因为node参数是用的引用,因此,可以通过修改形参已达到修改实参的目的(这时形参是实参的别名)
else if(newNode->data < node->data)
_insertNode(node->left, newNode);
else
_insertNode(node->right, newNode);
}
//插入结点
//递归方法
void BinSTree::insertNode(const int item)
{
TreeNode *newNode = new TreeNode(item); //调用TreeNode的默认构造函数
_insertNode(root, newNode);
}
//删除结点
【思想】
1、查找删除结点D:首先查找值等于item的结点D及其双亲结点P(P:parent, D:delete, R:replace);
2、查找替换结点R并将D的子树连接到R上:如果D可以找到,则再找D的替换结点R,这一点只与D的子树有关,而与D的上层结点都无关。该步骤分为三种情况:
(1)D无左子树:选择右子树的根结点作为R。
(2)D无右子树:选择左子树的根结点作为R。
另外,前两种情况有交叉,即D既无左子树又无右子树,这种情况包含在了前两种情况中,故无需单独考虑
(3)D既有左子树,又有右子树:选择D的左子树最右边的结点作为R。这又分为两种子情况:
① D的左孩子没有右子树,这时,R为D的左孩子;
② D的左孩子有右子树,易找到R,将R的左子树连接到R原来的双亲结点PofR上并作为PofR的右子树。
3、将R连接到D的父结点P上:这时涉及到D与P相连接的问题。分为三种情况:
① P=NULL,即D为根结点,这时,令R作为根结点即可。
② D为P的左孩子,这时,令R作为P的左孩子。
③ D为P的右孩子,这时,令R作为P的右孩子。
4、删除D。
【注】全部过程与P的上层结点毫无关系,它影响的只是P及其下层的结构。
void BinSTree::deleteNode(const int item)
{
TreeNode *PNodePtr, *DNodePtr, *RNodePtr, *PofRNodePtr;
//P:parent, D:delete, R:replace,PofR:parent of R
//第一步:寻找D以及它的双亲结点P
for(PNodePtr=NULL,DNodePtr=root; DNodePtr!=NULL && item!=DNodePtr->data; )
//寻找到第一个与item相等的结点则结束循环
{
PNodePtr = DNodePtr; //依次向下寻找
if(item < DNodePtr->data)
DNodePtr = DNodePtr->left;
else
DNodePtr = DNodePtr->right;
}
if(NULL == DNodePtr) //特殊情况,没找到D
return;
//第二步:查找R并将D的子树连接到R上
if(NULL == DNodePtr->left) //(1)D没有左子树,不用考虑将D的子树连接到R的问题
RNodePtr = DNodePtr->right;
else if(NULL == DNodePtr->right) //(2)D没有右子树不用考虑将D的子树连接到R的问题
RNodePtr = DNodePtr->left;
else //(3)D既有左子树又有右子树,需要考虑将D的子树连接到R的问题
{
//查找R(左子树的最右结点)
for(PofRNodePtr=DNodePtr,RNodePtr=DNodePtr->left; RNodePtr->right!=NULL; PofRNodePtr=RNodePtr,
RNodePtr=RNodePtr->right);
//将D的子树连接到R上,又分为两种情况:
if(PofRNodePtr == DNodePtr) //① D的左孩子没有右子树
RNodePtr->right = DNodePtr->right;
else //② D的左孩子有右子树
{
PofRNodePtr->right = RNodePtr->left;
RNodePtr->left = DNodePtr->left;
RNodePtr->right = DNodePtr->right;
}
}
//第三步:将R连接到D的父结点上
if(NULL == PNodePtr) //特殊情况,D即原树的根结点
root = RNodePtr;
else if(PNodePtr->left == DNodePtr)
PNodePtr->left = RNodePtr;
else
PNodePtr->right = RNodePtr;
//第四步:删除D
delete DNodePtr;
}
int main()
{
int a[10] = {5,2,6,3,9,4,1,8,10,7};
BinSTree bstree;
//二叉搜索树的构建。事实上,二叉搜索树可以实现了数组的排序(只需通过LNR遍历将各结点的数据分别赋回给原数组便可以
//达到排序的效果)
for(int i=0; i!=10; ++i)
{
bstree.insertNode(a[i]);
}
bstree.printTree();
}