C++【二叉树进阶(二叉搜索树)】

news2024/11/17 1:33:13

文章目录

  • 前言
  • 1、二叉搜索树
    • 1-1、 二叉搜索树概念
  • 2、二叉搜索树操作
    • 2-1、树和节点的基本框架
    • 2-2、二叉搜索树的查找
    • 2-3、中序遍历
    • 2-4、二叉搜索树的插入
    • 2-5、二叉搜索树的删除
  • 3、二叉搜索树的模拟实现
    • 3-1、循环版本
    • 3-2、递归版本
  • 4、二叉搜索树的应用
    • 4-1、K模型
    • 4-2、KV模型
    • 4-3、KV模型的代码样例
  • 5、二叉搜索树的性能分析
  • 6、总结


前言

二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:

  1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  1. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
  1. 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
  1. 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

因此本节借二叉树搜索树,对二叉树部分进行收尾总结。


1、二叉搜索树

1-1、 二叉搜索树概念

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

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

在这里插入图片描述


2、二叉搜索树操作

2-1、树和节点的基本框架

template<class k>
class BSTreeNode //结构体——包含节点,左指针和右指针
{
public:
	BSTreeNode<k>* _left;//节点的左指针
	BSTreeNode<k>* _right;//节点的右指针
	k _key;//节点的值

	BSTreeNode(const k& key)//构造函数,不写的话new生成不了节点
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}
};

template<class k>
class BSTree
{
	typedef BSTreeNode<k> Node;//这里重定义节点,方便后面的操作
public:
	Node* _root = nullptr;//根节点

2-2、二叉搜索树的查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。

bool Find(const k& key)//查找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;
}

2-3、中序遍历

对于二叉搜索树而言,每一个节点的左子树值都比该节点的值小,右子树的值都比该节点的值要大。所以,中序遍历二叉搜索树是可以得到一个升序序列的!

void _InOrder(Node * root)//中序遍历
{
	if (root == nullptr)
	{
		return;
	}
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

2-4、二叉搜索树的插入

插入的具体过程如下:

a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

在这里插入图片描述

bool Insert(const k& key)//插入
{
	if (_root == nullptr)//空树直接new节点就行
	{
		_root = new Node(key);
		return true;
	}
	Node* cur = _root;//cur从根开始找
	Node* parent = nullptr;//记录父节点
	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);//到这里就找到了我们要插入节点的为止,我们new(key)
	if (parent->_key < key)//如果父亲的_key值小于我们,就链接在父节点的右边
	{
		parent->_right = cur;
	}
	else//如果父亲的_key值大于我们,就链接在父节点的左边
	{
		parent->_left = cur;
	}
	return true;
}

2-5、二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

a. 要删除的结点无孩子结点

b. 要删除的结点只有左孩子结点

c. 要删除的结点只有右孩子结点

d. 要删除的结点有左、右孩子结点

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来:

b. 右子树为空
c. 左子树为空
d. 左右子树都不为空

因此真正的删除过程如下:

情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小:右子树的最左值,也就是右子树最小节点),用它的值填补到被删除节点 中,再来处理该结点的删除问题–替换法删除

在这里插入图片描述
情况b:
在这里插入图片描述
情况c:
在这里插入图片描述
情况d:
在这里插入图片描述

我们来看看代码:

bool Erase(const k& key)//删除
{
	Node* cur = _root;//cur向下面走,找删除节点的key值
	Node* parent = nullptr;//parent就是删除节点的父节点——也就是cur的父节点
	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)// 1、左为空
			{
				//if(parent == nullptr)
				if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
				{
					//这里的cur和_root都是根节点了,我们让_root等于_root的右节点,这样下面删除cur,
					//也就是删除原来的根节点了,而_root现在变成了原来树的右子树的第一个节点
					_root = cur->_right;
				}
				else
				{
					if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右子树链接父亲左边
					{
						parent->_left = cur->_right;
					}
					else//如果父节点的右边是cur,删除cur之后,cur的右子树链接父亲右边
					{
						parent->_right = cur->_right;
					}
				}
				delete cur;//这个时候删除就没问题了
			}
			else if (cur->_right == nullptr)// 2、右为空
			{
				//if(parent == nullptr)
				if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
				{
					//这里的cur和_root都是根节点了,我们让_root等于_root的左节点,这样下面删除cur,
					//也就是删除原来的根节点了,而_root现在变成了原来树的左子树的第一个节点
					_root = cur->_left;
				}
				else
				{
					if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右左子树链接父亲左边
					{
						parent->_left = cur->_left;
					}
					else//如果父节点的右边是cur,删除cur之后,cur的左子树链接父亲右边
					{
						parent->_right = cur->_left;
					}
				}
				delete cur;
			}
			else// 3、左右都不为空,替换删除
			{
				//这里parent不能初始化为nullptr,如果删除的cur是根,那么parent就不会进入while循环进行更新
				//那么parent就一直为空,下面parent->_left等操作就非法了!!!
				Node* parent = cur; //parent就是替换完之后,删除节点的父节点
				Node* min = cur->_right;//min是右子树的最小值
				while (min->_left)//找右子树最小的数,(也可以找左子树最大的数,但是这样太麻烦了)
				//while (min && min->_left)//这里不用这么写,因为前提条件就是cur删除节点左右都不为空
				{
					parent = min;
					min = min->_left;
				}
				cur->_key = min->_key;//把右子树最小值给给cur
				if (parent->_left == min)//min是最左值,所以没有左子树,只有右子树需要托孤
				{
					parent->_left = min->_right;
				}
				else
				{
					parent->_right = min->_right;
				}
				delete min;
			}
			return true;
		}
	}
	return false;
}

