高级数据结构——二叉搜索树

news2024/12/23 8:39:01

目录

1. 二叉搜索树的概念

2. 二叉搜索树的实现

结点类

二叉搜索树的类

2.1 默认成员函数

2.1.1 构造函数

2.1.2 拷贝构造函数

2.1.3 赋值运算符重载函数

2.1.4 析构函数

2.2 中序遍历

2.3 insert插入函数

2.3.1 非递归实现

2.3.2 递归实现

2.4 erase删除函数

2.4.1 非递归实现

2.4.2 递归版本

2.5 find查找函数

2.5.1 非递归实现

2.5.2 递归实现

3. 二叉搜搜数的应用

3.1 k模型

3.2 KV模型

4. 二叉搜索树性能分析

1. 二叉搜索树的概念

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

  1. 若它的左子树不为空,则左子树上所有节点的值小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值大于根节点的值
  3. 它的左右子树也分别为二叉搜索树。

总结:任意一颗子树都满足左子树的值 < 根 < 右子树的值。
二叉搜索树又称二叉排序树,且任何一颗子树都满足左子树的值 < 根 < 右子树的值,由此我们进行中序遍历(左子树 根 右子树)得到的就是一个升序序列

2. 二叉搜索树的实现

要实现一颗二叉搜索树,要实现两个类,一个是结点类,用于存放节点值、左指针、右指针。第二个类专门用于二叉搜索树的增删查改。

结点类

结点类主要包含如下内容:

  1. 成员变量:节点值、左指针、右指针。
  2. 只需要一个构造函数对成员变量的初始化即可。
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:
    //……
private:
	Node* _root = nullptr;
};

2.1 默认成员函数

2.1.1 构造函数

这里的构造函数直接让编译器默认生成就可以,不需要自己实现,但是后面的拷贝构造函数写了之后编译器就不会默认生成了,但是我们可以强制让它默认生成构造函数,不过要利用C++11的特性,具体看代码:

//强制编译器自己生成构造函数,忽视拷贝构造带来的影响
BSTree() = default;//C++11才支持

2.1.2 拷贝构造函数

注意这里的拷贝构造完成的是深拷贝,这里我们直接用前序递归的方式创建一颗与原来一样的二叉树即可。而递归前序拷贝结点的方式这里我们专门封装一个Copy函数即可。

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;
}
//拷贝构造函数--深拷贝
BSTree(const BSTree<K>& t)
{
	_root = CopyTree(t._root);
}

2.1.3 赋值运算符重载函数

这里直接给出现代写法:写法很巧妙,假设把t2赋值给t1,t2传参的时候直接利用传值传参调用拷贝构造生成t,t就是t2的拷贝,此时再调用swap函数交换t1和t 的_root根结点即可,而拷贝构造出来的t会在赋值运算符重载结束后自动调用自己的析构函数完成释放。

//赋值运算符重载函数 t1 = t2
BSTree<K>& operator=(BSTree<K> t)//t就是t2的拷贝
{
	//现代写法
	swap(_root, t._root);
	return *this;
}

2.1.4 析构函数

析构函数是为了释放二叉搜索树的所有结点,这里我们优先采用后序的递归释放,可以采用封装一个Destory函数来专门用于递归删除结点,如下:

void DestoryTree(Node* root)
{
	if (root == nullptr)
		return;
	//通过递归删除所有结点
	DestoryTree(root->_left);//递归释放左子树中的结点
	DestoryTree(root->_right);//递归释放右子树中的结点
	delete root;
}
//析构函数
~BSTree()	
{
	DestoryTree(_root);//复用此函数进行递归删除结点
	_root = nullptr;
}

2.2 中序遍历

中序遍历的核心宗旨是左子树 -> 根结点 -> 右子树,这里我们采用递归的方式去实现中序遍历。

  • 代码如下:
//中序遍历 -- 递归	
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
//中序遍历的子树
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);//递归到左子树
	cout << root->_key << " ";//访问根结点
	_InOrder(root->_right);//递归到右子树
}

