一篇文章教会你什么是二叉搜索树

在这里插入图片描述

二叉搜索树

  • 二叉搜索树概念
  • 二叉搜索树操作
    • 1.二叉搜索树的查找
    • 2.二叉搜索树的插入
    • 3.二叉搜索树的删除
    • 4.二叉搜索树的遍历
  • 二叉搜索树的实现
    • 1.二叉搜索树节点结构
    • 2.二叉搜索树类
    • 3.二叉搜索树的构造及析构
    • 4.二叉搜索树的拷贝构造及赋值重载
    • 5.二叉搜索树插入
    • 6.二叉搜索树查找
    • 7.二叉搜索树删除
    • 8.二叉搜索树中序遍历
    • 9.二叉搜索树所有代码
  • 二叉搜索树的应用
  • 二叉搜索树的性能分析

二叉搜索树概念

二叉搜索树Binary Search Tree,BST)是一种二叉树的特殊形式,它具有以下关键性质:

  1. 有序性:每个节点都包含一个值,且左子树中的所有节点的值都小于等于该节点的值,右子树中的所有节点的值都大于等于该节点的值。这意味着对于任何节点,它的左子树都包含比它小的值,右子树都包含比它大的值。
  2. 递归性质:整棵二叉搜索树本身也是一个二叉搜索树,因为它的左子树和右子树也满足二叉搜索树的性质。这意味着可以递归地应用相同的规则来构建和操作树。
  3. 无重复值:通常,BST中不允许包含重复的值。每个值在树中只能出现一次。

由于这些性质,二叉搜索树在很多应用中都非常有用,尤其是在需要高效地进行搜索、插入和删除操作的情况下。

以下是一些关于二叉搜索树的详细解释:

  1. 插入操作
    • 插入操作从根节点开始,根据节点值的大小逐步向下搜索树,直到找到一个合适的位置插入新节点。
    • 如果要插入的值小于当前节点的值,就继续在左子树中搜索,否则在右子树中搜索,直到找到一个空位置插入新节点。
  2. 搜索操作
    • 搜索操作也从根节点开始,根据节点值的大小逐步向下搜索树。
    • 如果要搜索的值等于当前节点的值,就找到了目标节点。
    • 如果要搜索的值小于当前节点的值,就在左子树中搜索;如果大于当前节点的值,就在右子树中搜索。
    • 如果一直搜索到叶子节点仍然没有找到目标值,则说明目标值不在树中。
  3. 删除操作
    • 删除操作通常比较复杂,因为需要考虑多种情况。删除节点时有以下几种情况:
      • 被删除节点是叶子节点(没有子节点):直接删除即可。
      • 被删除节点有一个子节点:将子节点替换为被删除节点的位置。
      • 被删除节点有两个子节点:找到被删除节点的中序遍历前驱或后继节点,用其值替换被删除节点的值,然后递归删除前驱或后继节点。
  4. 遍历操作
    • 二叉搜索树可以通过不同的遍历方式进行访问,包括中序遍历、前序遍历和后序遍历。
    • 中序遍历可以按照升序顺序遍历树中的所有节点,因为它首先遍历左子树,然后当前节点,最后右子树。
  5. 平衡性
    • 当二叉搜索树的高度不平衡时,搜索、插入和删除操作的时间复杂度可能会退化为O(n),其中n是节点数。为了避免这种情况,通常使用平衡二叉搜索树(如AVL树或红黑树)来确保树的高度保持在较小的范围内。

总之,二叉搜索树是一种基于节点值大小有序排列的二叉树,它支持高效的搜索、插入和删除操作,但需要注意平衡性问题以避免性能退化。

请添加图片描述

二叉搜索树操作

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

1.二叉搜索树的查找

在二叉查找树b中查找x的过程为:

  1. 若b是空树,则搜索失败,否则:
  2. 若x等于b的根节点的数据域之值,则查找成功;否则:
  3. 若x小于b的根节点的数据域之值,则搜索左子树;否则:
  4. 查找右子树

2.二叉搜索树的插入

