二叉搜索树详解

前言

        相信大家一定了解过二分查找吧,在一个有序的数组中查找一个值,例如在下图中查找2,只需要每次取中间的元素比较,就可以排除一半不可能的数据,时间复杂度为O(lgN),但在插入删除的效率为O(N)是不理想的。

        我们今天要了解的二叉搜索树查找的效率一般为O(lgN),也会有最坏情况O(N)的时候,不过后序有平衡二叉树解决。

        在这里给大家推荐一个视频,可以快速了解二叉搜索树的性质,视频中没有配套代码,只有讲解,看完后可以在来看这篇博客代码,两者结合效果更好。

        数据结构合集 - 二叉搜索树(二叉排序树)(二叉查找树)_哔哩哔哩_bilibili

        

二叉搜索树概念

          二叉搜索树(Binary Search Tree)顾名思义是一种特殊的二叉树,主要用于查找与去重,与我们常见的二叉树不同,他对于结点的值有特殊的要求,二叉搜索树的定义如下主要有以下3点。

        1.二叉搜索树的左边所有非空结点值小于根结点

        2.二叉搜索树的右边所有非空结点值大于根结点

        3.二叉搜索树的左右子树也是二叉搜索树

        我们接下来看几个例子,判断下是否是二叉搜索树。

        

          第一个就不是二叉搜索树,因为5小于10,不应该在10的右子树上面,应该在10的左子树上面。修改如下图后结果就是正确的了。

        接下来我们继续看一个例子

        这个是二叉搜索树么?结果是否定的。33大于15不应该出现在15的左子树上面,那么我们把33修改在15的右子树上面如下图对么?

        这里我们的33大于15,在15的右子树上面,仿佛没有错,但是我们以30为根节点来看,33大于30,却出现在30的左子树上面,这是错误的!我们定义的第一个要求就是1.二叉搜索树的左边所有非空结点值小于根结点,而不是只看左子树第一个结点就可以了!!所以我们还要修改为如下图。

        33此时在30的右子树上面,又在41的左子树上面。

        我们最后再看一个例子

        这个二叉搜索树是正确的,我们可以如何快速的判断呢?

        我们知道二叉搜索树满足 左节点值<根结点<右节点值,于是我们如果进行一次中序遍历,那么二叉搜索树的遍历结果一定是有序的!

        上面的中序遍历结果为1 2 4 6  7 8 10 13 14,是有序的所以这个二叉树一定为二叉搜索树。

对于中序遍历我们有一种简单的方法求出结果,分为两步,

        第一步是描点,在所有结点下面描个点,如下图

        第二步是描边,用一条线,将二叉搜索树从根结点开始描一圈,如下图

        然后将线上的按照出现的顺序依次写出来1 3 4 6 7 8 10 13 14,这样就可以得到一个二叉树的中序遍历结果了,前序遍历就是在结点左边描点,后序遍历就是在结点右边描点,大家感兴趣可以尝试,在这里就不多赘述了。

二叉搜索树实现

        二叉搜索树与二叉树十分相似,只不过二叉搜索树的结点按照特殊的方式排列罢了,我们就可以定义一个二叉树结点,然后封装在一个二叉搜索树类中实现。

        于是我们可以模仿二叉树的实现,先构造出如下的代码框架

#pragma once
#include<iostream>
using namespace std;

template<class T>
struct BSTNode
{
	BSTNode(T k= T())
		:_left(nullptr)
		, _right(nullptr)
		, _key(k)
	{

	}

	BSTNode* _left;
	BSTNode* _right;
	T _key;
};


template<class T>
class BSTree
{
	typedef BSTNode<T> node;
public:
	BSTree(T k)
	{
		_root = new node(k);
	}

	BSTree()
		:_root(nullptr)
	{

	}


private:
	node* _root;
};

        可以重新定义个头文件,将上述代码放在头文件中,用于实现二叉搜索树,这个采用模板的写法,可以提高代码的复用性。

        接下来我们就要开始实现一个数据结构的主要函数即功能了。其中最主要的无外乎增删查改,改在普通的二叉搜索树中没有什么具体的意义,与删操作有些重合,在这里就不再写了。我们首先来实现增加功能。

二叉搜索树的增加

        我们定义插入函数的返回值时bool类型,如果key已经在二叉搜索树中,就返回false,插入失败,否则就正常插入。(一般二叉搜索树不支持插入相同的元素,当然也有变种的二叉搜索树支持,这里以前者为例)

