二叉搜索树性质与实现

一、概念

       搜索树支持许多动态集合操作,包括SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT和DELETE等,因此,一棵搜索树既可以作为一个字典又可以作为一个优先队列。

      二叉搜索树上的基本操作所费时间与这棵树的高度成正比,对于有N个结点的一棵完全二叉树,这些操作的最坏运行时间为O(logN),但对于N个结点构成的线性链而言,同样的操作要花费O(N)的最坏运行时间,幸运的是一棵随机构造的二叉搜索树的期望高度是O(logN),所以这样一棵树上的动态集合的基本操作平均运行时间为O(logN)。

     实际上,我们并不能总是保证随机的构建二叉搜索树,却可以设计它的变体,来保证基本操作具有好的最坏情况性能。红黑树、B树就是这样的例子。其中B树特别适用于二级(磁盘)存储器上的数据库维护。

什么是二叉搜索树?

       二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:

     (1)、若它的左子树不为空,则左子树上所有结点的值均小于等于它的根结点的值;
     (2)、若它的右子树不为空,则右子树上所有结点的值均大于等于它的根结点的值;
     (3)、它的左、右子树也分别为二叉查找树。

      组织形式为:每个结点就是一个对象,除了key和卫星数据外,每个结点还包括left、right和parent。有木有感觉很熟悉?中序遍历其实就是对数据排序结果!所以二叉搜索树又可以用来排序。

为什么需要二叉搜索树?

      目的并非为了排序,而是为了提高查找和删除关键字的速度,不管怎么说,在一个有序的数据集上查找,速度总是要快于无序的数据集。

什么是卫星数据?

      在算法导论里,指的是一条纪录(一个对象中)中除了关键字key以外的其他数据。例如在排序算法中,参与排序的数据称做关键字key,而该对象附带的其他数据则称做卫星数据。在排序的过程中,我们只考虑关键字key的大小。形象一点说,其他数据可以看作是关键字key的卫星,反映了其他数据与key的依属关系。

操作与性能

     假设数据结构为BSTree(二叉树)中内含BSNode(树节点)数据结构,且有成员treeRoot作为这棵树的根节点。

     1.遍历:

      对于具有n个结点的子树的根x,从x遍历一次需要O(n)时间,恰好调用自己两次,一次是左孩子,一次是右孩子。

/*
中序遍历  伪代码
*/
void InOrder(BSNode *pRoot)
{
	if(pRoot){
		InOrder(pRoot->LChild);
		cout<<pRoot->data<<endl;
		InOrder(pRoot->RChild);
	}
}

     2.查找:

     从树根开始向下寻找,遇到节点x,比较关键字k与x.data,如果k>x.data说明k如果存在的话一定在x的左子树,否则在右子树。这样递归下去直到叶子节点,如果到达叶子节点都没找到这样的关键字为k的节点,说明找不到。运行时间为O(h),h为这棵树的高度。

/*
查找  伪代码
输入一个指向树根的指针pRoot与关键字k,
如果存在返回指向该节点的指针,不存在返回NUll
*/
void TreeSearch(<span style="font-family: Arial, Helvetica, sans-serif;">BSNode </span>*pRoot , int k)//递归版本
{
	if(pRoot==NULL && pRoot->data==k)
		return pRoot;
	if(pRoot->data < k)
		return TreeSearch(pRoot->LChild , k);
	else
		return TreeSearch(pRoot->RChild , k);
}
void TreeSearch(BSNode *pRoot , int k)//遍历版本
{
	while(pRoot && pRoot->data!=k){
		if(pRoot->data < k)
			pRoot = pRoot->LChild;
		else
			pRoot = pRoot->RChild;
	}
	return pRoot;
}

      3.最大关键字元素和最小关键字元素:时间复杂度O(h)

       最大关键字元素:通过树根找最左边的那个元素,如果无左孩子,则为根节点最大;

       最小关键字元素:通过树根找最右边的那个元素,如果无右孩子,则为根节点最小;

/*
最大最小关键字元素  伪代码
*/
BSTree* TreeMin(BSNode *pRoot)
{
	while(pRoot->LChild)
		pRoot = pRoot->LChild;
	return pRoot;
}
BSTree* TreeMax(BSNode *pRoot)
{
	while(pRoot->RChild)
		pRoot = pRoot->RChild;
	return pRoot;
}

       4.后继和前驱,时间复杂度O(h)。

