c++ - 第15节 - 二叉树进阶

目录

1. 二叉搜索树

1.1.二叉搜索树概念

1.2.二叉搜索树的实现

1.3.二叉搜索树的应用

1.4.二叉搜索树的性能分析

2.二叉树进阶面试题


1. 二叉搜索树

1.1.二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
\bullet  若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
\bullet  若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
\bullet  它的左右子树也分别为二叉搜索树
也就是说,搜索二叉树的任意一个子树都需要满足,左子树的值<根<右子树的值

c++ - 第15节 - 二叉树进阶_第1张图片

注:

1.搜索二叉树数据的查找效率为O(N),当搜索二叉树接近完全时,数据的查找效率较高,接近log_{2}^{N}

2.当使用中序遍历遍历搜索二叉树时,遍历出来的数据是增序的。

1.2.二叉搜索树的实现

二叉搜索树普通实现版本

BinarySearchTree.h文件:

#pragma once

template
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;

	K _key;

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

template
class BSTree
{
	typedef BSTreeNode Node;
private:
	void DestoryTree(Node* root)
	{
		if (root == nullptr)
			return;

		DestoryTree(root->_left);
		DestoryTree(root->_right);
		delete root;
	}

	Node* CopyTree(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copyNode = new Node(root->_key);
		copyNode->_left = CopyTree(root->_left);
		copyNode->_right = CopyTree(root->_right);

		return copyNode;
	}
public:
	// 强制编译器自己生成构造
	// C++11
	BSTree() = default;

	BSTree(const BSTree& t)
	{
		_root = CopyTree(t._root);
	}

	// t1 = t2
	BSTree& operator=(BSTree t)
	{
		swap(_root, t._root);
		return *this;
	}

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

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		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->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		return true;
	}

	//const Node* Find(const K& key)
	bool 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 true;
			}
		}

		return false;
	}

	bool Erase(const K& key)
	{
		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
			{
				// 一个孩子--左为空 or 右为空
				// 两个孩子 -- 替换法
				if (cur->_left == nullptr)
				{
					//if (parent == nullptr)
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
				}
				else if (cur->_right == nullptr)
				{
					//if (parent == nullptr)
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}

					delete cur;
				}
				else // 两个孩子都不为空
				{
					// 右子树的最小节点替代
					Node* minParent = cur;
					Node* minRight = cur->_right;
					while (minRight->_left)
					{
						minParent = minRight;
						minRight = minRight->_left;
					}

					swap(minRight->_key, cur->_key);
					//cur->_key = minRight->_key;
					if (minParent->_left == minRight)
					{
						minParent->_left = minRight->_right;
					}
					else
					{
						minParent->_right = minRight->_right;
					}

					delete minRight;
				}

				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);
	}
private:
	Node* _root = nullptr;
};

void TestBSTree1()
{
	BSTree t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	t.Insert(16);
	t.Insert(9);

	t.InOrder();
}

