【C++】二叉搜索树

目录

[1. 二叉搜索数概念](#1. 二叉搜索数概念)

[2. 二叉搜索树模拟实现](#2. 二叉搜索树模拟实现)

[2.1 树节点类初始结构](#2.1 树节点类初始结构)

[2.2 树类初始结构](#2.2 树类初始结构)

[2.3 Insert(插入)](#2.3 Insert(插入))

[2.5 Find(查找)](#2.5 Find(查找))

[2.6 Erase(删除)](#2.6 Erase(删除))

[2.7 中序遍历](#2.7 中序遍历)

[2.8 析构](#2.8 析构)

[2.9 拷贝构造](#2.9 拷贝构造)

[2.10 赋值重载](#2.10 赋值重载)

[3. 二叉搜索树的性能](#3. 二叉搜索树的性能)

[4. 二叉搜索树的应用](#4. 二叉搜索树的应用)

[5. 编程题](#5. 编程题)

[5.1 二叉树的最近公共祖先](#5.1 二叉树的最近公共祖先)

[5.2 二叉搜索树与双向链表](#5.2 二叉搜索树与双向链表)

[5.3 从前序与中序遍历序列构造二叉树](#5.3 从前序与中序遍历序列构造二叉树)

[5.4 二叉树的前序遍历(非递归版本)](#5.4 二叉树的前序遍历(非递归版本))


1. 二叉搜索数概念

2. 二叉搜索树模拟实现

2.1 树节点类初始结构

  1. 一个指针指向左子树。

  2. 一个指针指向右子树。

  3. 一个变量存放数据。

  4. 构造函数:传入数据,初始化列表进行初始化。

cpp 复制代码
template<class K>
struct BSTreeNode
{
	//左右指向和数据
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	//构造
	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};

2.2 树类初始结构

  1. 私有成员变量只有一个根节点的指针。

2.3 Insert(插入)

  1. 如果树为空,那么用待插入的值创建节点作为根节点。插入成功返回true。

  2. 如果树不为空,用cur标记根节点的位置,插入的值和cur位置的值比较,比你大,cur就去右边,比你小,cur就去左边,继续比较,直到cur走到空,就在cur位置创建一个节点。连接的话需要一个前驱指针,每次cur移动之前赋值给前驱指针。

  3. 如果值相等就不插入,返回false。

  4. 创建完新节点后需要判断插入在上面节点的哪一边,用值比较,如果比上面大就插在右边,小就插在左边。

cpp 复制代码
	bool Insert(const K& key)
	{
		//判空
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* cur = _root;
		Node* prev = nullptr;
		//找位置
		while (cur)
		{
			prev = cur;

			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		//建节点
		cur = new Node(key);
		if (key > prev->_key)
		{
			prev->_right = cur;
		}
		else
		{
			prev->_left = cur;
		}
		return true;
	}

【递归版本】

  1. 从根节点开始比较,你比我大就去我的右子树,你比我小就去我的左子树,相等就不插入,走到空就插入,参数是节点的引用。
cpp 复制代码
	bool InsertR()
	{
		return _InsertR(_root);
	}

	bool _InsertR(Node*& root, const K& key)
	{
		//结束条件
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		//找位置
		if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else if(key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
	}

2.5 Find(查找)

  1. 值比较,从根节点开始比,比你小就继续往左边比,比你大就往右边,遇到相等就是找到了,走到空就是没有。
cpp 复制代码
	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;
	}

【递归版本】

  1. 比我小就去我的左子树找,比我大就去我的右子树找,相等就返回true,找到空就返回false。
cpp 复制代码
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}

	bool _FindR(Node* root, const K& key)
	{
		//结束条件
		if (root == nullptr)
		{
			return false;
		}

		if (key > root->_key)
		{
			return _FindR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return true;
		}
	}

2.6 Erase(删除)

删除的节点有四种情况:

a:节点无孩子

b:节点有左孩子

c:节点有右孩子

d:节点有两个孩子

  1. 先找到要删除的节点,记录前驱指针。

  2. 如果删除的节点左为空,那么前驱指针指向这个节点的右,释放这个节点,同时要判断前驱指针的哪一边指向你。特殊情况:如果这个节点是根节点,那么就将它的右作为根节点。

  3. 如果删除的节点右为空,和上面同理。

  4. 如果删除的节点左右都空,和上面同理。

  5. 如果删除的节点左右都不为空,需要替换法,一般选择左边最大或右边最小交换,先去右子树找最左边的节点,记得保留前驱指针,找到后和删除节点值交换,然后删除这个节点,前驱节点指向你的右边,记得判断前驱节点哪边指向你。

cpp 复制代码
	bool Erase(const K& key)
	{
		Node* cur = _root; 
		Node* prev = nullptr;
		while (cur)
		{
			if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				prev = cur;
				cur = cur->_left;
			}
			//开始删除
			else
			{
				//左为空
				if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (prev->_right == cur)
						{
							prev->_right = cur->_right;
						}
						else
						{
							prev->_left = cur->_right;
						}
					}
					delete cur;
				}
				//右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (prev->_right == cur)
						{
							prev->_right = cur->_left;
						}
						else
						{
							prev->_left = cur->_left;
						}
					}
					delete cur;
				}
				//都不为空
				else
				{
					//找替换
					Node* change = cur->_right;
					Node* prev = cur;
					while (change->_left)
					{
						prev = change;
						change = change->_left;
					}

					swap(cur->_key, change->_key);

					if (prev->_right == change)
					{
						prev->_right = change->_right;
					}
					else
					{
						prev->_left = change->_right;
					}
					delete change;
				}
				return true;
			}
		}
		return false;
	}

【递归版本】

  1. 比我大就去右子树,比我小就去左子树,空就删除失败,相等就开始删除。

  2. 删除的节点左为空,先记录当前节点,在把节点的右边赋值给自己,因为当前root是上一层的别名。

  3. 删除节点右为空同理。

  4. 删除节点左右都不为空,先找到右子树的最小节点,然后替换,再去交给右子树删除。

cpp 复制代码
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

	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);
		}
		else
		{
			if (root->_left == nullptr)
			{
				Node* cur = root;
				root = root->_right;
				delete cur;
			}
			else if (root->_right == nullptr)
			{
				Node* cur = root;
				root = root->_left;
				delete cur;
			}
			else
			{
				Node* change = root->_right;
				while (change->_left)
				{
					change = change->_left;
				}

				swap(root->_key, change->_key);

				return _EraseR(root->_right, key);
			}
		}
	}

2.7 中序遍历

  1. 左根右,遇到空就返回。

  2. 先走左边,然后打印,然后走右边。

  3. 由于需要传根节点,但外面拿不到,所以套一层无参的。

cpp 复制代码
	void InOrder()
	{
		_InOrder(_root);
	}

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

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

2.8 析构

  1. 利用后序思想,先释放左再释放右,最后释放自己,然后置空,参数可以传引用。
cpp 复制代码
	~BSTree()
	{
		_Destroy(_root);
	}

	void _Destroy(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}

		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
		root = nullptr;
	}

2.9 拷贝构造

  1. 递归拷贝,先拷贝当前节点,再拷贝节点的左边,然后拷贝节点的右边。
cpp 复制代码
	BSTree(const BSTree<K>& bst)
	{
		_root = _Copy(bst._root);
	}

	Node* _Copy(Node* bst)
	{
		if (bst == nullptr)
		{
			return nullptr;
		}

		Node* newroot = new Node(bst->_key);
		newroot->left = _Copy(bst->_left);
		newroot->_right = _Copy(bst->_right);

		return newroot;
	}

2.10 赋值重载

  1. 传值传参,利用拷贝构造传给参数,然后和参数交换。
cpp 复制代码
	BSTree<K>& operator=(BSTree<K> bst)
	{
		swap(_root, bst._root);
		return *this;
	}

3. 二叉搜索树的性能

  1. 插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

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

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

4. 二叉搜索树的应用

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。本质是判断一个值在不在。

  2. KV模型:每一个关键码key,都有与之对应的值Value,既可以判断key在不在,也可以通过key查找value。

5. 编程题

5.1 二叉树的最近公共祖先

链接:. - 力扣(LeetCode)

【思路1】

  1. 两个节点都在公共祖先的两边,当某个节点是祖先是特殊情况。

  2. 所以分三种情况节点在两边(或者节点是祖先),节点都在右边,节点都在左边,那么祖先从根节点开始不断往下调整。

  3. 这个方法最坏情况是N方,当树是单分支的时候。

【思路2】

  1. 记录两个节点的路径(记录节点),用栈,从根节点开始入栈比较,如果找到了就不继续了,如果没找到,就去左边找,左边找到了也不找了,左边没找到就去右边找,遇到空就是没找到,每次比较都是先入栈再比较,都没找到就出栈。

  2. 找到两个节点路径之后,让长的出栈,直到两个路径长度相等。

  3. 路径长度相等之后,不断出栈,直到两个栈顶元素相同就停下。

cpp 复制代码
class Solution 
{
public:
    bool path(TreeNode* root, TreeNode* key, stack<TreeNode*>& st)
    {
        //空节点
        if(root == nullptr)
        {
            return false;
        }

        //相等就返回
        st.push(root);
        if(st.top() == key)
        {
            return true;
        }

        //左子树找到就返回
        if(path(root->left, key, st))
        {
            return true;
        }

        //右子树找到就返回
        if(path(root->right, key, st))
        {
            return true;
        }

        //都没找到
        st.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 
    {
        //获取路径信息
        stack<TreeNode*> pst;
        stack<TreeNode*> qst;
        path(root, p, pst);
        path(root, q, qst);

        //减去多余路径
        while(pst.size() != qst.size())
        {
            if(pst.size() > qst.size())
            {
                pst.pop();
            }
            else
            {
                qst.pop();
            }
        }

        //找出相同节点
        while(pst.top() != qst.top())
        {
            pst.pop();
            qst.pop();
        }

        return pst.top();
    }
};

5.2 二叉搜索树与双向链表

链接:二叉搜索树与双向链表_牛客题霸_牛客网

【思路】

  1. 利用中序遍历,前后指针修改节点指针指向。

  2. 每一层需要改变prev,需要加引用。

cpp 复制代码
class Solution 
{
public:
	void InOrder(TreeNode* cur, TreeNode*& prev)
	{
		if(cur == nullptr)
		{
			return;
		}

		InOrder(cur->left, prev);
		cur->left = prev;
		if(prev)
		{
			prev->right = cur;
		}
		prev = cur;
		InOrder(cur->right, prev);
	}
    TreeNode* Convert(TreeNode* pRootOfTree) 
	{
        TreeNode* cur = pRootOfTree;
		TreeNode* prev = nullptr;

		InOrder(cur, prev);
		TreeNode* head = pRootOfTree;
		while(head && head->left)
		{
			head = head->left;
		}
		return head;
    }
};

5.3 从前序与中序遍历序列构造二叉树

链接:. - 力扣(LeetCode)

【思路】

  1. 前序确定根,中序确定左右子树。前序是根左右,所以先构建左子树的根。

  2. 划分左右区间,不断递归找根,同时利用返回值建立连接。

cpp 复制代码
    TreeNode* Bulid(vector<int>& preorder, vector<int>& inorder, int& preIndex, int begin, int end)
    {
        if(begin > end)
        {
            return nullptr;
        }

        //找根
        int rootIndex = 0;
        while(rootIndex <= end)
        {
            if(inorder[rootIndex] == preorder[preIndex])
            {
                break;
            }
            ++rootIndex;
        }

        //左右区间递归,顺便建立连接
        TreeNode* root = new TreeNode(inorder[rootIndex]);
        ++preIndex;
        root->left = Bulid(preorder, inorder, preIndex, begin, rootIndex-1);
        root->right = Bulid(preorder, inorder, preIndex, rootIndex+1, end);

        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) 
    {
        int preIndex = 0;
        TreeNode* root = Bulid(preorder, inorder, preIndex, 0, inorder.size()-1);
        return root;
    }

5.4 二叉树的前序遍历(非递归版本)

链接:. - 力扣(LeetCode)

【思路】

  1. 前序是先走根,再走左,再走右。

  2. 将每颗树或子树划分为左路节点和左路节点的右子树。

  3. 先走左路节点,每走一个节点它就是根,然后继续走它的左边,一直到左边为空意味着根走完了左边也走完了,接着就是走右边了。

  4. 通过栈实现回退的效果,下面的节点根左右都走完就回退上面的节点走它的右。

cpp 复制代码
    vector<int> preorderTraversal(TreeNode* root) 
    {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
            while(cur)
            {
                v.push_back(cur->val);
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            st.pop();
            cur = top->right;
        }
        
        return v;
    }
相关推荐
C++忠实粉丝2 分钟前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
Chris _data11 分钟前
二叉树oj题解析
java·数据结构
我们的五年28 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
程序猿阿伟44 分钟前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
Lenyiin1 小时前
02.06、回文链表
数据结构·leetcode·链表
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
烦躁的大鼻嘎2 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝2 小时前
C/C++内存管理
java·c语言·c++
fhvyxyci2 小时前
【C++之STL】摸清 string 的模拟实现(下)
开发语言·c++·string