从C语言到C++_24(二叉搜索树)概念+完整代码实现+笔试题

本文详细介绍了二叉搜索树(BinarySearchTree)的概念、优势和劣势,以及在C++中实现二叉搜索树的插入、查找、删除操作,包括非递归和递归方法。讨论了最坏情况下时间复杂度为O(N)的问题,并提出改良方案,如平衡二叉搜索树。此外,文章还探讨了二叉搜索树在K模型和KV模型中的应用,以及如何处理拷贝构造和赋值操作。
摘要由CSDN通过智能技术生成

目录

1. 二叉搜索树(BinarySearchTree)

1.1 二叉搜索树的优势和劣势

二叉搜索树的时间复杂度:O(N)

1.2 二叉搜索树的改良

2. 二叉搜索树的实现

2.1 二叉搜索树的定义

2.2 二叉搜索树的插入

2.3 二叉搜索树的查找

2.4 二叉搜索树的删除

2.5 二叉搜索树的查找(递归)

2.6 二叉搜索树的插入(递归)

2.7 二叉搜索树的删除(递归)

2.8 析构和拷贝构造和赋值

3. 搜索二叉树的应用

3.1 K 模型

3.2 KV 模型

4. 笔试选择题

答案:

5. 完整代码:

本篇完。


此篇算是用C++讲高阶数据结构第一篇,在C++完结之前高阶数据结构内容都放在④⑤两个专栏,等后面C++完结还会学图和算法的内容。

先讲二叉搜索树是因为讲解 map 和 set 的特性需要二叉搜索树做铺垫,理解搜索二叉树有助于更好地理解 map 和 set 的特性。第二个原因是为了后期讲解查找效率极高的平衡搜索二叉树,随后再讲完红黑树,我们就可以模拟实现 map 和 set 了。

1. 二叉搜索树(BinarySearchTree)

概念:搜索二叉树(二叉搜索树)又称为二叉排序树,它或者是一颗空树,

或者是具有以下性质的二叉树:

  • 若其左子树不是空,则左子树上所有节点的值都小于根结点的值
  • 若其右子树不是空,则右子树上所有结点的值都大于根结点的值
  • 其左右子树必须都是二叉搜索树

至于叫它 "搜索二叉树",还是 "二叉搜索树",都是可以的,就是叫搜索二叉树的英文缩写不太好。

结论:任意一个子树都需要满足,左子树的值 < 根 < 右子树的值,才能构成二叉搜索树。 


1.1 二叉搜索树的优势和劣势

既然叫搜索二叉树,它肯定是用来搜索的,当满足搜索二叉树时你将可以快速地查找任意的值。

举个例子: 查找7

放到以前我们如果不用二分查找,可能会选择用暴力的方式去从头到尾遍历一遍。

但现在学了搜索二叉树,我们就可以轻松找到这个7了,7 比 8(根节点) 小,

根据搜索二叉树的性质,它必然不会出现在右子树 (右边大) ...

搜索二叉树查找一个值的最坏情况,也只是查找高度次。

二叉搜索树的时间复杂度:O(N)

上面的例子会让人误以为搜索二叉树的增删查改的时间复杂度是O(logN),

但实际上是O(N),因为这棵树是有可能会 蜕化 的,极端情况下会蜕化成一个 "单边树" 

比如按有序插入:

最差情况:二叉搜索树退化为单边树(或类似单边),其平均比较次数为:O(N)

最优情况:二叉搜索树为完全二叉树(或接近完全二叉树),其平均比较次数为:O(logN)

对于时间复杂度的分析我们要做一个悲观主义者,根据最差情况去定时间复杂度:O(N)


1.2 二叉搜索树的改良

如果搜索二叉树退化成了单边树,其性能也就失去了,能否进行改进让它保持性能?

如何做到不论按照上面次序插入关键码,二叉搜索树的性能均能达到最优?

搜索二叉树由于控制不了极端情况,与 O(logN)失之交臂,但平衡二叉搜索树做到了。

严格意义上来说满二叉树才是O(logN),完全二叉树是接近O(logN)。

而平衡搜索二叉树维持左右两边均匀程度,让它接近完全二叉树,从而让效率趋近O(logN)。

后面我们学的各种树(AV树,红黑树)可以说都是二叉搜索树的改良。


2. 二叉搜索树的实现

2.1 二叉搜索树的定义

此时我们的BinarySearchTree就缩写成BSTree了。

这里我们用模板,模板参数我们给了一个K,表示 key 的意思(模板参数并非一定要用 T)。