void TestBSTree2()
{
	BSTree t;
	int a[] = { 8, 7, 9, 12, 5, 19, 20, 30,7,12 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();
}

void TestBSTree3()
{
	BSTree t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	t.Erase(3);
	t.Erase(8);

	t.InOrder();
	t.Erase(14);
	t.Erase(7);
	t.InOrder();

	for (auto e : a)
	{
		t.Erase(e);
	}
	t.InOrder();
}

void TestBSTree4()
{
	BSTree t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	BSTree copy = t;
	copy.InOrder();
}

test.cpp文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;

#include "BinarySearchTree.h"

int main()
{
	TestBSTree3();

	return 0;
}

注:

1. 二叉搜索树的查找介绍
a.从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b.最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入介绍
插入的具体过程如下:
a.树为空,则直接新增节点,赋值给root指针
b.树不空,按二叉搜索树性质查找插入位置,插入新节点
具体思路为:定义两个指针parent和cur,cur查找插入位置,parent记录cur上一个位置,当cur找到插入位置时(cur为空时),开辟节点给cur并和parent进行链接。
c++ - 第15节 - 二叉树进阶_第2张图片

注:

(1)搜索二叉树一般不允许插入和里面数据相同的数据,会造成冗余。

(2)搜索二叉树数据插入的顺序不同,树的形状一般也不同,当以近似有序的数据顺序依次插入,那么二叉树的形状近似一个链表,搜索的时间复杂度接近O(N)

3. 二叉搜索树的删除介绍  

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况d:在删除结点的左子树中寻找最大值结点(左子树中序下的最后一个结点),或者删除结点的右子树中寻找最小值结点(右子树中序下的第一个结点),用左子树最大值结点的值或右子树最小值结点的值填补到被删除节点中,再来处理左子树最大值结点或右子树最小值结点的删除问题--替换法删除
c++ - 第15节 - 二叉树进阶_第3张图片

情况b和情况c的实现代码(情况a融合在情况b中)如果如下图一所示,那么是有BUG的,如下图二所示的搜索树要删除根节点,此时parent指针指向空指针,parent->left或者parent->right会访问空指针,程序报错。解决方法是让搜索树的_root成员变量指向根结点的那个孩子结点即可,如下图三所示。

c++ - 第15节 - 二叉树进阶_第4张图片c++ - 第15节 - 二叉树进阶_第5张图片c++ - 第15节 - 二叉树进阶_第6张图片

情况d的代码实现如果如下图一所示,那么是错误的,因为Swap交换之后该树不再是一个搜索二叉树,交换之后再递归调用Erase函数删除key是找不到key值结点并返回false的,所以我们应该手动去删除。

我们利用minRight指针找到右子树最小值结点,minParent指针指向minRight指针指向结点的父节点,然后将minRight指针指向结点的值(右子树最小值)赋值给cur指针指向的结点值,minParent指针指向结点的_left指针和minRight指针指向结点的_right指针链接(此时一定是minParent指针指向结点的_left指针指向minRight指针指向的结点,minRight指针指向结点的_left指针一定是NULL,_right指针可能为空可能指向其他结点,所以要删除minRight指针指向结点直接将minParent指针指向结点的_left指针指向minRight指针指向结点的_right指针指向的地址,然后释放minRight指针指向结点即可),释放minRight指针指向的结点,如下图三所示。

c++ - 第15节 - 二叉树进阶_第7张图片  c++ - 第15节 - 二叉树进阶_第8张图片

上面图三所示的代码是有BUG的,因为有可能删除结点的右子树的根就是最小结点,如下图一所示,要删除值为8的结点(根节点),此时该节点右子树的根(值为10的结点)就是最小结点。开始minRight指向值为10的结点,while循环判断条件为假不进入循环,minRight最终指向值为10的结点,也就是右子树的最小结点,而minParent指针为空,minParent指针应该为值为8的结点,所以应该用cur对minParent指针初始化。并且此时minParent指针指向结点的_right指针和minRight指针指向结点的_right指针链接(此时一定是minParent指针指向结点的_right指针指向minRight指针指向的结点,minRight指针指向结点的_left指针一定是NULL,_right指针可能为空可能指向其他结点,所以要删除minRight指针指向结点直接将minParent指针指向结点的_right指针指向minRight指针指向结点的_right指针指向的地址,然后释放minRight指针指向结点即可),释放minRight指针指向的结点。代码中,因为最后minRight指针指向结点既有可能是minParent指针指向结点的左节点也有可能是minParent指针指向结点的右节点,因此需要进行判断,如下图二所示。

 c++ - 第15节 - 二叉树进阶_第9张图片

4.搜索二叉树上面这种结构允许增删查功能,不允许改,修改一个节点的数值那么这个树很可能不再是搜索二叉树。

5.搜索二叉树的拷贝构造函数和赋值运算符重载函数需要进行深拷贝,这里深拷贝不能复用insert插入函数,因为插入的顺序不同搜索二叉树也不同。我们写一个CopyTree函数来递归创建一个相同的树,如下图一所示,然后搜索二叉树的拷贝构造函数复用CopyTree函数即可,如下图二所示

c++ - 第15节 - 二叉树进阶_第10张图片c++ - 第15节 - 二叉树进阶_第11张图片

如果写了构造函数,那么系统自动生成的默认构造函数就不会再自动生成了,拷贝构造函数也是构造函数,如果我们写了拷贝构造函数,系统自动生成的默认构造函数不再生成,我们需要自己去显式的写构造函数。在c++11中,可以使用default关键字强制编译器自己生成默认构造函数,如下图所示

搜索二叉树的赋值运算符重载函数可以复用拷贝构造函数来进行现代写法的代码实现,如下图所示

c++ - 第15节 - 二叉树进阶_第12张图片

搜索二叉树的析构函数,我们写一个DestoryTree函数来销毁释放树,如下图一所示,然后二叉树的析构函数复用DestoryTree函数即可,如下图二所示

c++ - 第15节 - 二叉树进阶_第13张图片c++ - 第15节 - 二叉树进阶_第14张图片

二叉搜索树递归实现版本

BinarySearchTree.h文件:

#pragma once

template
//struct BinarySearchTreeNode
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;

	K _key;

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

template
class BSTree
{
	typedef BSTreeNode Node;
private:
	void DestoryTree(Node* root)
	{
		if (root == nullptr)
			return;

		DestoryTree(root->_left);
		DestoryTree(root->_right);
		delete root;
	}

	Node* CopyTree(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copyNode = new Node(root->_key);
		copyNode->_left = CopyTree(root->_left);
		copyNode->_right = CopyTree(root->_right);

		return copyNode;
	}
public:
	// 强制编译器自己生成构造
	// C++11
	BSTree() = default;

	BSTree(const BSTree& t)
	{
		_root = CopyTree(t._root);
	}

	// t1 = t2
	BSTree& operator=(BSTree t)
	{
		swap(_root, t._root);
		return *this;
	}

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

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

	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

private:
	bool _EraseR(Node*& root, const K& key)
	{
		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* del = root;
			// 删除
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else
			{
				Node* minRight = root->_right;
				while (minRight->_left)
				{
					minRight = minRight->_left;
				}

				swap(root->_key, minRight->_key);

				return _EraseR(root->_right, key);
			}

			delete del;
			return true;
		}
	}

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			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;
		}
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

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

