C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)

博客主页:Morning_Yang丶
欢迎关注点赞收藏⭐️留言
本文所属专栏:【C++拒绝从入门到跑路】
作者水平有限,如果发现错误,敬请指正!感谢感谢!

文章目录

  • 前言
  • 一、二叉搜索树概念
  • 二、二叉搜索树实现
    • 2.1 节点 & 树的整体结构
    • 2.2 默认成员函数的实现
      • ① 构造函数
      • ② 拷贝构造函数 ⭐
      • ③ 赋值运算符重载 ⭐
      • ④ 析构函数
  • 三、二叉搜索树的相关接口实现
    • 3.1 二叉搜索树的查找
      • ① 非递归写法
      • ② 递归写法(优先用非递归)
    • 3.2 二叉搜索树的插入
      • ① 非递归写法
      • ② 递归写法(优先用非递归)
    • 3.3 二叉搜索树的中序遍历
    • 3.4 二叉搜索树的删除
      • ① 非递归写法
      • ② 递归写法(优先用非递归)
    • 3.5 总结 & 一些细节
  • 四、二叉搜索树的应用
    • 4.1 K的模型 – set
    • 4.2 KV的查找模型 – map
      • 示例1:英汉词典
      • 示例2:统计单词出现的次数
  • 五、二叉搜索树的性能分析

前言

普通的二叉树单纯用来存储数据意义不大,不如用数组和链表。

普通数组和链表,面对一些需要频繁查找、插入、删除的场景,也很麻烦。

  • 普通数组/链表:暴力查找,O(N)
  • 排序数组:二分查找,O(log2N)

所以这里引入了[二叉搜索树]。

一、二叉搜索树概念

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

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

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第1张图片

二叉搜索树的结构特点带来的好处:

  • 存储数据:对二叉搜索树进行「中序遍历」,正好是一个「升序」序列
  • 查找一个值:最多查找树的高度次
  • ……

注意:二叉搜索树的时间复杂度是O(N),比如只有右子树或者左子树的情况。只有当树的形状接近完全二叉树或者满二叉树,才能达到 logN。所以,实际中搜索二叉树在极端情况下没办法保证效率,要对他的特性拓展延申: AVL Tree、红黑树。他们对搜索二叉树的左右高度提出要求,非常接近完全二叉树,所以他们的效率可以达到 logN

上面的数据结构一般用于内存中查找

当数据在磁盘中时,对树高度进一步提出了要求:进一步衍生了B树系列,适合查找存储

他们都是搜索树基础上演变出来的,各有特点,适用于不同的场景

二、二叉搜索树实现

2.1 节点 & 树的整体结构

定义二叉搜索树节点类模板

#include
using namespace std;

// 定义二叉搜索树节点
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>
class BSTree
{
	typedef BSTreeNode<K> Node; // 重命名树节点类名
private:
	Node* _root = nullptr; // 根节点

public:
	// 构造函数
	BSTree();
    // 拷贝构造函数
	BSTree(const BSTree<K>& tree); // 引用
    // 赋值运算符重载函数
	BSTree<K>& operator=(BSTree<K> tree); // 传值
    // 析构函数
    ~BSTree();
    
	// 插入元素key
	bool Insert(const K& key); // 常引用:减少传参时的拷贝,保护形参不会被更改
	// 查找元素key,查找到了返回节点地址,否则返回nullptr
    Node* Find(const K& key);
    // 删除元素key
    bool Erase(const K& key);
    
    // 插入元素key(递归版本)
    bool InsertR(const K& key);
    // 查找元素key(递归版本)
	Node* FindR(const K& key);
    // 删除元素key(递归版本)
	bool EraseR(const K& key);
    
	// 中序遍历
	void InOrder();

private:
    // 拷贝构造子函数
	Node* _copy(Node* root);
    // 析构子函数
	void _Destroy(Node* root);
    
    // 插入元素key子函数(递归版本)
	bool _InsertR(Node*& root, const K& key); // 形参为引用
    // 查找元素key子函数(递归版本)
	Node* _FindR(Node* root, const K& key);
    // 删除元素key子函数(递归版本)
	bool _EraseR(Node*& root, const K& key); // 形参为引用
    
	// 中序遍历子函数
	void _InOrder(Node* _root);
};

2.2 默认成员函数的实现

① 构造函数

//...
public:
	// 构造函数
	BSTree()
    	:_root(nullptr)
    { }
//...

② 拷贝构造函数 ⭐