/*
查找后继  伪代码
*/
BSTree* TreeSUCCESSOR(BSNode *x)
{
	if(x->RChild != NULL)//有右子树,则直接定位到右子树最小关键字节点
		return TreeMin(x->RChild);
	//---没有右子树,沿树向上搜索到一个有左孩子的节点y,y左子树中包含x(这样x后继是y)
	BSTree* xParent = x->parent;
	while(xParent!=NULL && x==xParent->RChild){
		x = xParent;
		xParent = xParent->parent;
	}
	return xParent;
}
      5.插入,沿着根节点向下寻找合适的位置,如果欲插入关键值key>pNode.data,沿着右子树向下,key<=pNode.data,沿着左子树向下;直到叶子节点的某个NULL孩子处插入。时间复杂度为O(h)。

/*
插入节点  伪代码
在树T中插入节点x,注意到此时不能只传入pRoot,而需要传入T.root
*/
void TreeInsert(BSTree T , BSNode *x)
{
	BSNode *pRoot = T.root;
	BSTree *pPre = NULL;//存放pRoot的父节点
	while(pRoot!=NULL){
		pPre = pRoot;
		if(pRoot->data > x->data)
			pRoot = pRoot->LChild;
		else
			pRoot = pRoot->RChild;
	}
	x->parent = pPre;
	if(pPre == NULL)//输入是空树
		T.root = x;
	else if(x->data < pPre->data)
		pPre->LChild = x;
	else
		pPre->RChild = x;
}
       6.删除,从一棵二叉树删除节点分为如下三种情况,时间复杂度O(h)。

         设要删除的节点是x,注意到删除x并不会导致x的子树被删掉,且要保护好二叉搜索树的性质。于是:

             (1)x是一个叶子节点,没有孩子,直接delete x,并置x.parent相应孩子域为NULL。

             (2)x没有左孩子或没有右孩子,直接让唯一存在的那个孩子子树替代x;

             (3)x既有左孩子又有右孩子,找到x的后继y,y一定是最小的大于x的那个数,可以知道y一定没有左子树(如果有的话一定能找到一个小于y的节点),这时可以直接让y替代x,并删掉y节点,由于y节点是没有左孩子的,属于(1)情况,可以使用(1)的方法删除。

/*
删除节点  伪代码
在树T中删除节点x,假设x存在于树T中。
不存在的情况可以加一个简单的搜索判定,此处略去
*/
//将u节点用v子树取代,并维护u,parent与v的父子关系
void transplant(BSTree T , BSNode *u , BSNode *v)
{
	if(u->parent == NULL)//u是根节点,删掉根节点
		T.root = v;
	else if(u == u->parent->LChild)//u是父节点的左孩子
		u->parent->LChild = v;
	else//u是父节点的右孩子
		u->parent->RChild = v;
	if(v != NULL)
		v->parent = u->parent;
}
void TreeDelete(BSTree T , BSNode *x)
{
	if(x->LChild == NULL)//左子树为空
		transplant(T , x , x->RChild);
	else if(x->RChild == NULL)//右子树为空
		transplant(T , x , x->LChild);
	else{//左右子树均不为空
		BSNode *xSucc = TreeMin(x);//找到x的后继
		if(xSucc->parent != x){//x后继不是x的孩子
			transplant(T , xSucc , xSucc->RChild);//此时xSucc一定没有左孩子,将后继的右孩子替代后继节点
			//---置后继为原来x->RChild的双亲
			xSucc->RChild = x->RChild; 
			x->RChild->parent = xSucc;
		}
		//---将x用后继替换,并置后继为x双亲的孩子和x左孩子的双亲。
		transplant(T,x,xSucc);//其内已维护x.parent与x后继父子关系,只需关注x孩子与xSucc孩子关系
		xSucc->LChild = x->LChild;
		x->LChild->parent = xSucc;
	}
	delete x;
}

删除节点x一定是取x的后继吗?

       不一定,取x的前驱也可以的。考虑下图:可以将47的后继48替换47,自然也能用37来替换它,只要不破坏二叉搜索树的性质就好。

随机的构建二叉树

       据上所述,二叉搜索树每个基本操作都能在O(h)时间内完成,h为这棵树的高度。但是随着元素的插入和删除,二叉搜索树的高度是变化的,如果n个关键字按照严格递增次序插入,则这棵树一定是高度为n-1的一条链。幸运的是平均性能接近于最好情形而非最坏情形性能。

      一棵有N个不同关键字的随机构建二叉搜索树的期望高度为O(logN)。


     虽然如此,我们还是希望能让这棵二叉排序树能平衡就好了,这样引出了新的问题:平衡二叉树(AVL树),不过这是下一篇文章的事儿了。

