【C++】-二叉搜索树的详解(递归和非递归版本以及巧用引用)

news2024/9/25 1:18:48

在这里插入图片描述
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、什么是二叉搜索树?
  • 二、模拟实现
    • 2.1 中序遍历
    • 2.2 插入操作
    • 2.3查找操作
    • 2.4删除操作
    • 2.5拷贝构造
    • 2.6析构函数
    • 2.7赋值运算符
  • 三、二叉搜索树的性能分析
  • 四、二叉搜索树的应用
  • 五、非递归和递归的完整代码
  • 六、总结


前言

今天我要来给大家讲解一下二叉树的一些进阶部分的知识,与其这样说不如说是为了红黑树和AVL树和红黑树做铺垫,本篇的内容相对来说理解起来比前面的简单,博主也会分两个版本给大家介绍,一个递归版本的,一个非递归版本的,两个会一个介绍的,话不多说,我们开始进入正文


一、什么是二叉搜索树?

这个树不像普通的树一样每颗结点都是杂乱无章的,他符合一个特性:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

在这里插入图片描述
在这里插入图片描述

通过上面这个树我们分析,每棵子树的根节点的值都是这颗子树的左子树中最大的,是这颗子树的右子树中最小的,我们在来看,以8这个根节点为例,它的左子树中最大的值是这颗子树的最右边的结点,而它的右子树中最小的结点是这颗子树的最左边的结点
在这里插入图片描述
大家先提前了解这个特点因为一会在删除的时候需要使用到这个特点。


我们发现将二叉搜索树进行中序遍历,就是一个有序的,所以接下来我们验证的时候也是将二叉搜索树按照中序打印出来进行验证

二、模拟实现

我们今天实现的二叉搜索树是没有重复数字的,这个到AVL树的时候才能解决。我们就实现插入,删除,查找的主要功能。

我们现将结点进行封装,这个封装在list的实现的时候也了解过了,来一个框架:

template<class k>
struct BSNode
{
	BSNode<k>* _left;
	BSNode<k>* _right;
	k _data;

	BSNode(const k& data=k())
		:_left(nullptr)
		,_right(nullptr)
		,_data(data)
	{}
};

template<class k>
class BSTree
{
	typedef BSNode<k> Node;
public:
	BSTree(){}
private:
	Node* _root;//根节点
};

2.1 中序遍历

我们进行中序遍历的时候,需要传根节点进去的,如果不封装一层,我们在类外面是没有访问到私有的_root的,所以要进行封装,递归版本的都要进行封装。

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}

2.2 插入操作

我们普通二叉树进行插入没啥意义,但是对于二叉搜索树进行插入有意义,它插入的位置是确定的,不是随便插入的。

非递归:

bool insert(const k& data)
	{
		if (_root == nullptr)//树为空的情况
		{
			_root = new Node(data);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;//为了记录将要插入位置的父节点,不然申请结点没有办法进行链接操作
		while (cur)//为了找到插入位置
		{
			parent = cur;
			if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(data);
		if (parent->_data > data)//判断插入到父节点的哪边
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		return true;
	}

在这里插入图片描述

递归版本:

bool InsertR(const k& data)
	{
		return _InsertR(_root, data);
	}
bool _InsertR(Node*& root, const k& data)//传引用的好处就是得到父节点的指针,不用关心当前插入位置结点是父节点的哪个结点
	{
		if (root == nullptr)
		{
			root = new Node(data);
			return true;
		}
		if (root->_data < data)
		{
			return _InsertR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _InsertR(root->_left, data);
		}
		else//相等的时候就不用插入了
		{
			return false;
		}
	}

这个巧妙的设计就是传引用进去了,因为我要通过父节点来确定链接关系,结果通过引用直接获得父节点指向的指针,将指针里面的内容修改成要插入结点的就行了,不需要保留父节点,也不需要判断位于父节点那边了
在这里插入图片描述

2.3查找操作

查找操作就比较简单
非递归:

bool find(const k& data)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		while (cur)
		{
			if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else//找到了就返回真
			{
				return true;
			}
		}
		return false;//到这还没有返回,说明没有找到
	}

递归版本:

bool FindR(const k& data)
	{
		return _FindR(_root, data);
	}
bool _FindR(Node* root, const k& data)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_data < data)
		{
			return _FindR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _FindR(root->_left, data);
		}
		else
		{
			return true;
		}
	}