void TestBSTree1()
{
	BSTree t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.InsertR(9);

	BSTree copy = t;
	copy.InOrder();
}

void TestBSTree2()
{
	BSTree t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t.InsertR(e);
	}
	t.InOrder();
	t.EraseR(3);
	t.EraseR(8);

	t.InOrder();
	t.EraseR(14);
	t.EraseR(7);
	t.InOrder();

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

test.cpp文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;

#include "BinarySearchTree.h"

int main()
{
	TestBSTree2();

	return 0;
}

注:

1.搜索二叉树插入函数的递归实现,用要插入的值和根结点值进行比较,如果要插入的值大于根结点值,则转换成根节点的右子树插入该值,如果要插入的值小于根结点值,则转换成根节点的左子树插入该值,要删除的值等于根结点值则返回false(搜索二叉树内不允许有相同数值的结点),如果根节点为空,则创建一个值为要插入值的结点,然后和父亲结点进行链接即可,但是如何和父亲结点进行链接呢?

方法一:可以给_InsertR函数加一个parent指针参数,如下图所示,每次调用_InsertR函数的时候除了将root的左子树和右子树传过去也将root传过去,那么每次parent指针指向的都是本次root结点的父结点,最后如果根节点为空,则创建一个值为要插入值的结点,然后和parent指针指向的结点进行链接即可。

c++ - 第15节 - 二叉树进阶_第15张图片

方法二:可以给_InsertR函数的root参数加上引用,如下图所示,这样如果根节点为空时,则创建一个值为要插入值的结点,将结点指针赋值给root,因为这里root是上一层父节点的_left或_right的引用,那么赋值的时候就已经和父结点进行了链接。

c++ - 第15节 - 二叉树进阶_第16张图片

因为方法二的实现更加简洁,我们使用方法二的思路来实现递归版本的插入函数,如下图所示

c++ - 第15节 - 二叉树进阶_第17张图片

2.如下图所示是搜索二叉树删除函数的递归实现,用要删除入的值和根结点值进行比较,如果要删除的值大于根结点值,则转换成根节点的右子树删除该值,如果要删除的值小于根结点值,则转换成根节点的左子树删除该值,如果要删除的值等于根结点值,则和普通搜索二叉树删除函数一样,需要分三种情况(要删除的结点无孩子结点的情况融合在要删除的结点只有左孩子结点的情况内)

(1)要删除的结点只有左孩子结点,递归找到要删除的结点,此时root指向要删除的结点,那么需要将root指针指向结点的_left指针指向结点和root指针指向结点的父节点进行链接,然后释放root指针指向的结点,这里直接将root->_left赋值给root就可以完成除释放结点以外的其他工作,因为这里root是上一层父节点的_left或_right的引用,那么赋值的时候就已经将root指针指向结点的_left指针指向结点和root的父结点进行了链接。
(2)要删除的结点只有右孩子结点,递归找到要删除的结点,此时root指向要删除的结点,那么需要将root指针指向结点的_right指针指向结点和root指针指向结点的父节点进行链接,然后释放root指针指向的结点,这里直接将root->_right赋值给root就可以完成除释放结点以外的其他工作,因为这里root是上一层父节点的_left或_right的引用,那么赋值的时候就已经将root指针指向结点的_right指针指向结点和root的父结点进行了链接。
(3)要删除的结点有左、右孩子结点,与普通搜索二叉树那里一样,递归找到要删除的结点,此时root指向要删除的结点,使用minRight指针找到要删除结点的右子树最小值结点(找左子树最大值结点也行,这里以右子树最小值结点为例),将minRight指针指向的右子树最小值结点的值与root指向的要删除结点的值交换,然后释放minRight指针指向的结点即可。释放minRight指针指向的结点有两种方式。一种方式和普通搜索二叉树那里一样,使用minParent指针来找到要删除结点右子树最小值结点的父节点,将该父节点和右子树最小值结点的子节点进行链接然后释放minRight指针指向的右子树最小值结点。另一种方式是调用递归删除函数,删除root指向的要删除结点右子树中的要删除的值(因为原本要删除结点的右子树最小值结点交换后此时存的值是要删除的值)(交换后root指向的要删除结点右子树依然是一颗搜索树)