template<class K>
class BSTreeNode
{
public:
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};

下面我们来定义整个树,BSTreeNode<K> 有些长了,这里将其 typedef 成 Node 。

构造函数都没必要写,它自己生成的就够用了:

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

protected:
	Node* _root = nullptr;
};

2.2 二叉搜索树的插入

二叉搜索树的插入是会“去重”的。

我们先来实现最简单的插入操作:

  • 如果树为空,则直接新增结点,赋值给 root 指针。
  • 如果树不为空,按二叉搜索树性质查找插入位置,插入新节点。

Insert 的实现我们可以用递归,也可以用非递归,这一块递归比非递归更难理解。

秉着先难后易的态度,我们先讲比较难理解的非递归版本

Step1:首先检查是否有根结点 _root,如果没有我们就 new 一个结点出来作为根结点。此时插入成功,返回 true。

Step2:插入就需要找到插入位置,我们定义一个 cur 变量,从根节点开始,
根据搜索二叉树 性质,将 cur 结点的值与插入的值  进行大小比较。

如果插入的值大于当前结点值,则将 cur 结点向右移动 cur=cur->_right ;
如果插入的值小于当前节点值,就将 cur 结点向左移动 cur=cur->_left。

值得注意的是,还需要额外记录一下 cur 的父结点,

因为你不知道什么时候会碰到nullptr结束。

并且当我们找到插入位置后,仅仅 new 上一个新结点给 cur 是完成不了插入操作的。

因为直接这么做 cur 也只是一个局部变量而已,需要 cur 跟上一层(cur 的父亲)相链接才行。

为了能找到上一层,所以我们还需要额外定义一个 prev 变量来记录 cur 的父结点,

在我们更换 cur 结点时记录父结点的位置 prev=cur 即可。

当然了,还有一种插入失败的情况,就是判断大小时出现等于的情况,返回 false 即可。

(重复的值是不允许插入的,默认情况是不允许冗余的!但是也有针对这个的变形,后续再说)

Step3:插入。new 一个新结点给 cur,此时 cur 只是一个局部变量,必须要和父亲链接,

此时应该链接父亲的左边,还是链接父亲的右边?我们不知道,所以我们需要再做一个比较:

如果父节点的值大于插入的值,则将 cur 链接到父亲左边 prev->_left=cur;

反之将 cur 链接到父亲右边  prev->_right=cur。

最后,插入成功返回 true。

insert 代码:

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

		Node* prev = nullptr;
		Node* cur = _root;
		while (cur != nullptr) // 找到要插入的位置
		{
			if (key < cur->_key) // 要插入的值比当前值小
			{
				prev = cur; // 记录cur,等下cur更新就是cur的父亲
				cur = cur->_left; // 到左边插入
			}
			else if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 相等,插入失败
			}
		}

		cur = new Node(key); // 走到这,cur就是要插入的位置
		if (key < prev->_key) // 如果key比cur的父亲小
		{
			prev->_left = cur; // 插入到父亲的左孩子
		}
		else
		{
			prev->_right = cur;
		}
		return true;
	}

再写一个中序遍历来测试一下插入的效果:

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

模拟出一个测试用例:

void TestBSTree1() 
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (const auto& e : a) 
	{
		t.Insert(e);
	}
	t.InOrder();  // 没法传根
}

此时会出现一个问题,因为根是私有的,我们没办法把根传过去。

此时我们可以选择在类内部写一个成员函数 GetRoot 去取根,但是这里我们可以选择这么做:

干脆将刚才我们实现的中序设为 protected 保护,然后再写一个 InOrder 放在公有的区域。

这就是在类内访问 _root 了,没有什么问题。

如此一来我们在类外就可以直接调用 InOrder,并且也不需要传递参数了。

BinarySearchTree.h:

#pragma once

#include <iostream>
using namespace std;

template<class K>
class BSTreeNode
{
public:
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};

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

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

		Node* prev = nullptr;
		Node* cur = _root;
		while (cur != nullptr) // 找到要插入的位置
		{
			if (key < cur->_key) // 要插入的值比当前值小
			{
				prev = cur; // 记录cur,等下cur更新就是cur的父亲
				cur = cur->_left; // 到左边插入
			}
			else if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 相等,插入失败
			}
		}

		cur = new Node(key); // 走到这,cur就是要插入的位置
		if (key < prev->_key) // 如果key比cur的父亲小
		{
			prev->_left = cur; // 插入到父亲的左孩子
		}
		else
		{
			prev->_right = cur;
		}
		return true;
	}

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

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

	Node* _root = nullptr;
};

