C++实现二叉搜索树

一、二叉搜索树的定义

一棵二叉树,可以为空,但如果不为空则要满足

①非空左子树的所有键值小于其根节点的键值

②非空右子树的所有键值大于其根节点的键值

③左右子树都是二叉搜索树

④又名排序二叉树,他有个特性是中序遍历结果有序

二、二叉搜索树的实现

需要实现为类模板的格式:

template<class K, class V>
class BSTree{};

他的节点需要包括左右子树和自身的值

template<class K, class V>
struct BSTreeNode
{
	K _key;
	V _value;
	BSTreeNode<K,V>* _left;
	BSTreeNode<K,V>* _right;
}

为了简化节点的名称,实现BSTree的时候typedef一下

typedef BSTreeNode<K, V> Node;

2.1查找

返回值设置为节点指针,可以凭借循环的逻辑直接查找,找不到返回为空

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

2.2插入

插入需要保证符合二叉搜索树无冗余的原则,然后按照查找逻辑走,遇到空即可进行插入

因为查找逻辑只能帮我们找到当前节点的指针,但是插入需要父节点指针来定位,所以我们可以选择用双指针的思想辅助找到父节点

之后我们再插入的时候,可以通过比较来确定插入左还是右

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);
	//没有重复,parent此时指向要插入节点的父节点,cur直接存新节点
	if (key < parent->_key)
	{
		
		parent->_left = cur;
	}
	else
	{
		
		parent->_right = cur;
	}
	return true;
}

2.3中序遍历

这里选择递归实现可以大幅度简化代码

但是我们要递归就要传入根节点,但正常调用的中序遍历是不可能传入作为私有的根节点的,为此我们提供两种解决方案

①实现一个私有的子函数

void _InOrder(Node* root)

来进行递归

②实现一个共有的GetRoot函数来获取_root的值

个人感觉第二个有点不符合使用习惯,这里用第一种方式来实现

void InOrder()
{
	_InOrder(_root);
}


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

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

2.4删除

删除的情况分三种

①叶子节点,没有孩子

②只有一个孩子

③有两个孩子

2.4.1删除逻辑大框架

删除的传参是一个值,所以仍然需要先走查找的逻辑,对树的结构进行修改也仍然需要父节点,所以双指针也是需要的。

在我们查找过程中,只有找到了才要进行删除的逻辑,所以大框架可以得出来

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
		{
			//走删除的逻辑,成功删除的话返回true
			
		}
	}
	return false;//没成功删除
}

2.4.2删除逻辑前两个情况

接下来就要看删除的逻辑了,我们总结一下三种情况的应对方案会发现:因为②需要直接改变父节点指向,使其指向该节点的孩子再delete该节点,而确定该节点的方式是通过左为空或者右为空,所以①的情况属于是该节点两个孩子都为空,走哪一个逻辑都可以达到目标,

所以①可以当作一种特殊的②

