图解:什么是二叉查找树?

news2024/11/20 9:13:12

文章目录

  • 1. 二叉查找树的概念
  • 2. 二叉查找树的实现
    • 🍑 定义节点
    • 🍑 函数接口总览
    • 🍑 构造函数
    • 🍑 拷贝构造
    • 🍑 赋值重载
    • 🍑 析构函数
    • 🍑 查找操作
      • 🍅 动图演示
      • 🍅 非递归实现
      • 🍅 递归实现
    • 🍑 插入操作
      • 🍅 动图演示
      • 🍅 非递归实现
      • 🍅 递归实现
    • 🍑 删除操作
      • 🍅 非递归实现
      • 🍅 递归实现
    • 🍑 中序遍历
  • 3. 二叉查找树的性能分析


1. 二叉查找树的概念

还记得我们之前学过的二叉树吗?

二又树是树的一种特殊形式,每个节点最多有 2 个孩子节点,下图就是一棵典型的二叉树:

在这里插入图片描述

那什么是二叉查找树呢?

二叉查找树(Binary Search Tree),也称二叉排序树或二叉搜索树,顾名思义,是用来查找数据的,它在二叉树的基础上,增加了几个规则:

  • 如果左子树不为空,则左子树上所有节点的值均小于根节点的值。
  • 如果右子树不为空,则右子树上所有节点的值均大于根节点的值。
  • 左、右子树也都是二叉搜索树。

下图就是一棵标准的二叉查找树:

在这里插入图片描述

这样一棵树,如何进行查找呢?

比如我们要查找的值是 6,查找过程如下:

(1)访问根节点 8,发现 6 < 8。

在这里插入图片描述

(2)访问根节点 8 的左孩子节点 3,发现 6 > 3。

在这里插入图片描述

(3)访问节点 3 的右孩子节点 6,发现正是要查找的节点。

在这里插入图片描述

对于一个节点分布相对平衡的二叉查找树,如果节点总数是 n,那么查找节点的时间复杂度就是 O ( l o g n ) O(logn) O(logn),和树的深度成正比。

另外,二叉查找树不仅可以用于查找,还有一个重要功能:维持节点的有序性。

我们来给这棵二叉树做一个中序遍历,先访问左子树,再访问根节点,最后访问右子树。

因此,对于上面例子中的二叉查找树,中序遍历的访问顺序(节点旁边的数字)如下:

在这里插入图片描述

按顺序输出,结果为:1, 3, 4, 6, 7, 8, 10, 13, 14

输出结果完全按照升序排列,二叉查找树保持了有序性!

正是这个原因,二叉查找树也被称为二叉排序树(Binary Sort Tree)。这样的二叉树无论进行多少次插入,删除操作,都始终保持有序。

2. 二叉查找树的实现

前面的二叉树是用 C 语言来实现的,那么这次的二叉查找树我们选择用 C++ 来实现。

🍑 定义节点

首先构建一个二叉树查找树的节点类,并且实现一个构造函数

// 节点类
template<class K>
struct BSTreeNode
{
	K _key; // 节点值
	BSTreeNode<K>* _left; // 左指针
	BSTreeNode<K>* _right; // 右指针

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

🍑 函数接口总览

有了节点以后,我们就可以来实现二叉查找树了,下面是所有的接口函数:

//二叉搜索树
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node; // 把节点重定义成Node
public:
	// 构造函数
	BSTree();

	// 拷贝构造
	BSTree(const BSTree<K>& t);

	// 赋值重载
	BSTree<K>& operator=(BSTree<K> t);

	// 析构函数
	~BSTree();

	// 插入函数
	bool Insert(const K& key);

	// 删除函数
	bool Erase(const K& key);

	// 查找函数
	bool Find(const K& key);

	// 中序遍历
	void InOrder();
private:
	Node* _root; //指向二叉搜索树的根结点
};

🍑 构造函数

因为我们待会儿要实现拷贝构造,而在 C++ 里面,不管是拷贝构造,还是构造,只要是显示的写了,那么编译器就不会默认生成,所以我们可以用一个关键字 default 强制编译器自己生成构造函数。

