二叉搜索树

        二叉搜索树又叫二叉排序树,它可以是一颗空树,或者它是一颗满足以下性质的二叉树:

1、若它的左子树不为空,则左子树所有结点的值都小于等于根结点的值

2、若它的右子树不为空,则右子树所有结点的值都大于等于根结点的值

3、它的左右子树也分别为二叉搜索树

        二叉搜索树的底层结构就是之前学习过的二叉树,只不过给普通的二叉树添加了一些性质而已,通过这个性质,让我们能够更好的查找二叉树里存储的数据。最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),此时搜索的效率为:O(log2 N)。在最差情况下,二叉搜索树退化为单支树(或者类似单支树),此时的搜搜效率为:O( N/2 )。综合来看二叉搜索树的增删查改的时间复杂度为:O(N)。

        一、二叉搜索树的构建

        二叉搜索树的每一个存储数据的结点都是一个链表的结点,我们不能确定存储的数据时什么类型的所以需要用到模版。

	struct BSTNode
    {
        K _key;
		BSTNode<K>* _left;
		BSTNode<K>* _right;

		BSTNode(const K& key)
			:_key(key)
			, _left(nullptr)
			, _right(nullptr)
		{}
    };

        二叉搜索树的结构:我们本质上通过这个树的根结点就能找到这棵树,所以二叉搜索树里只要存储根节点的指针即可,为了方便后续的操作我们把结点的类型typedef成了Node。


	template<class K>
	class BSTree
	{
		typedef BSTNode<K> Node;

    private:
	    Node* _root = nullptr;
    };

        结点的插入:当根结点为空时,我们把新结点赋值给根结点即可;当根节点不为空的时候,我们可以通过一个指针对结点的值进行比较,找到最终要插入的那个位置,但是我们只能向下寻找,不能向上返回,所以我们在寻找插入的位置的时候,要多用一个指针来记录最终位置的父结点,只有这样才能进行插入操作,这里我们实现的是不允许插入相同值的数据的,所以如果发现二叉搜索树内已经有这个数据,就会插入失败。

bool Insert(const K& key)
{
    //当根节点为空时
  	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

    //通过cur找到要插入的位置,再通过parent进行插入操作
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
        //要插入的数据比当前的结点的数据大,就在当前结点的右子树里寻找
        //比当前结点的数据小,就在当前结点的左子树里寻找
        //如果一样就插入失败了
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
			return false;
	}

	cur = new Node(key);
    //判断要插入的位置是在父亲结点的左边还是右边
	if (parent->_key > key)
		parent->_left = cur;
	else
		parent->_right = cur;

	return true;
}

        二、节点的寻找

        结点的寻找就按照插入寻找最终插入位置的方式去寻找即可,如果找到了就返回true,如果没找到返回false即可。

bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key > key)
			cur = cur->_left;
		else if (cur->_key < key)
			cur = cur->_right;
		else
			return true;
	}
	return false;
}

        三、结点删除

        结点的删除是几种操作中最复杂的。

        首先查找元素是否在二叉搜索树里,如果不存在要返回false。

        如果查找元素存在则要分四种情况分别处理:(假设要删除的结点为N)

·        1、要删除结点N左右孩子均为空

        此时只要把N结点的父亲对应孩子的指针指向空,直接删除N结点即可。

        2、要删除的结点N左孩⼦位空,右孩⼦结点不为空

        此时只要把N结点的父亲对应孩子的指针指向N的右孩子,然后直接删除N结点。(这里可以直接把第一种情况结合到这里一起处理)

        3、要删除的结点N右孩⼦位空,左孩⼦结点不为空

        此时只要把N结点的父亲对应孩子的指针指向N的左孩子,然后直接删除N结点。

        4、要删除的结点N左右孩子均不为空

        这个时候无法直接删除N结点,因为删除N结点以后,N结点的两个孩子无法确定要放到什么位置,所以要采用替换法删除。找N左子树的值最大结点或者右子树的值最小结点,因为把这两个结点中任意一个放到N的位置,都不会破坏这棵树原本的性质。