向一个二叉搜索树b中插入一个节点s的算法,过程为:

  1. 若b是空树,则将s所指节点作为根节点插入,否则:
  2. s->data等于b的根节点的数据域之值,则返回,否则:
  3. s->data小于b的根节点的数据域之值,则把s所指节点插入到左子树中,否则:
  4. 把s所指节点插入到右子树中。(新插入节点总是叶子节点)

在这里插入图片描述

3.二叉搜索树的删除

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

a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:

情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题–替换法删除

在这里插入图片描述

4.二叉搜索树的遍历

二叉搜索树的遍历最常用的是中序遍历,因为二叉搜索树(BST)的中序遍历结果是升序的。中序遍历是一种遍历二叉树的方式,按照以下规则进行:

  1. 首先遍历左子树。
  2. 然后遍历当前节点。
  3. 最后遍历右子树。

由于BST的性质,即左子树的值都小于当前节点的值,右子树的值都大于当前节点的值,所以中序遍历的过程中,首先访问左子树,然后访问当前节点,最后访问右子树。这导致中序遍历结果是按照升序排列的。

因此,中序遍历是一种有助于获取BST中所有节点按升序排列的有效方法。这个性质也是BST在进行搜索操作时能够快速定位目标值的关键之一。

二叉搜索树的实现

1.二叉搜索树节点结构

template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};
  1. _left:指向左子节点的指针。
  2. _right:指向右子节点的指针。
  3. _key:存储节点的关键字或值。

每个节点都有一个关键字 _key,用来表示节点在二叉搜索树中的位置。这个关键字通常是用来比较节点的大小,以便将节点插入到正确的位置,以满足二叉搜索树的性质。根据节点关键字的大小,节点将被放置在左子树或右子树中。

这是一个模板类,它可以用于创建不同类型的二叉搜索树,只需指定不同的关键字类型 K 即可。这使得代码更加通用,可以适用于不同类型的数据。

2.二叉搜索树类

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
private:
	Node* _root = nullptr;
}

二叉搜索树Binary Search Tree,BST)类模板 BSTree 的定义。这个类包含一个私有成员 _root,它是指向树的根节点的指针。

  • template<class K>:这是一个类模板声明,使用了模板参数 K,它表示关键字或值的数据类型。这允许你在创建 BSTree 实例时指定不同的数据类型。
  • typedef BSTreeNode<K> Node;:这行代码用于定义一个别名 Node,它是 BSTreeNode<K> 类的别名。这个别名使得在类中使用节点类型更加方便。
  • Node* _root = nullptr;:这是一个指向根节点的指针 _root,它初始化为 nullptr,表示树开始为空树。

3.二叉搜索树的构造及析构

构造函数

BSTree() = default;

BSTree() = default; 是C++中的默认构造函数的声明方式。这行代码表示你正在声明一个默认构造函数(无参数的构造函数),并使用 = default 表示使用默认的构造函数实现。

默认构造函数是一个特殊的构造函数,它在对象创建时被自动调用,不接受任何参数。默认构造函数的主要作用是初始化对象的成员变量,确保对象在创建后处于一个合法的初始状态。

使用 = default 表示你希望编译器生成默认的构造函数实现,而不需要自己编写构造函数的定义。这通常用于以下情况:

  1. 你的类不需要特殊的构造逻辑,只需要成员变量的默认初始化。
  2. 你想确保编译器生成默认构造函数,以便可以在不提供自定义构造函数的情况下创建对象。

这里需要注意的是在C++中,如果你提供了自定义的拷贝构造函数,编译器通常不会再自动生成默认的拷贝构造函数,所以我们需要再将默认构造自定义提供

析构函数

~BSTree()
{
    _Destory(_root);
}

