二叉搜索树

概念

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

· 若它的左子树不为空,则左子树上所有结点的值都小于根结点的值。
· 若它的右子树不为空,则右子树上所有结点的值都大于根结点的值。
· 它的左右子树也分别是二叉搜索树。
如下所示,是一颗二叉搜索树
请添加图片描述

  • 特点:
    二叉搜索树以中序遍历得到升序序列。

二叉搜索树实现

  • 构造函数:
    构造空树即可。
//构造函数
BSTree()
	:_root(nullptr)
{}
  • 拷贝构造:递归拷贝
    利用递归,去构造树的左右子树,每次返回当前节点,就能链接起来左右子树。
//拷贝树
Node* _Copy(Node* root)
{
	if (root == nullptr) //空树直接返回
		return nullptr;
	// 依次拷贝左右孩子
	Node* copyNode = new Node(root->_key); //拷贝根结点
	copyNode->_left = _Copy(root->_left); //拷贝左子树
	copyNode->_right = _Copy(root->_right); //拷贝右子树
	return copyNode; //返回拷贝的树
}
//拷贝构造函数
BSTree(const BSTree<K>& t)
{
	_root = _Copy(t._root); //拷贝t对象的二叉搜索树
}
  • 赋值运算符重载:
    **现代写法:**拿来直接用
//现代写法
BSTree<K>& operator=(BSTree<K> t) //编译器接收右值的时候自动调用拷贝构造函数
{
	swap(_root, t._root); //交换这两个对象的二叉搜索树
	return *this; //支持连续赋值
}

  • 析构:
    递归地去释放
//释放树中结点
	void _Destory(Node* root)
	{
		if (root == nullptr) //空树无需释放
			return;

		_Destory(root->_left); //释放左子树中的结点
		_Destory(root->_right); //释放右子树中的结点
		delete root; //释放根结点
	}
	//析构函数
	~BSTree()
	{
		_Destory(_root); //释放二叉搜索树中的结点
		_root = nullptr; //及时置空
	}
  • 插入:非递归方式
  1. 如果是空树,直接插入节点作为二叉搜索树根节点。
  2. 如果不是空树,按照二叉搜索树性质进行节点插入。
  3. 依次把key和当前待插入节点做比较,待插入节点k较大,则插入到右子树,反之去遍历左子树,找到空位置就停下,说明到了可插位置。如果待插入节点值等于根节点值,则插入失败,因为已经存在。
  4. 来到空位置时,需要判断当前在父节点左边还是右边。

做法:

  1. 二叉树需要链接,所以需要父亲节点和当前cur节点,父亲初始置null,cur置root。
  2. 如果是根节点,直接创建新节点,然后返回true。
  3. 根据二叉搜索树性质,更新父亲和cur节点。
  4. 如果未经过返回说明到了合适位置,判断cur需要位于parent的左还是右。
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->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
				return false;
		}

		// 若到此,说明cur到了空位置,可以插入,但不知道在父节点左还是右
		cur = new Node(key, value);
		// 对比看父的值和当前插入值比较 决定插在哪边 
		if (parent->_key < cur->_key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}
  • 查找:非递归查找
    用cur即可,根据BST的性质,key大,就找右,key小找左,返回值类型是Node*,相等时直接返回cur即可。
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;
	}
  • 删除:
    非递归方式删除
    BST删除分为三种情况:
  1. 待删除结点的左子树为空。待删除结点左右子树均为空包含在内
  2. 待删除结点的右子树为空。
  3. 待删除结点的左右子树均不为空。

做法:

  1. 需要使用两个指针parent、cur,因为有链接关系。
  2. 更新parent和cur到合适位置。

情况1:左子树为空,先判断待删节点是父亲的左儿子还是右儿子,然后让父亲直接链接到该节点的右子树。

情况2:右子树为空,先判断待删节点是父亲的左儿子还是右儿子,然后让父亲直接链接到该节点的左子树。

情况3:左右均不为空,寻找右子树的最小值,我们直接把右子树最小值位置删掉,让当前节点值为右子树最小节点。
然而这样的做法也有两种情况,如下两种:请添加图片描述

  1. 右子树最小节点值,是右子树的左孩子
  2. 右子树最小节点值是单支的。
bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		// 先找父和它
		while (cur)
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			// 至此,找到了父亲和它:
			else
			{
				// 删除节点的左子树为空
				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;
					return true;
					// 每一分支需这两,因需停止.
				}
				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;
					return true;
					// 每一节分支都需这两,因需停止.
				}
				else   // 左右孩子都存在:找右子树中值最小的右节点,也就是右子树最左孩子
					// 让当前节点位置存最小右孩子,然后删除那个最小右孩子
				{
					Node* minParent = cur;	// 记录要删节点
					Node* minRight = cur->_right;	// 当前节点左右孩子都有,找右孩子最小,先确定当前右孩子
					// 寻找根节点右子树值最小节点
					while(minRight->_left)
					{
						// 一直往左走
						minParent = minRight;
						minRight = minRight->_left;
					}
					// 修改当前值,相当于删掉了当前节点
					cur->_key = minRight->_key;	// 不管右孩子有没有左孩子,这样都是对的。
					
					// 如果被删节点右孩子有树结构,经过循环,minP->left == minR 直接略过minR
					if (minRight == minParent->_left)
					{
						// minR可能有右孩子或没有右孩子,但是左边孩子一定没有了。
						minParent->_left = minRight->_right;
					}
					// 要删节点的右孩子就一个 或它的右孩子没有左孩子:是个部分右单支结构
					else
					{
						minParent->_right = minRight->_right;	// 直接链接右孩子的右指,当前cur已经变成原来的cur->right
					}
					delete minRight;
					return true;
				}
			}
		}
		return false;
	}
  • 遍历:(中序最经典的应用)
    遍历我们防止root节点暴露,所以直接用_Inorder来访问,然后外部只是调用_Inorder

  • 代码:

#include<iostream>
using namespace std;

//结点类
template<class K, class V>
struct BSTreeNode
{
	K _key;                 //结点值
	V _val;					
	BSTreeNode<K, V>* _left;   //左指针 
	BSTreeNode<K, V>* _right;  //右指针

	//构造函数
	BSTreeNode(const K& key = 0, const V& val = 0)
		:_key(key)
		,_val(val)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

template<class K, class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;
public:
	BSTree()
		:_root(nullptr) {};

	// 递归拷贝BST:每个递归,只创建一个节点,且返回,它的左右节点,都递归地链接起来。
	Node* _Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* copyNode = new Node();
		copyNode->_left = _Copy(root->_left);
		copyNode->_right = _Copy(root->_right);
		return copyNode;
	}

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

	/*
	插入:插入时刻需要两个节点 父节点和子节点
	找个合适位置,小于往左走 大于往右,等于就停止
	
	
	*/ 
	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->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
				return false;
		}

		// 若到此,说明cur到了空位置,可以插入,但不知道在父节点左还是右
		cur = new Node(key, value);
		// 对比看父的值和当前插入值比较 决定插在哪边 
		if (parent->_key < cur->_key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}

	/*
	find:
	查找:大了给左边 小了给右边
	
	*/
	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. 有左孩子无右

	3. 有右孩子无左

	4. 左右孩子都有
	
	*/
	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		// 先找父和它
		while (cur)
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			// 至此,找到了父亲和它:
			else
			{
				// 删除节点的左子树为空
				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;
					return true;
					// 每一分支需这两,因需停止.
				}
				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;
					return true;
					// 每一节分支都需这两,因需停止.
				}
				else   // 左右孩子都存在:找右子树中值最小的右节点,也就是右子树最左孩子
					// 让当前节点位置存最小右孩子,然后删除那个最小右孩子
				{
					Node* minParent = cur;	// 记录要删节点
					Node* minRight = cur->_right;	// 当前节点左右孩子都有,找右孩子最小,先确定当前右孩子
					// 寻找根节点右子树值最小节点
					while(minRight->_left)
					{
						// 一直往左走
						minParent = minRight;
						minRight = minRight->_left;
					}
					// 修改当前值,相当于删掉了当前节点
					cur->_key = minRight->_key;	// 不管右孩子有没有左孩子,这样都是对的。
					
					// 如果被删节点右孩子有树结构,经过循环,minP->left == minR 直接略过minR
					if (minRight == minParent->_left)
					{
						// minR可能有右孩子或没有右孩子,但是左边孩子一定没有了。
						minParent->_left = minRight->_right;
					}
					// 要删节点的右孩子就一个 或它的右孩子没有左孩子:是个部分右单支结构
					else
					{
						minParent->_right = minRight->_right;	// 直接链接右孩子的右指,当前cur已经变成原来的cur->right
					}
					delete minRight;
					return true;
				}
			}
		}
		return false;
	}

	// 为了不暴露root节点 
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key<<" ";
		_InOrder(root->_right);
	}

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

public:
	Node* _root;
};

void TestBSTree()
{
	/*BSTree<string, string> dict;
	dict.Insert("insert", "插入");
	dict.Insert("erase", "删除");
	dict.Insert("left", "左边");
	dict.Insert("string", "字符串");

	string str;
	while (cin >> str)
	{
		auto ret = dict.Find(str);
		if (ret)
		{
			cout << str << ":" << ret->_val << endl;
		}
		else
		{
			cout << "单词拼写错误" << endl;
		}
	}*/

	string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
	// 统计水果出现的次
	BSTree<string, int> countTree;
	for (auto str : strs)
	{
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_val++;
		}
	}
	countTree.InOrder();
}

调用:

#include"l5BST.h"

int main()
{
    BSTree<int, int> bst;
    bst.Insert(3, 1);
    bst.Insert(6, 1);
    bst.Insert(1, 1);
    bst.Insert(2, 1);

    bst.InOrder();
    cout << "删除 3" << endl;
    bst.Erase(3);
    bst.InOrder();
    cout << "字典BST" << endl;
    TestBSTree();
    return 0;
}

性能分析

对于一棵树,插入还是删除,都需要先查找,查找效率影响着其它的效率。
对于n个节点的二叉搜索树,
最优情况:二叉搜索树为完全二叉树, 我们比较树高次,logN的复杂度即可。
最差情况:二叉搜索树是单支,比较次数是N。

扩展:
B树和B+树是查找存储在磁盘当中的数据时经常用到的数据结构,B树系列对树的高度提出了更高的要求,普通二叉树不能满足要求,为降低树的高度,需要使用多叉树,而多叉树其实都是由二叉搜索树演变出来的,它们各有各的特点,适用于不同的场景。

知识点注意:

  1. 二叉搜索树一定是logN,错误,因为单支时退化为O(N)。
  2. 给定BST,根据节点值大小排序需要时间复杂度是线性的,因为中序是线性的。
  3. 给定一棵BST,线性时间内转为平衡二叉树,错误,因为经常涉及旋转等操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值