此时可以得出(当删除的节点为根节点,会没有父节点使用,需要添加两个逻辑

if (cur->_left == nullptr)
{
				if (parent == nullptr)
				{
					cur = _root;
					_root = _root->_right;
					delete cur;
					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)
				{
					cur = _root;
					_root = _root->_left;
					delete cur;
					return true;
				}
				if (parent->_left == cur)
				{
					parent->_left = cur->_left;
				}
				else
				{
					parent->_right = cur->_left;
				}
				delete cur;
				return true;
}
else
{
				//两边都不为空的删除逻辑

}

2.4.3删除逻辑第三种情况

我们要删除一个有两个孩子的节点,可以根据二叉树特性来找方法

左子树的最右右节点,右子树最左节点其实都可以满足这个节点的要求:

大于所有左子树键值,小于所有右子树键值

因此我们可以用其中之一来替换这个节点,再delete更换后的左子树的最右右节点/右子树最左节点

位置

//此时cur的左右两边都不为空,我们需要找cur右子树的最左来替代
Node* PrightMin = cur;
Node* rightMin = cur->_right;
//找到cur右子树的最左
while (rightMin->_left)
{
				PrightMin = rightMin;
				rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;

//有可能rightMin直接就是最小,此时他其实是父节点的右
if (rightMin == PrightMin->_left)
				PrightMin->_left = rightMin->_right;
else
				PrightMin->_right = rightMin->_right;


delete rightMin;
return true;

2.5析构

搜索二叉树的析构可以考虑使用递归,可以大大减少代码书写量

因为析构函数不能传参,我们可以选择用子函数来解决这一问题

public:
~BSTree()
{
	_Destroy(_root);
	_root = nullptr;
}

private:
void _Destroy(Node* root)
{
	if (root == nullptr)
	{
		return;
	}

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

在递归进行析构的过程中,我们必须要使用后序进行析构,否则可能会出现无法找到子节点的问题导致程序崩溃

2.6拷贝构造

对于二叉搜索树的拷贝构造来说,值拷贝一定不行

因为创建了一个新的二叉树,更改了其中节点的值会影响原来的二叉树

我们必须要显示实现:此处依旧可以考虑递归,但必须前序,否则可能会出现无法确定根节点的问题,显示实现过程中需要深拷贝,我们用new来申请新节点

public:
BSTree(const BSTree<K,V>& bs)
{
	_root = _BSTree(bs._root);
}

private:
Node* _BSTree(Node* root)
{
	if (root == nullptr)
	{
		return nullptr;
	}

	Node* newnode = new Node(root->_key, root->_value);
	_BSTree(root->_left);
	_BSTree(root->_right);
	
	return newnode;
}

三、二叉搜索树的特性

3.1二叉搜索树可以完成的事

①查找

②去重

③排序+去重(受二叉搜索树特性限制,不可重复)

3.2二叉搜索树的增删查时间复杂度

它的增删查时间复杂度是O(N),可既然是树,那么为什么不是O(logN)呢?

原因在于搜索二叉树并无完全二叉树的性质,他是有可能退化为链表之类的格式的,所以最优为logN,但是最差为N/2

但这一缺点其实有补充方案,那就是平衡二叉搜索树(包括AVL树,红黑树)

3.2补:二叉搜索树默认不支持修改,只有它的变种才可能支持

四、当前阶段的搜索方法

4.1暴力查找(循环遍历)

本身效率就比较低,非必要不使用

4.2排序+二分查找

插入与删除的效率比较低(常为O(N)的时间复杂度)

4.3搜索树

二叉搜索树O(N)->平衡二叉搜索树O(log(N))->多叉平衡搜索树(B树系列)

4.4哈希思想

哈希表

五、key模型和key/value模型

5.1二者定义

①key模型:检查在不在,如门禁系统

②key/value模型:通过一个值(key)去找另外一个值(value),如英汉字典,超市的商品价格等

5.2特殊场景中的模型

有时一些特殊场景也可以转化为这两种模型,如

①检查英文小说中是否有拼写错误的单词,可以转化为key模型(搜索树存英文词库,利用平衡二叉搜索树的logN时间复杂来找)

②统计英文小说中单词出现的次数,可以转化为key/value模型(遍历小说单词,key对应单词,value对应次数)

六、多组输入相关问题

我们在进行多组输入的时候,如

string str;
while(cin >> str)
{}

只有ctrl+Z再换行可以结束程序,为什么?(ctrl+C结束属于强制中断程序,暂时不讨论)

我们知道,cin >> str本质上调用的是string中重载的运算符

istream& operator>> (istream& is, string& str);

也就是operator>>(cin,str)

他的返回值类型实际上是istream&,那么问题来了,istream&怎么做条件判断呢?

原来,在istream中有一个重载

explicit operator bool() const;

属于自定义类型重载内置类型,本质上是重载了隐式类型转换,

正常情况下进行强制类型转换需要(bool),我们要重载的是(),但是很明显,()的重载已经被仿函数占用了,所以只能转变写法operator bool()了

而观察这一声明,我们发现没有返回值类型,但实际上他的返回值类型正是bool。

回到之前的问题,要做条件判断需要通过与不通过两种情况,而函数中有四个标志

good(默认),fail,failbit,badbit,默认情况下good会返回true

如果输入了ctrl+Z,会把good替换成fail,标志改变后返回会变成false,即出循环

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值