2.4删除操作

这个操作也是最复杂的,情况也是最多的

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
在这里插入图片描述
我们看到这种情况其实是有三种小情况的,而要删除的结点没有左右孩子的情况,他的左右指针都是空,所以可以放在b,c情况里面

情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
在这里插入图片描述

情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

在这里插入图片描述
非递归:

bool erase(const k& data)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				break;
			}
		}
		if (cur == nullptr)//没找到要删除的元素
		{
			return false;
		}

		if (cur->_left == nullptr)//情况b
		{
			if (cur == _root)
			{
				 _root= cur->_right;
			}
			else
			{
				if (parent->_left == cur)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;
				}
			}
			
		}
		else if (cur->_right == nullptr)//情况c
		{
			if (cur == _root)
			{
				 _root=cur->_left;
			}
			else
			{
				if (parent->_right == cur)
				{
					parent->_right= cur->_left;
				}
				else
				{
					parent->_left = cur->_left;
				}
			}
		}
		else//情况d
		{

			Node* pcur = cur->_left;//找左子树中最大值
			parent = cur;
			while (pcur->_right != nullptr)//找到左子树的最大值
			{
				parent = pcur;
				pcur = pcur->_right;
			}
			swap(pcur->_data, cur->_data);//替换
			if (parent->_left == pcur)//最右边的结点还有左子树·,但是没有右子树了
			{
				parent->_left = pcur->_left;
			}
			else//大部分都是这种情况
			{
				parent->_right =pcur->_left;
			}
			cur = pcur;
		}
		delete cur;
		return true;
	}

递归版本:

bool EraseR(const k& data)
	{
		return _EraseR(_root, data);
	}
bool _EraseR(Node*& root, const k& data)
	{
		if (root == nullptr)
			return false;
			
		if (root->_data < data)
		{
			return _EraseR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _EraseR(root->_left, data);
		}
		else//找到了删除的结点
		{
			Node* del = root;
			if (root->_left == nullptr)//左为空
			{
				 root = root->_right;//然后父亲指向我的右,不需要判断是父节点右还是左,传进来是什么就是什么,是父亲结点的指针的引用
			}
			else if (root->_right == nullptr)//右为空
			{
				root = root->_left;
			}
			else
			{
				Node* leftmax = root->_left;
				while (leftmax->_right)
				{
					leftmax = leftmax->_right;
				}
				swap(leftmax->_data, root->_data);

				return _EraseR(root->_left, data);//递归去删除替换后的结点
			}
			
			delete del;
			return true;
		}
	}

大家好好这个引用,节画画图来理解一下

2.5拷贝构造

这个需要一个一个的拷贝:两个版本是一样的

BSTree(const BSTree<k>& t)
	{
		_root = Copy(t._root);

	}
Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

2.6析构函数

~BSTree()
	{
		Destroy(_root);
	}

	void Destroy(Node*& root)
	{`在这里插入代码片`
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}

2.7赋值运算符

BSTree<k>& operator=(BSTree<k> t)
	{
		swap(_root, t._root);
		return *this;
	}

至此我们的两个版本的二叉搜索树就模拟实现完成了。

三、二叉搜索树的性能分析

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

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

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

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

四、二叉搜索树的应用

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
    在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

我们上面实现的写法就是k模型

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

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

在这里插入图片描述

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


在这里插入图片描述

我们来看测试代码:

#include<iostream>
using namespace std;
template<class k, class v >
struct BSNode
{
	BSNode<k,v>* _left;
	BSNode<k,v>* _right;
	k _data;
	v _value;

	BSNode(const k& data = k(), const v& value = v())
		:_left(nullptr)
		, _right(nullptr)
		, _data(data)
		,_value(value)
	{}
};