public:
	// 构造函数(强制编译器自己生成)
	BSTree() = default;

🍑 拷贝构造

如果我们不实现拷贝构造的话,编译器会去调用默认的拷贝构造,而默认的拷贝构造是浅拷贝(也就是值拷贝),会引发析构两次的问题,所以我们需要自己去实现一个拷贝构造完成深拷贝。

private:
	Node* CopyTree(Node* root)
	{
		// 如果是空树,直接返回空
		if (root == nullptr)
			return nullptr;

		Node* copyNode = new Node(root->_key); // 拷贝根结点
		copyNode->_left = CopyTree(root->_left); // 拷贝左子树
		copyNode->_right = CopyTree(root->_right); // 拷贝右子树

		return copyNode; // 返回拷贝的树
	}
public:
	// 拷贝构造(深拷贝)
	BSTree(const BSTree<K>& t)
	{
		_root = CopyTree(t._root);
	}

这里的深拷贝其实就是前序遍历递归创建的过程。

那么为什么我们要 CopyTree 函数并且封装在私有域里面呢?

如果我们递归调用需要传 _root,在 C 语言中可以直接传递,但是 C++ 涉及到封装,根变成了私有,怎么传呢?

所以需要写一个子函数,然后去调用。

🍑 赋值重载

赋值重载函数的实现很简单,比如 BSTree<int> copy = t

我们知道函数传参如果不是引用而是对象的话就会调用拷贝构造函数,所以我们只需将这个拷贝构造出来的对象 copythis 指向的对象 t 进行交换,就相当于完成了赋值操作,而拷贝构造出来的对象 copy 会在该赋值运算符重载函数调用结束时自动析构。

public:
	// 赋值重载
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

🍑 析构函数

我们析构可以用递归的方式来实现,那么就要去写一个 Destroy 子函数,采用后序遍历的方式去释放每一个节点,当树中的结点被全部释放完以后,将对象当中指向二叉查找树的指针及时置空。

private:
	// 递归销毁函数
	void DestroyTree(Node* root)
	{
		// 如果是空树,直接返回空
		if (root == nullptr)
			return;

		DestroyTree(root->_left); // 递归释放左子树中的节点
		DestroyTree(root->_right); // 递归释放右子树中的节点
		delete root; // 删除根结点
	}
public:
	// 析构函数
	~BSTree()
	{
		DestroyTree(_root); // 释放二叉查找树中的节点
		_root = nullptr; // 把根节点置为空
	}

注意:析构函数没有参数,那么就不能递归,所以这里套了一层子函数

🍑 查找操作

二叉查找树的查找操作与二分查找非常相似:

  • 如果要查找的树为空树,则直接返回空。
  • 如果不为空树,则从根节点开始比较,查找。
    • 若查找的值比根大,则往根的右子树查找
    • 若查找的值比根小,则往根的左子树查找。
    • 若查找的值等于当前节点的值,则查找成功,返回对应节点的地址。
  • 最多查找高度次,走到空,还没找到,这个值不存在。

现在我们要在下面这棵树中查找值为 13 的节点。

在这里插入图片描述

第一步:访问跟节点 8

在这里插入图片描述

第二步:根据二叉查找树的左子树均比根节点小,右子树均比根节点大的性质, 13 > 8 ,因此值为 13 的节点可能在根节点 8 的右子树当中,我们查看根节点的右子节点 10

在这里插入图片描述

第三步:与第二步相似, 13 > 10 ,因此查看节点 10 的右孩子 14

在这里插入图片描述

第四步:根据二叉查找树的左子树均比根节点小,右子树均比根节点大的性质, 13 < 14 ,因此查看 14 的左孩子 13 ,发现刚好和要查找的值相等:

在这里插入图片描述

🍅 动图演示

在下面动图中,假设要查找值为 50 的节点:

在这里插入图片描述

对于二叉查找树的查找代码,这里给出两个版本供大家参考:递归和非递归。