2.3 insert插入函数

2.3.1 非递归实现

结合二叉搜索树的性质,插入的实现非常简单(注意重复的值不允许再次插入,默认不允许冗余)。主要分为两类:

1、如果是空树,直接把插入的结点作为根结点即可。

2、如果不是空树,则按如下规则讨论:首先得找到待插入的值的合适位置,其次找到位置后,将插入的值与此树链接起来

  • 1、寻找待插入的值的合适位置

定义cur指针从根结点开始(cur指针用于找到待插入的合适位置),定义parent指针最开始为nullptr(parent指针用于找到位置后的链接操作),把待插入的结点值定位key。遍历cur指针

  1. 若key > cur指向的结点值,让parent走到cur的位置,让cur指针走到右子树,指向_right的位置,继续遍历。
  2. 若key < cur指向的结点值,让parent走到cur的位置,让cur指针走到左子树,指向_left的位置,继续遍历。
  3. 若key = cur指向的结点值,说明待插入的结点值与此树当前结点值重合,插入结点失败。返回false。

 遍历结束后,说明已经找到要插入的合适的位置(某一颗子树的尾部),接着指向第二步:

  • 2、将插入的值与父亲链接起来

链接的步骤很简单,确保链接位置即可:

  1. 若插入的值比父亲的值大,链接在父亲的右边。
  2. 若插入的值比父亲的值小,链接在父亲的左边。

  • 代码如下:
//Insert非递归
bool Insert(const K& key)
{
	if (_root == nullptr)//若一开始树为空
	{
		_root = new Node(key);//直接申请值为key的结点作为二叉搜索树的根结点
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	//1、找寻插入的合适位置
	while (cur)
	{
		if (cur->_key < key)//若key大于当前结点值
		{
			parent = cur;
			cur = cur->_right;//让cur走向右子树
		}
		else if (cur->_key > key)//若key小于当前结点值
		{
			parent = cur;
			cur = cur->_left;//让cur走向左子树
		}
		else
		{
			return false;//若key等于当前结点值,说明插入的值不合法,返回false
		}
	}
	//2、进行与父亲的链接
	cur = new Node(key);
	if (parent->_key < key)
	{
		parent->_right = cur;//比父亲的值大连接在右子树
	}
	else
	{
		parent->_left = cur;//比父亲的值小链接在左子树
	}
	return true;
}
  • 补充:搜索二叉树以相对有序的方式插入会比较坑,因为高度太高。

2.3.2 递归实现

 依旧是分为两大步骤走,1、先递归到合适位置,确定插入的值链接在何处,2、找到位置后链接即可。

  • 1、递归找到插入的正确位置

这里虽是递归,不过走的形式和非递归的找到正确位置整体思路大差不差:

  1. 若key > root指向的结点值,让root递归到右子树继续遍历。
  2. 若key < root指向的结点值,让root递归到左子树继续遍历。
  3. 若key = root指向的结点值,说明待插入的结点值与此树当前结点值重合,插入结点失败。返回false。

 当root结点递归到nullptr时,即可进行下一步:链接。

  • 2、找到位置后,进行链接插入的结点

先前非递归版本的链接过程中为了要找到新插入结点和父亲的链接关系,我们特地创建了parent指针,让cur结点在不断的遍历中更新parent的指向以此时刻保持parent为cur的父亲,这样链接关系就确认好了,不过这里的递归实现我们并不给与一个parent指针,而是采用一个巧妙的方法:参数为指针的引用!

 通过这里可以看出传指针的引用已然达到没有父指针,胜似父指针的效果!!! 

//递归版插入

//插入的子树
bool _InsertR(Node*& root, const K& key)//Node*&为指针的引用
{
	if (root == nullptr)
	{
		root = new Node(key);//当root为空,把自己创建成新结点
		return true;
	}
	if (root->_key < key)
		return _InsertR(root->_right, key);//如果比key小,转换到右子树去插入
	else if (root->_key > key)
		return _InsertR(root->_left, key);//如果比key大,转换到左子树去插入
	else
		return false;//如果相等,就返回false
}

2.4 erase删除函数

2.4.1 非递归实现

二叉搜索树的删除函数最为复杂,这里我们主要通过两大步骤进行删除的操作:

  1. 遍历找到待删值的位置
  2. 删除找到的位置并链接父亲与剩下的结点

接下来针对这两大步骤展开讨论:

  • 一、先找到要删除的结点

首先定义cur指针指向根结点(cur指针用于找到待删除结点的位置),定义parent指针指向nullptr(parent指针用于删除后的链接操作),定义key为删除结点的值,按如下规则进行遍历:

  1. 若key > cur指向结点的值,让parent走到cur的位置,让cur走到右子树进行遍历
  2. 若key < cur指向结点的值,让parent走到cur的位置,让cur走到左子树进行遍历
  3. 若key = cur指向结点的值,接下来进行删除结点和链接的操作。

 此时可以指向第二部,删除找到的位置并链接父亲与剩下的结点。

  • 二、删除结点并链接父亲与剩下的结点

当我删去结点后,一个最值得考虑的问题是,如果待删值还有孩子怎么办呢,因此还要考虑到链接父亲与孩子的问题,并且又要进行如下分类:

  1. 待删值只有一个孩子 -- 左为空 or 右为空 or 左右均为空
  2. 待删值两个孩子都在 -- 替换法删除

接下来同样是进行展开讨论:

1、待删值只有一个孩子 -- 左为空 or 右为空 or 左右均为空

我们按如下四步走:

  1. 如果左孩子为空且删除的值为根结点,直接更新根结点为右孩子(右孩子为空,就相反操作)。
  2. 如果父亲的左孩子为待删值,将父亲的左孩子指向待删值指向的右孩子。
  3. 如果父亲的左孩子不是待删值,将父亲的右孩子指向待删值指向的右孩子。
  4. 删除待删的结点。

2、待删值两个孩子都在 -- 替换法删除

替换法删除的目的在于我删除目标结点后,让左子树或右子树其中一个叶结点到删除的位置上来,又要保持其删除后依旧是一个二叉搜索树的特性(左子树 < 根 < 右子树),这就要用到替换法。

准备工作如下:

  1. 定义myParent指针为cur指针的位置(myParent指针用于链接要删除结点的孩子)。
  2. 定义minRight指针为cur的右孩子结点指针的位置(minRight用于找到右子树的最小值)。

具体替换法的操作如下:

  1. 遍历minRight找到待删结点右子树的最小值(或左子树的最大值结点),中途不断更新myParent。
  2. 找到后,利用swap函数交换此最小值结点的值(minRight->_key)和待删结点的值(cur->_key)。
  3. 交换后,链接父亲myParent指针与minRight结点的孩子。
  4. 最后记得delete删除minRight结点。

注意:若整个操作两大步骤遍历一遍找不到要删除的值,直接返回false。

//Erase删除
bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		//1、先找到要删除的结点
		if (cur->_key < key)
		{
			parent = cur;
			//让parent始终为cur的父亲
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			//让parent始终为cur的父亲
			cur = cur->_left;
		}
		else
		{
			//找到了,分两类情况讨论:
			//1、待删值只有一个孩子 -- 左为空 or 右为空
			//2、待删值两个孩子都在 -- 替换法删除
			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;
			}
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					//如果右孩子为空且删除的值为根结点,直接更新根结点为左孩子
					_root = cur->_left;
				}
				else
				{
					//右孩子为空
					if (cur == parent->_left)
					{
						//如果父亲的左孩子为待删值,将父亲的左孩子指向待删值指向的左孩子
						parent->_left = cur->_left;
					}
					else
					{
						//如果父亲的左孩子不是待删值,将父亲的右孩子指向待删值指向的左孩子
						parent->_right = cur->_left;
					}
				}
				//删除待删的结点
				delete cur;
			}
			else
			{
				//待删值的两个孩子都在,替换法删除。
				//找右子树的最小值或找左子树的最大值,下面为找右子树最小值
				Node* minParent = cur;//右子树的根可能就是minRight,所以这里minParent不能为nullptr,
				//因为此时不会进入while循环导致minParent就一直为nullptr,最后删除的时候堆野指针的非法访问
				Node* minRight = cur->_right;
				while (minRight->_left)
				{
					minParent = minRight;
					//让minParent始终为minRight的父亲
					minRight = minRight->_left;
				}
				swap(minRight->_key, cur->_key);//或者cur->_key = minRight->_key;
				//链接父亲minParent和要删除的结点的孩子
				if (minParent->_left == minRight)
				{
					//如果minParent的左孩子为待删值,让minParent的左孩子指向minRight的右
					minParent->_left = minRight->_right;
				}
				else
				{
					//如果minParent的右孩子为待删值,让minParent的右孩子指向minRight的右
					minParent->_right = minRight->_right;
				}
				//删除要删的结点
				delete minRight;
			}
			return true;
		}
	}
	//遍历一遍找不到要删除的值,直接返回false
	return false;
}