//...
public:
	// 拷贝构造函数
	BSTree(const BSTree<K>& tree)
	{
		// 深拷贝,用已存在的树tree去拷贝一个新树,然后返回新树的根
		_root = _copy(tree._root);
	}

private:
	// 拷贝构造子函数
	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;
	}
//...

③ 赋值运算符重载 ⭐

//...
public:
	// 赋值运算符重载函数
	BSTree<K>& operator=(BSTree<K> tree) // 传值
	{
		// 现代写法
		// 比如 t1 = t2,tree是t2的深拷贝,tree就是t1想要的,
		// 所以t1和tree换个头(根节点地址),但不换身体(整颗树),t1就指向了tree整棵树,然后返回
        // tree是临时变量,出函数作用域自动销毁
		std::swap(_root, tree._root);
		return *this;
	}
//...

④ 析构函数

//...
public:
	// 析构函数
	~BSTree()
	{
		_Destroy(_root);
        _root = nullptr;
	}
private:
	// 析构子函数
	void _Destroy(Node* root)
	{
		// 根节点不为空
		if (root)
		{
			// 建议使用后序遍历,左-右-根
			_Destroy(root->_left);
			_Destroy(root->_right);
			delete root;
			root = nullptr;
		}
	}
//...

三、二叉搜索树的相关接口实现

3.1 二叉搜索树的查找

① 非递归写法

如果根节点为空,返回 nullptr

如果根节点不为空,从根节点开始,查找 key:

  • 如果 key 比当前节点小,则去当前节点的左子树中查找
  • 如果 key 比当前节点大,则去当前节点的右子树中查找
  • 如果 key 等于当前节点,返回 节点地址
// 查找元素key,查找到了返回节点地址,否则返回nullptr
Node* Find(const K& key)
{
    // 树为空
    if (_root == nullptr)
    {
        return nullptr;
    }

    // 树不为空,从根节点开始查找元素key
    Node* cur = _root;
    while (cur) // 当cur为空,停止循环,说明没找到
    {
        if (cur->_key > key)//key比节点小,向左找
        {
            cur = cur->_left;
        }
        else if (cur->_key < key)//key比节点大,向右找
        {
            cur = cur->_right;
        }
        else//说明已经存在该值,false
        {
            return cur;
        }
    }
    // 没有找到
    return nullptr;
}

② 递归写法(优先用非递归)

分而治之的思想:

每一级递归时,在我们眼中,当前树就是这样的,只有 rootleftright 三个节点。

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第2张图片

递归算法思路

public:
	// 查找元素key(递归版本)
	// 调用函数需要传递树的根,根是私有成员,所以套一层函数_FindR来间接调用,从而保护根
	Node* FindR(const K& key)
    {
        return _FindR(_root, key);
    }
private:
	// 查找元素key子函数(递归版本)
	Node* _FindR(Node* root, const K& key)
	{
		// 递归出口(终止条件),当前树的根节点为空
		if (root == nullptr)
		{
			return nullptr; // 没找到,返回nullptr
		}

		// 当前树的根节点不为空
        if (root->_key < key)
        {
            return _FindR(root->_right, key);
        }
        else if (root->_key > key)
        {
            return _FindR(root->_left, key);
        }
        else
        {
            return root;
        }
	}
}

3.2 二叉搜索树的插入

① 非递归写法

  1. 树为空,直接插入。

    C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第3张图片

  2. 树不为空,根据「二叉搜索树性质」,从根节点开始,查找到适合插入 key 的空位置,然后插入。

    C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第4张图片

代码如下:

// 插入元素key
bool Insert(const K& key) // 常引用:减少传参时的拷贝,保护形参	不会被更改
{
    // 树为空
    if (_root == nullptr)
    {
        _root = new Node(key); // 直接插入新节点
        return true;
    }
    else
    {
        Node* parent = nullptr;// 记录cur的父节点,因为新节点最终会插入在cur的父节点左右孩子的位置
        Node* cur = _root;// 树不为空,从根节点开始,先查找到插入key的位置
        while (cur)// 当cur为空,说明找到插入key的位置了
        {					
            if (cur->_key > key)//key比节点小,向左找
            {
                parent = cur;
                cur = cur->_left;
            }				
            else if (cur->_key < key)//key比节点大,向右找
            {
                parent = cur;
                cur = cur->_right;
            }			
            else//说明已经存在该值,false
            {return false; }
        }
        //cur走到空,判断key和parent的关系
        if (parent->_key > key)
        {
            parent->_left = new Node(key);// key比父节点小,链接在左边
        }
        else
        {
            parent->_right = new Node(key);// key比父节点大,链接在右边
        }
        // 插入成功,返回true
        return true;
    }
}

