当前位置: 首页 > article >正文

二叉树搜索树(上)

二叉树搜索树(上)

在这里插入图片描述

概念

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

• 若它的左子树不为空,则左子树上所有结点的值都于等于根结点的值

• 若它的右子树不为空,则右子树上所有结点的值都于等于根结点的值

• 它的左右子树也分别为二叉搜索树

• 二叉搜索树中可以支持插⼊相等的值,也可以不支持插⼊相等的值,具体看使⽤场景定义,map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插⼊相等值,multimap/multiset支持插⼊相等值

二叉搜索树的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其⾼度为: O ( l o g 2 N ) O(log_2N) O(log2N)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其⾼度为: O ( N 2 ) O(\frac{N}{2}) O(2N)

所以综合而言二叉搜索树增删查改时间复杂度为: O(N)

那么这样的效率显然是无法满⾜我们需求的,二叉搜索树的变形:平衡二叉搜索树AVL树和红⿊树,才能适用于我们在内存中存储和搜索数据。

另外需要说明的是,二分查找也可以实现 O(logN) 级别的查找效率,但是二分查找有两⼤缺陷:

  1. 需要存储在支持下标随机访问的结构中,并且有序。
  2. 插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。

这⾥也就体现出了平衡二叉搜索树的价值。

二叉树的插入

插⼊的具体过程如下:

  1. 树为空,则直接新增结点,赋值给root指针
  2. 树不空,按二叉搜索树性质,插⼊值⽐当前结点大往右走,插⼊值⽐当前结点⼩往左⾛,找到空位置,插⼊新结点。
  3. 如果支持插⼊相等的值,插⼊值跟当前结点相等的值可以往右⾛,也可以往左⾛,找到空位置,插⼊新结点。(要注意的是要保持逻辑⼀致性,插⼊相等的值不要⼀会往右走,⼀会往左走)

参考代码:

template<class K>
class BSTNode
{
public:
	BSTNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{}

private:
	K _key;
	BTSNode<K>* _left;
	BTSNode<K>* _right;

};

template<class K>
class BSTree
{
	using Node = BSTNode<k>;//用using替代typedef的作用
public:
	bool Insert(const K& key)
	{
		//如果是空树
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;//这时就结束了
		}
		//如果不是空树
		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)//当cur不为空
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
				return false;//避免冗余
		}
		//到这里说明cur已经找到空位置了,但是不知道是从右走的还是往左走的,得再比一次
		cur = new Node(key);
		if (key < parent->_key)
		{
			parent->left = cur;
		}
		else
		{
			parent->right = cur;
		}
		return true;
	}
private:
	Node* _root = nullptr;//是结点指针类型,不是结点类型
};

如果允许已存在的值插入(或者说运行冗余):

我们会发现,如果走中序遍历(中序遍历顺序是先遍历左子树,然后访问根节点,最后遍历右子树),就是有序的。即使允许冗余的情况下,中序遍历也是有序的。

中序遍历代码参考:

template<class K>
class BSTree
{
//……

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

	Node* _root = nullptr;//是结点指针类型,不是结点类型
};

可以发现一个问题,我们需要传入根节点才能进行中序遍历,但是根节点在类中贝private修饰无法访问。

一种方法是提供GetRoot接口,但还有别的方法:

在C++的类中要写递归时,公开的方法都要套一层。

public:
void InOrder()//类外面拿不到_root,但是类里面是可以的
{
	_InOrder(_root);
}

类里面二叉树的递归基本都需要像这样套一层。

二叉树搜索树的查找

  1. 从根开始⽐较,查找x,x⽐根的值⼤则往右边⾛查找,x⽐根值⼩则往左边⾛查找。
  2. 最多查找⾼度次,⾛到到空,还没找到,这个值不存在。
  3. 如果不⽀持插⼊相等的值,找到x即可返回
  4. 如果⽀持插⼊相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。如下图,查找3,要找到1的右孩⼦的那个3返回

参考代码:

bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_key)
		{
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			cur = cur->_right;
		}
		else
		{
			return true;
		}
	}
	return false;
}

二叉搜索树的删除

⾸先查找元素是否在二叉搜索树中,如果不存在,则返回false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

  1. 要删除结点N左右孩⼦均为空
  2. 要删除的结点N左孩⼦位空,右孩⼦结点不为空
  3. 要删除的结点N右孩⼦位空,左孩⼦结点不为空
  4. 要删除的结点N左右孩⼦结点均不为空