template<class k,class v>
class BSTreeRKV
{
	typedef BSNode<k,v> Node;
public:
	BSTreeRKV() {}

	BSTreeRKV(const BSTreeRKV<k,v>& t)
	{
		_root = Copy(t._root);
	}

	BSTreeRKV<k,v>& operator=(BSTreeRKV<k,v> t)
	{
		swap(_root, t._root);
		return *this;
	}


	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool InsertRKV(const k& data,const v& value)
	{
		return _InsertRKV(_root, data,value);
	}

	Node* FindRKV(const k& data)
	{
		return _FindRKV(_root, data);
	}

	bool EraseRKV(const k& data)
	{
		return _EraseRKV(_root, data);
	}

	~BSTreeRKV()
	{
		Destroy(_root);
	}
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}


	bool _EraseRKV(Node*& root, const k& data)
	{
		if (root == nullptr)
			return false;
		if (root->_data < data)
		{
			return _EraseRKV(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _EraseRKV(root->_left, data);
		}
		else
		{
			Node* del = root;
			if (root->_left == nullptr)//左为空
			{
				root = root->_right;//然后父亲指向我的右,此不需要判断是父节点右还是左,传进来是什么就是什么,是父亲结点的指针的引用
			}
			else if (root->_right == nullptr)//右为空
			{
				root = root->_left;
			}
			else
			{
				Node* leftmax = root->_left;
				while (leftmax->_right)
				{
					leftmax = leftmax->_right;
				}
				swap(leftmax->_data, root->_data);

				return _EraseRKV(root->_left, data);
			}

			delete del;
			return true;

		}
	}

	Node* _FindRKV(Node* root, const k& data)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		if (root->_data < data)
		{
			return _FindRKV(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _FindRKV(root->_left, data);
		}
		else
		{
			return root;
		}
	}

	bool _InsertRKV(Node*& root, const k& data, const v& value)//传引用的好处就是得到父节点的指针,不用关心当前插入位置结点是父节点的哪个结点
	{
		if (root == nullptr)
		{
			root = new Node(data,value);
			return true;
		}
		if (root->_data < data)
		{
			return _InsertRKV(root->_right, data,value);
		}
		else if (root->_data > data)
		{
			return _InsertRKV(root->_left, data,value);
		}
		else
		{
			return false;
		}
	}


	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << ":"<<root->_value<<endl;
		_InOrder(root->_right);
	}
private:
	Node* _root;//根节点
};


void BSTRKV1()
{
	BSTreeRKV<string, string> b;
	b.InsertRKV("sort", "排序");
	b.InsertRKV("left", "左边");
	b.InsertRKV("right", "右边");
	string str;
	while (cin >> str)
	{
		auto* ret = b.FindRKV(str);
		if (ret != nullptr)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "没有此单词" << endl;
		}
	}
}

void BSTRKV2()
{
	BSTreeRKV<string, int> b;
	string str[] = { "苹果","香蕉","苹果","梨子","苹果","香蕉","梨子" };
	for (int i = 0; i < 7; i++)
	{
		auto* ret = b.FindRKV(str[i]);
		if (ret == nullptr)
		{
			b.InsertRKV(str[i], 1);
		}
		else
		{
			ret->_value++;
		}
	}

	b.InOrder();
}

五、非递归和递归的完整代码

非递归:

#include<iostream>
using namespace std;
template<class k>
struct BSNode
{
	BSNode<k>* _left;
	BSNode<k>* _right;
	k _data;

	BSNode(const k& data=k())
		:_left(nullptr)
		,_right(nullptr)
		,_data(data)
	{}
};

template<class k>
class BSTree
{
	typedef BSNode<k> Node;
public:
	BSTree(){}

	BSTree(const BSTree<k>& t)
	{
		_root = Copy(t._root);

	}

	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