c++ - 第15节 - 二叉树进阶_第18张图片

注:先使用del指针将root指针的内容保存起来,那么del指针和root指针此时指向相同的结点,最后要释放root指针指向结点的时候,此时root指针已经被修改,delete del就释放了原root指针指向的结点

1.3.二叉搜索树的应用

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

#include 

namespace key
{
	template
	//struct BinarySearchTreeNode
	struct BSTreeNode
	{
		BSTreeNode* _left;
		BSTreeNode* _right;

		K _key;

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

	template
	class BSTree
	{
		typedef BSTreeNode Node;
	private:
		void DestoryTree(Node* root)
		{
			if (root == nullptr)
				return;

			DestoryTree(root->_left);
			DestoryTree(root->_right);
			delete root;
		}

		Node* CopyTree(Node* root)
		{
			if (root == nullptr)
				return nullptr;

			Node* copyNode = new Node(root->_key);
			copyNode->_left = CopyTree(root->_left);
			copyNode->_right = CopyTree(root->_right);

			return copyNode;
		}
	public:
		// 强制编译器自己生成构造
		// C++11
		BSTree() = default;

		BSTree(const BSTree& t)
		{
			_root = CopyTree(t._root);
		}

		// t1 = t2
		BSTree& operator=(BSTree t)
		{
			swap(_root, t._root);
			return *this;
		}

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

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

		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		bool _EraseR(Node*& root, const K& key)
		{
			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* del = root;
				// 删除
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					Node* minRight = root->_right;
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}

					swap(root->_key, minRight->_key);

					return _EraseR(root->_right, key);
				}

				delete del;
				return true;
			}
		}

		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				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;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;

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

	void TestBSTree1()
	{
		BSTree t;
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		for (auto e : a)
		{
			t.InsertR(e);
		}
		t.InOrder();

		t.InsertR(9);

		BSTree copy = t;
		copy.InOrder();
	}

	void TestBSTree2()
	{
		BSTree t;
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		for (auto e : a)
		{
			t.InsertR(e);
		}
		t.InOrder();
		t.EraseR(3);
		t.EraseR(8);

		t.InOrder();
		t.EraseR(14);
		t.EraseR(7);
		t.InOrder();

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

}
2.KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。该种方式在现实生活中非常常见:
\bullet 比如:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文就构成一种键值对。
\bullet 比如:统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是就构成一种键值对。
KV模型二叉搜索树代码实现:
#pragma once

#include 

namespace key_value
{
#pragma once

	template
	struct BSTreeNode
	{
		BSTreeNode* _left;
		BSTreeNode* _right;

		K _key;
		V _value;

		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
	};

	template
	class BSTree
	{
		typedef BSTreeNode Node;
	public:

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

		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key, const V& value)
		{
			return _InsertR(_root, key, value);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		bool _EraseR(Node*& root, const K& key)
		{
			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* del = root;
				// 删除
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					Node* minRight = root->_right;
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}

					swap(root->_key, minRight->_key);

					return _EraseR(root->_right, key);
				}

				delete del;
				return true;
			}
		}

		bool _InsertR(Node*& root, const K& key, const V& value)
		{
			if (root == nullptr)
			{
				root = new Node(key, value);
				return true;
			}

			if (root->_key < key)
				return _InsertR(root->_right, key, value);
			else if (root->_key > key)
				return _InsertR(root->_left, key, value);
			else
				return false;
		}

		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return nullptr;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return root;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};

	void TestBSTree1()
	{
		BSTree ECDict;
		ECDict.InsertR("root", "根");
		ECDict.InsertR("string", "字符串");
		ECDict.InsertR("left", "左边");
		ECDict.InsertR("insert", "插入");
		//...
		string str;
		while (cin >> str)  //while (scanf() != EOF)
		{
			//BSTreeNode* ret = ECDict.FindR(str);
			auto ret = ECDict.FindR(str);
			if (ret != nullptr)
			{
				cout << "对应的中文:" << ret->_value << endl;
			}
			else
			{
				cout << "无此单词,请重新输入" << endl;
			}
		}
	}

	void TestBSTree2()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		// 水果出现的次数
		BSTree countTree;
		for (const auto& str : arr)
		{
			auto ret = countTree.FindR(str);
			if (ret == nullptr)
			{
				countTree.InsertR(str, 1);
			}
			else
			{
				ret->_value++;  // 修改value
			}
		}

		countTree.InOrder();
	}
}