2.4.2 递归版本

这里和非递归的主要实现思路大差不差,也是分为先找到删除的合适结点位置,找到后将其删除并确保链接关系正确这两大步骤。接下来,详细讨论下:

  • 一、先找到要删除的结点:

找到要删除的结点很简单,非递归是通过遍历的方式,只不过这里利用了递归来解决:

  1. 若当前结点root为空,说明此删除的结点不存在,返回false
  2. 若key > root指向的结点值,让root递归到右子树继续遍历。
  3. 若key < root指向的结点值,让root递归到左子树继续遍历。

 

  • 二、删除此结点 + 链接父子关系:

当删去结点后,面临和非递归的删除同样一个问题:如果待删值还有孩子怎么办呢,因此还要考虑到链接父亲与孩子的问题,并且又要进行如下分类:

  1. 待删值只有一个孩子 -- 左为空 or 右为空 or 左右均为空
  2. 待删值两个孩子都在 -- 替换法删除

这里的核心写法和插入的递归实现一样,传参要传指针的引用,接下来,这两种删除情况我都会详细讲解下如何利用好传参要传指针的引用

  • 1、待删值只有一个孩子 -- 左为空 or 右为空 or 左右均为空

我们按如下三步走:

  1. 先把要删除的结点指针root保存为del;
  2. 如果root的左孩子为空,执行root = root->_right;                                                           此时的root为指针的引用,即父结点的左指针或右指针,假设root为父结点的右指针。执行此段代码的意思是让父结点的右孩子指针(root)链接到root的右孩子,即可天然借助指针的引用建立了父子的链接关系。
  3. 如果root的右孩子为空,执行root = root->_left;

这种情况和上面无任何区别,只是链接方向变了,思路均一样。下面给出图示说明:

 

  • 2、待删值两个孩子都在 -- 替换法删除

准备工作如下:

  1. 先把要删除的结点指针root保存为del
  2. 定义minRight指针为root的右孩子结点指针的位置(minRight用于找到右子树的最小值)

具体替换法的操作如下:

  1. 遍历minRight找到待删结点右子树的最小值(或左子树的最大值结点)
  2. 找到后,利用swap函数交换此最小值结点的值(minRight->_key)和待删结点的值(root->_key)
  3. 交换后,到子树复用递归删除:return _EraseR(root->_right, key);意思是利用递归删除

图示说明:

 

