1、二叉搜索树的概念
二叉搜索树是一棵可能为空的二叉树,一棵非空的二叉搜索树有满足如下特征:
- 二叉树中所有的键值都是唯一的;
- 根节点所有左子树的键值(如果有的话)小于根节点的键值;
- 根节点所有右子树的键值(如果有的话)大于根节点的键值;
- 根节点的左右子树也都是二叉搜索树。
三个二叉搜索树如下:
2、二叉搜索树的C++描述
对于二叉搜索树,主要操作有插入一个元素(Insert)删除键值为k的元素(Delete)、查找键值为k的元素(Search)、输出当前二叉搜索树(Ascend)。
但由于二叉搜索树的特殊性:
- 当删除二叉搜索树中的元素或向二叉搜索树中插入元素时,需要调整此时二叉搜索树中的元素成为一个新的二叉搜索树。因此需要重新定义Insert和Delete函数。
- 由于在文章数据结构与算法(11)中,并没有定义二叉树的搜素函数,因此搜索操作也需要在二叉搜素数类中实现。
- 至于二叉搜索树的输出操作,可通过二叉树的中序遍历输出完成。
2.1 二叉搜索树类的声明及简单函数的实现
为了能让BSTree的对象访问二叉树类BinaryTree的私有成员,需将BSTree声明为BinaryTree类的友类。
template <class E,class K>
class BSTree:public BinaryTree<E>
{
public:
bool Search(const K&k, E &e)const; //查找关键值为k的元素
BSTree<E, K>&Insert(const E&e); //插入值为e的元素
BSTree<E, K>&Delete(const K&k, E &e); //删除键值为k的节点,并将值放入e中
void Ascend() { InOutput(); } //通过调用二叉树的中序输出函数实现
};
2.2 查找元素操作—Search()
查找键值为k的元素,并将其赋给e。
该函数从根节点开始,依次判断父节点的左孩子和右孩子。若二叉搜索树为空,则无从查找。。。。。。
若二叉搜索树不为空,查找策略为:
1. 若要查找的键值大于父节点的键值,说明需要查找的元素可能存在于右子树中,而不可能存在于父节点的左子树中。因为父节点的键值大于所有其左子树中元素的键值(如果存在)。
2. 相反的,若要查找的键值小于父节点的键值,说明需要查找的元素可能存在于左子树中,而不可能存在于父节点的右子树中。因为父节点的键值小于所有其右子树中元素的键值(如果存在)。、
3. 若要查找的元素的价值等于父节点的键值,则查找成功;
4. 若遍历一遍父节点及对应的叶子节点后,没找到等于待查找键值的节点,则查找失败。
将上述策略用C++实现如下:
//查找关键值为k的元素,并将值赋给e
template <class E, class K>
bool BSTree<E, K>::Search(const K&k, E &e) const
{
BinaryTreeNode<E> *p=root; //取根节点
while (p)
{
if (k < p->data) //若元素小于根节点的值,则只需在左子树中寻找
p = p->LeftChild;
else if (k > p->data) //若元素大于根节点的值,则只需在右子树中寻找
p = p->RightChild;
else //元素等于根节点的值
{
e = p->data;
return true;
}
}
return false; //没找到关键值为k的元素
}
2.3 插入元素—Insert()函数
插入键值为e的元素。
如果将元素e插入一个空二叉搜素树,则直接将元素赋给root即可;若二叉搜索树不为空,则:
从根节点开始搜索,依次判断父节点的左右孩子:
- 若要插入元素的键值大于父节点,则继续从父节点的右子树中寻找插入点;
- 若要插入元素的键值小于父节点,则继续从父节点的左子树中寻找插入点;
若要插入元素的键值等于父节点,则不能够将元素插入到二叉搜索树中,抛出输如错误异常。
当按以上策略遍历到叶子节点时,若元素键值大于当前节点的键值,则元素作为当前节点的右孩子插入;若元素键值小于当前节点的键值,则元素作为当前节点的左孩子插入。
上述过程的C++实现如下:
//插入值为e的元素
template <class E, class K>
BSTree<E, K> &BSTree<E, K>::Insert(const E&e)
{
BinaryTreeNode<E> *p = root, //搜索指针
*pp=root; //p的父节点指针
//遍历节点,pp最终为符合条件节点的父节点
while (p)
{
pp = p;
if (e > p->data)
p = p->RightChild;
else if (e < p->data)
p = p->LeftChild;
else
throw BadInput();
}
BinaryTreeNode<E> *r = new BinaryTreeNode<E>(e);
if (root) //如果树不为空
{
if (e < pp->data)
pp->LeftChild = r;
else
pp->RightChild = r;
}
else //树为空
root = r;
return *this;
}
2.4 删除元素—Delete()
对于二叉树搜索树节点的删除操作,关键在于删除元素后,二叉搜索树的重组。
首先,将键值为k的元素删除,就是利用上面的搜素操作,搜索到键值为k的元素,将元素的值赋给函数引用变量e中。紧接着,进行二叉搜索树的重组。
删除节点的位置存在四种可能:
- 节点为叶子节点,无左右子树;
- 节点既有左子树又有右子树
- 节点只有左子树;
- 节点只有右子树。
下面分别讨论上述情形:
对于第一种情形:若删除节点为叶子节点,直接将节点删除即可;
对于第二种情形:由于父节点的键值大于所有的左子树中元素的键值,小于所有的右子树中元素的键值。因此,当删除当前节点后,可以将其左子树中所有元素的最大键值或右子树中所有元素的最小键值作为当前节点的新值。
对于最后两种情形,可归结为至少有一个子树的情形:若删除节点存在左子树,则将删除节点的左子树作为删除节点的父节点的左子树;若删除节点存在右子树,则将删除节点的右子树作为删除节点的父节点的右子树。
对于最后两种情形,借下图予以具体说明:
上图中,若删除键值为“35”的节点(红圈中元素),因为当前节点只有左子树,根据上述规则,删除元素“35”后,将元素“35”的左子树作为元素“40”的左子树。判断一下操作后的二叉搜索树,发现符合定义。
利用C++实现上述删除策略:
//删除键值为k的节点,并将值放入e中
template <class E, class K>
BSTree<E, K> &BSTree<E, K>::Delete(const K&k, E &e)
{
if (!root) //树为空
throw NoMerm();
BinaryTreeNode<E> *p = root, //遍历节点
*pp=root, //p的父节点
*null=0; //空节点
//找到键值为k的节点p
while (p)
{
pp = p;
if (p->data == k)
break;
else if (p->data > k)
p = p->LeftChild;
else
p = p->RightChild;
}
if (!p) //节点p为空,即没找到符合条件的节点
throw BadInput();
else
{
e = p->data; //保存数据
//重新构建二叉搜索树
if (!p->LeftChild && !p->RightChild) //如果p节点为叶子节点
p = null; //将p置为空
else
{
if (p->LeftChild && p->RightChild) //p既有左孩子又有右孩子
{
//利用p左子树中的最大值替换删除节点
BinaryTreeNode<E> *s = p->LeftChild, //p的左子树
*ps; //s的父节点
while (s->RightChild)
{
ps = s;
s = s->RightChild;
}
p->data = s->data; //左子树最大值赋给删除节点p
p = s;
pp = ps;
}
//p最多有一个孩子
BinaryTreeNode<E> *child;
if (p->LeftChild)
child = p->LeftChild;
else
child = p->RightChild;
//将p删除
if (p == root) //p是根节点
root = child;
else
{
if (p == pp->LeftChild) //p是pp的左孩子
pp->LeftChild = child;
else
pp->RightChild = child;
}
delete p;
}
}
return *this;
}
2.5 测试
将二叉搜索树的节点元素声明为DataStruct类型,该类型元素有数据域data和键值域key。声明如下:
#pragma once
#include <iostream>
using namespace std;
//template <class E,class K>
class DataStruct
{
friend void main();
friend ostream & operator<< (ostream &out, DataStruct &DS);
public:
operator int()const { return data; }
private:
int data;
char key;
};
//template <class E, class K>
ostream & operator<< (ostream &out, DataStruct &DS)
{
out << "数据为: " << DS.data << " " << "键值为: " << DS.key << endl;
return out;
}
对DSTree类进行一下测试:
#include "BSTree.h"
#include "DataStruct.h"
#include<iostream>
using namespace std;
void main()
{
try
{
BSTree<DataStruct, int> Bt;
DataStruct DS;
DS.data = 1; DS.key = 'a';
Bt.Insert(DS);
DS.data = 2; DS.key = 'b';
Bt.Insert(DS);
DS.data = 3; DS.key = 'c';
Bt.Insert(DS);
cout << "插入3个元素后: " << endl;
Bt.Ascend();
DataStruct e;
Bt.Delete(1, e);
cout << "第二个元素为: " << endl << e << endl;
cout << "删除1个元素后: " << endl;
Bt.Ascend();
}
catch (NoMerm)
{
cout << "内存不足!!!" << endl;
}
catch (OutOfRange)
{
cout << "越界啦!!!" << endl;
}
catch (BadInput)
{
cout << "错误的输入!!!" << endl;
}
}
测试结果如下:
参考文献:
[1] 数据结构算法与应用:C++描述(Data Structures, Algorithms and Applications in C++ 的中文版)