🍅 非递归实现

二叉查找树的查找函数非递归实现:

public:
	// 查找函数
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key) // 如果key值大于当前节点的值
			{
				cur = cur->_right; // 就去当前节点的右子树当中查找
			}
			else if (cur->_key > key) // 如果key值小于当前节点的值
			{
				cur = cur->_left; // 就去当前节点的左子树当中查找
			}
			else // 当前节点的值等于key值
			{
				return true; // 说明找到了,直接返回true
			}
		}
		// 树为空或查找失败,返回false
		return false;
	}

🍅 递归实现

二叉查找树的查找函数递归实现(递归涉及到传参的问题,所以需要写一个子函数):

private:
	// 查找函数(递归实现的子函数)
	bool _FindR(Node* root, const K& key)
	{
		// 如果是空树,那么直接返回空
		if (root == nullptr)
			return false;
		
		if (root->_key < key) // 如果key值大于根节点的值
		{
			return _FindR(root->_right, key); // 就去当根节点的右子树当中查找
		}
		else if (root->_key > key) // 如果key值小于根节点的值
		{
			return _FindR(root->_left, key); // 就去当根节点的左子树当中查找
		}
		else // 如果key值等于根节点的值
		{
			return true; // 查找成功,返回true
		}
	}
public:
	// 查找函数(递归实现)
	bool FindR(const K& key)
	{
		return _FindR(_root, key); // 去调用查找的子函数
	}

🍑 插入操作

二叉查找树在插入新节点的时候,都必须遵守原有的规则(二叉查找树的规则)。

对于任意一个待插入的元素 x 都是插入在二叉排序树的叶子结点,问题的关键就是确定插入的位置,从根结点开始进行判断,直到到达叶子结点,则将待插入的元素作为一个叶子结点插入即可。

  • 如果是空树,则直接将插入节点 x 作为二叉查找树的根结点。
  • 如果不是空树,按二叉查找树性质,从根结点开始进行判断插入位置。
    • 若插入节点的值小于根节点的值,则需要将该节点插入到根节点的左子树当中。
    • 若插入节点的值大于根节点的值,则需要将该节点插入到根节点的右子树当中。
  • 注意:如果插入的值等于当前节点的值,那么也是插入失败的(因为二叉查找树中不存在有两个相同节点的值)

假设我们现在要插入值为 0 的节点

在这里插入图片描述

第一步:访问根结点 8

在这里插入图片描述

第二步:根据二叉排序树的左子树均比根节点小,右子树均比根节点大的性质, 0 < 8 ,因此值为 0 的节点应该插入到根节点 8 的左子树当中,我们查看根节点的左子节点 3 :

在这里插入图片描述

第三步:根据二叉排序树的左子树均比根节点小,右子树均比根节点大的性质, 0 < 3 ,因此值为 0 的节点应该插入到根节点 3 的左子树当中,我们查看根节点的左子节点 1 :

在这里插入图片描述

第四步:根据二叉排序树的左子树均比根节点小,右子树均比根节点大的性质, 0 < 1 ,因此值为 0 的结点应该插入到节点 1 的左子树当中,访问节点 1 的左孩子,发现为空,则将 0 作为 1 号节点的左孩子插入。

在这里插入图片描述

🍅 动图演示

假设我们现在要插入值为 63 的节点:

在这里插入图片描述

有没有发现,插入操作和查找操作的原理是一样滴?

同样,我这里给大家提供递归和非递归两个版本。

🍅 非递归实现

使用非递归方式实现时,需要定义一个 cur 指针记录当前节点,还需要定义一个 parent 指针记录 cur 的父节点。当 cur 指向空时,通过 parent 判断将 key 插入到左边还是右边。

在这里插入图片描述