bool insert(T k)
{
	//二叉搜索树没有结点
	if (_root == nullptr)
	{
		_root = new node(k);
		return true;
	}

	//找到合适的位置
	node* parent = nullptr;
	node* cur = _root;

	while (cur)
	{
		//提前存下父节点
		parent = cur;

		//k大于结点值就去右边,小于就去左边
		if (cur->_key < k)
			cur = cur->_right;
		else if (cur->_key > k)
			cur = cur->_left;
		//一般的二叉搜索树不允许出现相同的数字
		else
			return false;
	}

	//先判断再插入
	if (parent->_key > k)
		parent->_left = new node(k);
	else
		parent->_right = new node(k);

	return true;
}

        在上面的代码中,我们要记住保留当前位置的父节点,这样我们最后插入数据的时候才知道插在哪里,否则只知道是当前结点为空是连接不了结点的。

        其次我们最终知道父亲结点是哪个还要再判断值的大小,否则我们不知道是插入父亲结点的左节点还是右节点。

二叉搜索树的中序遍历

        二叉搜索树的中序遍历与普通二叉树一样,都是先遍历左子树,左子树遍历完后,遍历当前结点,然后遍历右子树。

void Inorder(node* _root)
{
	if (_root == nullptr)
		return;

	Inorder(_root->_left);
	cout << root->_key << " ";
	Inorder(_root->_right);
}

        但是这样写是有问题的,我们为了安全性将根结点设置为私有的,再类外面访问不了,于是这样写是不可以的,我们可以将他再次用函数封装以下,如下代码。

	void Inorder()
	{
		_Inorder(_root);
	}
	
private:
	void _Inorder(node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_key << " ";
		_Inorder(root->_right);
	}

        我们定义一个子函数_Inorder,他是类里面的函数,可以调用_root,我们在Inorder函数中调用void _Inorder(node* root)函数,从而避免了在类外面输入根结点。

        当上面的代码都写完时,我们可以写个测试用例看看,有没有错误。

#include"BSTree.h"


int main()
{
	BSTree<int> t;

	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	
	for (int e : a)
		t.insert(e);

	t.Inorder();

	return 0;
}

        头文件"BSTree.h"就包含了我们刚才写的代码。我们就可以通过中序遍历检测我们二叉树构建是否正确。

        运行结果如下图,中序遍历是有序的,说明插入的过程中也是正确的。

二叉搜索树的查找

        二叉搜索树的查找实现与之前insert函数中找插入结点十分的相似。将key与当前结点比较,如果大于当前结点就遍历右子树,小于就遍历左子树,等于就返回当前结点指针,查询不到就返回空指针。

	node* find(T k)
	{
		node* cur = _root;

		while (cur)
		{
			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
				cur = cur->_right;
			else if (cur->_key > k)
				cur = cur->_left;
			else
				return cur;
		}

		return nullptr;
	}

        同样在写完查找函数后,我们可以写个测试用例来看看是否可以正常的运行。如下测试代码

void test1()
{
	BSTree<int> t;

	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };

	for (int e : a)
		t.insert(e);

	if (t.find(8))
		cout << 8 << endl;

	if (t.find(13))
		cout << 13 << endl;

	if (t.find(0))
		cout << 0 << endl;
}

        运行结果如下图,结果正确,大家也可以测试多组来判断,这里就不过多的赘述了