注意一点

插入元素的顺序不同,树的结构也会不同,但中序遍历的结果是一样的。

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第5张图片

② 递归写法(优先用非递归)

分而治之的思想:

每一级递归时,在我们眼中,当前树就是这样的,只有 rootleftright 三个节点。

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第6张图片

递归算法思路

如果当前树的根节点为空,则直接插入;

如果当前树的根节点不为空:

  • 插入的值 key 如果比当前树的根节点大,则去往当前树的右子树中插入;
  • 插入的值 key 如果比当前树的根节点小,则去往当前树的左子树中插入;
public:
		// 插入元素key(递归版本)
		// 调用函数需要传递树的根,根是私有成员,所以套一层函数InsertR来间接调用,从而保护根
		//如果树中存在key,返回false
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
private:
	// 插入元素key子函数(递归版本)
	bool _InsertR(Node*& root, const K& key) // 形参是根节点的引用,这里很巧妙
	{
		// 当前树的根节点为空
		if (root == nullptr)
		{
			root = new Node(key); // 插入新节点
			return true;          // 返回true
		}

		// 当前树的根节点不为空
		if (key > root->_key)
		{
			// 去往当前树的右子树中插入
			return _InsertR(root->_right, key);
		}
		else if (key < root->_key)
		{
			// 去往当前树的左子树中插入
			return _InsertR(root->_left, key);
		}
		else
		{
			// 二叉搜索树不允许数据冗余,返回false
			return false;
		}
	}
}

拓展

函数形参是根节点的引用 bool _InsertR(Node*& root, const K& key);,这里很巧妙,我们在函数体内就不用定义一个变量来保存要插入的节点的父节点了,这样就能直接更换上一层的节点的左右指针。

我们以插入节点 10 为例:

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第7张图片

这样一来,通过改变 root,从而控制 root 父节点(节点 9)右指针的指向。

3.3 二叉搜索树的中序遍历

注意一点

我们在类外面,用对象调用函数时需要传递树的根,但根是私有成员,只能再去写一个 GetRoot 接口来传递树的根,但这样根又被暴露出去了,所以我们在这里,套一层无参函数 InOrder() 来调用有参函数 _InOrder(Node* _root),从而保护了根节点。

public:
	// 中序遍历
	// void InOrder(Node* _root)
	// 调用函数需要传递树的根,根是私有成员,所以套一层无参函数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);
    }

3.4 二叉搜索树的删除

① 非递归写法

二叉搜索树的删除比较复杂,要分情况讨论:

首先查找元素 key 是否在二叉搜索树中,如果不存在,则返回 false,否则删除结点,分下面几种情况:

情况1和2:要删除的结点「无孩子」结点(叶子节点),或者要删除的结点「只有左孩子」结点

  • 先判断被删除节点 cur 是父节点 parent左孩子 还是 右孩子
  • 让父结点 parent 的左 / 右指针指向被删除节点的 左孩子 (我被删除了,我的父亲要帮我接管左孩子)
  • 然后删除该节点。

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第8张图片

注意】:

还有一种情况需要考虑到,删除的是根节点,cur 没有父节点,所以直接把 cur 的左孩子变为根:

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第9张图片


情况3:要删除的结点「只有右孩子」结点

  • 先判断被删除节点 cur 是父节点 parent左孩子 还是 右孩子

  • 让父结点 parent 的左 / 右指针指向被删除节点的 右孩子。(我被删除了,我的父亲要帮我接管右孩子)

  • 然后删除该节点。

  • 这里就不详细画图演示了,和上面的类似。

    C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第10张图片

注意】:

还有一种情况需要考虑到,删除的是根节点,cur 没有父节点,所以直接把 cur 的右孩子变为根:

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第11张图片


情况4:要删除的结点「有左右孩子」结点

有两个孩子,不好直接删除,所以我们用替代法删除:

找一个「替代节点」,比被删除节点的左孩子值大,比被删除节点右孩子的值小。

即被删除节点左子树中的最大节点或者右子树中的最小节点。

  1. 左子树中的最大节点 --> 即左子树的最右侧节点(它的右孩子一定为空)
  2. 右子树中的最小节点 --> 即右子树的最左侧节点(它的左孩子一定为空)

替代节点」找到后,将替代节点中的值赋给「要的删除节点」,转换成删除替代节点。