二叉查找树的插入函数非递归实现:

	// 插入函数(非递归实现)
	bool Insert(const K& key)
	{
		// 第一次插入,根节点为空
		if (_root == nullptr)
		{
			_root = new Node(key); // 直接把key节点作为树的根结点
			return true; // 插入成功,返回true
		}

		// 第二次插入的时候
		Node* parent = nullptr; // 记录cur的父节点
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key) // key大于当前节点,cur往右边走
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key) // key小于当前节点,cur往左边走
			{
				parent = cur;
				cur = cur->_left;
			}
			else // key等于当前节点的值
			{
				return false; // 插入失败,返回false
			}
		}

		// 当循环结束,说明cur找到了空的位置
		cur = new Node(key);
		if (parent->_key < key) // 如果key值大于当前parent节点的值
		{
			parent->_right = cur; // 就把key连接到parent的右边
		}
		else // 如果key值小于当前parent节点的值
		{
			parent->_left = cur; // 就把key连接到parent的左边
		}

		// 插入成功,返回true
		return true; 
	}

🍅 递归实现

二叉查找树的查找函数递归实现(递归涉及到传参的问题,所以需要写一个子函数):

private:
	// 插入函数(递归实现的子函数)
	bool _InsertR(Node*& root, const K& key) // 引用传参,root是_root的别名
	{
		if (root == nullptr) // 如果是空树
		{
			root = new Node(key); // 直接把key节点作为树的根结点
			return true; // 插入成功,返回true
		} 

		// 如果不是空树
		if (root->_key < key) // 如果key大于当前节点的值
		{
			return _InsertR(root->_right, key); // 把key插入到右子树当中
		}
		else if (root->_key > key) // 如果key小于当前节点的值
		{
			return _InsertR(root->_left, key); // 把key插入到左子树当中
		}
		else // 如果key等于当前节点的值
		{
			return false; // 插入失败,直接返回false
		}
	}
public:
	// 插入函数(递归实现)
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

🍑 删除操作

删除操作与查找和插入操作不同,首先查找元素是否在二叉搜索树中,如果不存在,则返回 nullptr,否则要删除的结点可能分以下三种情况进行处理:

  • 待删除的节点 x 是叶子节点(也就是 x 无左右孩子节点)。
  • 待删除的节点 x 有一个孩子(左孩子或者右孩子)。
  • 待删除的节点 x 有两个孩子(即左右孩子都存在)。

那么就这三种情况,我们来分类讨论一下:

情况一:被删除的节点 x 是叶子节点,那么直接从二叉排序树当中移除即可,也不会影响树的结构

在这里插入图片描述

动图演示:删除值为 5 的节点

在这里插入图片描述

情况二:被删除的节点 x 仅有一个孩子

  • 如果只有左孩子,没有右孩子,那么只需要把要删除节点的左孩子链接到要删除节点 x 的父亲节点,然后直接删除 x 节点;

  • 如果只有右孩子,没有左孩子,那么只需要把要删除节点的右孩子链接到要删除结点 x 的父亲节点,然后直接删除 x 节点;

假设我们要删除值为 14 的结点,其只有一个左孩子结点 13 ,没有右孩子 。

第一步:先找到 14 的父节点 10,让其父节点 10 指向其左孩子 13

在这里插入图片描述

第二步:删除释放 14 节点即可。

在这里插入图片描述

我们再以删除结点 10 为例,再看一下没有左孩子,只有一个右孩子的情况。

第一步:先找到 10 的父节点 8,让其父节点 8 指向其右孩子 14

在这里插入图片描述

第二步:删除释放 10 节点即可。

在这里插入图片描述

动图演示:删除值为 71 的节点,该节点只有一个左孩子

在这里插入图片描述

动图演示:删除值为 7 的节点,该节点只有一个右孩子

在这里插入图片描述

情况三:被删除结点的左右孩子都存在

对于这种情况就复杂一些了,我们以下面这颗二叉查找树为例进行说明:

在这里插入图片描述

对于上面的二叉查找树的中序遍历结果如下所示:

在这里插入图片描述

现在我们先不考虑二叉排序上的删除操作,而仅在得到的中序遍历结果上进行删除操作。我们以删除中序遍历结果当中的根节点 8 为例进行说明:

在这里插入图片描述