注:

1.在KV模型的搜索二叉树中插入、查找、删除函数仍然都是以key为依据进行的操作的。
2.KV模型的搜索二叉树中,每个节点的value值可能相同,每个节点的key值都不相同
3.与K模型搜索二叉树不同的是,KV模型在_FindR搜索函数中返回值应该为找到的结点指针。K模型搜索二叉树_FindR搜索函数不返回结点指针的原因是不能修改key的值,否则很可能不再是一个搜索树,KV模型搜索二叉树_FindR搜索函数返回结点指针的原因是key的值不能修改,但是可以修改value值,因此找到对应结点后需要返回结点的指针,为了保证返回结点指针后key值不被修改,库里面的办法是将结点的_key成员变量用const修饰,这样建立结点的时候可以对key值进行初始化,后面无法再被修改,如下图所示。
但是如果给_key成员变量用const修饰,那么_EraseR删除函数会报错,因为删除函数中调用了swap交换函数将两个结点的_key成员变量值进行交换,这样修改了结点的_key成员变量值,因此会报错。官方库的解决办法是直接将两个结点进行交换(将两个结点的链接关系进行更改)。这里因为给结点的_key成员变量加上const修饰后还需要修改_EraseR删除函数里面的swap代码,将两个结点真正交换,过于复杂,这里不进行演示,所以上面KV模型实现代码中结点的_key成员变量我们不加const修饰。
4.代码while (cin >> str)和代码while (scanf() != EOF)的功能相同,都是持续输入输出内容。当scanf函数读取控制台数据时遇到文件结束/文件读取失败标志EOF时会返回EOF;string类里面重载的operator>>流提取函数返回的是cin,如下图一所示,cin是istream类的对象。
ios类中有两个函数c++98中的operator void* ()和c++11中的operator bool()如下图一二所示,这两个函数的功能是判断是否提取到文件结束/文件读取失败标志EOF,提取到EOF则返回真,没有提取到EOF则返回假。
istream类是继承ios类的,因此istream类的对象cin也有operator void* ()和operator bool()两个函数。当cin >> str代码返回cin进行while循环条件判断的时候,c++98中cin对象会进行隐式类型转换,会cin.operator void*调用operator void* ()函数,该函数判断是否遇见文件结束/文件读取失败标志EOF,如果是EOF标志就返回空指针,如果不是EOF就返回非空指针;c++11中cin对象会进行隐式类型转换,会cin.operator bool调用operator bool()函数,该函数判断是否遇见文件结束/文件读取失败标志EOF,如果是EOF标志就返回0,如果不是EOF就返回1。
c++ - 第15节 - 二叉树进阶_第19张图片

c++ - 第15节 - 二叉树进阶_第20张图片

如果想手动输入EOF标志来退出持续输入输出状态可以使用Ctrl+z,Ctrl z控制台就会输入EOF,然后回车让scanf或>>进行读取即可。

ctrl+c也可以退出持续输入输出状态,ctrl c的功能是直接结束进程。

下图一所示的代码与上面while (cin >> str)部分的原理类似,while的判断部分是A类的对象a,在判断的时候对象a会进行隐式类型转换,自动去调用对象a里面的operator bool函数,operator bool函数的返回值作为while循环的判断条件。其中代码while(a)与while(a.operator bool)是等价的。

如下图二所示,这里while(a)中a对象的隐式类型转换和A aa = 10中的隐式类型转换相同。A aa = 10代码因为类型不同,会先调用A类的构造函数构造成员变量_a为10的对象,此时类型相同,再拷贝构造给aa(编译器优化成直接用10对aa对象调用构造函数进行构造);while(a)代码因为对象a和真假判断的bool类型不同,会先调用A类的operator bool函数,operator bool函数返回一个bool类型的值,此时类型相同可以进行while循环的判断。那么代码bool ret = a,使用A类型的对象a初始化bool类型的对象ret也是可以的,这里类型不同,发生隐式类型转换,对象a调用operator bool函数返回一个bool类型的值,赋值给ret。

c++ - 第15节 - 二叉树进阶_第21张图片  c++ - 第15节 - 二叉树进阶_第22张图片

我们发现库中的operator bool函数是用explicit关键字修饰的,如下图一所示,explicit关键字限制不能使用该函数进行隐式类型转换,那为什么我们前面使用代码while (cin >> str),cin对象还能调用其operator bool函数呢?