bool Earse(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
        //通过相同的方法寻找要删除的结点
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			//待删除结点左为空及左右都为空
			if (cur->_left == nullptr)
			{
                //如果要删除的结点为根结点 且没有左孩子 则直接把右孩子赋给根结点再删除原本的根结点即可
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
                //这里可能判断的是要删除的结点是父亲结点的左孩子还是右孩子
                //把待删除结点的右子树接到父亲结点的对应位置即可
					if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}

				delete cur;
			}
			//待删除结点右为空
			else if (cur->_right == nullptr)
			{
                //如果要删除的结点为根结点 且没有右孩子 则直接把左孩子的赋给根结点再删除原本的根结点即可
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
                //判断的是要删除的结点是父亲结点的左孩子还是右孩子    
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
			}
			//待删除结点的两个孩子结点都不为空
			else
			{
            //这里采用的是寻找右子树中的最小结点
            //也就是利用一个循环到待删除结点的右子树中 找到值最小的结点以及它的父亲结点
				Node* replaceParent = cur;
				Node* replace = cur->_right;
				while (replace->_left)
				{
					replaceParent = replace;
					replace = replace->_left;
				}

				cur->_key = replace->_key;
                //但是也存在没有不会进入上面循环的情况
                //当右子树的根结点就是最小值结点的时候,不会进入上面的循环,此时replace结点是父亲结点的右孩子
				if (replaceParent->_left == replace)
				{
					replaceParent->_left = replace->_right;
				}
				else
				{
					replaceParent->_right = replace->_right;
				}

				delete replace;
			}
			return true;
		}
	}
	return false;
}

        四、打印二叉搜索树

        二叉搜索树再次一定程度上可以提高搜索的效率,同时如果我们对这棵树进行中序遍历,我们可以发现,打印出来的内容是按照升序排列好的,但是这里有个问题,我们在遍历这棵树的时候要用到根节点,但是根节点作为private修饰的数据在类外我们是拿不到的。为了解决这个问题,我们可以对这样函数多进行一层包装,先写一个私有的中序遍历的函数,然后对开发一个共有的接口,在这个共有函数的内部调用这个私有的函数就能实现遍历的功能了。

    public:
        void InOrder()
        {
	        _InOrder(_root);
	        cout << endl;
        }

	private:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;
			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}

        五、析构函数

        在使用二叉搜索树的过程中我们new了很多的结点,这些结点都需要我们主动去释放,所以系统自动生成的析构函数是不够用的。利用递归的方式来删除结点是很方便的,但是析构函数本身并不能写成递归的方式,所以我们可以利用和遍历一样的方式,先编写一个私有的函数让析构函数来调用它,这样就能很轻松的完成结点的清除了。

public:
    ~BSTree()
	{
		Destory(_root);
		_root = nullptr;
	}

private:
    void Destory(Node* root)
    {
	    if (root == nullptr)
		    return;
	    Destory(root->_left);
	    Destory(root->_right);
	    delete root;
    }

        六、拷贝构造和赋值重载

        默认生成的拷贝构造函数只会完成浅拷贝,如果两个对象指向同一片空间,在调用析构函数之后会对同一块空间进行重复的释放,这样是不行的。我们要用先序的方式对二叉树中的每一个结点都进行复制。

        赋值重载就更简单了,我们在传参的时候,把参数传给一个形参,这个形参就会自动完成拷贝构造,我们只要把形参的根结点交换给我们this指针即可,在这个函数结束的时候,形参会自动销毁,直接就把我们要销毁空间也一起回收了。

public:
	BSTree() = default;

	BSTree(const BSTree& t)
	{
		_root = Copy(t._root);
	}
	BSTree& operator=(BSTree tmp)
	{
		swap(_root, tmp._root);
		return *this;
	}

private:
    Node* Copy(Node* root)
    {
	    if (root == nullptr)
		    return nullptr;

	    Node* newRoot = new Node(root->_key,root->_value);
	    newRoot->_left = Copy(root->_left);
	    newRoot->_right = Copy(root->_right);
	    return newRoot;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值