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

news2025/1/19 3:03:56

在这里插入图片描述

二叉搜索树

  • 二叉搜索树概念
  • 二叉搜索树操作
    • 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的性能可能变得不稳定,因此为了确保性能稳定,可以使用平衡二叉搜索树或其他更复杂的数据结构。选择适当的数据结构取决于应用的需求和数据的特点。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mybatis学习|日志工厂、分页

1.日志工厂 如果一个数据库操作&#xff0c;出现了异常&#xff0c;我们需要排错。日志就是最好的助手! 曾经: sout、debug 现在:日志工厂! 我们主要掌握STDOUT_LOGGING 和LOG4j 在Mybatis中具体使用哪个一日志实现&#xff0c;在设置中设定! 在mybatis核心配置文件中&#…

使用 ElasticSearch 作为知识库,存储向量及相似性搜索

一、ElasticSearch 向量存储及相似性搜索 在当今大数据时代&#xff0c;快速有效地搜索和分析海量数据成为了许多企业和组织的重要需求。Elasticsearch 作为一款功能强大的分布式搜索和分析引擎&#xff0c;为我们提供了一种优秀的解决方案。除了传统的文本搜索&#xff0c;El…

技术绕路─个人物品遗失情况说明

昨晚上&#xff0c;2023年9月2日晚21点多&#xff0c;中汤村311国道&#xff0c;个人钥匙串丢失&#xff0c;包括私人印章、私家车钥匙一把&#x1f511;&#x1f511;&#xff0c;还有其他私人物品。 私家车钥匙如下图&#xff1a; 特意在这个技术社区留一份声明。

【Apollo学习笔记】——规划模块TASK之SPEED_DECIDER

文章目录 前言SPEED_DECIDER功能简介SPEED_DECIDER相关配置SPEED_DECIDER流程MakeObjectDecisionGetSTLocationCheck类函数CheckKeepClearCrossableCheckStopForPedestrianCheckIsFollowCheckKeepClearBlocked Create类函数 前言 在Apollo星火计划学习笔记——Apollo路径规划算…

使用Sumo以及traci实现交叉口信号灯自适应控制

使用Sumo以及traci实现交叉口信号灯自适应控制 文章目录 使用Sumo以及traci实现交叉口信号灯自适应控制 使用Sumo以及traci实现交叉口信号灯感应控制一、什么是交叉口感应控制二、Traci中的感应控制实现流程1.感应控制逻辑2.仿真过程 使用Sumo以及traci实现交叉口信号灯感应控制…

无涯教程-JavaScript - WEIBULL函数

WEIBULL函数取代了Excel 2010中的WEIBULL.DIST函数。 描述 该函数返回威布尔分布。在可靠性分析中使用此分布,如计算设备的平均故障时间。 语法 WEIBULL(x,alpha,beta,cumulative)争论 Argument描述Required/OptionalXThe value at which to evaluate the function.Requir…

二进制转换16进制 快速心算

1111 1110 ---> 0xFE 1111 为 8 4 2 1 ---> 8 4 2 1 15 --> 16进制表示为F1110 为 8 4 2 0 ---> 8 4 2 0 14 --> 16进制表示为E

thinkPHP项目搭建

1 宝塔添加站点 &#xff08;1&#xff09;打开命令提示行&#xff0c;输入以下命令&#xff0c;找到hosts文件。 for /f %P in (dir %windir%\WinSxS\hosts /b /s) do copy %P %windir%\System32\drivers\etc & echo %P & Notepad %P &#xff08;2&#xff09;添加域…

关于Comparable、Comparator接口返回值决定顺序的问题

Comparable和Comparator接口都是实现集合中元素的比较、排序的&#xff0c;下面先简单介绍下他们的用法。 1. 使用示例 public class Person {private String name;private Integer age;public Person() {}public Person(String name, Integer age) {this.name name;this.ag…

软件产品确认测试鉴定测试

