二叉搜索树

127 阅读7分钟

特征

( 1 ) 左子树上所有结点的值,都小于根;右子树上所有结点的值,都大于根

( 2 ) 同时它的左右子树也满足上述性质

image.png

( 3 ) 默认的二叉搜索树不允许键值冗余(不能有相同的值).

( 4 ) 中序遍历的结果是升序.

代码实现

--成员属性

二叉搜索树中仅保存根结点指针.

	template<class K>
  //结点类型
	struct BinarySearchTreeNode
	{
		BinarySearchTreeNode(const K& key = K())
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
		{}

		typedef BinarySearchTreeNode<K> BSTreeNode;
		BSTreeNode* _left;
		BSTreeNode* _right;
		K _key;
	};
        
  //二叉搜索树
	template<class K>
	class BinarySearchTree
	{
		typedef BinarySearchTreeNode<K> Node;
	private:
		Node* _root;
   }

--查找

从根开始找:

若要查找的key比当前结点的键值大,说明在当前结点的右子树中;

若key比当前结点的键值小,说明在当前结点的左子树中.

		bool find(const K& key)
		{
			Node* cur = _root;
			while (cur != nullptr)
			{
				if (key == cur->_key)
					return true;
				else if (key < cur->_key)
					cur = cur->_left;
				else if (key > cur->_key)
					cur = cur->_right;
			}
			return false;
		}
//递归实现,内部封装_findR,在类外不需要传_root
public:
bool findR(const K& key)
{
	return _findR(_root, key);
}
private:
bool _findR(Node* root, const K& key)
{
		if (root == nullptr) return false;

		if (root->_key == key)
			return true;
		else if (key > root->_key)//去右子树找
			return _findR(root->_right, key);
		else//去左子树找
			return _findR(root->_left, key);
}

--插入

找到 插入位置 和 插入位置的父结点,进行链接

	bool insert(const K& key)
	{
		Node* newNode = new Node(key);

		//空树
		if (_root == nullptr)
		{
			_root = newNode;
			return true;
		}

		//找到插入位置
		Node* cur = _root;
		//记录插入位置的父亲结点
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			//如果搜索树有该key,插入失败
			//否则按搜索树的规则向后找插入位置
			if (key == cur->_key)
				return false;
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
		}

		//正式插入,比父亲结点大就插入到右边,否则插入到左边
		if (key > parent->_key)
			parent->_right = newNode;
		else if (key < parent->_key)
			parent->_left = newNode;
		return true;
	}
//如果形参是Node*,root就是局部变量,root的修改不会影响二叉搜索树
//如果改成Node*&, root就是 上一层父亲结点的孩子指针 别名
bool _insertR(Node*& root, const K& key)
{
	//找到插入位置
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}

	if (key > root->_key)//如果key比根大,往右子树插入
		return _insertR(root->_right, key);
	else if (key < root->_key)//key比根小,往左子树插入
		return _insertR(root->_left, key);
	else
		return false;
}

--删除

( 1 ) 找到要删除的结点

( 2 ) 待删除结点的情况:

image.png

image.png

//1 要删除的结点只有1/0个孩子
if (cur->_left == nullptr)//如果删除结点的左孩子为空,就把它的右孩子托付给parent
{
	//注意_root可能就是要删除的结点,此时要修改_root
	if (cur == _root)
            _root = cur->_right;
	else if (parent->_left == cur)
            parent->_left = cur->_right;
	else if (parent->_right == cur)
            parent->_right = cur->_right;
	delete cur;
}
else if (cur->_right == nullptr)//如果删除结点的右孩子为空,就把它的左孩子托付给parent
{
	if (_root == cur)
		_root = cur->_left;
	else if (parent->_left == cur)
		parent->_left = cur->_left;
	else if (parent->_right == cur)
		parent->_right = cur->_left;
	delete cur;
}

若删除结点有两个孩子:

使用替换法删除 —— 在删除结点的子树中找可以顶替该位置的值,

把该值赋值给删除结点,然后删除掉被替换的结点.

可以用删除结点的左子树最大结点的值替换,或者用删除结点的右子树最小结点的值替换.

被替换结点要么没有孩子结点,要么只有一个孩子结点,可以直接删除

