【C++】二叉树进阶(二叉搜索树)

文章目录

  • 前言
  • 1、二叉搜索树
    • 1-1、 二叉搜索树概念
  • 2、二叉搜索树操作
    • 2-1、树和节点的基本框架
    • 2-2、二叉搜索树的查找
    • 2-3、中序遍历
    • 2-4、二叉搜索树的插入
    • 2-5、二叉搜索树的删除
  • 3、二叉搜索树的模拟实现
    • 3-1、循环版本
    • 3-2、递归版本
  • 4、二叉搜索树的应用
    • 4-1、K模型
    • 4-2、KV模型
    • 4-3、KV模型的代码样例
  • 5、二叉搜索树的性能分析
  • 6、总结


前言

二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:

  1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  1. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
  1. 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
  1. 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

因此本节借二叉树搜索树,对二叉树部分进行收尾总结。


1、二叉搜索树

1-1、 二叉搜索树概念

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

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

【C++】二叉树进阶(二叉搜索树)_第1张图片


2、二叉搜索树操作

2-1、树和节点的基本框架

template<class k>
class BSTreeNode //结构体——包含节点,左指针和右指针
{
public:
	BSTreeNode<k>* _left;//节点的左指针
	BSTreeNode<k>* _right;//节点的右指针
	k _key;//节点的值

	BSTreeNode(const k& key)//构造函数,不写的话new生成不了节点
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}
};

template<class k>
class BSTree
{
	typedef BSTreeNode<k> Node;//这里重定义节点,方便后面的操作
public:
	Node* _root = nullptr;//根节点

2-2、二叉搜索树的查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。

bool Find(const k& key)//查找key值
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)//如果查找的值大,向右继续查找
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)//如果查找的值小,向左继续查找
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}
	}
	return false;
}

2-3、中序遍历

对于二叉搜索树而言,每一个节点的左子树值都比该节点的值小,右子树的值都比该节点的值要大。所以,中序遍历二叉搜索树是可以得到一个升序序列的!

void _InOrder(Node * root)//中序遍历
{
	if (root == nullptr)
	{
		return;
	}
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

2-4、二叉搜索树的插入

插入的具体过程如下:

a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

【C++】二叉树进阶(二叉搜索树)_第2张图片

bool Insert(const k& key)//插入
{
	if (_root == nullptr)//空树直接new节点就行
	{
		_root = new Node(key);
		return true;
	}
	Node* cur = _root;//cur从根开始找
	Node* parent = nullptr;//记录父节点
	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);//到这里就找到了我们要插入节点的为止,我们new(key)
	if (parent->_key < key)//如果父亲的_key值小于我们,就链接在父节点的右边
	{
		parent->_right = cur;
	}
	else//如果父亲的_key值大于我们,就链接在父节点的左边
	{
		parent->_left = cur;
	}
	return true;
}

2-5、二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

a. 要删除的结点无孩子结点

b. 要删除的结点只有左孩子结点

c. 要删除的结点只有右孩子结点

d. 要删除的结点有左、右孩子结点

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来:

b. 右子树为空
c. 左子树为空
d. 左右子树都不为空

因此真正的删除过程如下:

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

【C++】二叉树进阶(二叉搜索树)_第3张图片
情况b:
【C++】二叉树进阶(二叉搜索树)_第4张图片
情况c:
【C++】二叉树进阶(二叉搜索树)_第5张图片
情况d:
【C++】二叉树进阶(二叉搜索树)_第6张图片

我们来看看代码:

bool Erase(const k& key)//删除
{
	Node* cur = _root;//cur向下面走,找删除节点的key值
	Node* parent = nullptr;//parent就是删除节点的父节点——也就是cur的父节点
	while (cur)
	{
		if (cur->_key < key)//老规矩,比你大向右找
		{
			parent = cur;//这个时候父节点要更新
			cur = cur->_right;
		}
		else if (cur->_key > key)//比你小向左找
		{
			parent = cur;//同理
			cur = cur->_left;
		}
		else到这里就找到了
		{
			//三种情况:
			// 1、左为空
			// 2、右为空
			// 3、左右都不为空,替换删除

			if (cur->_left == nullptr)// 1、左为空
			{
				//if(parent == nullptr)
				if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
				{
					//这里的cur和_root都是根节点了,我们让_root等于_root的右节点,这样下面删除cur,
					//也就是删除原来的根节点了,而_root现在变成了原来树的右子树的第一个节点
					_root = cur->_right;
				}
				else
				{
					if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右子树链接父亲左边
					{
						parent->_left = cur->_right;
					}
					else//如果父节点的右边是cur,删除cur之后,cur的右子树链接父亲右边
					{
						parent->_right = cur->_right;
					}
				}
				delete cur;//这个时候删除就没问题了
			}
			else if (cur->_right == nullptr)// 2、右为空
			{
				//if(parent == nullptr)
				if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
				{
					//这里的cur和_root都是根节点了,我们让_root等于_root的左节点,这样下面删除cur,
					//也就是删除原来的根节点了,而_root现在变成了原来树的左子树的第一个节点
					_root = cur->_left;
				}
				else
				{
					if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右左子树链接父亲左边
					{
						parent->_left = cur->_left;
					}
					else//如果父节点的右边是cur,删除cur之后,cur的左子树链接父亲右边
					{
						parent->_right = cur->_left;
					}
				}
				delete cur;
			}
			else// 3、左右都不为空,替换删除
			{
				//这里parent不能初始化为nullptr,如果删除的cur是根,那么parent就不会进入while循环进行更新
				//那么parent就一直为空,下面parent->_left等操作就非法了!!!
				Node* parent = cur; //parent就是替换完之后,删除节点的父节点
				Node* min = cur->_right;//min是右子树的最小值
				while (min->_left)//找右子树最小的数,(也可以找左子树最大的数,但是这样太麻烦了)
				//while (min && min->_left)//这里不用这么写,因为前提条件就是cur删除节点左右都不为空
				{
					parent = min;
					min = min->_left;
				}
				cur->_key = min->_key;//把右子树最小值给给cur
				if (parent->_left == min)//min是最左值,所以没有左子树,只有右子树需要托孤
				{
					parent->_left = min->_right;
				}
				else
				{
					parent->_right = min->_right;
				}
				delete min;
			}
			return true;
		}
	}
	return false;
}

3、二叉搜索树的模拟实现

这里有两个版本:循环和递归
我们只用掌握一个版本就可以了!

3-1、循环版本

BSTree.h:循环版本

#pragma once
///循环版本
template<class k>
class BSTreeNode //结构体——包含节点,左指针和右指针
{
public:
	BSTreeNode<k>* _left;
	BSTreeNode<k>* _right;
	k _key;

	BSTreeNode(const k& key)//构造函数,不写的话new生成不了节点
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}
};