private:
	void _Destory(Node*& root)
    {
        if (root == nullptr)
        {
            return;
        }

        _Destory(root->_left);
        _Destory(root->_right);
        delete root;
        root = nullptr;
    }
  1. ~BSTree()
    • 这是BST类的析构函数。析构函数在对象被销毁时自动调用,用于执行清理和释放资源的操作。
    • 在这个析构函数中,它调用了私有辅助函数 _Destory(_root),传递根节点 _root 作为参数,开始递归地销毁整棵树。
  2. _Destory(Node*& root)
    • 这是一个递归辅助函数,用于销毁二叉搜索树中的节点。
    • 函数接受一个指向节点指针的引用作为参数,以便可以修改节点指针,将其设置为 nullptr,以避免悬挂指针问题。
    • 函数首先检查节点是否为空。如果节点为空,表示已经到达树的底部,函数直接返回,不执行任何操作。
    • 如果节点不为空,函数递归地调用自己来销毁左子树和右子树。
    • 然后,函数删除当前节点,释放节点的内存。
    • 最后,将当前节点指针设置为 nullptr,以确保不会再访问已释放的节点。

这样,当你销毁BST对象时,析构函数会从根节点开始递归地销毁整棵树,确保释放所有节点的内存,防止内存泄漏。这是一种正确释放资源的方式,以确保程序的健壮性。

4.二叉搜索树的拷贝构造及赋值重载

拷贝构造

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


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;
    }
  1. BSTree(const BSTree<K>& t)
    • 这是拷贝构造函数,它用于创建一个新的BST对象,该对象是根据另一个BST对象 t 进行拷贝构造的。
    • 在构造函数内部,它调用了私有的辅助函数 _Copy(t._root),传递 t 的根节点 _root 作为参数,从而创建了一个新的BST。
  2. _Copy(Node* root)
    • 这是一个递归辅助函数,用于复制一棵二叉树。
    • 如果传入的根节点 root 为空(nullptr),则返回 nullptr,表示空树。
    • 如果根节点不为空,函数首先创建一个新的节点 copyRoot,并将其关键字设置为 root 节点的关键字。
    • 然后,函数递归地调用自己来复制左子树和右子树,并将它们分别赋值给 copyRoot->_leftcopyRoot->_right
    • 最后,返回 copyRoot,这样递归过程会构建出一棵与原始树相同结构和内容的树。

这样,拷贝构造函数 _Copy 会递归地复制整棵树,确保你创建的新BST对象与原始对象具有相同的结构和内容。这对于在不破坏原始树的情况下创建一个新树非常有用。

5.二叉搜索树插入

迭代写法

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

		return true;
	}
  1. 首先,函数检查树是否为空(即 _root 是否为 nullptr)。如果树为空,它会创建一个新的根节点,该节点的关键字为 key,然后返回 true 表示插入成功。
  2. 如果树不为空,函数会进入一个循环,用来搜索插入位置。循环中的代码执行以下操作:
    • 如果当前节点的关键字小于 key,则继续向右子树移动,将 parent 设置为当前节点,以便稍后插入新节点到右子树。
    • 如果当前节点的关键字大于 key,则继续向左子树移动,同样将 parent 设置为当前节点,以便稍后插入新节点到左子树。
    • 如果当前节点的关键字等于 key,则返回 false,表示不允许重复关键字的节点插入。
  3. 一旦找到插入位置,函数创建一个新的节点 cur,该节点的关键字为 key
  4. 接着,根据 parent 节点的关键字与 key 的比较结果,将新节点 cur 插入到正确的位置。如果 parent->_key < key,则将 cur 插入为 parent 的右子节点,否则插入为 parent 的左子节点。
  5. 最后,函数返回 true,表示插入成功。

这个 Insert 函数实现了BST的插入操作,它确保新节点按照BST的有序性质被插入到正确的位置。如果关键字已经存在于树中,函数会返回 false,表示插入失败(因为BST不允许重复关键字)。

递归写法