当删除中序遍历结果中的 8 之后,哪一种方式不会改变中序遍历结果的有序性呢?

很简单,我么可以用 7 或者 9 来填充 8 的位置,都不会影响整个数组的有序性。

那么此时就相当于删除 根节点 8,然后根节点 左子树当中的最大元素 7 来替换根节点 8 的位置,或者用根节点的 右子树当中最小元素 9 来替换根节点 8 的位置。

所以删除操作要用替换的方式来进行,也就是替换 左子树的最大值节点 或者 右子树的最小值节点

下面就来看删除左右孩子都存在的结点是如何实现的,依旧以删除根节点 8 为例。首先我们先用根节点 8 的左子树当中值最大的结点 7 来替换根节点的情况。

第一步:获得待删除节点 8 的左子树当中值最大的节点 7(这一步可以通过从删除节点的左孩子 3 开始,一个劲地访问右子结点,直到叶子结点为止获得,因为左子树的最大值一定在最右边):

在这里插入图片描述

第二步:将删除节点 8 的值替换为 7

在这里插入图片描述

第三步:删除根节点左子树当中值最大的节点(这一步可能左子树中值最大的节点存在左子节点,而没有右子节点的情况,那么删除就退化成了第二种情况,递归调用即可):

在这里插入图片描述

我们再来看一下使用根节点 8 的右子树当中值最小的节点 9 来替换根节点的情况。

第一步:查找删除节点 8 的右子树当中值最小的节点,即 9 (先访问删除节点的右子节点 10,然后一直向左走,直到左子结点为空,则得到右子树当中值最小的节点,因为右子树的最小值一定在最左边)。

在这里插入图片描述

第二步:将删除结点 8 的值替换为 9

在这里插入图片描述

第三步:删除根节点右子树当中值最小的节点。

在这里插入图片描述

动图演示:删除值为 15 的节点,该节点左右孩子都存在

在这里插入图片描述

以上就是删除二叉查找树的三种情况的分析。

这里还是提供两个版本给大家参考:递归和非递归。

🍅 非递归实现

我这里采用 右子树的最小值节点 来和待删除节点的值进行替换:

  • 定义 minParent 用来记录待删除节点的右子树当中最小值节点的父节点。
  • 定义 minRight 用来记录待删除节点的右子树当中最小值节点。

二叉查找树的删除操作非递归实现:

public:
	// 删除函数(非递归实现)
	bool Erase(const K& key)
	{
		Node* parent = nullptr; // 记录待删除节点的父节点
		Node* cur = _root; // 记录删除节点

		while (cur) // 先找到要删除的节点
		{
			if (cur->_key < key) // key大于当前节点的值,就往该节点的左子树查找
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key) // key小于当前节点的值,就往该节点的右子树查找
			{
				parent = cur;
				cur = cur->_left;
			}
			else // 找到了要删除的节点,分三种情况讨论
			{
				if (cur->_left == nullptr) // 如果待删除节点的左子树为空
				{
					if (cur == _root) // 如果要删除的cur是根节点(此时parent为nullptr)
					{
						_root = cur->_right; // 那么直接把二叉查找树的根节点改为cur的右孩子即可
					}
					else // 如果要删除的cur不是根节点(此时parent不为nullptr)
					{
						if (cur == parent->_left) // 如果待删除节点(cur)是其父节点(parent)的左孩子
						{
							parent->_left = cur->_right; // 那么就让父节点(parent)的左指针(left)指向删除节点(cur)的右子树
						}
						else // 如果待删除节点(cur)是其父节点(parent)的右孩子
						{
							parent->_right = cur->_right; // 那么就让父节点(parent)的右指针(right)指向删除节点(cur)的右子树
						}
					}
					delete cur; // 释放待删除节点
				}
				else if (cur->_right == nullptr) // 如果待删除节点的右子树为空
				{
					if (cur == _root) // 如果要删除的cur是根节点(此时parent为nullptr)
					{
						_root = cur->_left; // 那么直接把二叉查找树的根节点改为cur的左孩子即可
					}
					else // 如果要删除的cur不是根节点(此时parent不为nullptr)
					{
						if (cur == parent->_left) // 如果待删除节点(cur)是其父节点(parent)的左孩子
						{
							parent->_left = cur->_left; // 那么就让父节点(parent)的左指针(left)指向待删除节点(cur)的左子树
						}
						else // 如果待删除节点(cur)是其父节点(parent)的右孩子
						{
							parent->_right = cur->_left; // 那么就让父节点(parent)的右指针(left)指向待删除节点(cur)的左子树
						}
					}
					delete cur; // 释放待删除节点
				}
				else // 如果待删除节点的左右子树都不为空
				{
					// 这里选择用右子树的最小值节点替换(左子树当中最大值也可以)
					Node* minParent = cur; // 记录待删除节点右子树当中值最小节点的父节点
					Node* minRight = cur->_right; // 记录待删除节点右子树当中值最小的节点
					while (minRight->_left) // 寻找待删除节点右子树当中值最小的节点
					{
						minParent = minRight;
						minRight = minRight->_left;
					}
					// 循环结束,说明找到了
					cur->_key = minRight->_key; // 把待删除节点的值替换成minRight的值
					if (minParent->_left == minRight) // 如果minRight是其父节点的左孩子
					{
						minParent->_left = minRight->_right; // 就让父节点的左指针(left)指向minRight的右子树即可
					} 
					else // 如果minRight是其父节点的右孩子
					{
						minParent->_right = minRight->_right; // 就让父节点的右指针(right)指向minRight的右子树即可
					}

					delete minRight; // 释放minRight节点
				}
				return true; // 删除成功,返回true
			}
		}
		return false; // 没有找到待删除节点,即删除失败,返回false
	}

🍅 递归实现

二叉查找树的删除操作递归实现思路如下:

  • 定义 minParent 用来记录待删除节点的右子树当中最小值节点的父节点。
  • 定义 minRight 用来记录待删除节点的右子树当中最小值节点。

当找到根节点的右子树当中最小值节点 minRight 时,先把根节点的值和 minRight 进行交换,然后再重新调用递归删除函数从当前根节点的右子树开始,删除右子树当中的 minRight

private:
	// 删除函数(递归实现的子函数)
	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr) // 如果是空树
			return false; // 删除失败,直接返回false

		if (root->_key < key) // 如果key大于根节点的值
		{
			return _EraseR(root->_right, key); // 那么待删除节点在根的左子树当中
		}
		else if (root->_key > 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* minRight = root->_right; // 记录根节点右子树当中值最小的节点
				while (minRight->_left) // 寻找根节点右子树当中值最小的节点
				{
					minRight = minRight->_left; // 右子树当中值最小的节点一定是在最左边,所以一直往左查找
				}
				// 找到以后,把根节点的值和minRight的值交换
				swap(root->_key, minRight->_key);
				return _EraseR(root->_right, key); // 此时,就转换成在根的右子树当中去删除这个key,这里删除这个key一定会走作为空的场景。
			}
			delete del; // 释放根节点
			return true; // 删除成功,返回true
		}
	}
public:
	// 删除函数(递归实现)
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

🍑 中序遍历

中序遍历和二叉树的中序实现一样,只不过因为中序是递归遍历,涉及到传参,所以需要写一个子函数。

private:
	// 中序遍历(递归实现的子函数)
	void _InOrder(Node* root)
	{
		if (root == nullptr) // 如果是空树,直接返回空
			return;
		_InOrder(root->_left); // 递归遍历左子树
		cout << root->_key << " "; // 打印每个节点的值
		_InOrder(root->_right); // 递归遍历右子树
	}
public:
	// 中序遍历(递归实现)
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

3. 二叉查找树的性能分析

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

二叉查找树的插入和查找、删除操作的最坏时间复杂度为 O ( h ) O(h) O(h),其中 h 是二叉查找树的高度。最极端的情况下,我们可能必须从根节点访问到最深的叶子节点,斜树的高度可能变成 n,插入和删除操作的时间复杂度将可能变为 O ( n ) O(n) O(n)