3、二叉搜索树的模拟实现

这里有两个版本:循环和递归
我们只用掌握一个版本就可以了!

3-1、循环版本

BSTree.h:循环版本

#pragma once
///循环版本
template<class k>
class BSTreeNode //结构体——包含节点,左指针和右指针
{
public:
	BSTreeNode<k>* _left;
	BSTreeNode<k>* _right;
	k _key;

	BSTreeNode(const k& key)//构造函数,不写的话new生成不了节点
		:_left(nullptr)
		, _right(nullptr)
		,_key(key)
	{}
};

template<class k>
class BSTree
{
	typedef BSTreeNode<k> Node;
public:
	bool Insert(const k& key)//插入
	{
		if (_root == nullptr)//空树直接new节点就行
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;//cur从根开始找
		Node* parent = nullptr;//记录父节点
		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);//到这里就找到了我们要插入节点的为止,我们new(key)
		if (parent->_key < key)//如果父亲的_key值小于我们,就链接在父节点的右边
		{
			parent->_right = cur;
		}
		else//如果父亲的_key值大于我们,就链接在父节点的左边
		{
			parent->_left = cur;
		}
		return true;
	}

	bool Find(const k& key)//查找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* cur = _root;//cur向下面走,找删除节点的key值
		Node* parent = nullptr;//parent就是删除节点的父节点——也就是cur的父节点
		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)// 1、左为空
				{
					//if(parent == nullptr)
					if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
					{
						//这里的cur和_root都是根节点了,我们让_root等于_root的右节点,这样下面删除cur,
						//也就是删除原来的根节点了,而_root现在变成了原来树的右子树的第一个节点
						_root = cur->_right;
					}
					else
					{
						if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右子树链接父亲左边
						{
							parent->_left = cur->_right;
						}
						else//如果父节点的右边是cur,删除cur之后,cur的右子树链接父亲右边
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;//这个时候删除就没问题了
				}
				else if (cur->_right == nullptr)// 2、右为空
				{
					//if(parent == nullptr)
					if (cur == _root)//如果是一颗单枝树,删除根节点,parent就为空了,那么parent->_left等操作违法
					{
						//这里的cur和_root都是根节点了,我们让_root等于_root的左节点,这样下面删除cur,
						//也就是删除原来的根节点了,而_root现在变成了原来树的左子树的第一个节点
						_root = cur->_left;
					}
					else
					{
						if (parent->_left == cur)//如果父节点的左边是cur,删除cur之后,cur的右左子树链接父亲左边
						{
							parent->_left = cur->_left;
						}
						else//如果父节点的右边是cur,删除cur之后,cur的左子树链接父亲右边
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				else// 3、左右都不为空,替换删除
				{
					//这里parent不能初始化为nullptr,如果删除的cur是根,那么parent就不会进入while循环进行更新
					//那么parent就一直为空,下面parent->_left等操作就非法了!!!
					Node* parent = cur; //parent就是替换完之后,删除节点的父节点
					Node* min = cur->_right;//min是右子树的最小值
					while (min->_left)//找右子树最小的数,(也可以找左子树最大的数,但是这样太麻烦了)
					//while (min && min->_left)//这里不用这么写,因为前提条件就是cur删除节点左右都不为空
					{
						parent = min;
						min = min->_left;
					}
					cur->_key = min->_key;//把右子树最小值给给cur
					if (parent->_left == min)//min是最左值,所以没有左子树,只有右子树需要托孤
					{
						parent->_left = min->_right;
					}
					else
					{
						parent->_right = min->_right;
					}
					delete min;
				}
				return true;
			}
		}
		return false;
	}

	void InOrder()//树的递归要使用根节点,根节点都是私有的,所以要通过嵌套的方法来调用
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node * root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	Node* _root = nullptr;//根节点
};