//要删除的结点有两个孩子(替换法)

//用cur的左子树最大结点 或者 cur的右子树最小结点 可以替换删除
	Node* max = cur->_left;
	Node* maxParent = cur;
	//找到替换结点和它的父亲结点
	while (max->_right != nullptr)
	{
		maxParent = max;
		max = max->_right;
	}

  //把替换结点的键值交给cur
	cur->_key = max->_key;
	//删除替换结点max,max可能有左孩子
	if (maxParent->_left == max)
		maxParent->_left = max->_left;
	else
		maxParent->_right = max->_left;
	delete max;

完整代码

	bool erase(const K& key)
	{
		// 1 找到要删除的结点
		Node* cur = _root;
		Node* parent = nullptr;

		while (cur != nullptr)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else//成功找到要删除的结点,开始删除
			{
				//1 要删除的结点只有1/0个孩子
				if (cur->_left == nullptr)//如果删除结点的左孩子为空,就把它的右孩子托付给parent
				{
					//注意_root可能就是要删除的结点,此时要修改_root
					if (cur == _root)
						_root = cur->_right;
					else if (parent->_left == cur)
						parent->_left = cur->_right;
					else if (parent->_right == cur)
						parent->_right = cur->_right;
					delete cur;
				}
				else if (cur->_right == nullptr)//如果删除结点的右孩子为空,就把它的左孩子托付给parent
				{
					if (_root == cur)
						_root = cur->_left;
					else if (parent->_left == cur)
						parent->_left = cur->_left;
					else if (parent->_right == cur)
						parent->_right = cur->_left;
					delete cur;
				}
				else//要删除的结点有两个孩子(替换法)
				{
					//用cur的左子树最大结点 或者 cur的右子树最小结点 可以替换删除
					Node* max = cur->_left;
					Node* maxParent = cur;
					//找到替换结点和它的父亲结点
					while (max->_right != nullptr)
					{
						maxParent = max;
						max = max->_right;
					}

					//把替换结点的键值交给cur
					cur->_key = max->_key;
					//删除替换结点
					if (maxParent->_left == max)
						maxParent->_left = max->_left;
					else
						maxParent->_right = max->_left;
					delete max;
				}
				return true;
			}
		}
			
		//没有找到要删除的结点,删除失败
		return false;
	}
bool _eraseR(Node*& root, const K& key)
{
	if (root == nullptr) return false;

	if (key > root->_key)
		return _eraseR(root->_right, key);
	else if (key < root->_key)
		return _eraseR(root->_left, key);
	//开始删除
	//1 右为空
  //2 左为空
	//3 左右都不为空
	Node* del = root;//保存要删除的结点
	if (root->_left == nullptr)
	{
		root = root->_right;//这里的root已经是 上一层父结点的孩子指针 别名
		delete del;
	}
	else if (root->_right == nullptr)
	{
		root = root->_left;
		delete del;
	}
	else//root有两个子结点,用替换法删除
	{//用删除结点root的左子树的最大结点来替换删除
		Node* max = root->_left;
		Node* maxParent = root;
		while (max->_right != nullptr)
		{
			maxParent = max;
			max = max->_right;
		}
		root->_key = max->_key;
		if (maxParent->_left == max)
			maxParent->_left = max->_left;
		else
			maxParent->_right = max->_left;
		delete max;
	}
	return true;
}

--插入删除测试代码

            //中序遍历二叉树递归实现
		void InOrder()const
		{
			_InOrderR(_root);
			cout << endl;
		}
                
		void _InOrderR(Node* root)const
		{
			if (root == nullptr) return;

			_InOrderR(root->_left);
			cout << root->_key << " ";
			_InOrderR(root->_right);
		}                