下图就是两颗斜树(就相当于单链表)。这也是二叉查找树在进行多次插入操作后可能发生的不平衡问题,也是二叉查找树的缺陷所在,但这依旧不妨碍其作为一个伟大的数据结构。

在这里插入图片描述

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

但对于同一个关键码 key 集合,如果各关键码 key 插入的次序不同,可能得到不同结构的二叉查找树。

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

如果退化成单支树,二叉搜索树的性能就失去了。那么后面的 AVL 树和红黑树就可以上场了。

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

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

相关文章

【机器学习】线性回归(实战)

线性回归&#xff08;实战&#xff09; 目录一、准备工作&#xff08;设置 jupyter notebook 中的字体大小样式等&#xff09;二、构建实验所需的数据&#xff08;以下实验将基于此数据&#xff09;三、实现线性回归的两种方式方法一&#xff1a;通过直接求解得到拟合方程参数&…

Python金融风控模型案例实战大全

大家好&#xff0c;我是Toby老师&#xff0c;今天介绍 《Python金融风控模型案例实战大全》。 1.《Python金融风控模型案例实战大全》程覆盖多个核心知识点&#xff0c;包括风控建模全流程知识介绍&#xff0c;信用评分卡&#xff0c;信用评分卡知识包含个人信用评分卡和企业信…

ifconfig-显示和配置网络

ifconfig是linux中用于显示或配置网络设备&#xff08;网络接口卡&#xff09;的命令&#xff0c;英文全称是network interfaces configuring。配置网卡的IP地址语法例&#xff1a;ifconfig eth0 192.168.0.1 netmask 255.255.255.0 系统命令 语法 ifconfig [网络设备][down up…

读Go语言精进之路

主要是摘取书中&#xff0c;个人感觉比较重要的内容。 文章目录第一部分 熟知Go的一切理解Go的设计哲学使用Go语言原生编程思维写Go代码第二部分 项目结构、代码风格和标识符命名第三部分 声明、类型、语句与控制结构13 了解切片的底层原理14 了解Map实现原理并高效使用15. str…

Word处理控件Aspose.Words功能演示:在 C# .NET 中将 DOC/DOCX 转换为 PNG

Aspose.Words 是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。此外&#xff0c; Aspose API支持流行文件格式处…

Linux系统中常见的压缩命令和特殊权限说明

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;Java案例分…

PHP多进程(二)之pcntl_wait

上篇文章我们说到父进程应该回收子进程结束之后产生的数据,这样才会不浪费系统资源。 一个程序启动之后&#xff0c;变成了一个进程&#xff0c;进程在以下情况会退出 1&#xff09;运行到最后一行语句 2) 运行时遇到return 时 3) 运行时遇到exit()函数的时候 4) 程序异常的时…

docker搭建maven私服(nexus3),整合springboot上传下载依赖

一、前言 我们在JavaWeb开发中必不可少的就是jar包管理-maven&#xff0c;在没有maven之前&#xff0c;都是自己手动下载jar包导入到项目中&#xff0c;非常的繁琐。 maven出现之后&#xff0c;又迎来新的问题&#xff0c;对于仓库里人家发布的都可以引用下载&#xff0c;但是…

音视频面试基础题

编码原理 为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢?核心思想就是去除冗余信息&#xff1a; 1&#xff09;空间冗余&#xff1a;图像相邻像素之间有较强的相关性 2&#xff09;时间冗余&#xff1a;视频序列的相邻图像之间内容相似 3&#xff09…

CVPR21 - BasicVSR:简单有效的视频超分辨率Baseline

文章目录原文信息初识相知组件分析BasicVSRIconVSR部分实验回顾原文信息 原文链接 初识 相比于图像超分&#xff0c;视频超分(VSR&#xff0c;Video Super-Resolution)显然是一件更具挑战性的任务。视频超分比图像超分多了时间维度的信息、更为复杂&#xff0c;而在当时&…