C++实现二叉查找树

      1.注意在类的成员函数内如果删除this指针就不能再访问this的数据成员,分离BSTree与BSNode的实现可以将树的根节点所占内存释放。即BSTree中有一个BSNode类成员root作为本树的根节点。这样不需要删掉BSTree对象也能释放树申请的结点内存。

      2.分离了实现后可以做到BSNode析构函数专门删除某个节点(BSTree类的deleteNode函数用到了)用delete Node调用;BSTree中定义一个destory函数专门用来删除以pRoot为根的所有子树并释放堆内存,可以在BSTree析构函数中调用destory函数释放所有BSNode申请的堆内存。

/*
二叉搜索树的实现:建立、插入、删除、查询和遍历
二叉搜索树是这样的:左子树节点<=根节点<=右子树节点
二叉树结构BSTree
二叉树节点结构BSNode
*/
struct BSNode{
	BSNode():LChild(NULL),RChild(NULL),pParent(NULL){}
	~BSNode(){//默认析构函数只删除一个节点
	}
	BSNode *LChild;
	BSNode *RChild;
	BSNode *pParent;
	int data;
};
class BSTree{
private:
	int printAtLevel(BSNode *pRoot , int k);//打印第k层的层序遍历结果
	void printNodeInOrder(BSNode *pRoot);
	void transplant(BSNode*u , BSNode *v);//用v子树替换u,维护u的父节点与新节点v的关系
	void destory(BSNode **pRoot);//递归销毁以pRoot为根的子树
public:
	BSTree():root(NULL){}
	~BSTree();
	void insertNode(int x);
	bool deleteNode(BSNode* pNode);//删除x所在节点,成功返回true
	BSNode* searchNode(int x);//查询x所在节点,如果查不到返回NULL
	BSNode* MinKeyNode();//查询最小关键字节点
	BSNode* MaxKeyNode();//查询最大关键字节点
	BSNode* TreeSuccessor(BSNode *x);//得到某个节点的后继
	void printInOrder();//中序遍历,从小到大输出排序结果
	void printLevelOrder();//整棵树层序遍历,查看建立的二叉树情况
private:
	BSNode *root;//保存本树的根节点
};
BSTree::~BSTree()
{
	destory(&root);
}
void BSTree::destory(BSNode **pRoot)
{
	if(*pRoot){
		destory(&((*pRoot)->LChild));
		destory(&((*pRoot)->RChild));
		delete *pRoot;
		*pRoot = NULL;
	}
}
void BSTree::insertNode(int x)
{
	BSNode *tempNode = new BSNode;
	tempNode->data = x;
	if(root == NULL){//空子树
		root = tempNode;
		return;
	}
	//---寻找插入的位置,x值比根节点值大走右子树,小走左子树,直到走到叶子节点的NULL处插入
	//注意需要保存最终NULL的父节点,以便插入成为它的孩子节点
	BSNode *pRoot = root;//根节点
	BSNode *parent = root->pParent;
	while (pRoot != NULL){
		parent = pRoot;
		if(pRoot->data > tempNode->data)//走左子树
			pRoot = pRoot->LChild;
		else
			pRoot = pRoot->RChild;
	}
	
	if(parent->data > tempNode->data)
		parent->LChild = tempNode;
	else
		parent->RChild = tempNode;
	tempNode->pParent = parent;
	
}
/*删除一个节点,分两种情况:
(1)设要删除的结点是x,x有两个孩子的情况
step1:找到结点x的后继y,可知:y是x右子树中最左结点,且y没有左孩子(否则y左孩子为x的后继)
step2:把y的关键字给x,即让结点x成为结点y(移花接木)
step3:删除y,因为y没有左孩子,所以按照(2)的方法删除y
(2)设要删除的结点是y,y只有左孩子或右孩子的情况
     直接置y的左孩子(或右孩子)的节点值(无论是否为NULL)为y的父节点相应孩子。
*/
void BSTree::transplant(BSNode*u , BSNode *v)
{
	 if(u->pParent == NULL){//u是根节点,直接删掉根节点
		 root = v;
		delete u;
	 }
	 if(u->pParent->LChild == u)//u是父节点的左孩子
		 u->pParent->LChild = v;
	 else if(u->pParent->RChild == u)//u是父节点的右孩子
		 u->pParent->RChild = v;
	 if(v != NULL)
		 v->pParent = u->pParent;
}
bool BSTree::deleteNode(BSNode* pNode)
{
	if(pNode == NULL)//删除空节点
		return false;
	if(pNode == root){//删除根节点
		destory(&root);
		return true;
	}
	//---处理两种情况
	//没有左孩子或右孩子的情况
	if(pNode->LChild == NULL)//只有右孩子
		transplant(pNode , pNode->RChild);
	else if(pNode->RChild == NULL)//如果只有左孩子	
		transplant(pNode , pNode->LChild);
	else{//如果既有左孩子又有右孩子
		BSNode *pNodeSucc = TreeSuccessor(pNode->RChild);//pNode的后继节点
		if(pNodeSucc->pParent != pNode){
			//则pNodeSucc一定没有左孩子,让pNodeSucc的右孩子替换pNodeSucc
			transplant(pNodeSucc , pNodeSucc->RChild);
			pNodeSucc->RChild = pNode->RChild;//维护pNode后继与pNode右孩子的父子关系
			pNode->RChild->pParent = pNodeSucc;
		}
		//---维护pNode后继和pNode左孩子
		transplant(pNode,pNodeSucc);
		pNodeSucc->LChild = pNode->LChild;
		pNode->LChild->pParent = pNodeSucc;
	}
	delete pNode;
	pNode = NULL;
	return true;
}