//递归版删除
bool EraseR(const K& key)
{
	return _EraseR(_root, key);
}
//删除的子树
bool _EraseR(Node*& root, const K& key)
{
	//1、递归查找删除的位置
	if (root == nullptr)
	{
		//如果是空就返回false
		return false;
	}
	if (root->_key < key)
	{
		return _EraseR(root->_right, key);//如果比key小,转换到右子树去插入
	}
	else if (root->_key > key)
	{
		return _EraseR(root->_left, key);//如果比key大,转换到左子树去插入
	}
	//2、确认链接关系
	else
	{
		Node* del = root;//提前保存root结点的位置
		//开始删除
		if (root->_left == nullptr)
		{
			//如果左为空
			root = root->_right;
		}
		else if (root->_right == nullptr)
		{
			//如果右为空
			root = root->_left;
		}
		else
		{
			Node* minRight = root->_right;//minRight用于找到右子树的最小值
			while (minRight->_left)
			{
				minRight = minRight->_left;
			}
			swap(root->_key, minRight->_key);
			return _EraseR(root->_right, key);
		}
		delete del;
		return true;
	}
}

2.5 find查找函数

2.5.1 非递归实现

Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:

  1. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  2. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  3. 若key值等于当前结点的值,则查找成功,返回true。
  4. 若遍历一圈cur走到nullptr了说明没有此结点,返回false
//Find非递归
bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
		}
		else if (cur->_key > key)
		{
			cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
		}
		else
		{
			return true;//若key值等于当前结点的值,则查找成功,返回true。
		}
	}
	return false;//遍历一圈没找到返回false
}

2.5.2 递归实现

递归的实现主要是转换成子问题来解决。针对于Find的递归实现,只需遵循如下规则即可:

  1. 若树为空树,则查找失败,返回nullptr。
  2. 若key值小于当前结点的值,则递归到该结点的左子树当中进行查找。
  3. 若key值大于当前结点的值,则递归到该结点的右子树当中进行查找。
  4. 若key值等于当前结点的值,则查找成功,返回对应结点的地址。
//递归版查找
bool FindR(const K& key)
{
	return _FindR(_root, key);
}
//查找的子树
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
	{
		//找到了
		return true;
	}
}

3. 二叉搜搜数的应用

3.1 k模型

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值

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

以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
其实我前面模拟实现的二叉搜索树就是一个K模型。

3.2 KV模型

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

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
我们可以针对K模型,在其内部实现进行稍稍修改即可达到KV模型的实现。

#pragma once
#include<iostream>
#include<string>
using namespace std;
namespace key_value
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left; //左指针
		BSTreeNode<K, V>* _right;//右指针
		const K _key;//节点值,假设const修饰防止后续修改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:
		//中序遍历 -- 递归
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		///递归版的插入、删除、查找/
			//查找
		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		//插入
		bool InsertR(const K& key, const V& value)
		{
			return _InsertR(_root, key, value);
		}
		//删除
		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		//查找的子树
		Node* _FindR(Node* root, const K& key)//查找的时候要返回结点的指针,为了方便后续达到可以修改value而不能修改key的效果
		{
			if (root == nullptr)
				return nullptr;
			if (root->_key < key)
			{
				//如果比key小,转换到右子树去找
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				//如果比key大,转换到左子树去找
				return _FindR(root->_left, key);
			}
			else
			{
				//找到了
				return root;
			}
		}
		//插入的子树
		bool _InsertR(Node*& root, const K& key, const V& value)//Node*&为指针的引用
		{
			if (root == nullptr)
			{
				root = new Node(key, value);//当root为空,把自己创建成新结点
				return true;
			}
			if (root->_key < key)
				return _InsertR(root->_right, key, value);//如果比key小,转换到右子树去插入
			else if (root->_key > key)
				return _InsertR(root->_left, key, value);//如果比key大,转换到左子树去插入
			else
				return false;//如果相等,就返回false
		}
		//删除的子树
		bool _EraseR(Node*& root, const K& key)
		{
			//1、递归查找删除的位置
			if (root == nullptr)
			{
				//如果是空就返回false
				return false;
			}
			if (root->_key < key)
			{
				return _EraseR(root->_right, key);//如果比key小,转换到右子树去插入
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);//如果比key大,转换到左子树去插入
			}
			//2、确认链接关系
			else
			{
				Node* del = root;//提前保存root结点的位置
				//开始删除
				if (root->_left == nullptr)
				{
					//如果左为空
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					//如果右为空
					root = root->_left;
				}
				else
				{
					Node* minRight = root->_right;//minRight用于找到右子树的最小值
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}
					swap(root->_key, minRight->_key);
					return _EraseR(root->_right, key);
				}
				delete del;
				return true;
			}
		}

		//中序遍历的子树
		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;
			_InOrder(root->_left);//递归到左子树
			cout << root->_key << ":" << root->_value << endl;;//访问根结点
			_InOrder(root->_right);//递归到右子树
		}
	private:
		Node* _root = nullptr;
	};
}

