深入浅出C++ ——二叉搜索树

文章目录

  • 一、二叉搜索树概念
  • 二、二叉搜索树操作
    • 1. 二叉搜索树的查找
    • 2. 二叉搜索树的插入
    • 3. 二叉搜索树的删除
  • 三、二叉搜索树的实现
  • 四、二叉搜索树的性能分析

一、二叉搜索树概念

  二叉搜索树又称二叉排序树/二次查找树,它是一棵空树或者是每颗子树都具有以下性质的二叉树

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树

深入浅出C++ ——二叉搜索树_第1张图片

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

二、二叉搜索树操作

1. 二叉搜索树的查找

  • 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找
  • 最多查找高度次,走到到空,还没找到,这个值不存在。

2. 二叉搜索树的插入

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

3. 二叉搜索树的删除

  1. 左为空,父亲指向他的右。 也就是说左子节点为空,让它父节点指向该节点的右子节点,再直接删除该节点。
  2. 右为空,父亲指向他的左。 也就是说右子节点为空,让它父节点指向该节点的左子节点,再直接删除该节点。
  3. 左右子节点都不为空时,使用替换法删除

  在第一节的例子中,删除1、4、7、13、14、10节点属于前两种情况,删除3、8、10、6节点属于第三种情况。


替换法删除

  找到左子树的最右节点或者右子树的最左节点,替换该节点赋值给删除节点,直接删除替换节点,因为替换节点没子节点或者只有一个子节点,再归类到前两种情况。


修改

   K模型的搜索二叉树不支持修改,增删查的时间复杂度为O(h),h是树的高度,最坏的情况是h为N。


三、二叉搜索树的实现

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。 该种方式在现实生活中非常常见,例如:刷卡进楼。
template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		: _left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};
template<class K> //key
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* parent = nullptr;	//这里父节点指针给nullptr不会报错,因为考虑极端情况只有一个节点,那也会进入下面的循环中,执行parent = cur;,所以父节点->不会报错
		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); 
		if (parent->_key < key)
			parent->_right = cur;
		else
			parent->_left = cur;
		return true;
	}

	bool 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 true;
		}
		return false;
	}

	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						//找到了
			{
				//开始删除
				//1、左为空 ,父亲指向它的右 ,把另一个孩子托管给父亲
				//2、右为空 ,父亲指向它的左
				//3、左右都不为空
				if (cur->_left == nullptr) // 左为空 ,父亲指向它的右
				{
					if (cur == _root)//考虑极端情况删除的是_root根节点的情况
					{
						_root = cur->_right; //左为空,更新根
					}
					else
					{
						if (cur == parent->_left) //判断该节点是父亲的左还是右
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}
					delete cur;
					cur = nullptr;
				}
				else if (cur->_right == nullptr) //右为空 ,父亲指向它的左
				{
					if (cur == _root)//考虑极端情况删除的是_root根节点的情况
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
					delete cur;
					cur = nullptr;
				}
				else//左右都不为空
				{
					//	替换法删除,替换的节点可以是左树的最大节点或者是右树的最小节点
					//	这里采用 找到右树的最小节点进行替换
					Node* minParent = cur; //	考虑极端情况,要删除的是根节点,所以minparent不可以为空
					Node* min = cur->_right;
					while (min->_left)
					{
						minParent = min;
						min = min->_left;
					}
					swap(cur->_key, min->_key);
					if (minParent->_left == min)  //判断minParent和min的关系
					{
						minParent->_left = min->_right;
					}
					else
						minParent->_right = min->_right;

					delete min;
					min = nullptr;
				}
				return true;
			}
		}
		return false;
	}

	void InOrder()//中序遍历打印
	{
		_InOrder(_root);//套用了一层,第一次传过去了_root,后面递归就传他的子树
		cout << endl;
	}

	bool FindR(const K& key)//递归版本的查找
	{
		return _Find(_root, key);
	}

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

	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
	
	BSTree() = default;//C++11 强制编译器生成默认的构造

	~BSTree()
	{
		_Destsory(_root);
	}

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

	// 传值传参
	BSTree<K>& operator = (BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

private:
	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 _Destsory(Node*& root)
	{
		if (root == nullptr)
			return;
		_Destsory(root->_left);
		_Destsory(root->_right);
		delete root;
		root = nullptr;
	}
	//_InOrder(_root)   递归调用不能写这个,不然每次传过去都是根节点,只能第一次传递根节点,用一个形参cur来接收根节点,
	void _InOrder(Node* cur) //递归必须 显示 的传递子树,而在外面调用的时候要传根节点_root,但是拿不到私有的_root。可以写一个getroot函数去拿到_root,或者像这样
	{
		if (cur == nullptr)
			return;
		_InOrder(cur->_left);
		cout << cur->_key << " ";
		_InOrder(cur->_right);
	}

	bool _Find(Node* cur, const K& key)
	{
		if (cur == nullptr)
			return false;

		if (cur->_key < key)
			return _Find(cur->_right, key);

		else if (cur->_key > key)
			return _Find(cur->left, key);

		else
			return true; //return true 按顺序依次从开辟的栈帧返回到最外层
	}

	bool _InsertR(Node*& cur, const K& key)
	{
		if (cur == nullptr)
		{
			cur = new Node(key); //cur是一个局部变量,所以要用引用
			return true;
		}
		if (cur->_key < key)
			return _InsertR(cur->_right, key);

		else if (cur->_key > key)
			return	_InsertR(cur->_right, key);

		else
			return false; //天然去重

	}
	bool _EraseR(Node*& cur, const K& key)
	{
		if (cur == nullptr)
			return false;

		if (cur->_key < key)
			return _EraseR(cur->_right, key);

		else if (cur->_key > key)
			return _EraseR(cur->_left, key);

		else  //删除
		{
			Node* del = cur;
			if (cur->_left == nullptr) //左为空
			{
				cur = cur->_right;     
			}
			else if (cur->_right == nullptr) //右为空
			{
				cur = cur->_left;
			}
			else              //左右都不为空
			{
				//找右树的最左节点
				Node* min = cur->_right;
				while (min->_left)
				{
					min = min->_left;
				}
				swap(cur->_key, min->_key);
				return _EraseR(cur->_right, key); //从这个点的右树开始删除key
			}
			delete del;
			del = nullptr;
			return true;
		}
	}
	Node* _root = nullptr;
};

应用:

int main()
{
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto& e : a)
	{
		t.Insert(e);
	}
	//排序+去重
	t.InOrder(); 
	t.Erase(3);
	t.InOrder();
}

  1. KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。 该种方式在现实生活中也非常常见,例如:统计某物品出现的次数
template<class K, class V>
struct BSTreeNode
{
	BSTreeNode<K, V>* _left;
	BSTreeNode<K, V>* _right;
	K _key;
	V _value;

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

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

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

	bool Erase(const K& key)
	{
		//...
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << ":" << root->_value << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

应用:

int main()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		//BSTreeNode* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.InOrder();
}

四、二叉搜索树的性能分析

  插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

  但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

  • 最优情况下,二叉搜索树为完全二叉树,或者接近完全二叉树,其平均比较次数为:log2 N
  • 最差情况下,二叉搜索树退化为单支树,或者类似单支,其平均比较次数为:N

  如果退化成单支树,二叉搜索树的性能就失去了。将二叉搜素树改进为AVL树和红黑树,不论按照什么次序插入关键码,性能都能达到最优。

你可能感兴趣的:(C++,数据结构,c++,数据结构,算法,二叉搜索树,树)