	~BSTree()
	{
		Destroy(_root);
	}

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}

	BSTree<k>& operator=(BSTree<k> t)
	{
		swap(_root, t._root);
		return *this;
	}
	bool insert(const k& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)//为了找到插入位置
		{
			parent = cur;
			if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(data);
		if (parent->_data > data)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		return true;
	}

	bool erase(const k& data)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				break;
			}
		}
		if (cur == nullptr)
		{
			return false;
		}

		if (cur->_left == nullptr)
		{
			if (cur == _root)
			{
				 _root= cur->_right;
			}
			else
			{
				if (parent->_left == cur)
				{
					parent->_left = cur->_right;
				}
				else
				{
					parent->_right = cur->_right;
				}
			}
			
		}
		else if (cur->_right == nullptr)
		{
			if (cur == _root)
			{
				 _root=cur->_left;
			}
			else
			{
				if (parent->_right == cur)
				{
					parent->_right= cur->_left;
				}
				else
				{
					parent->_left = cur->_left;
				}
			}
		}
		else
		{

			Node* pcur = cur->_left;//找左子树中最大值
			parent = cur;
			while (pcur->_right != nullptr)//找到最大值
			{
				parent = pcur;
				pcur = pcur->_right;
			}
			swap(pcur->_data, cur->_data);//替换
			if (parent->_left == pcur)//此时就左子树就一个结点
			{
				parent->_left = pcur->_left;
			}
			else//大部分都是这种情况
			{
				parent->_right =pcur->_left;
			}
			cur = pcur;
		}
		delete cur;
		return true;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}


	bool find(const k& data)
	{
		if (_root == nullptr)
		{
			return false;
		}
		Node* cur = _root;
		while (cur)
		{
			if (cur->_data > data)
			{
				cur = cur->_left;
			}
			else if (cur->_data < data)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}


private:
	Node* _root;//根节点
};

递归:

#include<iostream>
using namespace std;
template<class k>
struct BSNode
{
	BSNode<k>* _left;
	BSNode<k>* _right;
	k _data;

	BSNode(const k& data = k())
		:_left(nullptr)
		, _right(nullptr)
		, _data(data)
	{}
};
template<class k>
class BSTreeR
{
	typedef BSNode<k> Node;
public:
	BSTreeR() {}

	BSTreeR(const BSTreeR<k>& t)
	{
		_root = Copy(t._root);
		
	}

	BSTreeR<k>& operator=(BSTreeR<k> t)
	{
		swap(_root, t._root);
		return *this;
	}
	
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool InsertR(const k& data)
	{
		return _InsertR(_root, data);
	}

	bool FindR(const k& data)
	{
		return _FindR(_root, data);
	}

	bool EraseR(const k& data)
	{
		return _EraseR(_root, data);
	}

	~BSTreeR()
	{
		Destroy(_root);
	}
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* copynode = new Node(root->_data);
		copynode->_left = Copy(root->_left);
		copynode->_right = Copy(root->_right);
		return copynode;
	}

	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;//加引用可以置空,因为你要删除的结点的父节点就要指向空,而root刚好是指针的别名,置空,就相当于将父节点的指向置空了
	}

	bool _EraseR(Node*& root, const k& data)
	{
		if (root == nullptr)
			return false;
		if (root->_data < data)
		{
			return _EraseR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _EraseR(root->_left, data);
		}
		else
		{
			Node* del = root;
			if (root->_left == nullptr)//左为空
			{
				 root = root->_right;//然后父亲指向我的右,此不需要判断是父节点右还是左,传进来是什么就是什么,是父亲结点的指针的引用
			}
			else if (root->_right == nullptr)//右为空
			{
				root = root->_left;
			}
			else
			{
				Node* leftmax = root->_left;
				while (leftmax->_right)
				{
					leftmax = leftmax->_right;
				}
				swap(leftmax->_data, root->_data);

				return _EraseR(root->_left, data);
			}
			
			delete del;
			return true;
			
		}
	}

	bool _FindR(Node* root, const k& data)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_data < data)
		{
			return _FindR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _FindR(root->_left, data);
		}
		else
		{
			return true;
		}
	}

	bool _InsertR(Node*& root, const k& data)//传引用的好处就是得到父节点的指针,不用关心当前插入位置结点是父节点的哪个结点
	{
		if (root == nullptr)
		{
			root = new Node(data);
			return true;
		}
		if (root->_data < data)
		{
			return _InsertR(root->_right, data);
		}
		else if (root->_data > data)
		{
			return _InsertR(root->_left, data);
		}
		else
		{
			return false;
		}
	}


	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root;//根节点
};

