C++:二叉搜索树的剖析

120 阅读5分钟

二叉搜索树


前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的。那什么是二叉搜索树?其底层是二叉搜索树吗?

二叉搜索树的概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

int a [] = {5,3,4,1,7,8,2,6,0,9};
int a [] = {5,3,4,1,7,8,2,6,0,9};

二叉搜索树操作

二叉搜索树的查找

在这里插入图片描述

二叉搜索树的插入

插入的具体过程如下:

a. 树为空,则直接插入
在这里插入图片描述

b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
在这里插入图片描述

二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点

看起来有待删除节点有4种情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:

  • 情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
  • 情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
  • 情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中, 再来处理该结点的删除问题

二叉搜索树的模拟实现

头文件

#pragma once

#include <vector>
#include <stack>

namespace dg {

template <class T>
class TreeNode
{
	T m_data;
	TreeNode<T>* m_left;
	TreeNode<T>* m_right;
public:
	TreeNode(const T& val = T()) :
		m_data(val),
		m_left(nullptr),
		m_right(nullptr)
	{}

	template <class T>
	friend class BinarySortTree;
};

template <class T>
class BinarySortTree
{
	TreeNode<T>* m_root;

	void destroy(TreeNode<T>* root)
	{
		if (root)
		{
			destroy(root->m_left);
			destroy(root->m_right);
			delete root;
		}
	}
public:
	BinarySortTree() :
		m_root(nullptr)
	{}

	bool insert(const T& val)
	{
		if (m_root == nullptr)
		{
			m_root = new TreeNode<T>(val);
			return true;
		}

		TreeNode<T>* cur = m_root;
		TreeNode<T>* pre = nullptr;

		while (cur)
		{
			if (val < cur->m_data)
			{
				pre = cur;
				cur = cur->m_left;
			}
			else if (val > cur->m_data)
			{
				pre = cur;
				cur = cur->m_right;
			}
			else
			{
				return false;
			}
		}

		cur = new TreeNode<T>(val);
		if (val < pre->m_data)
		{
			pre->m_left = cur;
		}
		else
		{
			pre->m_right = cur;
		}

		return true;
	}

	bool erase(const T& val)
	{
		if (m_root == nullptr)
		{
			return false;
		}

		TreeNode<T>* cur = m_root;
		TreeNode<T>* pre = m_root;

		while (cur)
		{
			if (val < cur->m_data)
			{
				pre = cur;
				cur = cur->m_left;
			}
			else if (val > cur->m_data)
			{
				pre = cur;
				cur = cur->m_right;
			}
			else
			{
				break;
			}
		}

		if (cur == nullptr)
		{
			return false;
		}

		if (cur->m_left && cur->m_right)
		{
			TreeNode<T>* cur2 = cur->m_left;
			TreeNode<T>* pre2 = cur;

			if (cur2->m_right)
			{
				for (; cur2->m_right; pre2 = cur2, cur2 = cur2->m_right);
				pre2->m_right = cur2->m_left;	// 将这个孩子的左子树挂在它父亲的右子树上
				cur2->m_left = cur->m_left;		// 它继承要删除节点的人际关系(左子树)
			}

			// cur2 此时为要删除的点左子树中最大的值
			cur2->m_right = cur->m_right;	// 它继承要删除节点的人际关系(右子树)

			if (cur == pre)
			{
				m_root = cur2;
			}
			else
			{
				if (cur->m_data < pre->m_data)
				{
					pre->m_left = cur2;		// 它继承要删除节点的人际关系(父亲)
				}
				else
				{
					pre->m_right = cur2;	// 它继承要删除节点的人际关系(父亲)
				}
			}

			delete cur;
		}
		else if (cur->m_left)
		{
			if (cur == pre)
			{
				m_root = cur->m_left;
			}
			else
			{
				if (cur->m_data < pre->m_data)
				{
					pre->m_left = cur->m_left;
				}
				else
				{
					pre->m_right = cur->m_left;
				}
			}
			delete cur;
		}
		else
		{
			if (cur == pre)
			{
				m_root = cur->m_right;
			}
			else
			{
				if (cur->m_data < pre->m_data)
				{
					pre->m_left = cur->m_right;
				}
				else
				{
					pre->m_right = cur->m_right;
				}
			}
			delete cur;
		}
	}

	/*
	看是不是有左右子树:
		①左右子树都有:
			a、左子树没有右孩子
				直接让左孩子继承自己的右孩子和父亲
			b、左子树有右孩子
				一路向右,找到最后的一个右孩子,然后将这个孩子的
				左子树挂在它父亲的右子树上,然后让它继承要删除节
				点的人际关系(左右子树和父亲)
			当要删除的节点是根节点时,不用继承父亲关系,但要修改根节点指向。
		②只有左子树
			直接让左子树继承自己的父亲关系,如果要删的是根,那么
			直接换根即可。
		③其他
			直接让右子树(或者空)继承自己的父亲关系,其他同上
	*/

	//中序遍历
	std::vector<T> InOrder()
	{
		std::stack<TreeNode<T>*> s;
		std::vector<T> res;
		TreeNode<T>* cur = m_root;

		while (cur || !s.empty())
		{
			for (; cur; cur = cur->m_left)
			{
				s.push(cur);
			}

			if (!s.empty())
			{
				cur = s.top();
				res.push_back(cur->m_data);
				s.pop();

				cur = cur->m_right;
			}
		}

		return res;
	}
};

};

测试主函数

#include "binarySortTree.h"
#include <iostream>
using namespace std;

int main()
{
	dg::BinarySortTree<int> bst;

	bst.insert(5);
	bst.insert(3);
	bst.insert(8);
	bst.insert(6);
	bst.insert(4);
	bst.insert(7);
	bst.insert(1);
	bst.insert(2);

	bst.erase(2);
	vector<int> v = bst.InOrder();

	for (auto& i : v)
	{
		cout << i << ' ';
	}

	return 0;
}

代码生成图:
在这里插入图片描述

二叉数的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: log2N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N/2


如有不同见解,欢迎留言讨论~~