C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第12张图片


以找被删除节点 左子树中的最大节点 作为替代节点为例,删除思路如下:

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第13张图片

注意】:

在第 3 步:

  • 先要判断一下最大节点 maxleft 是父节点 maxleft_parent左孩子 还是 右孩子
  • 让父结点 maxleft_parent 的左 / 右指针指向被删除节点的 左孩子 (我被删除了,我的父亲要帮我接管左孩子,因为左子树的最大节点没有的右孩子)

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第14张图片

代码如下

bool Erase(const K& key)
{
    // 树为空,删除失败
    if (_root == nullptr)
    {
        return false;
    }
    // 树不为空,从根节点开始,查找元素key
    Node* cur = _root;// 记录元素key的位置
    Node* parent = cur;// 记录cur的父节点

    //先遍历找这个节点
    while (cur) // 如果cur为空,说明没有找到元素key的位置
    {
        if (cur->_key > key)//key比节点小,向左找
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (cur->_key < key)//key比节点大,向右找
        {
            parent = cur;
            cur = cur->_right;
        }
        else//说明找到了
        {
            break;
        }
    }

    //为空就是没找到,就false
    if(!cur){return false; }
    //处理第一、二、三种情况,只有一个子节点或者没有子节点
    if (cur->_left == nullptr)//左边为空交右边
    {
        if (cur == _root)//cur是根节点
        {
            _root = cur->_right;
        }
        else
        {
            if (parent->_left == cur)//cur是左节点,右边交给parent的左
            {
                parent->_left = cur->_right;
            }
            else
            {
                parent->_right = cur->_right;
            }
        }
        delete cur;
        cur = nullptr;
    }
    else if(cur->_right == nullptr)//右边为空就交左边
    {
        if (cur == _root)
        {
            _root = cur->_left;
        }
        else
        {
            if (parent->_left == cur)//cur是左节点,将左边交给parent的左
            {
                parent->_left = cur->_left;
            }
            else
            {
                parent->_right = cur->_left;
            }
        }		
        delete cur;
        cur = nullptr;
    }
    else//左右都不为空
    {
        Node* minRight = cur->_right;//去找右子树里面的最小值
        Node* minParent = cur;//右子树里面最小值的父节点
        while (minRight->_left)//它的左节点最后一定为空
        {
            minParent = minRight;
            minRight = minRight->_left;
        }

        cur->_key = minRight->_key;//把值替换过去
        if (minParent->_left == minRight)//待删除节点是父节点的左边
        {
            minParent->_left = minRight->_right;
        }
        else//待删除节点是父节点的右边,出现这种情况,说明 minRight就是cur节点的右子树根节点
        {
            minParent->_right = minRight->_right;
        }
        delete minRight;
        minRight = nullptr;
    }
    return true;

}

② 递归写法(优先用非递归)

分而治之的思想:

每一级递归时,在我们眼中,当前树就是这样的,只有 rootleftright 三个节点。

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第15张图片

递归算法思路

   public:
	// 删除元素key(递归版本)
	bool EraseR(const K& key)
    {
        return _EraseR(_root, key);
    }

private:
	// 删除元素key子函数(递归版本)
bool _EraseR(Node*& root, const K& key)
{
    if (root == nullptr)//走到空都找不到返回false;
    {
        return false;
    }

    if (root->_key < key)
    {
        return _EraseR(root->_right, key);
    }
    else if (root->_key > key)
    {
        return _EraseR(root->_left, key);
    }
    else
    {
        if (root->_left == nullptr)//左子树为空
        {
            Node* del = root;//root是上一层左(右)指针的别名
            root = root->_right;//root接收右子树
            delete del;//删掉对应的节点
        }
        else if (root->_right == nullptr)//右子树为空
        {
            Node* del = root;
            root = root->_left;
            delete del;
        }
        else//左右子树都不为空
        {
            //迭代
            //去找右子树的最小节点
            Node* minRight = root->_right;
            Node* minParent = root;
            while (minRight->_left)//它的左节点最后一定为空
            {
                minParent = minRight;
                minRight = minRight->_left;
            }
            root->_key = minRight->_key;
           
            if (minParent->_left == minRight)
            {
                //说明 minRight不是root的右节点
                minParent->_left = minRight->_right;
            }
            else
            {
                //minRight是root的右节点
                minParent->_right = minRight->_right;
            }
            delete minRight;
            minRight = nullptr;

            /*递归写法
			Node* minRight = root->_right;
			while (minRight->_left)//它的左节点最后一定为空
			{
				minParent = minRight;
				minRight = minRight->_left;
			}
			K Min = minRight->_key;//记下他的值

			_EraseR(root->_right, Min);//从root的右树里面删除Min
			//删除Min的情况一定不是左右子树都不为空
			root->_key = Min;//替换值
			*/
        }
        return true;
    }
}