4. 二叉搜索树性能分析

 插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

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

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN。
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N / 2。
  • 综上时间复杂度为O(N)。

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?AVL树和红黑树就可以优化二叉树的性能。
 

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

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

相关文章

App Inventor 2 语音交互机器人Robot,使用讯飞语音识别引擎

应用介绍 App Inventor 2 语音识别及交互App。识别语言指令并控制机器人运动&#xff0c;主要用到语音识别器及文本朗读器组件&#xff0c;语音识别相关开发最佳入门。代码逻辑简单&#xff0c;App交互性及趣味性非常强~ 视频预览 语音Robot教程&#xff08;难度系数&#xf…

中科院、中科大团队精确测量子引力对量子自旋的影响

光子盒研究院 由中国科学院盛东教授、陆征天教授和中国科学技术大学的合作研究小组利用高精度氙气同位素磁力仪研究了中子自旋和引力之间的耦合效应。5月15日&#xff0c;这项题为Search for Spin-Dependent Gravitational Interactions at Earth Range的研究发表在《物理评论快…

three.js常用几何体介绍以及自定义几何体

一、自定义三角形几何体 核心代码&#xff1a; // 添加物体 // 创建几何体 for (let i 0; i < 50; i) {// 每一个三角形&#xff0c;需要3个顶点&#xff0c;每个顶点需要3个值const geometry new THREE.BufferGeometry();const positionArray new Float32Array(9);for …

Java创建多线程的五种写法

目录 一.lambda表达式(强烈推荐,最简单) 基础格式 举例 运行结果 二.继承 Thread, 重写 run 基础格式 举例 运行结果 三.实现 Runnable, 重写 run 基础格式 举例 运行结果 四.使用匿名内部类,继承 Thread, 重写 run 基础格式 举例 运行结果 五.使用匿名内部类,实…

locust学习教程(8) - event 事件

目录 1、对请求的测试前置、后置处理 2、在web界面添加新内容 3、监听测试的失败率或阀值 4、汇总总结 &#x1f381;更多干货 1、对请求的测试前置、后置处理 请求有一个上下文参数&#xff0c;通过数据有关的请求&#xff08;之类的用户名&#xff0c;标签等&#xff09…

Leaflet实现要素点击查询弹窗展示属性

leaflet是一个非常轻量的webgis框架,同时呢代码结构也比较简单。 如果项目上有需求需要大家实现对于个行政区点击查询相关属性并且展示,就像下图这样: 我们可以这样做。首先要清楚leaflet框架的构造,leaflet在加载图层的时候是对图层添加了事件监听的,也就是说用户对于图…

C++基础强化项目-职工管理系统

通过本项目练习c的基础知识 项目界面头文件workermanager.h&#xff08;管理类&#xff09;worker.h&#xff08;职工抽象类&#xff09;manager.h&#xff08;经理类&#xff09;employee.h&#xff08;普通职工类&#xff09;boss.h&#xff08;老板类&#xff09; 源文件emp…

