数据结构之二分查找树

说明:本文仅供学习交流,转载请标明出处,欢迎转载!

        二分查找树BST(也叫二叉查找树、二叉排序树)的提出是为了提供查找效率,之所以称为二分查找树,因为该二叉树对应着二分查找算法,查找平均的时间复杂度为o(logn),所以该数据结构的提出是为了提高查找效率。

       定义

       二分查找树或者是一棵空树,或者具有下列性质:

       1.若它的左子树不为空,则左子树上所有结点的值均小于根结点的值;

       2.若它的右子树不为空,则右子树上所有结点的值均大于根结点的值;

       3.它的左右子树均为二分查找树。

       操作

       二分查找树的操作主要包括:插入查找删除

       1.插入操作

        设s指向待插入的结点,root指向二叉查找树的根结点,则插入操作的步骤如下:

        (1)若root为空,则将s指向的结点作为跟结点插入,否则执行(2)、(3);

        (2)若s->data < root->data,则将s指向的结点插入到根结点的左子树中;

        (3)若s->data > root->data,则将s指向的结点插入到根结点的右子树中。

        总结:二叉树的构造就是通过不断地插入新的元素。

      2.查找操作

      在二分查找树中查找给定值k的查找过程如下:

     (1)若root=NULL,则查找失败;

     (2)若root->data=k,则查找成功;

     (3)若k <  root->data,则去root的左边查找;

      (4)若k > root->data,则去root的右边查找。

      总结:若二分查找树接近平衡二叉树,则其时间复杂度为O(logn),若二分查找树是斜的(如插入是有序插入的情况下),则其实际复杂度为O(n),即退化为线性表。

      3.删除操作

      设p指向待删除的结点,pre指向待删除结点的父亲,则删除操作视如下情况而定:

    (1)若待删除的结点是叶子结点,不妨设pre->right=p(即待删除的结点为其父亲结点的右孩子),则直接删除p,对应为:pre->right=NULL,delete p

    (2)若待删除的结点只有左子树或右子树,则只需将待删除结点的父亲与左孩子(或右孩子)连接起来,对应为,不妨设pre->right=p,以待删除结点仅有左子树的情况为例(右子树同理),对应为:pre->right=p->left,delete p

     (3)若待删除结点左右子树都有,则执行如下步骤:

        总体来说,整个线索是:找到右子树的最小值结点-->连接断开结点-->对最小值结点的上下文做善后工作

        I.先找到待删除结点的右子树中的最小值(或左子树中的最大值),对应的指针为min,并记下min的父亲结点为min_pre;

       II.用min所指结值覆盖待删结点的值,对应为:p->data=min->data;

       III.分两种情况(如下图所示):


       特殊情况:若待删除结点的右孩子无左子树,也就是说待删结点的右孩子就是右子树的最大值,则直接连接即可,对应为:p->right=min->right,delete min

       一般情况:若待删除结点的右孩子有左子树,则将min_pre所指结点的右孩子指向min所只结点的右孩子,对应为:min_pre->right=min->right,delete min

      举例

      假设有下图所示的二分查找树,现在我们分别对该树做杉树情况的删除操作。

     删除元素1叶子结点):


     删除元素9只有一个孩子):


     删除元素7有左右孩子


     4.懒惰删除

       通常情况下的删除策略都是用待删除结点的右子树中的最小结点来替换待删除的结点,但这种删除方法的效率其实不高,因为它沿着该树进行两趟搜索,第一次搜索是为了找到待删除的结点的位置,即p,第二趟搜索是为了找到最小的替补,即min。如果删除的次数不多,则通常采用的策略是懒惰删除(lazy deletion)。

        懒惰删除的思想是:当一个元素被删除时,它仍然留在树中,只对它做删除标记。下次我们做搜素操作时,把该元素当作是一个存在的元素(当然,如果查找的元素就是该元素,我们只要看下该元素是否有删除标记),当作删除操作时,该元素被当作一个空元素,当在带有删除标记的点处插入新元素时,就把该位置当作一个空位置。

       5.含有重复元素的二分查找树

       含有重复元素的二分查找树的难题在于如何元素插入的问题,这里提供了一种解决的方法:通过在每个结点记录中保留一个附加域以指示该结点元素出现的次数。当然这也大大增加了附加空间,但是却比重复信息放到树中要好(因为如果将重复元素放到树中会增加树的深度)。

        元素的删除问题:由于树中可能存在重复的元素,这时候我们可以采用懒惰删除。即当元素出现删除操作时,就将该元素出现的次数减1,若减到次数为0,则将该元素标记为被删除状态(而实际上并未真正被删除)。

        测试代码如下:

#include<iostream>
using namespace std;
struct Node
{
	int data;
	Node *left;
	Node *right;
	Node(int data)
	{
		this->data=data;
		this->left=NULL;
		this->right=NULL;
	}
};
class BST//假设这棵树不存在重复的元素
{
public:
	Node *root; 
    
	void Insert(int value);//插入值
	void  Insert(Node* &r,Node *p);//将p指向的结点插入到BST中,用引用
	Node* Insert1(Node *r,Node *p);//用返回值
	
	void BuildBST(int a[],int n);//创建一棵BST

	BST():root(NULL){}
	Node* Search(Node *r,int value);//查找值value所在的位置
	Node *FindMax(Node *r);//返回最大值的位置
	Node *FindMin(Node *r);//返回根结点r对应的树中的最小值的位置
    void Delete(Node* &r,int value);//删除值value

    /******遍历BST*****/
	void PreOrder(Node *r);//先序遍历,加参数是为了便于递归调用
	void InOrder(Node *r);//先序遍历
	void PostOrder(Node *r);//后序遍历
	
};

void BST::Insert(int value)//插入值
{
	Node *p=root;
	Node *pre=p;
	while(p)
	{
		if(value > p->data)//待插入的值大于当前值
		{
			pre=p;
			p=p->right;
		}
		else if(value < p->data)//待插入的值小于当前值
		{
			pre=p;
			p=p->left;
		}
		else
		{
			cout<<"树中已经存在值"<<value<<",插入失败!"<<endl;
			return ;
		}
	}
	if(value > pre->data)//插入到叶子节点的右边
	{
		pre->right=new Node(value);
	}
	else//插入到叶子结点的左边
	{
		pre->left=new Node(value);
	}
}
void BST::Insert(Node * &r,Node *p)//将p指向的结点插入到BST中
{
	if(r==NULL)//如果是第一个插入的结点
	{
		r=p;
	}
	else if(p->data > r->data)//如果待插入的结点值大于当前结点,则插入位置必然在当前结点的右子树上
	{
		Insert(r->right,p);
	}
	else
	{
		Insert(r->left,p);
	}
}

Node* BST::Insert1(Node *r,Node *p)//用返回值实现
{
	if(r==NULL)
	{
		r=p;
	}
		else if(p->data > r->data)//如果待插入的结点值大于当前结点,则插入位置必然在当前结点的右子树上
	{
		Insert(r->right,p);
	}
	else
	{
		Insert(r->left,p);
	}
	return r;
}

void BST::BuildBST(int a[],int n)//创建一个BST
{
	int i;
	for(i=0;i<n;i++)
	{
		Node *s=new Node(a[i]);
		root=Insert1(root,s);
	}
}

void BST::PreOrder(Node *r)//先序遍历
{
	if(r==NULL)
	{
		return ;
	}
	else
	{
		cout<<r->data<<" ";
		this->PreOrder(r->left);
		this->PreOrder(r->right);
	}
}
void BST::InOrder(Node *r)//中序遍历
{
	if(r==NULL)
	{
		return ;
	}
	else
	{
		InOrder(r->left);
		cout<<r->data<<" ";
    	InOrder(r->right);
	}
}

void BST::PostOrder(Node *r)//递归实现后序遍历
{
	if(r==NULL)
	{
		return ;
	}
	else
	{
		PostOrder(r->left);
		PostOrder(r->right);
		cout<<r->data<<" ";
	}
}