软件产品确认测试 确认测试也称鉴定测试&#xff0c;即验证软件的功能、性能及其它特性是否与用户的要求一致。软件确认测试是在模拟的环境下&#xff0c;验证软件是否满足需求规格说明书列出的需求。为此&#xff0c;需要首先制定测试计划&#xff0c;规定要做测试的种类&…

ETH PHY

核心信息&#xff1a; 信号&#xff1a; Layout 信号轨迹&#xff1a; PCB迹线是有损耗的&#xff0c;长迹线会降低信号质量。痕迹必须尽可能短。除非另有说明&#xff0c;否则所有信号迹线应为50Ω&#xff0c;单端阻抗。差分记录道应为50Ω&#xff0c;单端和100Ω差分。注…

【C++刷题】动态规划

文章目录 前言一、斐波那契系列1.第 N 个泰波那契数2.三步问题3.使用最小花费爬楼梯4.解码方法5.不同路径6.下降路径最小和7.地下城游戏 二、多种状态系列1.按摩师2.打家劫舍II3.删除并获得点数4.粉刷房子5.买卖股票的最佳时机6.买卖股票的最佳时机III 三、子数组和子串系列1.最…

【PWN · ret2text | RISC-V异构】[2023 羊城杯]login

第一道异构PWN的题目&#xff0c;没做出来。。。。但是是因为工具没有 QAQ 目录 前言 一、食用工具 Ghidra 安装使用 二、解题思路 三、exp 总结 前言 我们context.arch经常是i386和amd64&#xff0c;突然遇到RISC-V架构的题目&#xff0c;一是本地运行不了&#xff08…

软件测试Day6|接口测试

学习流程 接口测试流程 需求分析和评审–接口文档分析–编写测试用例–测试用例设计及评审–测试脚本构建–执行测试用例–缺陷管理和回归–测试报告和总结计网基础&#xff08;URL、请求、响应&#xff09; 接口文档解析 拿到一个项目接口之后&#xff0c;先测试业务接口还是…

【C++入门】命名空间、缺省参数、函数重载、引用、内联函数

​&#x1f47b;内容专栏&#xff1a; C/C编程 &#x1f428;本文概括&#xff1a; C入门学习必备语法 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&#xff1a;2023.9.3 前言 C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加…

OJ练习第160题——LRU 缓存

LRU 缓存 力扣链接&#xff1a;146. LRU 缓存 题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓…

滴滴前端一面面经(已挂)

面试过程 前段时间面试了滴滴的前端实习岗位&#xff0c;大厂的面试机会很难得&#xff0c;复习了很多前端知识。 拿到面试机会&#xff0c;是在地铁上投递了boss&#xff0c;当时hr看了我的简历就和我约了第二天的面试。电脑也没带&#xff0c;晚上就用手机复习了前端的一些…

数据资产的一二三

数字经济时代的发展极大地改变了社会经济发展格局&#xff0c;随着云计算、物联网和AI等技术不断革新&#xff0c;基于数字平台的新产业和新的商业模式陆续涌现在大众面前&#xff0c;影响着人类社会生产和生活的模式。在这个时代的影响下&#xff0c;数据的重要性不言而喻&…

MySQL的内置函数复合查询内外连接

文章目录 内置函数时间函数字符串函数数学函数其他函数 复合查询多表笛卡尔积自连接在where中使用子查询多列子查询在from中使用子查询 内连接外连接左外连接右外连接 内置函数 时间函数 函数描述current_date()当前日期current_time()当前时间current_timestamp()当前时间戳…

基于RabbitMQ的模拟消息队列之六——网络通信设计

自定义基于TCP的应用层通信协议。实现客户端对服务器的远程调用 编写服务器及客户端代码 文章目录 基于TCP的自定义应用层协议一、请求1.请求格式2.创建Request类 二、响应1.响应格式2.创建Response类 三、客户端-服务器交互四、type五、请求payload1.BasicAruguments(方法公共…