C++进阶 —【二叉树进阶】

目录

1、二叉搜索树

 1.1 二叉搜索树概念

 1.2 二叉搜索树操作

2. 二叉搜索树的实现

 2.1二叉搜索树的结构

2.2 二叉搜索树的插入

 2.3 Find查找

2.4 InOrder中序遍历

 2.5 Erase删除

2.6 构造和拷贝构造

 2.7 赋值重载和析构

 2.8 插入(递归)

 2.9 删除(递归)

3.二叉搜索树的应用 

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


1、二叉搜索树

 1.1 二叉搜索树概念

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

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

 1.2 二叉搜索树操作

  1.  二叉搜索树的查找                                                                                                      a、从根开始比较,查找,比根大则往右边走继续查找,比根小则往左边走继续查找。  b、最多查找高度次,走到到空,还没找到,那么这个值不存在。
  2. 二叉搜索树的插入                                                                                                         插入的具体过程如下:                                                                                                 a. 树为空,则直接新增节点,赋值给root指针                                                                 b. 树不空,按二叉搜索树性质查找对应的插入位置,插入新节点                                  
  3. 3. 二叉搜索树的删除                                                                        ​​​​​​​                                    首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:                                                                                                              a. 要删除的结点无孩子结点                                                                                              b. 要删除的结点只有左孩子结点                                                                                    c. 要删除的结点只有右孩子结点                                                                                      d. 要删除的结点有左、右孩子结点                                                                                     ​​​​​​​看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程 如下:                                                                                                         情况b:删除该结点且让被删除节点的父结点指向被删除节点的左孩子结点--直接删除   情况c:删除该结点且让被删除节点的父结点指向被删除结点的右孩子结点--直接删除   情况d:寻找右子树中最小的节点,用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

2. 二叉搜索树的实现

 2.1二叉搜索树的结构

template<class K>
	struct BSTreeNode
	{
		BSTreeNode(const K& key)
			:_key(key)
			, _left(nullptr)
			, _right(nullptr)
		{}

		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;
	};
template<class K>
	class BSTree
	{
	public:
		typedef BSTreeNode<K> Node;

    private:
		Node* _root = nullptr;
	};

2.2 二叉搜索树的插入

        这里需要注意的是插入的节点与父节点之间链接的问题,因为子节点无法向上找到父节点,所以需要提前定义一个父节点的指针,跟着一步一步向下走。找到位置后插入新节点,再与父节点链接,链接的时候需要注意,因为我们并不知道插入的节点是父亲的左边还是右边,所以需要一个if 帮我们判断是左还是右。注意二叉搜索树中不能插入相同的值!而且插入的顺序不同还可能导致树里的节点位置不同,还有可能会造成树向一侧偏移,如下图:

bool Insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			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 = new Node(key);
			if (parent->_key > key)
				parent->_left = cur;
			else
				parent->_right = cur;
			return true;
		}

 2.3 Find查找

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

2.4 InOrder中序遍历

        因为中序遍历需要一个根节点,所以我们封装一个出来,直接在类里调用自己的根节点。

void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
//下面的可以放到private里面
void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;
			_InOrder(root->_left);
			cout << root->_key << ' ';
			_InOrder(root->_right);
		}

 2.5 Erase删除

        删除这里考虑的比较多,所以会稍微复杂一点。首先还是先找到要删除的节点,其次有三种情况需要考虑:

1. cur节点左子树为空,那么还是跟插入一样,把要删除节点的父节点也整到位,cur节点左子树为空,就把cur节点的右子树托孤给他的父节点(相当于让父节点跳过他指向他的右子树),然后删除节点。

2. cur节点右子树为空,跟左子树差不多一样,变个方向就可以了。

3.cur节点的左子树和右子树都不为空,这里主要是替换法删除,有两个值可以选,一个是左子树最大值,另一个是右子树最小值,选哪个都可以,看自己怎么实现。我选的是右子树最小值,这里选值不像下图一样那么简单,要定义一个指针去找。找到之后可以把minRight(最小值)的值直接赋值给cur(也可以交换),然后让父节点指向minRight的右节点,为什么是右呢?因为minRight是cur的右子树中最小值,不可能再有左节点了。当然父节点这里还是要判断一下,链接完成后删除minRight后就结束了。

当然还有一种,就是找不到要删除的值所对应的节点。

bool Erase(const K& key)
		{
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					//1.左为空
					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;
					}
					//2.右为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
							_root = cur->_left;
						else
						{
							if (parent->_right == cur)
								parent->_right = cur->_left;
							else
								parent->_left = cur->_left;
						}
						delete cur;
					}
					//3.都不为空
					else
					{
						Node* parent = cur;
						Node* minRight = cur->_right;
						while (minRight->_left)
						{
							parent = minRight;
							minRight = minRight->_left;
						}
						cur->_key = minRight->_key;
						if (minRight == parent->_left)
							parent->_left = minRight->_right;
						else
							parent->_right = minRight->_right;
						delete minRight;
					}
					return true;
				}
			}
			return false;
		}

2.6 构造和拷贝构造

        构造比较简单,拷贝构造就是用前序遍历的方法走一遍,依次插入节点。