bool InsertR(const K& key)
{
    return _InsertR(_root, key);
}	
private:
	bool _InsertR(Node*& root, const K& key)
    {
        if (root == nullptr)
        {
            root = new Node(key);
            return true;
        }

        if (root->_key < key)
            return _InsertR(root->_right, key);
        else if (root->_key > key)
            return _InsertR(root->_left, key);
        else
            return false;
    }
  1. bool InsertR(const K& key)
    • 这是公有函数,用于在二叉搜索树中插入新节点。它首先调用私有递归函数 _InsertR,并将根节点 _root 和要插入的关键字 key 作为参数传递给它。
  2. bool _InsertR(Node*& root, const K& key)
    • 这是私有递归辅助函数,用于在给定的二叉搜索树中插入新节点。
    • 函数首先检查当前根节点 root 是否为空。如果为空,表示找到了插入位置,它会创建一个新节点并将其关键字设置为 key,然后将 root 指向新节点,最后返回 true 表示插入成功。
    • 如果当前根节点 root 不为空,它会比较当前节点的关键字 root->_key与要插入的关键字 key
      • 如果 root->_key < key,则递归地在右子树中插入新节点,调用 _InsertR(root->_right, key)
      • 如果 root->_key > key,则递归地在左子树中插入新节点,调用 _InsertR(root->_left, key)
      • 如果 root->_key == key,表示已经存在相同关键字的节点,直接返回 false 表示插入失败。

6.二叉搜索树查找

迭代写法

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 Find(const K& key)

  • 这是一个公有函数,用于查找二叉搜索树中是否存在指定关键字 key
  • 函数首先初始化一个指针 cur 为根节点 _root,然后进入一个循环。
  • 在循环中,它会不断根据当前节点的关键字与目标关键字 key 的比较结果来决定向左子树还是右子树移动:
    • 如果当前节点的关键字小于 key,则说明要查找的关键字应该在当前节点的右子树中,所以将 cur 指向右子节点 cur->_right
    • 如果当前节点的关键字大于 key,则说明要查找的关键字应该在当前节点的左子树中,所以将 cur 指向左子节点 cur->_left
    • 如果当前节点的关键字等于 key,则表示找到了目标关键字,函数返回 true 表示查找成功。
  • 循环会一直进行,直到 cur 指向空(nullptr),这意味着整棵树都被搜索过了,但没有找到目标关键字,所以函数返回 false 表示查找失败。

递归写法

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 _FindR(root->_right, key);
    else if (root->_key > key)
        return _FindR(root->_left, key);
    else
        return true;
}
  1. bool FindR(const K& key)
    • 这是公有函数,用于在二叉搜索树中查找指定关键字 key 是否存在。它首先调用私有的递归函数 _FindR,并将根节点 _root 和要查找的关键字 key 作为参数传递给它。
  2. bool _FindR(Node* root, const K& key)
    • 这是私有的递归辅助函数,用于在给定的二叉搜索树中递归查找指定关键字 key 是否存在。
    • 函数首先检查当前根节点 root 是否为空(nullptr)。如果为空,表示已经搜索到了树的底部,关键字 key 不存在于树中,所以返回 false 表示查找失败。
    • 如果当前根节点root 不为空,函数会比较当前节点的关键字 root->_key 与要查找的关键字 key
      • 如果 root->_key < key,则说明要查找的关键字应该在当前节点的右子树中,所以递归调用 _FindR(root->_right, key) 在右子树中查找。
      • 如果 root->_key > key,则说明要查找的关键字应该在当前节点的左子树中,所以递归调用 _FindR(root->_left, key) 在左子树中查找。
      • 如果 root->_key == key,表示找到了目标关键字,函数返回 true 表示查找成功。

这个递归查找操作会在树的节点之间不断递归地进行,直到找到目标关键字或确认目标不存在。如果目标关键字存在于树中,函数返回 true;否则,返回 false

7.二叉搜索树删除

迭代写法

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 = 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 (_root == cur)
                {
                    _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;
                Node* min = cur->_right;
                while (min->_left)
                {
                    minParent = min;
                    min = min->_left;
                }

                swap(cur->_key, min->_key);
                if (minParent->_left == min)
                    minParent->_left = min->_right;
                else
                    minParent->_right = min->_right;

                delete min;
            }

            return true;
        }
    }

    return false;
}
  1. 首先,函数初始化两个指针 parentcur 分别为 nullptr 和根节点 _root,然后进入一个循环。这个循环用于在BST中找到待删除节点,并进行删除操作。
  2. 在循环中,函数会不断根据当前节点的关键字与目标关键字 key 的比较结果来决定向左子树还是右子树移动,同时更新 parentcur 指针。
  3. 如果找到了目标关键字 key,表示要开始删除操作。删除操作分为以下几种情况:
    • 情况1:当前节点的左子树为空。在这种情况下,直接用当前节点的右子树替换当前节点即可。
    • 情况2:当前节点的右子树为空。在这种情况下,直接用当前节点的左子树替换当前节点即可。
    • 情况3:当前节点的左右子树都不为空。在这种情况下,需要找到右子树中的最小节点(通常是右子树中最左边的节点),将其关键字与当前节点的关键字进行交换,然后删除最小节点。
  4. 删除操作执行完毕后,函数返回 true 表示删除成功。
  5. 如果循环结束时仍然没有找到目标关键字 key,说明关键字不存在于树中,函数返回 false 表示删除失败。

