二叉搜索树

一.二叉搜索树的概念

二叉搜索树/搜索二叉树,也叫做二叉排序树,它或是一颗空树,或是具有以下性质的二叉树:

1、若它的左子树不为空,那它左子树上的所有节点的值都小于等于根节点的值;

2、若它的右子树不为空,那它右子树上的所有节点的值都大于等于根节点的值;

3、它的左右子树也是二叉搜索树;

4、二叉搜索树既可以插入相同的值,也可以不插入相同的值,具体看场景,map/set/multimap/multiset的底层就是二叉搜索树,其中map/set不支持插入相同的值,multimap/multiset支持插入相同的值。

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

对于二叉搜索树的插入和删除来说,我们都要先要进行查找。所以遍历二叉搜索树的效率就非常重要了。

对于最好的情况来说,二叉搜索树是一个完全二叉树(近似于完全二叉树),假设有N的节点,那么它的高度为log(N),所以遍历这棵树的时间复杂度为O(log(N));

对于最坏情况来说,二叉搜索树类似于一颗单边树,它的节点全部都在树的一侧分布,此时它的结构就类似于链表的结构了,假设有N个节点,那么它的高度就是N,所以遍历这个树的时间复杂度为O(N).

所以综合来看,查找二叉搜索树节点的时间复杂度为O(N)。

三.二叉搜索树的结构

二叉搜索树是由一个一个节点组成的,所以我们首先要有一个节点类,类中包含了节点存储的数据_key,以及指向左孩子和右孩子的指针。

二叉搜索树这个类我们可以只使用一个根节点的指针来维护。

//二叉搜索树节点
template <typename K>
struct bstreeNode
{
	K _key;
	bstreeNode<K>* _left;
	bstreeNode<K>* right;

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

//二叉搜索树
template <typename K>
class bstree
{
	typedef bstreeNode<K> node

private:
	node* _root = nullptr;
};

四.插入 

1、树为空,直接新增节点,让root指向这个新增节点即可。

2、树不为空,以此与节点进行比较,如果插入值大于该结点的值就向右走,如果插入值小于该节点的值就像左走,直到走到空时,插入到这个位置。

3、含重复值,与第二点逻辑类似,只不过遇到相等的值是规定一个统一的移动方向,不要一会左一会右。

我们需要用一个cur变量来遍历二叉搜索树,用它找到位置后,我们要想把它链到树上的话,还需要这个位置的父亲节点,所以我们还要创建一个父亲节点,用来记录cur的父节点。 

链接的时候不可以直接连接,因为我们不知道把新节点连接到parent的左边还是右边,所以我们先要进行判断,然后再进行连接。

bool Insert(const K& key)
{
	//树为空
	if (_root == nullptr)
	{
		_root = new node(key);
		return true;
	}
	else//树不为空
	{
		node* cur = _root;
		node* parent = nullptr;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		//创建新节点,判断cur位于parent的那边
		node* newnode = new node(key);
		if (parent->_key > newnode->_key)
		{
			parent->_left = newnode;
		}
		else
		{
			parent->_right = newnode;
		}
		return true;
	}
}

五.中序遍历二叉搜索树

我们仔细观察这个二叉树的构造,如果采取中序遍历的话,结果就是有序的。

中序遍历的规则是 左子树-根-右子树

void _Inorder(const node* root)
{
	if (root == nullptr)
	{
		return;
	}

	_Inorder(root->_left);
	cout << root->_key << " ";
	_Inorder(root->_right);
}

如果我们将中序遍历定义为公有的话,我们有两种方法:

1、不传参数,直接使用this指针,但是这种方式的话,我们无法使用递归来遍历二叉树:首先,我们不可以传参数,其次, 我们无法调用该函数,因为该函数是二叉树的行为,而不是节点的行为,_root->left是一个节点,无法调用该函数

void _Inorder()
{
	if (_root == nullptr)
	{
		return;
	}

	_root->left->_Inorder();
	cout << _root->_key << " ";
	_root->_right->_Inorder();
}

2、传参数,但是要穿根节点,而根节点是私有成员变量,如果公开的话,会破坏类的封装性。

所以将该函数公有是不合理的,所以我们选择将其私有,但是私有的话,也要传参数,并且类外无法访问,那要如何实现呢? 

我们可以定义一个外壳程序,将他公有,他可以调用该遍历函数,而且也可以使用root,直接传给该函数即可。

public:
    void Inorder()
    {
	    _Inorder(_root);
    }

private:
    void _Inorder(const node* root)
    {
	    if (root == nullptr)
	    {
		    return;
	    }

	    _Inorder(root->_left);
	    cout << root->_key << " ";
	    _Inorder(root->_right);
    }

六.查找

查找的逻辑与插入的逻辑类似,也是一个一个的比较,大于根节点就向右走,小于就向左走,等于就找到了,如果走到了空,就说明这棵树中没有该节点,返回false即可。

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

七.删除

删除指定元素,首先我们要先查找该元素是否存在,如果不存在直接返回false。

bool Erase(const K& key)
{
    node* cur = _root;
    while (cur)
    {
	    if (key > cur->_key)
	    {
		    cur = cur->_right;
	    }
	    else if (key < cur->_key)
	    {
		    cur = cur->_left;
	    }
	    else
	    {
            //删除操作
		    return true;
	    }
     }
    return false;
}

1、如果删除的是叶子节点的话,直接删除该节点,然后让指向该节点的指针指向空

2、如果删除的节点N左孩子为空,那么就让父亲节点的对应指针指向N节点的右孩子,然后删除N节点

3、如果删除的节点N右孩子为空,那么就让父亲节点的对应指针指向N节点的左孩子,然后删除N节点

上面三种情况比较简单,而且第一种情况可以归到第二种/第三种,所以我们没有必要自己专门写一个针对叶子结点的删除

需要注意的是,当上面这三种情况遇到删除的是根节点时,需要特殊处理,直接将不为空的那一支赋值给_root,然后删除cur即可

4、如果删除的节点N左右孩子都不为空,有两种删除方式:

  • 找到节点N的左子树的最右节点,因为删除之后要保证依旧是二叉搜索树,而N节点的左子树中都是比N小的值,而其中最右节点是左子树中最大的,然后将该处的值赋值给N节点,这样,就可以满足N节点的值比左子树的值大,比右子树的值小,然后删除该节点即可。
  • 找到节点N的右子树的最左节点,这个节点比N大,但又是右子树中最小的,所以交换之后依旧可以保证该树依旧是二叉搜索树。

 这里以找左子树的最右节点来分析:

        我们要删除8,所以我们先创建replace用来寻找要替代的元素,因为是左子树的最右节点,所以replace的初始值就是cur->_left,replaceParent = cur。然后replace一直向右走,直到走到空,期间replaceParent也要跟着变化,找到后将replace的值赋给cur,然后删除repalce。

        删除replace时,虽然其右子树为空,但是左子树不一定为空,所以我们不管是否为空,都直接将repalceParent的对应指针指向repalce的左孩子。如果不为空,刚好可以连接上,如果为空,replaceParent刚好指向空。

bool Erase(const K& key)
{
	node* parent = nullptr;
	node* cur = _root;
	while (cur)
	{
		if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			//找到了指定数据
			//此时cur指向的就是待删除的数据,parent就是待删除节点的父亲节点

			if (cur->_left == nullptr)// 左孩子为空 , 叶子节点
			{
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
					delete cur;
					cur = nullptr;
				}
			}
			else if (cur->_right == nullptr)//右孩子为空
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
					delete cur;
					cur = nullptr;
				}
			}
			else//左右孩子都不为空
			{
				//找左子树的最右节点
				node* replace = cur->_left;//最右节点
				node* replaceParent = cur;//最右节点的父亲
				while (replace->_right)
				{
					replaceParent = replace;
					replace = replace->_right;
				}

				cur->_key = replace->_key;//覆盖要删除的数

				//删除replace
				//不论replace是否有左孩子,都直接与replaceParent连接
				if (replaceParent->_left == replace)
				{
					replaceParent->_left = replace->_left;
				}
				else
				{
					replaceParent->_right = replace->_left;
				}
				delete replace;
				replace = nullptr;
			}

			return true;
		}
	}
	return false;
}

八.二叉搜索树的key和key/value使用场景

1.key搜索场景

在二叉树搜索树的节点中,只有key作为关键码,节点中只需要存储key即可。关键码即为需要搜素的值,搜索场景只需要判断key在没在。key的搜索场景实现的二叉搜索树支持增删查,但是不支持修改,修改会破坏搜索树的结构,使其不满足二叉搜索树的性质。

场景1:

        小区的无人值守车库,买了小区车位的业主的车才能进入小区,物业会将业主的车牌录入到后台系统,车辆进入时扫描车牌在不在系统中,在的话就抬杆,不在就不抬杆。

场景2:

        检查一篇英文文章中的单词是否拼写正确,将所有的单词都放入一个二叉搜索树中,读取文章的单词,在二叉树中进行查找,如果没有的话就标红

2.key/value搜索场景

每一个关键码key,都有一个与之对应的value,value可以是任意类型的对象。二叉树的节点除了要存储key还需要存储对应的value。

增删查还是利用key的逻辑,因为查找时只根据关键码进行查找,这样可以快速找到key对应的value。

key/value支持二叉搜索树的修改,但是不是修改key,而是key对应的value。

场景一:

        简单的中英文互译字典,树的节点存储key(英文)和vlaue(中文),搜索时输入英文,找到该英文时,就找到了对应的中文。

场景二:

        商场的无人值守车库,入场时扫描车牌,记录车牌以及进入的时间,出车库时,扫描车牌,在后台中去搜索该车牌,找到对应的入场时间,用现在的时间-入场时间得出停车时长,然后计算出停车费,缴费后抬杆离场。