Test.c:

#include "BinarySearchTree.h"

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

int main()
{
	TestBSTree1();

	return 0;
}


2.3 二叉搜索树的查找

二叉搜索树的查找 Find 实现很容易,用和刚才一样的思路,从根结点开始查找。 

从根开始,如果要查找的值大于 cur 目前的值,则让 cur 往右走,反之往左走。

当查找得值与 cur 的值相等时则说明找到了,返回 true。

当 cur 触及到空(while 循环结束)则说明找不到,返回 false。

代码:

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

2.4 二叉搜索树的删除

搜索二叉树删除的实现是有很有难度的。

没有孩子或者只有一个孩子,可以直接删除,孩子托管给父亲。

两个还是没办法给父亲,父亲养不了这么多孩子,但是可以找个人替代父亲养孩子。

当然,也不能随便找,找的人必须仍然维持搜索二叉树的性质,这是原则。

必须比左边的大,比右边的小。所以在家族中找是最合适的。

找左子树的最大值结点,或者右子树的最小值结点。

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

如果存在,那么删除的结点可能分为下面四种情况:

a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左孩子结点也有右孩子结点

看起来有待删除节点有 4 种情况,但实际上 a 和 b,或 a 和 c 可以合并。

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

我们还是定义一个 cur 变量,定义一个prev为空,后面是cur的父亲。

先找到要删除的结点,然后分下面三种情况

① 该结点无左孩子

如果要删除下面这颗二叉树的 10 节点和 4 节点:

当 cur 找到 10 结点后,如果左侧为空情况如下:

若该结点为 _root,直接让 _root 等于它的右孩子结点。
对于删除 10 结点:若 cur是右孩子,则令其父亲的右孩子指向其右孩子 (如图所示)
对于删除 4 结点:若 cur是左孩子,则令其父亲的左孩子指向其右孩子(如图所示)
最后删除 cur 结点

② 该结点无右孩子

如果要删除 14 结点,删除逻辑和删除左孩子是类似的:

③ 该结点有左右两个孩子

如果删除的结点有左右两个孩子,我们就在它的右子树中寻找中序的第一个结点。

即与右子树的最小值进行替换,当然也可以选择左子树的最大值进行替换。

例子:比如下面这颗子树,我们要删除 3 结点:

 如果该结点有两个孩子,则采用如下替换法:

该结点和右子树的最小值或左子树的最大值进行值的替换,然后删除替换后的结点。

这里我们采用与右子树的最小值进行替换。

Erase代码:

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* father = nullptr;
		while (cur) // 找到要删除的结点
		{
			if (key < cur->_key)
			{
				father = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				father = cur;
				cur = cur->_right;
			}
			else // 找到后开始删除,分三种情况
			{
				if (cur->_left == nullptr) // ①该结点无左孩子
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_right;
						}
						else //(cur == father->_right)
						{
							father->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //  ②该结点无右孩子
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_left;
						}
						else //(cur == father->_left)
						{
							father->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else // ③有两个结点,替换法删除
				{
					Node* MinNode = cur->_right;
					Node* MinParNode = cur;
					while (MinNode->_left) // 找右子树的最小
					{
						MinParNode = MinNode;
						MinNode = MinNode->_left;
					}
					swap(cur->_key, MinNode->_key); // 找到后交换

					if(MinParNode->_right == MinNode) // 链接父亲结点,这步易漏
					{
						MinParNode->_right = MinNode->_right;
					}
					else
					{
						MinParNode->_left = MinNode->_right;
					}
					delete MinNode;
					MinNode = nullptr;
				}
				return true;
			}
		}
		return false;
	}

Test.c:

#include "BinarySearchTree.h"

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

	t.Erase(8);
	t.Erase(3);
	t.InOrder();
	for (const auto& e : arr)
	{
		t.Erase(e);
	}
	t.InOrder();
}

int main()
{
	TestBSTree1();

	return 0;
}


2.5 二叉搜索树的查找(递归)

二叉搜索树的递归查找很简单,因为外面不能传根,这里像InOrder一样封装起来

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

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (key < root->_key)
		{
			_FindR(root->_left, key);
		}
		else if (key > root->_key)
		{
			_FindR(root->_right, key);
		}
		else
		{
			return true;
		}
	}

2.6 二叉搜索树的插入(递归)