递归写法

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

    if (root->_key < key)
        return _EraseR(root->_right, key);
    else if (root->_key > key)
        return _EraseR(root->_left, key);
    else
    {
        Node* del = root;
        if (root->_left == nullptr)
        {
            root = root->_right;
        }
        else if (root->_right == nullptr)
        {
            root = root->_left;
        }
        else
        {
            // 找右树的最左节点替换删除
            Node* min = root->_right;
            while (min->_left)
            {
                min = min->_left;
            }
            swap(root->_key, min->_key);
            return _EraseR(root->_right, key);
        }

        delete del;
        return true;
    }
}
  1. bool EraseR(const K& key)
    • 这是公有函数,用于在二叉搜索树中删除指定关键字 key 的节点。它首先调用私有的递归函数 _EraseR,并将根节点 _root 和要删除的关键字 key 作为参数传递给它。
  2. bool _EraseR(Node*& root, const K& key)
    • 这是私有的递归辅助函数,用于在给定的二叉搜索树中递归删除指定关键字 key 的节点。
    • 函数首先检查当前根节点 root 是否为空(nullptr)。如果为空,表示已经搜索到了树的底部,关键字 key 不存在于树中,所以返回 false 表示删除失败。
    • 如果当前根节点 root 不为空,函数会比较当前节点的关键字 root->_key与要删除的关键字 key
      • 如果 root->_key < key,则说明要删除的关键字应该在当前节点的右子树中,所以递归调用 _EraseR(root->_right, key) 在右子树中进行删除操作。
      • 如果 root->_key > key,则说明要删除的关键字应该在当前节点的左子树中,所以递归调用 _EraseR(root->_left, key) 在左子树中进行删除操作。
      • 如果 root->_key == key,表示找到了要删除的目标节点,此时需要执行删除操作。
  3. 删除操作执行的逻辑分为以下几种情况:
    • 情况1:当前节点的左子树为空。在这种情况下,可以直接用当前节点的右子树替换当前节点。
    • 情况2:当前节点的右子树为空。在这种情况下,可以直接用当前节点的左子树替换当前节点。
    • 情况3:当前节点的左右子树都不为空。在这种情况下,需要找到右子树中的最小节点,通常是右子树中最左边的节点,将其关键字与当前节点的关键字进行交换,然后继续递归删除右子树中的最小节点。
  4. 删除操作执行完毕后,函数返回 true 表示删除成功。
  5. 如果循环结束时仍然没有找到目标关键字 key,说明关键字不存在于树中,函数返回 false 表示删除失败。

8.二叉搜索树中序遍历

void InOrder()
{
    _InOrder(_root);
    cout << endl;
}
private:
	void _InOrder(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }

        _InOrder(root->_left);
        cout << root->_key << " ";
        _InOrder(root->_right);
    }
  1. void InOrder()
    • 这是公有函数,用于执行中序遍历二叉搜索树的操作。
    • 首先,它调用了私有的递归函数 _InOrder(_root),并传递根节点 _root 作为参数,开始进行中序遍历。
    • 遍历完成后,会输出一个换行符,以便在输出时每个节点值都在一行上。
  2. void _InOrder(Node* root)
    • 这是私有的递归辅助函数,用于执行中序遍历的实际操作。
    • 函数首先检查当前根节点 root 是否为空。如果为空,表示到达树的底部,直接返回,不执行任何操作。
    • 如果当前节点不为空,函数按照中序遍历的顺序执行以下操作:
      • 递归调用 _InOrder(root->_left),遍历左子树。
      • 输出当前节点的关键字值 root->_key,通常是将节点值打印到控制台。
      • 递归调用 _InOrder(root->_right),遍历右子树。