其实explicit关键字只限制类似A aa = 10和bool ret = a这种调用explicit所修饰的函数进行隐式类型转换初始化的情况,而不会去限制while(a)这种的隐式类型转换,如下图一所示。

其实也可以使用类似operator int()的函数来满足类似int y = a和while(a),将A类型的对象a隐式类型转换成对应类型的功能,如下图二所示。

c++ - 第15节 - 二叉树进阶_第23张图片  c++ - 第15节 - 二叉树进阶_第24张图片

从这里可以看出,与构造函数支持将内置类型转换成自定义类型类似,c++支持使用类似operator int、operator bool这种函数将自定义类型转换成内置类型。

3.二叉搜索树具有去重+排序功能。将所有的数据依次插入到二叉搜索树中(去重),然后进行中序遍历(排序),得到的结果就是对数据集去重排序后的结果,整个去重排序的效率可以达到N\times log_{2}^{N}(搜索树是完全二叉树的情况)。

注:

1.搜索二叉树如果插入顺序不同,树的形状也不同,如果形状近似一个完全二叉树,那么无论是K模型、KV模型的搜索效率还是去重排序的效率都很高,但是如果形状近似一个链表,那么K模型、KV模型的搜索效率和去重排序的效率都很低。

2.我们后面会学到平衡树,平衡树是对搜索二叉树的一种改进,使得插入数据后树的形状接近完全二叉树

3.K模型搜索二叉树对应std库中的数据结构是set,KV模型搜索二叉树对应std库中的数据结构是map

1.4.二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
c++ - 第15节 - 二叉树进阶_第25张图片

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log_{2}^{N}

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:\frac{N}{2}
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


2.二叉树进阶面试题

练习题一:

题目描述:

c++ - 第15节 - 二叉树进阶_第26张图片

c++ - 第15节 - 二叉树进阶_第27张图片

题目链接:606. 根据二叉树创建字符串 - 力扣(LeetCode)

代码1:

c++ - 第15节 - 二叉树进阶_第28张图片

代码2:(优化版本)

c++ - 第15节 - 二叉树进阶_第29张图片

注:

1.该题目如果左右子树都为空,或者右子树为空,则空位置的括号省略,如果左子树为空右子树不为空,则空位置的括号不能省略

2.代码1的实现方式每一次递归后返回str字符串都是传值返回,需要进行拷贝构造然后+=到上一层递归的str字符串后面,这样效率很低。这里不能简单的将tree2str函数的返回值类型string改成string&类型进行传引用返回,因为返回的string类型的str是局部变量,出了本次递归,本次的string类型对象str和str的成员变量指向堆区的数据空间都会被释放,造成野指针的问题,然后上层递归再去+=下层str的成员变量指向堆区的数据,会造成越界访问的问题。我们可以在tree2str函数中直接创建好一个string类型的str,然后将该str传给_tree2str函数进行递归,这样str在_tree2str函数中就不是局部变量,在_tree2str函数中str进行引用传参,每次都在str中+=即可,如代码2的实现。

练习题二:

题目描述:

c++ - 第15节 - 二叉树进阶_第30张图片

c++ - 第15节 - 二叉树进阶_第31张图片

题目链接:102. 二叉树的层序遍历 - 力扣(LeetCode)

代码:

c++ - 第15节 - 二叉树进阶_第32张图片

练习题三:

题目描述:

c++ - 第15节 - 二叉树进阶_第33张图片

c++ - 第15节 - 二叉树进阶_第34张图片

题目链接:107. 二叉树的层序遍历 II - 力扣(LeetCode)

代码:(在练习题二代码的基础上加一个逆置即可)

c++ - 第15节 - 二叉树进阶_第35张图片

练习题四:

题目描述:

c++ - 第15节 - 二叉树进阶_第36张图片

c++ - 第15节 - 二叉树进阶_第37张图片

题目链接:236. 二叉树的最近公共祖先 - 力扣(LeetCode)

代码1:

c++ - 第15节 - 二叉树进阶_第38张图片

代码2:(优化版本)

c++ - 第15节 - 二叉树进阶_第39张图片

注:

1.找两个结点p、q的最近公共祖先问题可以转换成找一个结点,如果p在该节点的左(右)子树中,q在该节点的右(左)子树中,那么该节点就是p和q的最近公共祖先。

特殊情况:如果p(q)在q(p)的左右子树中那么q(p)也是p和q的最近公共祖先。

2.所有的OJ练习题检查的是语法逻辑,虽然下面的代码理论上不会走到else部分,但是不写的话不符合语法逻辑会报错