二叉搜索树的递归插入如果这样写是插入不了的:

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

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

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

因为递归到最后一步的时候,root只是一个局部变量,根本插入不了数据。

可以一步一步的把父亲传下来,但是这里有一个神之一手:加引用:

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

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

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

这里的引用最后一步才起作用,它是空,但它也是上一层传下来的别名。

给给root,就把父亲链接起来了,可以用二级指针,但是用引用很方便。


2.7 二叉搜索树的删除(递归)

这里的递归删除和上面的递归插入一样,也用到了非常巧妙的引用:

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

	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* MinNode = root->_right;
				while (MinNode->_left)
				{
					MinNode = MinNode->_left;
				}
				swap(root->_key, MinNode->_key);
				//return EraseR(key);  错的
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;
		}
	}

Test.c:

#include "BinarySearchTree.h"

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

	t.Erase(8);
	t.Erase(3);
	t.InOrder();
	for (const auto& e : arr)
	{
		t.Erase(e);
	}
	t.InOrder();
}

void TestBSTree2()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.EraseR(8);
	t.EraseR(3);
	t.EraseR(2);
	t.InOrder();
	cout << t.Find(10) << endl;
	cout << t.Find(100) << endl;
	cout << t.FindR(10) << endl;
	cout << t.FindR(100) << endl;
	for (const auto& e : arr)
	{
		t.EraseR(e);
	}
	t.InOrder();
}

int main()
{
	TestBSTree2();

	return 0;
}


2.8 析构和拷贝构造和赋值

Test.c: 默认生成的:

#include "BinarySearchTree.h"

void TestBSTree3()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();
}

int main()
{
	TestBSTree3();

	return 0;
}

这里也是浅拷贝的问题,指向的是同一颗树,没崩只是没写析构,写下析构:

	~BSTree()
	{
		_Destory(_root);
	}

protected:
	void _Destory(Node*& root) // 加引用下面的置空就起作用了
	{
		if (root == nullptr)
		{
			return;
		}
		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
		root = nullptr;
	}

再运行刚才测试,程序崩溃:

 这时我们就应该自己写拷贝构造了:

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

protected:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

这时运行就会报错:

错误(活动)    E0291    类 "BSTree<int>" 不存在默认构造函数

这时写个默认的拷贝构造:

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	//BSTree() // 这样写也行,但是下面是C++11的用法
	//{}
	BSTree() = default; // C++11的关键字,强制编译器生成默认的构造

protected:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

运行程序:

 写了拷贝构造我们就可以直接用现代写法写一个赋值:

	BSTree<K> operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

测试:

void TestBSTree3()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();

	BSTree<int> t2;
	t2.Insert(3);
	t2.Insert(5);
	t2.Insert(4);
	copy = t2;
	t2.InOrder();
	copy.InOrder();
}


3. 搜索二叉树的应用

3.1 K 模型

K模型,即只有 key 作为关键码,我们上面写的就是K模型,

结构中只需存储 key 即可,关键码就是需要搜索到的值。

举个例子:对于单词 word,我们需要判断该单词是否拼写正确

以单词集合中的每个单词作为 key,构建一个搜索二叉树。
在二叉树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。


3.2 KV 模型

KV模型,每一个关键码 key,都有与之对应的值 Value,即 <Key, Value> 的键值对。

这就像 Python 中的 dict 字典类型一样,key 和 value 对应。

这在生活中也是非常常见的,比如英汉词典就是英文与中文的对应关系,通过英文可以快读检索到对应的中文,英文单词也可以与其对应的中文构建出一种键值对:

<string, string>   即  <word, chinese>


再比如统计水果次数,就构建出了一种键值对:

<string, int>   即  <水果, count>

直接放用于测试的代码:

//二叉搜索树的KV结构
namespace KeyValue
{
	//定义两个类模板参数K、V
	template<class K, class V>
	class BSTreeNode
	{
	public:
		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}

		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key; //存放了两个类型的数据,相比较与K模型
		V _value;
	};

	//同样的,定义两个类模板参数K、V
	//搜索二叉树依旧是按照K的数据进行排序,和V无关
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				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, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}

			return true;
		}

		//查找只和数据_key有关,与数据_value无关
		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;
		}

		//删除只和数据_key有关,与数据_value无关
		bool Erase(const K& key)
		{
			//... 和K模型一样
			return true;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
	protected:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}

		Node* _root = nullptr;
	};

	void TestBSTree1()
	{
		BSTree<string, string> dict; // 字典树,如果所有单词都在里面就能很准确查找
		dict.Insert("sort", "排序");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");
		dict.Insert("string", "字符串");
		dict.Insert("insert", "插入");
		string str;
		while (cin >> str) // Crtl+z+换行结束,或者Crtl+c结束
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << "对应的中文:" << ret->_value << endl;
			}
			else
			{
				cout << "对应的中文->无此单词" << endl;
			}
		}
	}

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

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

		countTree.InOrder();
	}
}