template<class k>
class BSTree
{
	typedef BSTreeNode<k> Node;
public:
	bool Insert(const k& key)//插入
	{
		if (_root == nullptr)//空树直接new节点就行
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;//cur从根开始找
		Node* parent = nullptr;//记录父节点
		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);//到这里就找到了我们要插入节点的为止,我们new(key)
		if (parent->_key < key)//如果父亲的_key值小于我们,就链接在父节点的右边
		{
			parent->_right = cur;
		}
		else//如果父亲的_key值大于我们,就链接在父节点的左边
		{
			parent->_left = cur;
		}
		return true;
	}

	bool Find(const k& key)//查找key值
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)//如果查找的值大,向右继续查找
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)//如果查找的值小,向左继续查找
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

	bool Erase(const k& key)//删除
	{
		Node* cur = _root;//cur向下面走,找删除节点的key值
		Node* parent = nullptr;//parent就是删除节点的父节点——也就是cur的父节点
		while (cur)
		{
			if (cur->_key < key)//老规矩,比你大向右找
			{
				parent = cur;//这个时候父节点要更新
				cur = cur->_right;
			}
			else if (cur->_key > key)//比你小向左找
			{
				parent = cur;//同理
				cur = cur->_left;
			}
			else到这里就找到了
			{
				//三种情况:
				// 1、左为空
				// 2、右为空
				// 3、左右都不为空,替换删除

				if (cur->_left == nullptr)// 1、左为空
				{
					//if(parent == nullptr)
					if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
					{
						//这里的cur和_root都是根节点了,我们让_root等于_root的右节点,这样下面删除cur,
						//也就是删除原来的根节点了,而_root现在变成了原来树的右子树的第一个节点
						_root = cur->_right;
					}
					else
					{
						if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右子树链接父亲左边
						{
							parent->_left = cur->_right;
						}
						else//如果父节点的右边是cur,删除cur之后,cur的右子树链接父亲右边
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;//这个时候删除就没问题了
				}
				else if (cur->_right == nullptr)// 2、右为空
				{
					//if(parent == nullptr)
					if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
					{
						//这里的cur和_root都是根节点了,我们让_root等于_root的左节点,这样下面删除cur,
						//也就是删除原来的根节点了,而_root现在变成了原来树的左子树的第一个节点
						_root = cur->_left;
					}
					else
					{
						if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右左子树链接父亲左边
						{
							parent->_left = cur->_left;
						}
						else//如果父节点的右边是cur,删除cur之后,cur的左子树链接父亲右边
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				else// 3、左右都不为空,替换删除
				{
					//这里parent不能初始化为nullptr,如果删除的cur是根,那么parent就不会进入while循环进行更新
					//那么parent就一直为空,下面parent->_left等操作就非法了!!!
					Node* parent = cur; //parent就是替换完之后,删除节点的父节点
					Node* min = cur->_right;//min是右子树的最小值
					while (min->_left)//找右子树最小的数,(也可以找左子树最大的数,但是这样太麻烦了)
					//while (min && min->_left)//这里不用这么写,因为前提条件就是cur删除节点左右都不为空
					{
						parent = min;
						min = min->_left;
					}
					cur->_key = min->_key;//把右子树最小值给给cur
					if (parent->_left == min)//min是最左值,所以没有左子树,只有右子树需要托孤
					{
						parent->_left = min->_right;
					}
					else
					{
						parent->_right = min->_right;
					}
					delete min;
				}
				return true;
			}
		}
		return false;
	}

	void InOrder()//树的递归要使用根节点,根节点都是私有的,所以要通过嵌套的方法来调用
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node * root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	Node* _root = nullptr;//根节点
};

void Test1()//测试用例
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> t;
	for (const auto& e : a)
	{
		t.Insert(e);
	}
	for (const auto& e : a)
	{
		t.Erase(e);
		t.InOrder();
	}
}

3-2、递归版本

BSTreeR.h:递归版本
这里递归版本就不做过多解释了,重要代码都有注释

#pragma once
///递归版本
template<class k>
class BSTreeNode //结构体——包含节点,左指针和右指针
{
public:
	BSTreeNode<k>* _left;
	BSTreeNode<k>* _right;
	k _key;

	BSTreeNode(const k& key)//构造函数,不写的话new生成不了节点
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

template<class k>
class BSTreeR
{
	typedef BSTreeNode<k> Node;
public:
	BSTreeR()
		:_root(nullptr)
	{}
	~BSTreeR()
	{
		Destory(_root);
		_root = nullptr;
	}
	BSTreeR(BSTreeR<k>& t)
	{
		this->_root = Cope(t._root);
	}
	BSTreeR operator=(BSTreeR<k> t)
	{
		swap(this->_root, t._root);
		return *this;
	}
	bool InsertR(const k& key)
	{
		return _InsertR(_root, key);
	}
	bool FindR(const k& key)
	{
		return _FindR(_root, key);
	}
	bool EraseR(const k& key)
	{
		return _EraseR(_root, key);
	}

	void InOrder()//树的递归要使用根节点,根节点都是私有的,所以要通过嵌套的方法来调用
	{
		_InOrder(_root);
		cout << endl;
	}

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

	}
	Node* Cope(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newnode = new Node(root->_key);
		newnode->_left = Cope(root->_left);
		newnode->_right = Cope(root->_right);
		return newnode;
	}
	bool _InsertR(Node*& root, const k& key)//这里的root使用引用,这样就不用找parent节点了,root是指针的引用
	{
		if (root == nullptr)//空树直接new节点
		{
			root = new Node(key);
			return true;
		}
		if (root->_key < key)
			return _InsertR(root->_right, key);
		else if (root->_key > key)
			return _InsertR(root->_left, key);
		else
			return false;
	}
	bool _FindR(Node* root, const k& key)//查找
	{
		if (root == nullptr)
			return false;
		if (root->_key < key)
			return _FindR(root->_right, key);
		else if (root->_key > key)
			return _FindR(root->_left, key);
		else
			return true;
	}
	bool _EraseR(Node*& root, const k& key)//root是引用,是父节点指向删除节点的指针的引用
	{
		if (root == nullptr)
			return false;
		if (root->_key < key)
			return _EraseR(root->_right, key);
		else if (root->_key > key)
			return _EraseR(root->_left, key);
		else
		{
			Node* dl = root;
			if (root->_right == nullptr)//如果删除的节点的右子树没有,就让父节点指向我的左
			{
				root = root->_left;
			}
			else if (root->_left == nullptr)//如果删除节点的左子树没有,就让父节点指向我的右
			{
				root = root->_right;
			}
			else
			{
				Node* minright = root->_right;//找右边最小值
				while (minright->_left)
				{
					minright = minright->_left;
				}
				swap(root->_key, minright->_key);//交换两个值
				return _EraseR(root->_right, key);//交换完之后,删除的key值在右子树的最左边
			}
			delete dl;
			return true;
		}
	}
	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	Node* _root = nullptr;//根节点
};

void Test1()//测试用例
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTreeR<int> t;
	for (const auto& e : a)
	{
		t.InsertR(e);
	}
	BSTreeR<int> copyt(t);
	copyt.InOrder();

	int b[] = { 15,54,545,4,46,65,464,84,9 };
	BSTreeR<int> ax;
	for (const auto& e : b)
	{
		ax.InsertR(e);
	}
	ax.InOrder();

	copyt = ax;
	copyt.InOrder();

	for (const auto& e : a)
	{
		t.EraseR(e);
		t.InOrder();
	}
}