二叉搜索树的删除

        二叉搜索树的删除较为复杂我们可以一个一个情况来分析看。

       1.当前为空树,没有结点,不可能删除成功

        2.删除结点没有子树,两个指针指向空

        如删除下图中的7

        此时只需要删除当前结点,并将其父亲节点6的右指针设置为空就可以了。

  //找到删除结点与其父亲结点
		node* parent = nullptr;
		node* cur = _root;

		while (cur)
		{
			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > k)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				break;
		}

        
		//二叉树中没有改结点
		if (cur == nullptr)
			return false;

		//删除结点没有子树的情况
		if (cur->_left == nullptr && cur->_right == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				delete _root;
				_root = nullptr;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = nullptr;
			else
				parent->_right = nullptr;

			delete cur;
			return true;
		}

       

       3.删除结点有一个子树,另外一个为空

        例如我们要删除下图二叉搜索树中的10

        10只有右子树,直接将其父亲结点8的右指针指向10的右子树就可以了。修改后结果如下图

        再看下面的例子,删除1

        此时只需要将1的父亲结点3的左指针指向4就可以了,修改后如下图

        删除只有一个孩子的结点相对来说还是比较简单的,我们要找到要删除节点的父亲结点,并判断删除结点是父亲节点的左边还是右边,子节点的左边还是右边存在,最后连接起来就可以了。


		//删除结点只有一个子树的情况
		if (cur->_left == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				node* tmp = _root->_right;
				delete _root;
				_root = tmp;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = cur->_right;
			else
				parent->_right = cur->_right;

			delete cur;
			return true;
		}
		else if (cur->_right == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				node* tmp = _root->_left;
				delete _root;
				_root = tmp;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = cur->_left;
			else
				parent->_right = cur->_left;

			delete cur;
			return true;
		}

        4.删除结点有两个子树

        如下图删除3结点

        此时3结点的左右子树都不为空,我们要删除3结点,此时有许多种方法

        4.1先删除,再重新插入

        我们可以以3为根结点遍历下3左右子树值,1 6 4 7,最后再依次将这个数组的值插入到二叉搜索树里面,这样也可以实现,删除某一结点,并保持其他的数据不变,但这种方式较为暴力。

        4.2先找到特殊值

        假如要删除3,我们想要最大利用原来的数据结构,不从头再来重新插入。可以分析下3位置的数有什么特点。

        将3删除后,替代他的数一定再他的左右子树中,这个数要大于左子树任意节点值,又要小于右子树任意值。通过观察图像,可以发现只有左子树最右边的值即左子树最大值,和右子树最左边的值即右子树最小值满足要求。

        于是我们便可以找到特殊值来替换要删除的结点,最后在删除特殊结点,从而简化操作。

        下面以找左子树最大值为例,只要将结点一直向右移动到为空就行,具体代码等最后再想i下解释,如下是核心代码。

while (leftCur->_right)
{
	leftCur = leftCur->_right;
}

        最后具体的代码如下

	bool erase(T k)
	{
		//无节点情况
		if (_root == nullptr)
			return false;

		//找到删除结点与其父亲结点
		node* parent = nullptr;
		node* cur = _root;

		while (cur)
		{
			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > k)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				break;
		}

        
		//二叉树中没有改结点
		if (cur == nullptr)
			return false;

		//删除结点没有子树的情况
		if (cur->_left == nullptr && cur->_right == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				delete _root;
				_root = nullptr;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = nullptr;
			else
				parent->_right = nullptr;

			delete cur;
			return true;
		}


		//删除结点只有一个子树的情况
		if (cur->_left == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				node* tmp = _root->_right;
				delete _root;
				_root = tmp;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = cur->_right;
			else
				parent->_right = cur->_right;

			delete cur;
			return true;
		}
		else if (cur->_right == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				node* tmp = _root->_left;
				delete _root;
				_root = tmp;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = cur->_left;
			else
				parent->_right = cur->_left;

			delete cur;
			return true;
		}


		//删除左右子树都存在 找左子树最右边的值
		node* leftParent = cur;
		node* leftCur = cur->_left;

		while (leftCur->_right)
		{
			leftParent = leftCur;
			leftCur = leftCur->_right;
		}

		//覆盖删除结点
		cur->_key = leftCur->_key;
		//左子树只有一个特殊情况
    if (leftParent == cur)
	    leftParent->_left = nullptr;
    else//删除结点一定没有右子树,但可能有左子树
	    leftParent->_right = leftCur->_left;

		delete leftCur;
		return true;
	}

        同样可以写个测试用例试试

void test2()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (int e : a)
		t.insert(e);


	for (int e : a)
	{
		t.Inorder();
		cout << endl;
		t.erase(e);
	}

}

        运行结果如下与预期结果相同且没有报错,基本确定程序的正确性。这里就测试一例,大家可以多测几次。

        上述的第二种情况,两个子树都为空可以合并到第三种情况,看成第三种情况下的特殊情况,于是便可以删除下面代码

//删除结点没有子树的情况
		if (cur->_left == nullptr && cur->_right == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				delete _root;
				_root = nullptr;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = nullptr;
			else
				parent->_right = nullptr;

			delete cur;
			return true;
		}

        删除后,运行结果如下图,依旧是正确的。就可以简化代码。

二叉树的销毁

        二叉树的内存是用new在堆上开辟出来的,我们必须要些析构函数来主动的释放内存,否则会造成内存泄漏。

        我们可以采用递归式的销毁,先释放左子树再释放右子树,最后释放当前结点。

        同理析构函数不可以再外部调用参数,我们可以再设置一个子函数。

    ~BSTree()
    {
	    BSTDestory(_root);
	    _root = nullptr;
    }