4. 笔试选择题

1. 关于二叉搜索树特性说法错误的是( )

A.二叉搜索树最左侧的节点一定是最小的

B.二叉搜索树最右侧的节点一定是最大的

C.对二叉搜索树进行中序遍历,一定能够得到一个有序序列

D.二叉搜索树的查找效率为O(log_2N)

2. 下面的哪个序列可能是二叉搜索树中序遍历的结果? ( )

A.73 8 2 9 4 11

B. 2 3 4 7 8 9 11

C.11 2 9 3 8 4 7

D.以上均可

3. 将整数序列(7-2-4-6-3-1-5)按所示顺序构建一棵二叉排序树a(亦称二叉搜索树),之后将整数8按照二叉排序树规则插入树a中,请问插入之后的树a中序遍历结果是( ) 

A.1-2-3-4-5-6-7-8

B.7-2-1-4-3-6-5-8

C.1-3-5-2-4-6-7-8

D.1-3-5-6-4-2-8-7

E.7-2-8-1-4-3-6-5

F.5-6-3-4-1-2-7-8

4. 下面关于二叉搜索树正确的说法是( )

A.待删除节点有左子树和右子树时,只能使用左子树的最大值节点替换待删除节点

B.给定一棵二叉搜索树的前序和中序遍率历结果,无法确定这棵二叉搜索树

C.给定一棵二叉搜索树,根据节点值大小排序所需时间复杂度是线性的

D.给定一棵二叉搜索树,可以在线性时间复杂度内转化为平衡二叉搜索树

答案:

1. D

二叉搜索树的概念:

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

   1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

   2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

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

  从概念中可以得出以下性质:

   1. 二叉搜索树中最左侧节点一定是最小的,最右侧节点一定是最大的

   2. 对二叉搜索树进行中序遍历,可以得到一个有序的序列

  A ,B,C:正确

  D:错误,二叉搜索树最差情况下会退化为单支树,因此:其查找的效率为O(N)

2. B

二叉搜索树的特性:如果对二叉搜索树进行中序遍历,可以得到有序的序列

3. A

插入之后的树仍旧是二叉搜索树,因此只要是有序的结果则正确,而有序的结果只有A

4. C

 A:错误,当待删除节点的左右子树均存在时,既可以在左子树中找一个最大的节点作为替代节 点,也可以在右子树中找一个最小的节点作为替代节点,左右子树中都可以找替代节点

  B:错误,根据前序遍历和中序遍历,是可以确定一棵树的结构,使用两个遍历结果确定树的结构, 其中有一个遍历结果必须要是中序遍历结果。

  C:正确,二叉搜索树遍历一遍,就可以得到一个有序序列,因此,时间复杂度为O(N)

  D:错误,这里面还需要牵扯到旋转等其他操作,时间复杂度不是线性的


5. 完整代码:

#pragma once

#include <iostream>
#include <algorithm>
using namespace std;