对应以上四种情况的解决⽅案:

  1. 把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)

  2. 把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点

  3. 把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点

  4. ⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替换法删除。

    找N左⼦树的值最⼤结点R(最右结点)或者N右⼦树的值最⼩结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满⾜⼆叉搜索树的规则。

    替代N的意思就是N和R的两个结点的值交换,转⽽变成删除R结点,R结点符合情况2或情况3,可以直接删除

参考代码(比较多要注意的细节,写在注释里了):

bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if(key>cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			//找到了要删除的数据,接下来进行删除,但是要判断情况
			
			//cur的左孩子为空(左右孩子都为空也被包含在内了),然后把cur的右孩子给给父(父可能为空)
			if (cur->_left == nullptr)
			{
				//父为空,也就是说要删的是头结点
				if (parent==nullptr)
				{
					_root = cur->_right;
				}
				else
				{
					//要判断父节点的左还是右指针指向cur的孩子
					if(parent->_left==cur)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}
				delete cur;
				return true;//别忘了
			}
			//cur的右孩子为空,把cur的左孩子给父
			else if (cur->_right == nullptr)
			{
				if (parent == nullptr)
				{
					_root = cur->_left;
				}
				else
				{
					//判断父节点的左还是右指针指向cur孩子
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
				delete cur;
				return true;//别忘了
			}
			else//现在是cur的左右孩子都不为空
			{
				//第一步,找R(这里采用cur的右子树的最小结点/也就是最左结点)
				Node* p = cur;//这里如果初始给空,后面如果不进循环,会造成对空指针的解引用
				Node* r = cur->_right;
				while (r->_left)
				{
					p = r;
					r = r->_left;
				}
				//第二步,进行交换(但不用真的把cur的值再去给r)
				cur->_key = r->_key;
				//第三步,删除R——很容易考虑不全面!!删除R又要把删除的问题考虑全面,但这里不能用递归,会找不到
				//在删除R时要把R的右孩子(只可能是右孩子)给给父,但是父的左还是右指针不确定
				if (p->_right == r)
				{
					p->_right = r->_right;
				}
				else
				{
					p->_left = r->_right;
				}
				delete r;
				return true;
			}
		}
	}

	return false;
}

那么,增删查改我们现在已经完成了增、删、查了,但是普通二叉树是不允许修改的。

本文到此结束。


http://www.kler.cn/a/390150.html

相关文章:

  • 万字长文分析函数式编程
  • Xshell,Shell的相关介绍与Linux中的权限问题
  • 使用electron-egg把vue项目在linux Ubuntu环境下打包并安装运行
  • 【vue2.0入门】vue单文件组件
  • 鸿蒙next版开发:订阅应用事件(ArkTS)
  • 【webrtc】 RTP 中的 MID(Media Stream Identifier)
  • 【lambda表达式】【DP】个人练习-Leetcode-1039. Minimum Score Triangulation of Polygon
  • QML —— 遮罩功能,模拟软件头像功能(附源码)
  • python printf中文乱码
  • JedisException:Could not get a resource from the pool
  • SpringCloud 微服务消息队列灰度方案 (RocketMQ 4.x)
  • SQL 窗口函数
  • 什么是C/C++,有什么特点
  • 物联网学习路线来啦!
  • 道可云人工智能元宇宙每日资讯|2024国际虚拟现实创新大会将在青岛举办
  • cache写策略 操作系统
  • nginx 部署2个相同的vue
  • 241111.学习日志——【CSDIY】Cpp零基础速成
  • 2024年11月10日系统架构设计师考试题目回顾
  • 【算法速刷(9/100)】LeetCode —— 42.接雨水
  • 2024年9月青少年软件编程(C语言/C++)等级考试试卷(四级)
  • flask logger 使用 TimedRotatingFileHandler 报错 PermissionError 另一个程序正在使用此文件
  • NVR录像机汇聚管理EasyNVR多品牌NVR管理工具/设备:大华IPC摄像头局域网访问异常解决办法
  • 哪家云服务器好跑AI?瞄准AutoDL(附NVIDIA GPU 算力排名表)
  • Linux基础之病毒编写
  • Docker 操作指令