结构体的声明使用及存储方式

文章目录 一、结构体的声明与使用 1、1 结构体的简单声明 1、2 结构体的特殊声明 1、3 结构体自引用 1、4 结构体变量的定义和初始化 1、5 结构体传参 二、结构体在内存中的存储方式 2、1 结构体在内存中的存储方式的引入 2、2 结构体的内存对齐 2、3 修改默认对齐数…

AcWing - 寒假每日一题2023(DAY 1——DAY 5)

文章目录一、AcWing 4261.孤独的照片&#xff08;简单&#xff09;1. 实现思路2. 实现代码二、AcWing 3400.统计次数&#xff08;简单&#xff09;1. 实现思路2. 实现代码三、AcWing 4366. 上课睡觉&#xff08;简单&#xff09;1. 实现思路2. 实现代码四、AcWing 3443. 学分绩…

程序员接私活最最完整攻略

接私活对于程序员这个圈子来说是一个既公开又隐私的话题&#xff0c;当你竭尽全力想要去接私活的时候一定做过这样的事情&#xff0c;百度搜索“程序员如何接私活”、“程序员在哪里接外包”等等问题&#xff0c;今天就送大家最完整攻略&#xff0c;千万别错过了。 做私活挣钱吗…

有趣且重要的Git知识合集(10)git stash操作

这种一般用于多分支&#xff0c;或者多人协同合作时会使用到的git命令 场景1&#xff1a; 当你在dev分支上写了很多代码&#xff0c;此时线上有bug&#xff0c;需要紧急在hotfix分支上修改&#xff0c;那直接git add提交又不太好&#xff0c;毕竟还没有开发完&#xff0c;那么…

JVM 学习笔记 内存结构

内存结构 程序计数器 作用&#xff1a;记录下一条JVM指令的执行地址 特点&#xff1a; 线程私有不存在内存溢出 虚拟机栈 每个线程运行时所需的内存称为虚拟机栈。每个栈由多个栈帧&#xff08;Frame&#xff09;组成&#xff0c;每个栈帧对应每次方法调用时占用的内存。每…

BIOS 的详细介绍

一、BIOS详解 对于不少新手&#xff0c;刷新BIOS还是比较神秘的。而对于一些BIOS相关的知识&#xff0c;不少人也是一知半解。在这里&#xff0c;我们将对BIOS作一次全面的了解。 1、什么是BIOS BIOS是英文"Basic Input Output System"的缩略语&#xff0c;直译…

NTN(三) Timing

微信同步更新欢迎关注同名modem协议笔记。这篇看下k_offset和k_mac&#xff0c;如38.300所述&#xff0c;k_offset是配置的调度偏移量&#xff0c;需要大于或等于service link RTT和Common TA之和&#xff1b;k_mac 是配置的偏移量&#xff0c;需要大于或等于 RP 和 gNB 之间的…

Chem. Eur. J.|针对细胞内靶点的环肽药物:肽药物发展的下一个前沿

​题目&#xff1a;Cyclic Peptides as Drugs for Intracellular Targets: The Next Frontier in Peptide Therapeutic Development 文献来源&#xff1a;Chem. Eur. J. 2021, 27, 1487 – 1513 代码&#xff1a;无&#xff08;环肽综述&#xff09; 内容&#xff1a; 1.简…

5-迷宫问题(华为机试)

题目 定义一个二维数组 N*M&#xff0c;如 5 5 数组如下所示&#xff1a; int maze[5][5] { 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; 它表示一个迷宫&#xff0c;其中的1表示墙壁&#xff0c;0表示可以走的路&#xff0c;只…

通用模型切片处理过程 CesiumLab系列教程

我们前面把每种格式的模型参数设置已经讲解清楚&#xff0c;下面我们应该弄清楚通用模型切片剩下的流程&#xff0c;不管是人工模型&#xff0c;还是shp矢量面、bim模型&#xff0c;剩下的处理过程都是一样的&#xff0c;这里我们一起讲述。 资源库 ​通用模型处理分为两个过程…