template<class K>
class BSTreeNode
{
public:
	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}

	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};

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

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

		Node* prev = nullptr;
		Node* cur = _root;
		while (cur != nullptr) // 找到要插入的位置
		{
			if (key < cur->_key) // 要插入的值比当前值小
			{
				prev = cur; // 记录cur,等下cur更新就是cur的父亲
				cur = cur->_left; // 到左边插入
			}
			else if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				return false; // 相等,插入失败
			}
		}

		cur = new Node(key); // 走到这,cur就是要插入的位置
		if (key < prev->_key) // 如果key比cur的父亲小
		{
			prev->_left = cur; // 插入到父亲的左孩子
		}
		else
		{
			prev->_right = cur;
		}
		return true;
	}

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

	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* father = nullptr;
		while (cur) // 找到要删除的结点
		{
			if (key < cur->_key)
			{
				father = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				father = cur;
				cur = cur->_right;
			}
			else // 找到后开始删除,分三种情况
			{
				if (cur->_left == nullptr) // ①该结点无左孩子
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_right;
						}
						else //(cur == father->_right)
						{
							father->_right = cur->_right;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //  ②该结点无右孩子
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == father->_left)
						{
							father->_left = cur->_left;
						}
						else //(cur == father->_left)
						{
							father->_right = cur->_left;
						}
					}
					delete cur;
					cur = nullptr;
				}
				else // ③有两个结点,替换法删除
				{
					Node* MinNode = cur->_right;
					Node* MinParNode = cur;
					while (MinNode->_left) // 找右子树的最小
					{
						MinParNode = MinNode;
						MinNode = MinNode->_left;
					}
					swap(cur->_key, MinNode->_key); // 找到后交换

					if(MinParNode->_right == MinNode) // 链接父亲结点,这步易漏
					{
						MinParNode->_right = MinNode->_right;
					}
					else
					{
						MinParNode->_left = MinNode->_right;
					}
					delete MinNode;
					MinNode = nullptr;
				}
				return true;
			}
		}
		return false;
	}

	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);
	}

	~BSTree()
	{
		_Destory(_root);
	}

	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}

	//BSTree() // 这样写也行,但是下面是C++11的用法
	//{}
	BSTree() = default; // C++11的关键字,强制编译器生成默认的构造

	BSTree<K> operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

protected:
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_key); // 中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

	void _Destory(Node*& root) // 加引用下面的置空就起作用了
	{
		if (root == nullptr)
		{
			return;
		}
		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
		root = nullptr;
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (key < root->_key)
		{
			_FindR(root->_left, key);
		}
		else if (key > root->_key)
		{
			_FindR(root->_right, key);
		}
		else
		{
			return true;
		}
	}

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

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

	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* MinNode = root->_right;
				while (MinNode->_left)
				{
					MinNode = MinNode->_left;
				}
				swap(root->_key, MinNode->_key);
				//return EraseR(key);  错的
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;
		}
	}

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

	Node* _root = nullptr;
};

//二叉搜索树的KV结构
namespace KeyValue
{
	//定义两个类模板参数K、V
	template<class K, class V>
	class BSTreeNode
	{
	public:
		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}

		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key; //存放了两个类型的数据,相比较与K模型
		V _value;
	};

	//同样的,定义两个类模板参数K、V
	//搜索二叉树依旧是按照K的数据进行排序,和V无关
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				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, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}

			return true;
		}

		//查找只和数据_key有关,与数据_value无关
		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;
		}

		//删除只和数据_key有关,与数据_value无关
		bool Erase(const K& key)
		{
			//... 和K模型一样
			return true;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
	protected:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}

		Node* _root = nullptr;
	};

	void TestBSTree1()
	{
		BSTree<string, string> dict; // 字典树,如果所有单词都在里面就能很准确查找
		dict.Insert("sort", "排序");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");
		dict.Insert("string", "字符串");
		dict.Insert("insert", "插入");
		string str;
		while (cin >> str) // Crtl+z+换行结束,或者Crtl+c结束
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << "对应的中文:" << ret->_value << endl;
			}
			else
			{
				cout << "对应的中文->无此单词" << endl;
			}
		}
	}

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

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

		countTree.InOrder();
	}
}
#include "BinarySearchTree.h"

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

	t.Erase(8);
	t.Erase(3);
	t.InOrder();
	for (const auto& e : arr)
	{
		t.Erase(e);
	}
	t.InOrder();
}

void TestBSTree2()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.EraseR(8);
	t.EraseR(3);
	t.EraseR(2);
	t.InOrder();
	cout << t.Find(10) << endl;
	cout << t.Find(100) << endl;
	cout << t.FindR(10) << endl;
	cout << t.FindR(100) << endl;
	for (const auto& e : arr)
	{
		t.EraseR(e);
	}
	t.InOrder();
}

void TestBSTree3()
{
	BSTree<int> t;
	int arr[] = { 8, 3, 1, 10, 2, 2, 3, 6, 4, 7, 14, 13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();

	BSTree<int> t2;
	t2.Insert(3);
	t2.Insert(5);
	t2.Insert(4);
	copy = t2;
	t2.InOrder();
	copy.InOrder();
}

int main()
{
	//TestBSTree3();
	KeyValue::TestBSTree2();

	return 0;
}

本篇完。

下一部分:树的OJ题,然后是map和set,再然后是AVL树和红黑树。

穿越回来复习顺便贴个下篇链接:

从C语言到C++_25(树的十道OJ题)力扣:606+102+107+236+426+105+106+144+94+145-CSDN博客

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值