void Test1()//测试用例
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTree<int> t;
	for (const auto& e : a)
	{
		t.Insert(e);
	}
	for (const auto& e : a)
	{
		t.Erase(e);
		t.InOrder();
	}
}

3-2、递归版本

BSTreeR.h:递归版本
这里递归版本就不做过多解释了,重要代码都有注释

#pragma once
///递归版本
template<class k>
class BSTreeNode //结构体——包含节点,左指针和右指针
{
public:
	BSTreeNode<k>* _left;
	BSTreeNode<k>* _right;
	k _key;

	BSTreeNode(const k& key)//构造函数,不写的话new生成不了节点
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

template<class k>
class BSTreeR
{
	typedef BSTreeNode<k> Node;
public:
	BSTreeR()
		:_root(nullptr)
	{}
	~BSTreeR()
	{
		Destory(_root);
		_root = nullptr;
	}
	BSTreeR(BSTreeR<k>& t)
	{
		this->_root = Cope(t._root);
	}
	BSTreeR operator=(BSTreeR<k> t)
	{
		swap(this->_root, t._root);
		return *this;
	}
	bool InsertR(const k& key)
	{
		return _InsertR(_root, key);
	}
	bool FindR(const k& key)
	{
		return _FindR(_root, key);
	}
	bool EraseR(const k& key)
	{
		return _EraseR(_root, key);
	}

	void InOrder()//树的递归要使用根节点,根节点都是私有的,所以要通过嵌套的方法来调用
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	void Destory(Node* root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;

	}
	Node* Cope(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newnode = new Node(root->_key);
		newnode->_left = Cope(root->_left);
		newnode->_right = Cope(root->_right);
		return newnode;
	}
	bool _InsertR(Node*& root, const k& key)//这里的root使用引用,这样就不用找parent节点了,root是指针的引用
	{
		if (root == nullptr)//空树直接new节点
		{
			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;
	}
	bool _EraseR(Node*& root, const k& key)//root是引用,是父节点指向删除节点的指针的引用
	{
		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* dl = root;
			if (root->_right == nullptr)//如果删除的节点的右子树没有,就让父节点指向我的左
			{
				root = root->_left;
			}
			else if (root->_left == nullptr)//如果删除节点的左子树没有,就让父节点指向我的右
			{
				root = root->_right;
			}
			else
			{
				Node* minright = root->_right;//找右边最小值
				while (minright->_left)
				{
					minright = minright->_left;
				}
				swap(root->_key, minright->_key);//交换两个值
				return _EraseR(root->_right, key);//交换完之后,删除的key值在右子树的最左边
			}
			delete dl;
			return true;
		}
	}
	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	Node* _root = nullptr;//根节点
};

void Test1()//测试用例
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	BSTreeR<int> t;
	for (const auto& e : a)
	{
		t.InsertR(e);
	}
	BSTreeR<int> copyt(t);
	copyt.InOrder();

	int b[] = { 15,54,545,4,46,65,464,84,9 };
	BSTreeR<int> ax;
	for (const auto& e : b)
	{
		ax.InsertR(e);
	}
	ax.InOrder();

	copyt = ax;
	copyt.InOrder();

	for (const auto& e : a)
	{
		t.EraseR(e);
		t.InOrder();
	}
}

4、二叉搜索树的应用

4-1、K模型

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

4-2、KV模型

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
·比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
·再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

4-3、KV模型的代码样例

样例1:将我们上面的二叉搜索树循环版本改为KV模型

//通过一个值找另一个值key value,也就是map(下一章节讲)
namespace KV
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;//这里的key值就是我们上面的key值,KV模型中,比较大小、查找等等操作都是与key值有关,但key值不能更改
		V _value;//_value我们用不上,value值可以更改

		BSTreeNode(const K& key, const V& value)
			:_key(key)
			, _value(value)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};
	//BSTree<string, vector<string>>  字典查找
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool insert(const K& key, const V& value)//与上面没有什么差别,就是参数多了个value
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}
			Node* cur = _root;
			Node* parent = _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;
		}
		void Inorder()
		{
			_Inorder(_root);
		}
		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;
		}
	private:
		void _Inorder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_Inorder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_Inorder(root->_right);
		}
		Node* _root = nullptr;
	};
}