这个中序遍历函数会遍历整个二叉搜索树,并按照从小到大的顺序输出节点的关键字值。这是一种常用的方式来访问二叉搜索树的所有节点,并按照升序排列输出它们的值。

9.二叉搜索树所有代码

namespace 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>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		bool Insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new Node(key);
				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);
			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 = 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 (_root == cur)
						{
							_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;
						Node* min = cur->_right;
						while (min->_left)
						{
							minParent = min;
							min = min->_left;
						}

						swap(cur->_key, min->_key);
						if (minParent->_left == min)
							minParent->_left = min->_right;
						else
							minParent->_right = min->_right;

						delete min;
					}

					return true;
				}
			}

			return false;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

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

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

		~BSTree()
		{
			_Destory(_root);
		}

		BSTree() = default;

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

		// t2 = t1
		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 _Destory(Node*& root)
		{
			if (root == nullptr)
			{
				return;
			}

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


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

			if (root->_key < key)
				return _EraseR(root->_right, key);
			else if (root->_key > key)
				return _EraseR(root->_left, key);
			else
			{
				Node* del = root;
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					// 找右树的最左节点替换删除
					Node* min = root->_right;
					while (min->_left)
					{
						min = min->_left;
					}
					swap(root->_key, min->_key);
					return _EraseR(root->_right, key);
				}

				delete del;
				return true;
			}
		}

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

			if (root->_key < key)
				return _InsertR(root->_right, key);
			else if (root->_key > key)
				return _InsertR(root->_left, key);
			else
				return false;
		}

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

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

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

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

二叉搜索树的应用

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

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

  2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见

    比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对

    再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

例如

namespace KeyValue
{
	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;
	};

	void TestBSTree1()
	{
		BSTree<string, string> dict;
		dict.Insert("sort", "排序");
		dict.Insert("left", "左");
		dict.Insert("right", "右");
		dict.Insert("vector", "向量");
		dict.Insert("insert", "插入");
		string str;
		while (cin >> str)
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << "对应的中文:" << ret->_value << endl;
			}
			else
			{
				cout << "对应的中文->无此单词" << endl;
			}
		}
	}

	void TestBSTree2()
	{
		string arr[] = {"苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉"};

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

		countTree.InOrder();
	}
}

二叉搜索树的性能分析

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

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

在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为log_2 N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为frac{N}{2}

优点:

  1. 快速的查找操作: BST的主要优点是在查找操作上具有较快的平均时间复杂度。在平均情况下,查找一个元素的时间复杂度是O(log n),其中n是树中节点的数量。这使得BST非常适合实现高效的搜索操作。
  2. 有序性: BST保持节点按照键的顺序排列。这意味着你可以轻松地执行范围查询,找到最小值和最大值,或者执行有序的遍历。
  3. 插入和删除操作: 插入和删除元素的平均时间复杂度也是O(log n)。这使得BST对于需要频繁插入和删除元素的应用非常有效。

缺点:

  1. 性能不稳定: 最坏情况下,BST的性能可能会下降到O(n),其中n是树中节点的数量。这种情况发生在BST变得非常不平衡时,例如,如果将有序数据插入BST,将导致树的高度为n,从而导致O(n)的查找时间。
  2. 不平衡性: BST的性能高度依赖于树的平衡性。如果树不平衡,例如,变成了链表,那么查找、插入和删除的性能都会下降到O(n)。为了解决这个问题,出现了平衡二叉搜索树(AVL树、红黑树等),它们确保树的高度保持在较小的范围内,以维持O(log n)的性能。
  3. 内存需求: BST通常需要额外的内存来存储左子树和右子树的指针,这可能会导致内存消耗较大。

总结来说,BST在平均情况下具有较好的性能,特别适用于有序数据的查找和动态数据的插入/删除操作。然而,在最坏情况下,BST的性能可能变得不稳定,因此为了确保性能稳定,可以使用平衡二叉搜索树或其他更复杂的数据结构。选择适当的数据结构取决于应用的需求和数据的特点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/102130.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