3.5 总结 & 一些细节

  • 二叉搜索树的查找,根据「二叉搜索树性质」来找某节点

  • 二叉搜索树的插入,

    • 先根据「二叉搜索树性质」来查找适合插入的空位置,
    • 必须要保存新节点的父节点,并判断新节点是左孩子还是右孩子,然后再插入新节点。(不允许冗余)
  • 二叉搜索树的删除,

    • 先根据「二叉搜索树性质」来查找要删除的节点 cur,

    • 并根据要删除节点 cur 所在位置的不同情况,来具体操作,

    • 必须要保存要删除节点 cur 的父节点,并判断要删除节点 cur 是左孩子还是右孩子,先让父亲接管要删除节点 cur 的孩子,然后再删除节点 cur。

      删除节点 cur 的 3 种情况汇总:cur左为空,父亲指向右;右为空,父亲指向左;左右都不为空,找替代节点。

      C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第16张图片

  • 对于上述接口的递归写法,一般能用循环(非递归)就用非递归,有些递归好是好,也容易让人理解,但是对于深度高的树,建立栈帧也是一笔不小的开销,有可能会导致栈溢出。

四、二叉搜索树的应用

4.1 K的模型 – set

K (Key) 模型:确定一个值在不在一个集合中,K 模型即只有 Key 作为关键码,二叉搜索树结构中只需要存储 Key 即可,关键码即为需要搜索到的值。

举个例子1

  • 学生宿舍楼的门禁系统,你的学生卡里有学号,你在这栋楼的学生学号集合中,才能刷卡进去。宿舍楼门禁 -》 宿舍楼里面同学的学号都存储到BSTree stuNumSet;

举个例子2:给一个单词 word,判断该单词是否拼写正确,具体方式如下:

  • 以单词集合中的每个单词作为 key,构建一棵二叉搜索树。
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

上面实现的二叉搜索树就是 K 模型

4.2 KV的查找模型 – map

KV (Key/Value) 模型:每一个关键码 Key,都有与之对应的值 Value,即 < Key, Value > 的键值对。

该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文 < word, chinese > 就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是 < word, count > 就构成一种键值对;
  • 再比如高铁站,网上买票,刷身份证进站。网上实名制购票,每张票都关联一个人的身份证号。刷身份证进站,机器读取到的是身份证号码,系统要通过身份证号,找到身份证号关联的车票。看有没有当天这个车次的车票信息,有的话再开门禁。

注意

KV模型中,二叉搜索树的每个节点不仅要存放 key,还要存放 value,但是在插入、删除的时候,还是按照 key 值来查找到该节点,对其进行插入、删除操作。

所以我们要对上面的二叉搜索树进行改造,主要是这几个改动:1、节点类模板 2、树类模板中的插入节点函数、中序遍历函数。而查找、删除函数无需改动。

以下是完整的KV版本的代码