样例2:映射——通过key值找value

void Test2()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	// 词库中单词都放进这个搜索树中
	// Key的搜索模型,判断在不在?
	// 场景:检查单词拼写是否正确/车库出入系统/...
	//K::BSTree<string> dict;

	// Key/Value的搜索模型,通过Key查找或修改Value
	KV::BSTree<string, string> dict;
	dict.Insert("sort", "排序");
	dict.Insert("string", "字符串");
	dict.Insert("left", "左边");
	dict.Insert("right", "右边");

	string str;
	while (cin>>str)
	{
		KV::BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "无此单词" << endl;
		}
	}
}

样例3:统计水果出现次数



void TestBSTree3()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓","苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	KV::BSTree<string, int> countTree;
	for (auto e : arr)
	{
		auto* ret = countTree.Find(e);
		if (ret == nullptr)
		{
			countTree.Insert(e, 1);
		}
		else
		{
			ret->_value++;
		}
	}

	countTree.Inorder();
}

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

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

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

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

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

6、总结

二叉搜索树只是开胃小菜,主要就是为了下面的set和map、avl、红黑树等内容做铺垫。但是也要掌握,毕竟这是为了后面的内容打基础!

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

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

相关文章

Linux 中断实验

目录 一、Linux 中断简介 上半部与下半部 二、添加设备树 三、编写驱动 1、定义宏 2、编写一个key结构体 3、imx6uirq设备添加成员 ​编辑4、按键中断处理函数 5、按键初始化 6、在驱动入口添加初始化 7、 驱动出口函数 代码如下 四、利用定时器进行消抖处理 1、添…

Spring Security in Action 第十四章 实现资源服务器端

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;以实战为线索&#xff0c;逐步深入SpringSecurity相关知识相关知识&#xff0c;打造完整的SpringSecurity学习步骤&#xff0c;提升工程化编码能力和思维能力&#xff0c;写出高质量代码。希望大家都能够从中有所收获&#…

ArcGIS中的附件功能

从ArcGIS10起,空间数据库增加了"附件"的功能,可灵活管理与要素相关的附加信息,可以是图像、PDF、文本文档或任意其他文件类型。例如,如果用某个要素表示建筑物,则可以使用附件来添加多张从不同角度拍摄的建筑物照片。 启动附件功能 要想使用附件功能,要素类必…

Docker 中遇到的问题

1&#xff1a;docker-tomcat 篇 第一天启动主机和虚拟机都可以正常访问&#xff0c;晚上睡觉的时候就挂起关机睡觉了&#xff0c;但到了第二天主机访问不了了&#xff0c;ping 也能ping 通&#xff0c;后来停掉容器&#xff0c;重启了虚拟机就好了&#xff0c;就很离谱。 这是成…

Web3CN|Damus刷频背后,大众在期待什么样的去中心化社交?

刚过去的一周&#xff0c;许多人的朋友圈包括Twitter、Faceboo在内都在被一串公钥字母刷屏&#xff0c;其重要起因就是 Twitter 前首席执行官 Jack Dorsey 发推称&#xff0c;&#xff08;2月1日&#xff09;基于去中心化社交协议 Nostr 的社交产品 Damus 和 Amethyst 已分别在…

互联网舆情监测系统的设计研究,TOOM舆情监测系统研究框架?

舆情监测研究分析是指通过对社会公众对某个事件、话题、品牌、政策等的态度和情绪进行收集、处理、分析和评估&#xff0c;帮助政府、企业、媒体等利益相关者及时掌握公众的反应&#xff0c;做好应对危机和制定舆情管理策略的工作&#xff0c;互联网舆情监测系统的设计研究&…

全志V853芯片 如何在Tina V85x平台切换sensor?

目的 V85x某方案目前默认Sensor是GC2053。实际使用时若需要用到GC4663&#xff08;比如wdr功能&#xff09;和SC530AI&#xff08;支持500W&#xff09;&#xff0c;可按如下步骤完成切换。 步骤 下面以GC4663为例&#xff0c;SC530AI按相应方式适配。 Step1 检查Sensor驱动…

Spring Security in Action 第十七章 全局方法安全:预过滤和后过滤

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;以实战为线索&#xff0c;逐步深入SpringSecurity相关知识相关知识&#xff0c;打造完整的SpringSecurity学习步骤&#xff0c;提升工程化编码能力和思维能力&#xff0c;写出高质量代码。希望大家都能够从中有所收获&#…