设置微软Edge浏览器主页和新标签页,摆脱扰人和分散注意力的主页

默认情况下&#xff0c;Microsoft Edge会向您显示世界上最令人分心和讨厌的主页&#xff08;也称为主屏幕&#xff09;。微软不想只向你展示一个搜索框&#xff0c;也许还有一个漂亮的背景或一些你喜欢的网站的快捷方式&#xff0c;而是想在你面前扔一堆新闻标题和广告。 你可…

抽象轻松的C语言

#include <stdio.h> /* 预处理指令*/ /* 函数 */ int main() {int log 3.14;printf("hello word * %d\n easy", log);getchar();/* 获取键盘输入的字母&#xff0c;在这个程序中的作用是防止程序瞬间关闭 */return 0; } 上一篇说过&#xff0c;C程序是C语言的…

Web网站服务器

目录 一、什么是Apache? 二、虚拟目录是什么&#xff1f; 三、Apcahe相关配置文件 四、httpd.conf主配置文件的常用配置参数 五、Web网站配置案例 5.1搭建基于用户的个人主页网站 5.2、配置虚拟目录 5.3、配置虚拟主机 5.3.1搭建两个基于IP地址的虚拟主机 5.3.2搭建两个基于域…

【一等奖方案】大规模金融图数据中异常风险行为模式挖掘赛题「NUFE」解题思路

第十届CCF大数据与计算智能大赛&#xff08;2022 CCF BDCI&#xff09;已圆满结束&#xff0c;大赛官方竞赛平台DataFountain&#xff08;简称DF平台&#xff09;正在陆续释出各赛题获奖队伍的方案思路&#xff0c;欢迎广大数据科学家交流讨论。 本方案为【大规模金融图数据中…

JDBC使用了哪种设计模式

JDK中提供了操作数据库的接口&#xff0c;比如 java.sql.Driver java.sql.Connection java.sql.Statement java.sql.PreparedStatement 不同的数据库厂商提供操作自己数据库的驱动包&#xff0c; 比如mysql public class Driver extends NonRegisteringDriver implements jav…

10. selenium API (二)

目录 1. 多层框架/窗口定位 2. 下拉框处理 2.1 前端界面 2.2 代码 3. 针对 alert 弹窗进行操作 3.1 前端界面 3.2 代码 4. 文件提交 4.1 前端界面 4.2 代码 5. 显示等待 6. 操作浏览器滚动条 7. 截图 8. 浏览器关闭 9. 窗口切换 在上篇文章中&#xff0c;我们学…

Django请求的生命周期

Django请求的生命周期是指: 当用户在浏览器上输入URL到用户看到网页的这个时间段内&#xff0c;Django后台所发生的事情。 直白的来说就是当请求来的时候和请求走的阶段中&#xff0c;Django的执行轨迹。 一个完整的Django生命周期: 用户从客户端发出一条请求以后&#xff…

css3英文文字换行,超过两行...展示

需求&#xff1a;超过两行...展示 开发的过程中发现div内容中文可以换行英文不换行&#xff0c;导致长度会溢出。 是英文全英文的话浏览器会解析成一个单词&#xff0c; 加上这句就好了 word-break:break-all; 一开始不知道是会解析成一个单词&#xff0c;用字符串拼接处理…

华为云新生代开发者招募

开发者您好&#xff0c;我们是华为2012UCD的研究团队 为了解年轻开发者的开发现状和趋势 正在邀请各位先锋开发者&#xff0c;与我们进行2小时的线上交流&#xff08;江浙沪附近可线下交流&#xff09; 聊聊您日常开发工作中的产品使用需求 成功参与访谈者将获得至少300元京…

基于亚马逊云科技无服务器服务快速搭建电商平台——性能篇

使用 Serverless 构建独立站的优势 在传统架构模式下&#xff0c;如果需要进行电商大促需要提前预置计算资源以支撑高并发访问&#xff0c;会造成计算资源浪费并且增加运维工作量。本文介绍一种新的部署方式&#xff0c;将 WordPress 和 WooCommerce 部署在 Amazon Lambda 中。…