六、总结

我们二叉搜索树实现起来还是比较简单的,要考虑的东西并不是特别多,但是二叉搜索树有最坏的情况,所以我们后面学的AVL树和红黑树可以解决这问题,其次连哥哥版本的实现,递归版本的代码两少很多,尤其在删除的时候,巧妙的使用了引用,希望大家下去画画图,理解一下怎么使用,接下来我们将通过一篇博客用刷题的刷题的方式,带大家再来更好的学习二叉树相关的知识,我们下篇再见

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

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

相关文章

JDBC案例

文章目录 案例1 &#xff1a;修改数据库中的数据案例2&#xff1a;查询数据库中的数据案例3&#xff1a;查询数据库中账户表数据&#xff0c;并将其封装成Account对象&#xff0c;&#xff0c;存储到ArrayList集合当中案例4&#xff1a;商品的增删改查1.准备环境2.查询3.添加4.…

1.微信小程序开发-快速上手

1.环境搭建 1.1 下载开发者工具 微信开发者工具下载地址与更新日志 | 微信开放文档微信开发者平台文档https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 1.2 注册小程序 获取小程序AppId 2.小程序组件 1.view&#xff1a;用于展示视图元素&#x…

WAF绕过-权限控制篇-后门免杀

WAF绕过主要集中在信息收集&#xff0c;漏洞发现&#xff0c;漏洞利用&#xff0c;权限控制四个阶段。 1、什么是WAF&#xff1f; Web Application Firewall&#xff08;web应用防火墙&#xff09;&#xff0c;一种公认的说法是“web应用防火墙通过执行一系列针对HTTP/HTTPS的安…

所有集群启动的命令

所有集群启动的命令 查询所有节点启动Hadoop集群(Yarn模式)关闭Hadoop集群Spark&#xff08;local模式&#xff09;启动Spark集群standalone模式(不用了)关闭standalone模式HA下的standalone模式关闭HA-standalone模式Yarn模式&#xff08;重点&#xff09; 关闭Spark集群启动f…

接口/Web自动化测试如何做?框架如何搭建封装?

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

数据可视化(5)热力图及箱型图

1.热力图 #基本热力图 #imshow&#xff08;x&#xff09; #x&#xff0c;数据 x[[1,2],[3,4],[5,6],[7,8],[9,10]] plt.imshow(x) plt.show() #使用热力图分析学生的成绩 dfpd.read_excel(学生成绩表.xlsx) #:表示行号 截取数学到英语的列数 xdf.loc[:,"数学":英语].…

技能生态链职业技能等级评价认定业务

一、项目背景 根据《关于公布广东省2022年第一批职业技能等级认定社会培训评价组织名单的通知》&#xff08;粤人社函〔2022〕76号&#xff09;&#xff0c;广东泰迪智能科技股份有限公司于2022年入选广东省2022年第一批职业技能等级认定社会评价组织&#xff0c;并根据《关于…

《水经注地图服务》发布的影像数据如何在OsgEarth中调用

OsgEarth 是一个用于OpenSceneGraph (OSG)的可扩展地形渲染工具包&#xff0c;它是一个开源、高性能、3D 图形工具包。 只需创建一个简单的 XML 文件&#xff0c;将其指向您的图像、高程和矢量数据&#xff0c;将其加载到您最喜欢的 OSG 应用程序中&#xff0c;然后开始&#…

蓝桥云课ROS机器人旧版实验报告-05导航功能

项目名称 实验五 导航功能 成绩 内容&#xff1a;创建变换、发布传感器消息、里程数据信息、创建基础控制器、创建地图&#xff0c;机器人配置、全局和局部代价地图、rviz详细配置、自适应蒙特卡洛定位&#xff0c;避障&#xff0c;目标发送 实验记录&#xff08;70分&…