BSNode* BSTree::searchNode(int x)
{
	BSNode *pRoot = root;
	while(pRoot && x!=pRoot->data){
		if(x > pRoot->data)
			pRoot = pRoot->RChild;
		else
			pRoot = pRoot->LChild;
	}
	if(pRoot == NULL)//空树
		return NULL;
	else
		return pRoot;
}

BSNode* BSTree::MinKeyNode()
{
	BSNode *pRoot = root;
	while(pRoot->LChild != NULL)
		pRoot = pRoot->LChild;
	//如果没有左孩子,则返回根节点
	return pRoot;
}

BSNode* BSTree::MaxKeyNode()
{
	BSNode* pRoot = root;
	while(pRoot->RChild != NULL)
		pRoot = pRoot->RChild;
	//如果没有右孩子,则返回根节点
	return pRoot;
}

BSNode* BSTree::TreeSuccessor(BSNode *x)
{
	BSNode* pRoot = root;
	if(pRoot == NULL)//空树
		return NULL;
	//---如果x节点有右孩子,后继就是右孩子
	if(x->RChild)
		return x->RChild;
	//---如果x节点没有右孩子,则由x向上查找,直到找到第一个左子树含有x的节点
	BSNode *parent = x->pParent;
	while(parent!=NULL && parent->RChild==x){
		x = parent;
		parent = parent->pParent;
	}
	return parent;//返回NULL或找到的后继(最后一个元素--根节点没有后继)
}

void BSTree::printInOrder()
{
	BSNode *pRoot = root;
	printNodeInOrder(pRoot);
}
void BSTree::printNodeInOrder(BSNode *pRoot)
{
	if(pRoot){
		printNodeInOrder(pRoot->LChild);
		cout<<pRoot->data<<" ";
		printNodeInOrder(pRoot->RChild);
	}
}
//思想:打印从根节点开始的第k层相当于打印根节点左右孩子开始的k-1层
int BSTree::printAtLevel(BSNode *pRoot , int k)
{

	if(pRoot == NULL || k<0)
		return 0;//未打印节点
	if(k == 0){
		cout<<pRoot->data<<" ";
		return 1;//打印一个节点
	}
	//先打印左子树,返回左子树打印的节点数
	int LeftNodeNum = printAtLevel(pRoot->LChild , k-1);
	//再打印右子树,返回右子树打印的节点数
	int RightNodeNum = printAtLevel(pRoot->RChild  , k-1);
	return LeftNodeNum+RightNodeNum;
}
void BSTree::printLevelOrder()
{  	
	BSNode *pRoot = root;
	if(pRoot == NULL)
		cout<<"空树!"<<endl;
	for(int k=0 ; ;k++){
		if(0 == printAtLevel(pRoot , k))//整层都未打印节点,返回
			break;
		cout<<endl;
	}
}




  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值