CVPR2022 Semi-Supervised Semantic Segmentation Using Unreliable Pseudo-Labels

Semi-Supervised Semantic Segmentation Using Unreliable Pseudo-Labels 使用不可靠的伪标签的半监督语义分割 Paper&#xff1a;https://openaccess.thecvf.com/content/CVPR2022/html/Wang_Semi-Supervised_Semantic_Segmentation_Using_Unreliable_Pseudo-Labels_CVPR_202…

Docker 容器逃逸漏洞 (CVE-2020-15257)复现

漏洞概述 containerd是行业标准的容器运行时&#xff0c;可作为Linux和Windows的守护程序使用。在版本1.3.9和1.4.3之前的容器中&#xff0c;容器填充的API不正确地暴露给主机网络容器。填充程序的API套接字的访问控制验证了连接过程的有效UID为0&#xff0c;但没有以其他方式…

C语言:大小端字节序存储

一、大小端字节序存储介绍 大端字节序存储模式&#xff1a;把一个数据低位字节处的数据存放在高地址处&#xff0c;数据高位字节处的数据存放在低地址处 小端字节序存储模式&#xff1a;把一个数据低位字节处的数据存放在低地址处&#xff0c;数据高位字节处的数据存放在高地址…

论文阅读_医疗知识图谱_GraphCare

英文名称: GraphCare: Enhancing Healthcare Predictions with Open-World Personalized Knowledge Graphs 中文名称: GraphCare&#xff1a;通过开放世界的个性化知识图增强医疗保健预测 文章: http://arxiv.org/abs/2305.12788 代码: https://github.com/pat-jj/GraphCare 作…

mall :hutool项目源码解析

文章目录 一、mall开源项目1.1 来源1.2 项目转移1.3 项目克隆 二、Hutool工具类库2.1 Hutool 简介 三、源码解析3.1 集成与配置3.1.1 导入依赖3.1.2 添加配置 3.2 核心工具类3.2.1 AnnotationUtil使用&#xff1a;注解工具类3.2.2 BeanUtil使用&#xff1a;JavaBean的工具类3.2…

redis实战-实现优惠券秒杀解决超卖问题

全局唯一ID 唯一ID的必要性 每个店铺都可以发布优惠券&#xff1a; 当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就存在一些问题&#xff1a; id的规律性太明显&#xff0c;容易被用户根据id的间隔来猜测…

【Linux】【驱动】注册字符设备号

【Linux】【驱动】注册字符设备号 1. 绪论1 、静态分配设备号2、动态分配设备号3、注销设备号 2 实现的代码3 加载驱动程序 1. 绪论 在之前杂项设备的时候&#xff0c;设备号是固定的&#xff0c;字符设备就需要自己去申请设备号了&#xff0c; 申请设备号有两个方式&#xff…

Python入门教程 - 基本语法 (一)

目录 一、注释 二、Python的六种数据类型 三、字符串、数字 控制台输出练习 四、变量及基本运算 五、type()语句查看数据的类型 六、字符串的3种不同定义方式 七、数据类型之间的转换 八、标识符命名规则规范 九、算数运算符 十、赋值运算符 十一、字符串扩展 11.1…

如何飞速成为开源贡献者(Contributor)

如何飞速成为开源贡献者Contributor 一、环境信息1.1 硬件信息1.2 软件信息 二、Git安装2.1 Git介绍2.2 Git下载安装 三、开源项目选定四、GitHub参与开源流程4.1 Fork项目4.2 SSH配置4.2.1 为什么要配置SSH4.2.2 如何配置SSH 4.3 Clone项目4.4 IDEA关联4.5 PR生成4.6 PR提交 一…

OceanBase 4.x改装:另一种全链路追踪的尝试

本文作者&#xff1a;夏克 OceanBase 社区文档贡献者&#xff0c;曾多次参与 OceanBase 技术征文比赛&#xff0c;获得优秀名次。从事金融行业核心系统设计开发工作多年&#xff0c;服务于某交易所子公司&#xff0c;现阶段负责国产数据库调研。 本文为 OceanBase 第七期技术征…