Rust学习入门--【8】复合类型

复合类型&#xff08;compound type&#xff09; 可以将多个不同类型的值组合为一个类型。 Rust中提供了两种内置的复合数据类型&#xff1a;元组&#xff08;tuple&#xff09;和数组&#xff08;array&#xff09;。 元组类型 元组是一个具有 固定长度 的数据集合 —— 无…

按键输入驱动

目录 一、硬件原理 二、添加设备树 1、创建pinctrl 2、创建节点 3、检查 编译复制 三、修改工程模板​编辑 四、驱动编写 1、添加keyio函数 2、添加调用 3、驱动出口函数添加释放 4、添加原子操作 5、添加两个宏定义 6、初始化原始变量 7、打开操作 8、读操作 总体代…

自启动管理 - Win10

自启动管理 - Win10前言关闭开机自启方案1&#xff1a;在软件中设置方案2&#xff1a;在任务管理器设置方案3&#xff08;不推荐&#xff09;&#xff1a;通过注册表管理方案4&#xff1a;通过第三方工具管理工具1&#xff1a;360安全卫士工具2&#xff1a;Autoruns工具3&#…

性能测试概述

目录 一.什么是性能测试 1.生活中软件存在的性能问题 2.性能测试的概念 3.功能测试和性能测试的区别 4.什么样的软件表现是性能好的表现&#xff0c;什么样的软件是性能不好的表现 二.一个项目为什么要进行性能测试 三.性能测试常见术语以及衡量指标 1.专业术语&#x…

Docker的数据卷管理与容器互联

目录 一、Docker数据管理介绍 二、数据卷 1、数据卷概念 三、数据卷容器 1、数据卷容器的概念 2、数据卷容器示例 四、容器互联 1、容器互联概念 2、容器互联示例 一、Docker数据管理介绍 用户在使用Docker的过程中&#xff0c;往往需要能查看容器内应用产生的数据&…

基于transformer和图卷积网络的人体运动预测时空网络

效果演示&#xff1a; python行为识别行为骨骼框架检测动作识别动作检测行为动作分类近年来&#xff0c;人体运动预测已成为计算机视觉领域的一个活跃研究课题。然而&#xff0c;由于人体运动的复杂性和随机性&#xff0c;它仍然是一个具有挑战性的问题。在以前的工作中&#x…

[golang] 实现 jwt 方式登录

1 Jwt 和 Session 登录方案介绍 JSON Web Token&#xff08;缩写 JWT&#xff09;是目前流行的跨域认证解决方案。 原理是生存的凭证包含标题 header&#xff0c;有效负载 payload 和签名组成。用户信息payload中&#xff0c;后端接收时只验证凭证是否有效&#xff0c;有效就…

【Spark分布式内存计算框架——Spark Core】11. Spark 内核调度(下)

8.5 Spark 基本概念 Spark Application运行时&#xff0c;涵盖很多概念&#xff0c;主要如下表格&#xff1a; 官方文档&#xff1a;http://spark.apache.org/docs/2.4.5/cluster-overview.html#glossary Application&#xff1a;指的是用户编写的Spark应用程序/代码&#x…

leetcode练习二:排序

文章目录排序一、排序算法1.1 冒泡排序1.1.1 算法步骤1.1.2 算法分析1.1.3 代码实现&#xff1a;1.1.4 冒泡排序优化1.2 选择排序1.2.1 算法步骤1.2.2 算法分析1.2.3 代码实现1.3 插入排序1.3.1 算法步骤1.3.2 算法分析1.3.3 代码实现1.4 希尔排序1.4.1 算法步骤1.4.2 算法分析…

【网络基础】DNS是什么

本文不会直接引入复杂枯燥概念&#xff0c;用形象例子通俗讲解&#xff0c;旨在入门理解。 DNS作用 DNS是用来做域名解析的。 相当于把网址翻译成实际ip地址&#xff0c;供其他设备访问。 一个例子 有一个网站的服务器IP地址为1.1.1.1&#xff0c;用电脑访问该网站的话只需…

挂载Samba到Windows系统和Linux系统

1、搭建Samba服务 1.1安装Samba服务 yum -y install samba结果如下&#xff1a; 1.2配置Samba服务 修改Samba服务的配置文件 vim /etc/samba/smb.conf[sambadir]自定义路径 comment Samba Directories自定义描述 path /samba/dir自定义路径 [global]workgroup SAMBAsec…