namespace 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;
	private:
		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)//走到空没找到就返回空
			{
				return nullptr;
			}
			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return root;
			}
		}
		bool _InsertR(Node*& root, const K& key, const V& value)
		{
			if (root == nullptr)//走到空,在空的位置插入
			{
				root = new Node(key, value);
				return true;
			}
			if (root->_key < key)
			{
				return _InsertR(root->_right, key, value);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key, value);
			}
			else//已经存在了,返回false
			{
				return false;
			}
		}
		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)//走到空都找不到返回false;
			{
				return false;
			}

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				if (root->_left == nullptr)
				{
					Node* del = root;
					root = root->_right;//root是上一层左(右)指针的别名
					delete del;
				}
				else if (root->_right == nullptr)
				{
					Node* del = root;
					root = root->_left;
					delete del;
				}
				else
				{
					Node* minRight = root->_right;
					Node* minParent = root;
					while (minRight->_left)//它的左节点最后一定为空
					{
						minParent = minRight;
						minRight = minRight->_left;
					}
					root->_key = minRight->_key;
					if (minParent->_left == minRight)
					{
						minParent->_left = minRight->_right;
					}
					else
					{
						minParent->_right = minRight->_right;
					}
					delete minRight;
					minRight = nullptr;

					/*递归写法
					Node* minRight = root->_right;
					while (minRight->_left)//它的左节点最后一定为空
					{
						minParent = minRight;
						minRight = minRight->_left;
					}
					K min = minRight->_key;

					_EraseR(root->_right, min);//从root的右树里面删除min
					root->_key = min;//替换值
					*/
				}
				return true;
			}
		}
		void Destory(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			Destory(root->_left);
			Destory(root->_right);
			delete root;
			root = nullptr;
		}
		Node* _copy(Node* root)
		{
			if (root == nullptr)
			{
				return nullptr;
			}
			Node* copyNode = new Node(root->_key, root->_value);
			copyNode->_left = _copy(root->_left);
			copyNode->_right = _copy(root->_right);
			return copyNode;
		}
	public:
		BSTree()
			:_root(nullptr)
		{
		}
		BSTree(const BSTree<K, V>& t)
		{
			_root = _copy(t._root);
		}
		BSTree<K, V>& operator=(BSTree<K, V> t)
		{
			swap(_root, t._root);
			return *this;
		}
		~BSTree()
		{
			Destory(_root);
			_root = nullptr;
		}
		//如果树中存在key,返回false
		bool InsertR(const K& key, const V& value)
		{
			return _InsertR(_root, key, value);
		}
		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		//如果树中不存在key,返回false
		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

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

示例1:英汉词典

实现一个简单的英汉词典 dict,可以通过英文找到与其对应的中文,具体实现方式如下:

  • < 单词,中文含义 > 为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较 Key
  • 查询英文单词时,只需给出英文单词,就可快速找到与其对应的 key,通过 key 就可知道中文含义 value 了。
void Test_KV_Tree()
{
	// KV模型 -- 英汉词典,通过英文找到与其对应的中文
	KV::BSTree<std::string, std::string> dict;
	// 插入 < 单词,中文含义 > 构建二叉搜索树
	dict.InsertR("insert", "插入");
	dict.InsertR("erase", "删除");
	dict.InsertR("left", "左边");
	dict.InsertR("string", "字符串");

	// 查找单词对应中文含义
	std::string str;
	while (cin >> str)
	{
		if (str == "q") // 输入q退出查找
		{
			cout << "quit!" << endl;
			break;
		}
		// 查找该单词
		auto ret = dict.FindR(str);
		if (ret)
		{
			cout << str << ":" << ret->_value << endl;
		}
		else
		{
			cout << "单词拼写错误" << endl;
		}
	}
}

示例2:统计单词出现的次数

统计 str 中每个单词出现的次数,方法如下:

  • 按照原来的做法可能是,对所有单词排序,然后遍历,如果遍历的结果一样,次数就加1……
  • 现在直接用KV模型,用 < 单词,单词出现次数 > 为键值对构造二叉搜索树,只需给出单词,就可快速找到与其对应的 key,通过 key 就可知道单词出现的次数 value 了。
void TestTree3()
{
    std::string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };

    // 统计水果出现的次
    KV::BSTree<std::string, int> countTree;
    // 遍历strs
    for (auto& str : strs)// 传引用,避免string深拷贝
    {
        // 先检查当前准备插入的单词,是否已经在二叉搜索树tree中了
        auto ret = countTree.FindR(str);
        if (ret == NULL)// 不在树中
        {
            // 插入 < 单词,单词出现次数 >
            countTree.InsertR(str, 1);// 当前次数是1次
        }
        else// 在树中
        {
            ret->_value++;  // 修改value,出现次数+1
        }
    }
    // 打印每个 < 单词,单词出现次数 >
    countTree.InOrder();
}

因为二叉搜索树的特性,插入字符串的时候就排好序了,中序遍历出来的结果也是有序的

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

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

对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是与树的深度有关,即树越深,则比较次数越多。

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

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第17张图片

二叉搜索树的查找是很快的,但是它很依赖于树的形状:

C++(第十三篇):二叉搜索树(二叉树进阶、二叉搜索树的结构、应用及模拟实现)_第18张图片

最优情况下,有 n 个结点的二叉搜索树为完全二叉树,其平均比较次数为:O(log2N)

最差情况下,有 n 个结点的二叉搜索树退化为单支树,其平均比较次数为:O(N)

留一个问题

如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以是二叉搜索树的性能最佳?这就要引出 AVL 树了。

你可能感兴趣的:(【C++拒绝从入门到跑路】,数据结构,c++,开发语言,算法,运维)