c++ - 第15节 - 二叉树进阶_第40张图片

3.题目测试用例给的二叉树不一定是满二叉树和完全二叉树,如果二叉树的形状接近于链表,如下图所示要找蓝色结点的最近公共祖先,代码1的效率很低。从根部结点往下每个结点依次判断,每次判断调用IsInSubTree函数从该节点往下遍历查找,因此上面代码1的时间复杂度为O(N^{2})。那么如何对代码1进行优化呢?

c++ - 第15节 - 二叉树进阶_第41张图片

思路1:如果题目测试用例给的二叉树是搜索二叉树,那么代码1的时间复杂度为O(N),因为如果是搜索二叉树只需要从根部结点往下每个结点依次判断,判断的时候不需要再使用IsInSubTree函数,可以直接用p和q数值的大小判断其是在判断结点的左边还是右边。

思路2:如果题目测试用例给的二叉树是三叉链的(子节点可以找到父节点),那么找p和q的最近公共祖先问题就可以转换为找p和q所在链表的相交问题,那么代码1的时间复杂度为O(N)。链表相交问题的解决方法是先算出p和q链表的长度,然后让长的链表先走差距步两个链表再同时走,第一个相同的结点就是两个链表的交点。

思路3:找到根节点到p和q的路径pPath和qPath,从根节点开始将根节点到p路径经过的结点依次入栈pPath,从根节点开始将根节点到q路径经过的结点依次入栈qPath,然后让大的栈出栈直到和小的栈大小相同,两个栈再一起出栈,直到出栈的值相同,那么该值所对应的结点就是p和q的最近公共祖先。如何得到p和q的路径pPath和qPath呢?

以p结点为例,以根左右的遍历顺序进行,每次结点判断前先将结点值入栈pPath,然后判断该结点值是不是p,如果是p则路径已找到,如果不是p则以根左右的遍历顺序对子结点先入栈再判断,如果到叶子节点了叶子结点值仍不是p,则递归返回上一层并且将叶子结点的值出栈,然后以根左右的遍历顺序继续进行,如果该结点的子节点值全部判断完没有p则返回上一层,将该结点的值出栈。这样最后递归结束pPath栈里面就是p结点的路径。思路3的实现如上面代码2所示。

练习题五:

题目描述:

c++ - 第15节 - 二叉树进阶_第42张图片

c++ - 第15节 - 二叉树进阶_第43张图片

题目链接:二叉搜索树与双向链表_牛客题霸_牛客网 (nowcoder.com)  

代码:

c++ - 第15节 - 二叉树进阶_第44张图片

注:

1.本题使用中序遍历进行,cur指针指向遍历到的结点,prev指向前一个遍历的结点,然后将cur指向结点的left指针指向prev指向的结点,prev指向结点的right指针指向cur指向的结点,也就是将本次cur指向结点和prev指向结点进行双向链接,然后prev指向cur指向的结点,以中序遍历的顺序cur指向下一个结点。

2.InOrderConvert函数中prev参数必须要加引用,如果不加引用,那么上一层递归prev的改变不会影响这一层递归的prev,加了引用那么每次prev的改变改变的都是同一个prev。

练习题六:

题目描述:

c++ - 第15节 - 二叉树进阶_第45张图片

c++ - 第15节 - 二叉树进阶_第46张图片

题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

代码:

c++ - 第15节 - 二叉树进阶_第47张图片

注:

1.根据一个树的前序遍历结果和中序遍历结果可以还原这个树

前序:根、左子树、右子树 —— 依次确定根

中序:左子树、根、右子树 —— 划分左右子树区间

例:preorder = [3,9,20,15,7]   inorder = [9,3,15,20,7]

根据前序preorder确定根,根据中序inorder确定根的左右区间。从preorder第一个数3开始,以3为根,3对应inorder中区间值为9 3 15 20 7,区间中3的左边有值右边有值说明3左右都不为空,preorder中3的后面是9那么9是3的左节点,左节点9对应inorder中区间值为9,preorder中9的后面是20那么20是3的右节点,右节点20对应inorder中区间值为15 20 7。preorder第二个数9,以9为根,9对应inorder中区间值为9,区间中9的左边没有值右边没有值说明9左右都为空。preorder第三个数20,以20为根,20对应inorder中区间值为15 20 7,区间中20的左边有值右边有值说明20左右都不为空,preorder中20的后面是15那么15是20的左节点,左节点15对应inorder中区间值为15,preorder中15的后面是7那么7是20的右节点,右节点7对应inorder中区间值为7。preorder第四个数15,以15为根,3对应inorder中区间值为15,区间中15的左边没有值右边没有值说明15左右都为空。preorder第五个数7,以7为根,7对应inorder中区间值为7,区间中7的左边没有值右边没有值说明7左右都为空。那么根据例中给的前序遍历和中序遍历的结果推出树的形状如下图所示