多传感器时频信号处理:多通道非平稳数据的分析工具(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

新浪微博“私信留言收费”:私域引流危险了

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 从今日起很多新浪微博用户发现&#xff1a;微博用私信要收费了&#xff0c;确切的说是对方没有回关或回复你之前&#xff0c;你只能发送一条消息。开通会员后能发送更多留言。如下图所示&#xff1…

卷积计算加速方法--slice卷积

文章目录 1、前言2、分块卷积存在的问题3、分块卷积问题的解决方案--slice卷积4、slice卷积每层所需切分尺寸计算4、结论及加速效果 1、前言 我们在上一篇卷积计算加速方法中讨论过&#xff0c;当卷积的输入太大导致内存不够用时&#xff0c;考虑将一大块卷积分成多个小块分别进…

多快好省!硫元素循环分析内容又升级啦!

元素循环是生物地球化学循环的重要环节&#xff0c;主要涉及碳、氮、磷、硫等元素的循环过程。凌恩生物强势推出基于宏基因组的硫循环研究方案&#xff0c;构建了完整的硫循环循环模式图&#xff0c;对宏基因组数据进行深入挖掘&#xff0c;各部分结果图可直接用于文章发表&…

iOS 开发 | 自定义不规则 label

把我之前发布在简书的博客搬运过来。 目录 场景思路具体实现1. 自定义一个继承自UILabel的IrregularLabel2. 在初始化方法中进行相应初始化和设置3. 在layoutSubviews方法中进行路径的设置 最终效果箭头 label 场景 最近 App 改版&#xff0c;以下是截取的部分 UI 设计图&…

报表测试如何做?软件测试实战,超详细测试点分析(全覆盖)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 报表测试是一项重…

qt学习 tcp实现 c++

这里写目录标题 qt网络编程qt下的TCP 编程设计ui界面监听关闭和发送调试 查找网络调试助手&#xff0c;用助手当客户端测试 编写的服务端newConnection()newConnection_Slot() Tcp Client界面设计代码部分1关闭客户端发送客户端整体疑惑 https://www.bilibili.com/video/BV1tp4…

Android CMake

首先了解几个名词 NDK The Android Native Development Kit The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C. For certain types of apps, this can help you reuse code libraries written in t…

虚实相生的元宇宙,不仅仅是在做虚拟社交?

互联网迭代速度已经超出了人们的想象&#xff0c;从Web1.0到Web 2.0&#xff0c;以及紧随其后的 Web 3.0。 不管我们愿不愿意承认&#xff0c;元宇宙的时代已经真真切切地到来了&#xff0c;它的兴起也是社会发展到一定阶段的必然现象。随着时代的发展&#xff0c;如今创作者的…

OpenWrt uci网络配置详解

配置文件 OpenWrt所有配置保存在/etc/config目录&#xff0c;以下为主要的网络配置文件 网络接口配置 /etc/config/network网络服务配置 /etc/config/dhcp防火墙配置 /etc/config/firewall 网络接口 OpenWrt网络接口一般包含lan口和wan口&#xff0c;但如果是X86等设备&…

十五.EtherCAT开发之对象字典的映射原理

十五.EtherCAT开发之对象字典的映射原理 15.1 协议栈文件含义 仔细阅读四个代码文件 l 文件el9800appl.c&#xff1a;主函数&#xff0c;数据收发函数所在 l 文件el9800appl.h&#xff1a;对象字典定义所在&#xff0c;包含对象字典的类型、权限、长度、映射关系、链接变量…

Restful风格笔记

Restful风格知识点 RestController注解 在类上添加RestController可以默认类中的所有方法都带有ResponseBody注解&#xff0c;可以省去一个个添加的麻烦。 RestController RequestMapping("/restful") //CrossOrigin(origins {"http://localhost:8080"…

第六节 元组、字典

文章目录 掌握知识点1. 元组1.1 元组概述1.2 语法格式1.3 元组场景使用 2. 字典2.1 概述2.2 字典的语法结构与注意2.3 字典CURD2.3.1 字典获取2.3.2 字典添加和修改2.3.3 字典删除2.3.4 字典遍历 2.4 enumerate 函数2.5 扩展练习2.5.1 判断是否能全部购买2.5.2 学生信息的排序2…