void testMyBSTreeInsertErase()
{
	BinarySearchTree<int> bsTree;
	
	//插入删除
	bsTree.insert(2);
	bsTree.insert(1);
	bsTree.insert(3);
	bsTree.insert(6);
	bsTree.insert(5);
	bsTree.insert(4);
	bsTree.InOrder();
	for (int i = 0; i < 6; ++i)
	{
		bsTree.erase(i+1);
		bsTree.InOrder();
	}

	//递归版本
	BinarySearchTree<int> bsTreeR;
	//插入删除
	bsTreeR.insertR(2);
	bsTreeR.insertR(1);
	bsTreeR.insertR(3);
	bsTreeR.insertR(6);
	bsTreeR.insertR(5);
	bsTreeR.insertR(4);
	bsTreeR.InOrder();
	for (int i = 0; i < 6; ++i)
	{
		bsTreeR.eraseR(i + 1);
		bsTreeR.InOrder();
	}
}

image.png

--拷贝构造函数与赋值运算符重载

		BinarySearchTree(const BinarySearchTree& copy)
		{
			_root = _copy(copy._root);
		}
                
		//拷贝root这棵树,返回拷贝出来的树的根
		Node* _copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;
			//先拷贝根,再递归拷贝左子树、右子树
			Node* copyRoot = new Node(root->_key);
			Node* copyLeft = _copy(root->_left);
			Node* copyRight = _copy(root->_right);

			//链接
			copyRoot->_left = copyLeft;
			copyRoot->_right = copyRight;
			return copyRoot;
		}
                
		//赋值运算符重载,复用拷贝构造
		//copy是形参,出了作用域会销毁
		//把this的_root交换给copy,让copy帮忙销毁原空间
		//同时拿到copy的拷贝完成的树.
		BinarySearchTree& operator=(BinarySearchTree copy)
		{
			swap(copy._root, _root);
			return *this;
		}                

--析构函数

		~BinarySearchTree()
		{
			_destory(_root);
		}
                
		void _destory(Node* root)
		{
			if (root == nullptr)
				return;

			//后序遍历
			_destory(root->_left);
			_destory(root->_right);
			delete root;
		}

Key模型与KeyValue模型

Key的搜索模型:用来判断关键字key在不在,二叉搜索树中key默认不可修改.

上述二叉搜索树的实现就是Key的搜索模型.

例:检查一篇英文文档中的单词拼写是否正确.

( 1 ) 将词库里的单词,全部插入到一棵二叉搜索树中(字符串可以比较大小)

( 2 ) 依次检查每个单词,是否在该二叉搜索树中.


KeyValue的搜索模型:通过key去找value,

搜索树中key默认不可修改,但是value可以修改.

即将value绑定在key中,但是二叉搜索树内的比较和查找,都只看key,

找到key,就相当于找到了对应的value,同时可以借此修改value

例:字符串中,统计字符出现次数

		//KeyValue模型 find 返回 结点的指针,可以用来修改对应的value
		Node* find(const K& key)
		{
			Node* cur = _root;
			while (cur != nullptr)
			{
				if (key == cur->_key)
					return cur;
				else if (key < cur->_key)
					cur = cur->_left;
				else if (key > cur->_key)
					cur = cur->_right;
			}
			return nullptr;
		}
                
		void InOrder()const
		{
			_InOrderR(_root);
			cout << endl;
		}
		void _InOrderR(Node* root)const
		{
			if (root == nullptr) return;

			_InOrderR(root->_left);
			cout << root->_key << "->"<< root->_val <<endl;
			_InOrderR(root->_right);
		}                
void testKV()
{
	string str = "aazzccddd";
	yh::BinarySearchTree<char, int> countChar;
	for (auto& x : str)
	{
		yh::BinarySearchTreeNode<char, int>* exist = countChar.find(x);
		//如果该字符存在,
		if (exist != nullptr)
			++exist->_val;
		else//否则插入字符,同时字符绑定的value设为1
			countChar.insert(x, 1);
	}

	countChar.InOrder();
}

image.png

普通二叉搜索树的缺陷

普通的二叉搜索树,它查找的时间复杂度是O(h),h是树的高度.

即它查找的效率,与它的高度相关.

在极端情况下,会一直往 左子树/右子树 插入,查找效率就会大大降低.

最坏情况下,h = N.

image.png

因此,二叉搜索树需要改进,改进的方案就是 平衡树.

平衡树有 红黑树、AVL树等:

每次插入结点或删除结点时,都会按一定的规则,判断是否需要旋转来保持搜索树的平衡,

使树的高度基本保持在 logN.