c++ - 第15节 - 二叉树进阶_第48张图片

2.我们定义_buildTree函数,函数参数中prei标记preorder中走到了哪个根,inBegin和inEnd标记inorder中根所对应的左右子树区间。其中上一层递归中prei的改变要能影响到下一层递归中的prei,所以参数prei要加引用,inBegin和inEnd每次递归都会传值,因此inBegin和inEnd不需要加引用。

3.本题是根据一个树的前序遍历结果和中序遍历结果还原这个树,前序遍历确定根,中序遍历划分左右子树区间;还可以根据一个树的中序遍历结果和后序遍历结果还原这个树,中序遍历划分左右子树区间,后序遍历确定根,思路与本题类似;不能根据一个树的前序遍历结果和后序遍历结果还原这个树。

练习题七:

题目描述:

c++ - 第15节 - 二叉树进阶_第49张图片

c++ - 第15节 - 二叉树进阶_第50张图片

题目链接:144. 二叉树的前序遍历 - 力扣(LeetCode)

代码:

c++ - 第15节 - 二叉树进阶_第51张图片

注:

1.简单的递归改非递归可以借助循环,复杂的递归改非递归需要借助栈。

2.前序遍历的顺序是根、左子树、右子树,那么非递归前序遍历一棵树,我们的思路如下图一所示:

c++ - 第15节 - 二叉树进阶_第52张图片  c++ - 第15节 - 二叉树进阶_第53张图片

以上图二的树为例,结合上面代码,解释非递归前序遍历的思路。我们创建一个顺序表v存储遍历的结果,创建一个栈st控制遍历的进程。

首先根为8,遍历根为8的左路节点依次入顺序表v和栈st当节点为空时停止,此时顺序表内容为8 3 1,栈的内容为8 3 1,然后st出栈得到1,将1的右子树设为根。根为空,遍历根为空的左路节点,相当于没遍历,此时顺序表内容为8 3 1,栈的内容为8 3,然后st出栈得到3,将3的右子树设为根。根为6,遍历根为6的左路节点依次入顺序表v和栈st当节点为空时停止,此时顺序表内容为8 3 1 6,栈的内容为8 6,然后st出栈得到6,将6的右子树设为根。根为7,遍历根为7的左路节点依次入顺序表v和栈st当节点为空时停止,此时顺序表内容为8 3 1 6 7,栈的内容为8 7,然后st出栈得到7,将7的右子树设为根。根为空,遍历根为空的左路节点,相当于没遍历,此时顺序表内容为8 3 1 6 7,栈的内容为8,然后st出栈得到8,将8的右子树设为根。根为10,遍历根为10的左路节点依次入顺序表v和栈st当节点为空时停止,此时顺序表内容为8 3 1 6 7 10,栈的内容为10,然后st出栈得到10,将10的右子树设为根,此时10的右子树根为空,栈为空,结束。

练习题八:

题目描述:

c++ - 第15节 - 二叉树进阶_第54张图片

c++ - 第15节 - 二叉树进阶_第55张图片

题目链接:94. 二叉树的中序遍历 - 力扣(LeetCode)

代码:

c++ - 第15节 - 二叉树进阶_第56张图片

注:

1.中序遍历的顺序是左子树、根、右子树,非递归中序遍历一棵树,只需要将非递归前序遍历树的代码中顺序表插入代码位置进行移动,使得根的的左子树访问完后顺序表中再插入根节点值。

练习题九:

题目描述:

c++ - 第15节 - 二叉树进阶_第57张图片

c++ - 第15节 - 二叉树进阶_第58张图片

题目链接:145. 二叉树的后序遍历 - 力扣(LeetCode)

代码:

c++ - 第15节 - 二叉树进阶_第59张图片

注:

1.后序遍历的顺序是左子树、右子树、根,非递归后序遍历一棵树时,左子树访问完后回到根,此时还需要根的右子树也访问完才能将根入顺序表,那么当回到根时如何辨别根的右子树是否已经访问完了呢?

我们借助prev指针指向上一个入顺序表的结点(这里不能用prev记录上一个入顺序表结点的值是因为任意两个结点的值有可能相同)。如果回到根时其右子结点不是prev指向的结点,那么说明此时根的右子树还没有访问,去访问根的右子树,不能将根的值入顺序表。如果回到根时其右子结点是prev指向的结点,那么说明此时根的右子树已经访问完,可以将根的值入顺序表。

你可能感兴趣的:(c++,c++,开发语言)