4、二叉搜索树的应用

4-1、K模型

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
·以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
·在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

4-2、KV模型

KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。该种方式在现实生活中非常常见:
·比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文就构成一种键值对;
·再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是就构成一种键值对。

4-3、KV模型的代码样例

样例1:将我们上面的二叉搜索树循环版本改为KV模型

//通过一个值找另一个值key value,也就是map(下一章节讲)
namespace KV
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;//这里的key值就是我们上面的key值,KV模型中,比较大小、查找等等操作都是与key值有关,但key值不能更改
		V _value;//_value我们用不上,value值可以更改

		BSTreeNode(const K& key, const V& value)
			:_key(key)
			, _value(value)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};
	//BSTree>  字典查找
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool insert(const K& key, const V& value)//与上面没有什么差别,就是参数多了个value
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}
			Node* cur = _root;
			Node* parent = _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, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}
		void Inorder()
		{
			_Inorder(_root);
		}
		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}
			return nullptr;
		}
	private:
		void _Inorder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_Inorder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_Inorder(root->_right);
		}
		Node* _root = nullptr;
	};
}

样例2:映射——通过key值找value

void Test2()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	// 词库中单词都放进这个搜索树中
	// Key的搜索模型,判断在不在?
	// 场景:检查单词拼写是否正确/车库出入系统/...
	//K::BSTree dict;

	// Key/Value的搜索模型,通过Key查找或修改Value
	KV::BSTree<string, string> dict;
	dict.Insert("sort", "排序");
	dict.Insert("string", "字符串");
	dict.Insert("left", "左边");
	dict.Insert("right", "右边");

	string str;
	while (cin>>str)
	{
		KV::BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "无此单词" << endl;
		}
	}
}

样例3:统计水果出现次数



void TestBSTree3()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓","苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	KV::BSTree<string, int> countTree;
	for (auto e : arr)
	{
		auto* ret = countTree.Find(e);
		if (ret == nullptr)
		{
			countTree.Insert(e, 1);
		}
		else
		{
			ret->_value++;
		}
	}

	countTree.Inorder();
}

5、二叉搜索树的性能分析

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

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

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
【C++】二叉树进阶(二叉搜索树)_第7张图片
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插
入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上
场了。

6、总结

二叉搜索树只是开胃小菜,主要就是为了下面的set和map、avl、红黑树等内容做铺垫。但是也要掌握,毕竟这是为了后面的内容打基础!

你可能感兴趣的:(c++,数据结构,算法)