场景三:

        统计一篇文章中单词的出现次数,读取一个单词,查找单词是否存在,若不存在则说明这个单词第一个出现,count = 1,单词存在,count++.

3.key/value二叉树的实现 

key/value的实现只需要在key的基础上进行修改即可:

节点增加一个模板参数,用来存储value

插入时不仅要插入key还要插入value

删除时当删除的节点左右孩子都在时,需要替换,替换时要将value和key都替换

find函数返回值不再是bool,而应该是指定key的节点指针,如果没有则返回nullptr。

namespace key_value
{
	//二叉搜索树节点
	template <typename K, typename T>
	struct bstreeNode
	{
		K _key;
		T _value;
		bstreeNode<K, T>* _left;
		bstreeNode<K, T>* _right;

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

	//二叉搜索树
	template <typename K, typename T>
	class bstree
	{
		typedef bstreeNode<K,T> node;

	public:

		bool Insert(const K& key,const T& value)
		{
			//树为空
			if (_root == nullptr)
			{
				_root = new node(key,value);
				return true;
			}
			else//树不为空
			{
				node* cur = _root;
				node* parent = nullptr;
				while (cur)
				{
					if (key > cur->_key)
					{
						parent = cur;
						cur = cur->_right;
					}
					else if (key < cur->_key)
					{
						parent = cur;
						cur = cur->_left;
					}
					else
					{
						return false;
					}
				}

				//创建新节点,判断cur位于parent的那边
				node* newnode = new node(key,value);
				if (parent->_key > newnode->_key)
				{
					parent->_left = newnode;
				}
				else
				{
					parent->_right = newnode;
				}
				return true;
			}
		}

		node* find(const K& key)
		{
			node* cur = _root;
			while (cur)
			{
				if (key > cur->_key)
				{
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}
			return nullptr;
		}

		void Inorder()
		{
			_Inorder(_root);
			cout << endl;
		}

		bool Erase(const K& key)
		{
			node* parent = nullptr;
			node* cur = _root;
			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					//找到了指定数据
					//此时cur指向的就是待删除的数据,parent就是待删除节点的父亲节点

					if (cur->_left == nullptr)// 左孩子为空 , 叶子节点
					{
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
							delete cur;
							cur = nullptr;
						}
					}
					else if (cur->_right == nullptr)//右孩子为空
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
							delete cur;
							cur = nullptr;
						}
					}
					else//左右孩子都不为空
					{
						//找左子树的最右节点
						node* replace = cur->_left;//最右节点
						node* replaceParent = cur;//最右节点的父亲
						while (replace->_right)
						{
							replaceParent = replace;
							replace = replace->_right;
						}

						//覆盖要删除的数
						cur->_key = replace->_key;
						cur->_value = replace->_value;

						//删除replace
						//不论replace是否有左孩子,都直接与replaceParent连接
						if (replaceParent->_left == replace)
						{
							replaceParent->_left = replace->_left;
						}
						else
						{
							replaceParent->_right = replace->_left;
						}
						delete replace;
						replace = nullptr;
					}

					return true;
				}
			}
			return false;
		}

	private:
		void _Inorder(const node* root)
		{
			if (root == nullptr)
			{
				return;
			}

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

		node* _root = nullptr;
	};
}

3.1key/value的析构

我们在删除树的节点时要递归着删除,根据后序遍历的顺序进行删除,这里遇到的问题与中序遍历打印该树的节点是一致的,所以我们也采取一个外壳程序来调用真正的销毁。

public:
    ~bstree()
    {
	    Destroy(_root);
    }

private:
    void Destroy(const node* root)
    {
	    if (root == nullptr)
	    {
		    return;
	    }

	    Destroy(root->_left);
	    Destroy(root->_right);

	    delete root;
	    root = nullptr;
    }

3.2key/value的拷贝构造

默认生成的拷贝构造会进行浅拷贝,浅拷贝在析构时就会出现问题,对同一块空间进行了多次的释放,所以我们要显示的实现拷贝构造。

拷贝构造依旧得借助递归来实现,要根据前序遍历的顺序将节点一个一个的复制出来.

需要注意的是,当我们实现了拷贝构造后,编译器就不会再自己生成默认构造了,所以我们要强制生成一个默认构造:

bstree() = default;//C++11强制生成默认构造

public:
    bstree() = default;

    bstree(const bstree& t)
    {
	    _root = Copy(t._root);
    }

private:
    node* Copy(const node* root)
    {
	    if (root == nullptr)
	    {
		    return nullptr;
	    }

	    node* newroot = new node(root->_key, root->_value);
	    newroot->_left = Copy(root->_left);
	    newroot->_right = Copy(root->_right);

	    return newroot;
    }

3.3key/value的赋值运算符重载

赋值运算符重载我们使用现代写法即可,因为这是交换的是指针,所以我们不必要自己写一个交换函数来避免深拷贝。

bstree& operator=(bstree t)
{
	std::swap(_root,t._root);

	return *this;
}

完!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值