Node* BST::Search(Node *r,int value)//查找值value的对应的结点的指针
{
	if(r==NULL)
	{
		return NULL;
	}
	if(r->data==value)
	{
		return r;
	}
	else if (value > r->data)
	{
		return Search(r->right,value);
	}
	else
	{
		return Search(r->left,value);
	}
}
Node* BST::FindMax(Node *r)//返回r对应的树中的最大值的位置
{
	if(r==NULL)
	{
		return NULL;
	}
	else if(r->right==NULL)
	{
		return r;
	}
	else
	{
		return FindMax(r->right);
	}
}
Node* BST::FindMin(Node *r)//返回r对应的树中的最小值的位置
{
	if(r==NULL)
	{
		return NULL;
	}
	else if(r->left==NULL)
	{
		return r;
	}
	else
	{
		return FindMin(r->left);
	}
}
void BST::Delete(Node* &r,int value)//删除值value
{

	if(r==NULL)
	{
		return ;
	}
	Node *pre=NULL,*p=NULL,*min_pre=NULL,*min=NULL;//pre指向待删结点的前一个位置,p指向待删结点的位置,min指向待删结点右子树的最新位置,min_pre指向最小位置的父亲结点
	pre=p=r;
	
	while(p)//p指向待删除的节点
	{
		if(p->data==value)//找到被删除结点的位置
		{
			break;
		}
		else if(value > p->data)//如果value大于当前结点值
		{
			pre=p;
			p=p->right;
		}
		else//如果value小于当前结点值
		{
			pre=p;
			p=p->left;
		}
	}

	if(p)//如果待删除的结点找到
	{
		bool hasTwo=true;//用于标记被删除的结点是否有两个孩子
		min_pre=min=p->right;
		if(pre->left==p)//如果待删除的结点p是父亲结点pre的左孩子
		{
			if(!p->left && !p->right)//如果待删除的节点是叶子节点
			{
				pre->left=NULL;
				delete p;
				hasTwo=false;
			}
			else if(!p->right)//如果待删除的结点只有左孩子
			{
				pre->left=p->left;
				delete p;
				hasTwo=false;
			}
			else if(!p->left)//如果待删除的结点只有右孩子
			{
				pre->left=p->right;
				delete p;
				hasTwo=false;
			}
		}
		else//如果被删除的结点p是父亲结点pre的右孩子
		{
			if(!p->left && !p->right)//如果待删除的结点为叶子结点
			{
				pre->right=NULL;
				delete p;
				hasTwo=false;
			}
			else if(!p->right)//如果待删除的结点只有左孩子
			{
				pre->right=p->left;
				delete p;
				hasTwo=false;
			}
			else if(!p->left)//如果待删除的结点只有右孩子
			{
				pre->right=p->right;
				delete p;
				hasTwo=false;
			}
		}
		if(hasTwo)//如果被删除的结点有两个孩子
		{
			min_pre=p;//min_pre总是指向被删除结点的上一个位置
			min=p->right;//将min指向p的右孩子
			while(min->left)//若min没有左孩子
			{
				min_pre=min;//min_pre表示min的父亲结点
				min=min->left;
			}
			p->data=min->data;//将最小结点的值覆盖被删除的值
			if(min_pre==p)//如果被删除的结点p的有孩子没有左子树
			{
				p->right=min->right;//直接用p指向min的右孩子
			}
			else//如果被删除的结点的有孩子有左子树
			{
				min_pre->left=min->right;//用最小结点的父亲结点指向最小结点的右孩子(因为最小结点必然没有左孩子)
			}
			delete min;
		//	return r;
		}
	}
}

int main()
{
	int a[]={3,1,7,5,11,9,10,12};
	BST bst;
	bst.BuildBST(a,sizeof(a)/sizeof(int));//创建一棵二分搜索树
	/*********遍历结果*********/
	cout<<"先序遍历结果: ";
	bst.PreOrder(bst.root);//先序遍历
	cout<<endl;
	cout<<"中序遍历结果: ";
	bst.InOrder(bst.root);//中序遍历
	cout<<endl;
	cout<<"后序遍历结果: ";
	bst.PostOrder(bst.root);//后序遍历
	cout<<endl<<endl;

	cout<<"插入值6,后删除中序遍历结果:"<<endl;
	bst.Insert(6);
	bst.InOrder(bst.root);
	cout<<endl<<endl;

	cout<<"插入值10";
	bst.Insert(10);
	cout<<endl;

	cout<<"删除7后删除中序遍历结果:"<<endl;
	bst.Delete(bst.root,7);
	bst.InOrder(bst.root);
	cout<<endl;
	return 0;
}

        测试结果如下:


参考资料

[1]《数据结构(C++版)王红梅等》

[2]《数据结构与算法分析---C语言描述 原书第二版》


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值