private:
	void BSTDestory(node*  _root)
	{
		if (_root == nullptr)
			return;

		BSTDestory(_root->_left);
		BSTDestory(_root->_right);

		delete _root;
	}

        写完后,同样可以测试一下,不过这个要通过调试窗口才可以观察

void test3()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (int e : a)
		t.insert(e);

	int c = 0;
}

        通过调试窗口可以看见,最后的内存都是成功的释放了的。

        如果文章有错误还望多多包涵,在评论区指出。看到这,喜欢的点点关注吧!

附录代码

        最后头文件的代码如下

#pragma once
#include<iostream>
using namespace std;

template<class T>
struct BSTNode
{
	BSTNode(T k= T())
		:_left(nullptr)
		, _right(nullptr)
		, _key(k)
	{

	}

	BSTNode* _left;
	BSTNode* _right;
	T _key;
};


template<class T>
class BSTree
{
	typedef BSTNode<T> node;
public:
	BSTree(T k)
	{
		_root = new node(k);
	}

	BSTree()
		:_root(nullptr)
	{

	}

	bool insert(T k)
	{
		//二叉搜索树没有结点
		if (_root == nullptr)
		{
			_root = new node(k);
			return true;
		}

		//找到合适的位置
		node* parent = nullptr;
		node* cur = _root;

		while (cur)
		{
			//提前存下父节点
			parent = cur;

			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
				cur = cur->_right;
			else if (cur->_key > k)
				cur = cur->_left;
			//一般的二叉搜索树不允许出现相同的数字
			else
				return false;
		}

		//先判断再插入
		if (parent->_key > k)
			parent->_left = new node(k);
		else
			parent->_right = new node(k);

		return true;
	}


	void Inorder()
	{
		_Inorder(_root);
	}

	node* find(T k)
	{
		node* cur = _root;

		while (cur)
		{
			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
				cur = cur->_right;
			else if (cur->_key > k)
				cur = cur->_left;
			else
				return cur;
		}

		return nullptr;
	}

	bool erase(T k)
	{
		//无节点情况
		if (_root == nullptr)
			return false;

		//找到删除结点与其父亲结点
		node* parent = nullptr;
		node* cur = _root;

		while (cur)
		{
			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > k)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				break;
		}

		//二叉树中没有改结点
		if (cur == nullptr)
			return false;

		//删除结点只有一个子树的情况
		if (cur->_left == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				node* tmp = _root->_right;
				delete _root;
				_root = tmp;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = cur->_right;
			else
				parent->_right = cur->_right;

			delete cur;
			return true;
		}
		else if (cur->_right == nullptr)
		{
			//删除头节点特殊情况
			if (parent == nullptr)
			{
				node* tmp = _root->_left;
				delete _root;
				_root = tmp;
				return true;
			}
			//判断是父亲结点的左子树还是右子树
			if (parent->_left == cur)
				parent->_left = cur->_left;
			else
				parent->_right = cur->_left;

			delete cur;
			return true;
		}


		//删除左右子树都存在 找左子树最右边的值
		node* leftParent = cur;
		node* leftCur = cur->_left;

		while (leftCur->_right)
		{
			leftParent = leftCur;
			leftCur = leftCur->_right;
		}

		//覆盖删除结点
		cur->_key = leftCur->_key;
		//左子树只有一个特殊情况
		if (leftParent == cur)
			leftParent->_left = nullptr;
		else
			leftParent->_right = leftCur->_left;
		

		delete leftCur;
		return true;
	}

	bool empty(void)
	{
		return _root == nullptr;
	}

	~BSTree()
	{
		BSTDestory(_root);
		_root = nullptr;
	}

private:
	node* _root;


	void _Inorder(node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_key << " ";
		_Inorder(root->_right);
	}

	void BSTDestory(node*  _root)
	{
		if (_root == nullptr)
			return;

		BSTDestory(_root->_left);
		BSTDestory(_root->_right);

		delete _root;
	}
};

        源文件的代码如下

#include"BSTree.h"


void test1()
{
	BSTree<int> t;

	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };

	for (int e : a)
		t.insert(e);

	if (t.find(8))
		cout << 8 << endl;

	if (t.find(13))
		cout << 13 << endl;

	if (t.find(0))
		cout << 0 << endl;
}

void test2()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (int e : a)
		t.insert(e);


	for (int e : a)
	{
		t.Inorder();
		cout << endl;
		t.erase(e);
	}

}

void test3()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (int e : a)
		t.insert(e);

	int c = 0;
}

int main()
{
	
	test3();



	return 0;
}

      

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值