Jenkins工具系列 —— 插件 实现用户权限分配与管理

文章目录 安装插件 Role-based Authorization Strategy添加用户注册配置权限查看当前使用者&#xff0c;获取user id配置管理员权限配置普通用户权限&#xff08;非管理员权限&#xff09; 小知识 安装插件 Role-based Authorization Strategy 点击 左侧的 Manage Jenkins —&…

[LeetCode]只出现一次的数字相关题目(c语言实现)

文章目录 LeetCode136. 只出现一次的数字ⅠLeetCode137. 只出现一次的数字 IILeetCode260. 只出现一次的数字 IIILeetCode268. 丢失的数字 LeetCode136. 只出现一次的数字Ⅰ 题目: 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元…

使用MyBatis(2){使用myBatis操作增删改查/动态SQL}

目录 一、定义接口、实体类、创建XML文件实现接口&#xff09; 二、MyBatis的增删改查 &#x1f345;1、MyBatis传递参数查询 &#x1f388;写法一 &#x1f388;写法二 &#x1f388;两种方式的区别 &#x1f345;2、删除操作 &#x1f345;3、根据id修改用户名 &#…

Java 基础进阶总结(一)反射机制学习总结

文章目录 一、初识反射机制1.1 反射机制概述1.2 反射机制概念1.3 Java反射机制提供的功能1.4 反射机制的优点和缺点 二、反射机制相关的 API2.1 一、初识反射机制 1.1 反射机制概述 JAVA 语言是一门静态语言&#xff0c;对象的各种信息在程序运行时便已经确认下来了&#xff0…

延长周末体验感

延长周末体验感 写在最前面周末的时间规划题外话善解人意的chatgpt 提升周末体验感的好方法随机选择一个周末活动 怎样才能获得充分的休息 写在最前面 话题征文~ https://activity.csdn.net/creatActivity?id10533&spm1011.2432.3001.9644 工作以后常常容易感到疲于奔命…

python简单小游戏代码100行,python小游戏程序源代码

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python简单小游戏代码100行&#xff0c;python小游戏代码能用的&#xff0c;现在让我们一起来看看吧&#xff01; Python编写简易猜数字小游戏&#xff08;附完整代码&#xff09; 猜数字游戏是一款非常经典的小游戏&am…

AD21 PCB设计的高级应用(九)3D PDF的输出

&#xff08;九&#xff09;3D PDF的输出 1.3D PDF的输出2.制作PCB 3D视频 1.3D PDF的输出 Altium Designer 19 带有 3D输出功能,能够直接将 PCB 的 3D效果输出到 PDF 中。 ’(1)打开带有 3D 模型的 PCB 文件,执行菜单栏中“文件”→“导出”→“PDF3D”命令&#xff0c;选择…

P1419 寻找段落(二分答案)(内附封面)

寻找段落 题目描述 给定一个长度为 n n n 的序列 a a a&#xff0c;定义 a i a_i ai​ 为第 i i i 个元素的价值。现在需要找出序列中最有价值的“段落”。段落的定义是长度在 [ S , T ] [S, T] [S,T] 之间的连续序列。最有价值段落是指平均值最大的段落。 段落的平均值…

SpringBoot整合TrueLicense生成和验证License证书

一 License介绍 License&#xff0c;也就是版权许可证书&#xff0c;一般用于收费软件给付费用户提供的访问许可证明。根据应用部署位置的不同&#xff0c;一般可以分为以下几种情况讨论&#xff1a; 应用部署在开发者自己的云服务器上。这种情况下用户通过账号登录的形式远程…

Halcon——在C#中各数据类型的相互转换

Halcon——在C#中各数据类型的相互转换 前言一、HObject to1.HObject to HImage 二、HTuple to1.HTuple to Int2.HTuple to Double3.HTuple to String4.HTuple to long5.HTuple to object6.HTuple to Arr 总结 前言 用c#进行Halcon代码转换的时候&#xff0c;虽然有halcon自带…