BSTree()
			:_root(nullptr)
		{}
	
		
		BSTree(const BSTree<K>& t)
		{
			_root = copy(t._root);
		}
		
	Node* copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;
			//前序
			Node* newNode = new Node(root->_key);
			newNode->_left =  copy(root->_left);
			newNode->_right =  copy(root->_right);

			return newNode;
		}

 2.7 赋值重载和析构

        赋值重载用的现代写法,不知道的可以回顾之前链表的博客。析构函数就是用后序遍历的方法进行删除,先删除左节点,再删除右节点 ,而后删除根节点。

        BSTree<K>& operator=(BSTree<K> t)
		{
            //现代写法
			swap(_root, t._root);
			return *this;
		}
		
        ~BSTree()
		{
			Destory(_root);
			_root = nullptr;
		}
		void Destory(Node* root)
		{
			//二叉树后续遍历删除
			if (root == nullptr)
				return;
			Destory(root->_left);
			Destory(root->_right);

			delete root;

		}

 2.8 插入(递归)

        参数这里用了一个很巧妙的引用,让链接变得十分简单。

        比如要往左子树插入值,此时root为空,new一个节点插入值,root这个节点就是父节点的左指针的引用,如果没有加这个引用,那么修改函数里的指针并不会影响函数外面的指针,即使new了节点,外面父节点的指针还是指向空,不会受影响。而加了引用,root就是父节点指向左子树这个指针的别名,此时修改root,就相当于修改了父节点的左指针。


		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
		bool _InsertR(Node*& root,const K& key)
		{
			if (root == nullptr)
			{
				root = new Node(key);
				return true;
			}
			if (root->_key > key)
				return _InsertR(root->_left, key);
			else if (root->_key < key)
				return _InsertR(root->_right, key);
			else
				return false;
		}

 2.9 删除(递归)

        先找到节点,删除节点还是分三种情况:1.root的左节点为空,把root先提前用指针存好,然后让root的右节点给自己,因为自己是父节点的指针,所以就相当于让父节点指向指向root的右节点(托孤),最后释放掉提前存好的root节点,就可以了;2.root的右节点为空,跟第一个差不多;3.root的左右节点都不为空,这里用的是交换值,然后到root的右子树中去删除原来的key(到下图画圈的位置删除3),简化问题去处理。

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

			if (root->_key > key)
				return _EraseR(root->_left, key);
			else if (root->_key < key)
				return _EraseR(root->_right, key);
			else
			{
				Node* del = root;
				//1.左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//2.右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//3.都不为空
				else
				{
					Node* minRight = root->_right;
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}
					//root->_key = minRight->_key;
					//return _EraseR(root->_right, minRight->_key);
					//转换到子树去删除
					swap(root->_key, minRight->_key);
					return _EraseR(root->_right, key);
				}
				delete del;
				return true;
			}
		}


3.二叉搜索树的应用 

1. K 模型: K 模型即只有 key 作为值,结构中只需要存储 Key 即可,值即为需要搜索到 的值
 比如:给一个单词word ,判断该单词是否拼写正确 ,具体方式如下:
        以词库中所有单词集合中的每个单词作为key ,构建一棵二叉搜索树
        在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV 模型:每一个值 key ,都有与之对应的值 Value ,即 <Key, Value> 的键值对 。该种方式在现实生活中非常常见:
        比如英汉词典就是英文与中文的对应关系 ,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文 <word, chinese> 就构成一种键值对;
        再比如统计单词次数 ,统计成功后,给定单词就可快速找到其出现的次数, 单词与其出
现次数就是 <word, count> 就构成一种键值对
namespace KV
{
	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)
			:_key(key)
			, _value(value)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};
	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;
		}
		void Inorder()
		{
			_Inorder(_root);
		}

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

			_Inorder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_Inorder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};

	void TestBSTree2()
	{

		// Key/Value的搜索模型,通过Key查找或修改Value
		KV::BSTree<string, string> dict;
		dict.Insert("sort", "排序");
		dict.Insert("string", "字符串");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");

		string str;
		while (cin >> str)
		{
			KV::BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << ret->_value << endl;
			}
			else
			{
				cout << "无此单词" << endl;
			}
		}
	}
	void TestBSTree3()
	{
		// 统计水果出现的次数
		string arr[] = { "苹果", "西瓜", "香蕉", "草莓","苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

		KV::BSTree<string, int> countTree;
		for (auto e : arr)
		{
			auto* ret = countTree.Find(e);
			if (ret == nullptr)
			{
				countTree.Insert(e, 1);
			}
			else
			{
				ret->_value++;
			}
		}

		countTree.Inorder();
	}
}


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

        插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。 但对于同一个值集合,如果各值插入的次序不同,可能得到不同结构的二叉搜索树:

 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:O(logN);

最差情况下,二叉搜索树退化为单支树 ( 或者类似单支 ) ,其平均比较次数为:O(N);

本篇文章就结束了,如有什么问题或者不懂的地方可以评论或私信。感谢大家的观看!

猜你喜欢

转载自blog.csdn.net/weixin_68993573/article/details/128892720