[数据结构] AVL树 模拟实现AVL树

news2024/9/22 1:34:43

标题:[数据结构] AVL树 && 模拟实现AVL树

@水墨不写bug



正文开始:

目录

(一)普通二叉搜索树的痛点 

 (二)AVL树简介

(1)AVL树的概念 

(三)AVL树的实现

(1)AVL树节点的定义

(2)AVL树的插入

(3)AVL树的旋转

i,左单旋(新节点插入较高右子树的右侧---右右:左单旋)

ii,右单旋(新节点插入较高左子树的左侧---左左:右单旋)

iii,左右双旋(新节点插入较高左子树的右侧---左右:先左单旋再右单旋)

iv,右左双旋(新节点插入较高右子树的左侧---右左:先右单旋再左单旋)

(四)AVL树的验证

(五)适用场景与性能分析


(一)普通二叉搜索树的痛点 

        在学习map和set之前,我们先认识一下AVL树和红黑树,他们是平衡二叉树,不同的是控制平衡的方法不同。本文主要讲解AVL树的概念以及实现的原理,从源代码角度带你理解AVL树的控制平衡的方法。

        map/multimap/set/multiset这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的。
但是一般的二叉搜索树有一个致命的缺陷:往树中插入元素有序或者接近有序,二叉搜索树会退化为接近链表形状的单支树结构,时间复杂度会退化为O(N)。

        为了能够利用二叉树的结构优势,同时避免二叉树时间复杂度退化的缺陷,AVL树横空出世。


 (二)AVL树简介

        AVL树的概念由两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年提出,目的是为了解决在  数据有序或者接近有序的二叉搜索树  中查找元素时的  时间复杂度退化的问题。

        平衡的二叉搜索树结构查找的时间复杂度为O(logN),是一个高效的查找复杂度。但是一旦在构建二叉树时,数据有序或者接近有序,那么二叉搜索树会退化为单支树,这就相当于在链表中查找元素,复杂度为O(N),效率低下。

        两位数学家提出:当向二叉搜索树中插入新节点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
 

(1)AVL树的概念 

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  •         它的左右子树都是AVL树
  •         左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

什么是平衡因子? 

        平衡因子是人为引入的快速判断二叉树是不是AVL树的一个标准。

        平衡因子是一个变量,它存储在每一个二叉树节点中。

        根据习惯,平衡因子=右子树的高度-左子树的高度。

(图中为平衡因子的计算结果)

        一旦一个节点的平衡因子的绝对值超过1,这表示这个根节点的左右子树的高度差超过1,那么就需要特殊操作(旋转),使得这棵树的左右子树的高度差的不超过1。

        如果一棵树是高度平衡的,那么它就是AVL树。如果他有n个节点,那么他的高度可以保持在log(n),那么在搜索时,时间复杂度就降下来了,为O(logN)。 


(三)AVL树的实现

        AVL树的特点是仅仅使用旋转这一种特殊操作来维持搜索树的平衡,这与其他的维持搜索树平衡的方法相比,是一个特点。本文我们依靠在节点中加入parent指针来更新平衡因子,来实现AVL树,但是这并不代表实现AVL树的方法仅此一种。

(1)AVL树节点的定义

        对于节点内存储的值,可以是一个值key,也可以是一个键值对pair{key,val},但是只存一个值key可以归为pair{key,key},存的值与键值相等,于是,本文的AVL树采用内部存储pair{key,val}键值对。

什么是key,val?

        key是存取时判断的标准,val是key对应的值。key,val都可以是int;也可以key是int,val是string。(需要根据实际情况判断)

什么是pair? 

        pair是一种STL中的类:

        该类将一对不同类型的值(T1和T2)耦合在一起。单个值可以通过它的公共成员first和second来访问。

AVL树节点的定义:

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K,V>& pair_ = pair<K,V>())
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)//根节点的_root是空
		, _pair(pair_)
		, _bf(0)
	{}

	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _pair;
	int _bf;   // 节点的平衡因子
};

        唯一需要注意的是缺省参数要给  pair<K,V>()  表示一个匿名对象,这种写法的好处我们以前文章讨论过,这里不再赘述。
 

(2)AVL树的插入

        AVL树的插入过程分为两个步骤:

                (1)与普通二叉树相同的插入操作;

                (2)调整节点的平衡因子,根据平衡因子,判断是否要进行旋转操作。

         就插入操作而言,AVL树相对于普通的二叉搜索树,对二叉树的结构(或者形状)更加关切。

插入操作:(普通二叉树)

        处理特殊情况,根为空,直接插入即可。

        对于一般情况,先根据键值key找到插入节点的位置,接下来进行的就是节点间指针的连接了——为了在找到插入位置时能够找到插入位置的_parent的位置,创建一个parent指针在cur向下查找的时候记录parent,这样就可以成功得到_parent。但是,现在子节点可以找到父亲,但是父亲没有办法确定子节点是他的左孩子还是右孩子,想要成功连接,还需要确定当前节点是parent的左还是右,所以需要有一次单独的判断。

(具体实现如下:)

// 在AVL树中插入值为data的节点
bool insert(const pair<K,V>& pair)
	//1,插入  2,调整平衡因子
{
	//处理特殊情况
	if (_root == nullptr)
	{
		_root = new Node(pair);
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_pair.first > pair.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_pair.first < pair.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else//插入的节点等于data时退出
		{
			return false;
		}
	}
	//cur就是要插入的位置,parent记录父亲位置
	cur = new Node(pair);
	if (pair.first > parent->_pair.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;
	

插入操作:(AVL树)

两个步骤:

        (1)与普通二叉树相同的插入操作;(与上面完全一致)

        (2)调整节点的平衡因子,根据平衡因子,判断是否要进行旋转操作。(这是AVL树多做的部分)


如何调整平衡因子?

        平衡因子是每一个节点都有的,并且插入一个节点,只会对插入的子树的父祖节点的平衡因子产生影响。所以我们使用迭代法,从下向上依次更新平衡因子:

       

        每一次迭代后,cur和parent会向上移动,同时平衡因子也会更新。

更新平衡因子的方法是比较容易想的:

        只看当前新插入节点和parent节点——如果心插入节点是parent左,平衡因子_bf = 右子树高度 - 左子树高度;在左侧插入会导致平衡因子减小1;

        只看当前新插入节点和parent节点——如果心插入节点是parent右,平衡因子_bf = 右子树高度 - 左子树高度;在→侧插入会导致平衡因子增大1;

if (cur == parent->_left)
    parent->_bf--;
else
    parent->_bf++;

(上述代码是在每次迭代后的更新平衡因子操作)

如何根据平衡因子来判断是否需要旋转?

        在更新平衡因子之后,需要根据平衡因子来判断是否需要旋转,更新平衡因子之后,父节点的平衡因子只有三种情况:

//parent的平衡因子为0    ——原来是1、-1,插入后为0,插入的是较矮的一边,高度不变化,不需要向上追踪父族
//parent平衡因子是-1、1  ——原来是0,插入后为1、-1,原来平衡,插入后不平衡,高度变化,需要向上
//parent平衡因子是2、-2  ——原来是1、-1,插入后加剧了不平衡,高度变化,需要旋转

 我们只需要将上述的三种情况转化为代码即可:

while (parent)
	//parent的平衡因子为0    ——原来是1、-1,插入后为0,插入的是较矮的一边,高度不变化,不需要向上追踪父族
	//parent平衡因子是-1、1  ——原来是0,插入后为1、-1,原来平衡,插入后不平衡,高度变化,需要向上
	//parent平衡因子是2、-2  ——原来是1、-1,插入后加剧了不平衡,高度变化,需要旋转
{

	if (cur == parent->_left)
		parent->_bf--;
	else
		parent->_bf++;

	if (parent->_bf == 0)
	{
		break;
	}
	else if (parent->_bf == 1 || parent->_bf == -1)
	{
		cur = parent;
		parent = parent->_parent;
	}
	else if (parent->_bf == 2 || parent->_bf == -2)
		//需要旋转
	{
		//....
	}
	else
	{
		assert(false);
	}
}

        在向上迭代的过程中,如果cur节点的parent节点的平衡因子为2或者-2,这就表明这棵树已经不平衡了,将parent和cur所在的子树单独拿出来,旋转处理。


(3)AVL树的旋转

        AVL树的旋转总结来说一共右四种情况,分别是:左单旋,右单旋,左右双旋,右左双旋。

这四种情况的区别仅仅是插入节点的位置不同。 


        以下四种情况是触发旋转后才考虑的,如果在插入后没有触发旋转,则对下面四种旋转的讨论是没有意义的 ;(画的图也是刚好可以触发旋转的抽象图,目的是便于理解和梳理思路,便于写代码)

i,左单旋(新节点插入较高右子树的右侧---右右:左单旋)

        通过观察,我们发现:

        1,旋转的过程需要将bf等于2或者-2的子树拿出来进行——新节点的插入导致15的bf==2,于是把15这课子树拿出来进行旋转处理。

        2,旋转后,整棵树变化为平衡二叉树(左子树和右子树高度之差小于2)。

        3,至于旋转的为什么这样旋,以及可行性问题等,可以参考当年提出AVL树的学术论文。


        这里我们先考虑具体实现:需要几个指针变量记录节点的地址,便于改变树的形状:

parent为树的根节点,subR,subRL不再赘述。

对于一种特殊情况:当高度H==0,这时subRL==nullptr,不能再对subRL解引用,所以在访问subRL的时候,需要特殊判空。

void RotateL(Node* parent)
{
	Node* subR = parent->_right, * subRL = subR->_left;
	

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	subR->_left = parent;
	parent->_parent = subR;

//....
}

        接下来,parent的父亲节点以及父亲节点和parent之间的关系没有确定,所以需要一个变量提前保存parent的父亲节点:

Node* Parentparent = parent->_parent;

        判断parent与他的父亲节点的关系:

        特殊的,如果parent的父亲是空,说明parent是整棵树的根,这种情况直接将旋转后的新根赋值给_root即可;

        一般情况,需要判断parent是他的父亲的左孩子还是右孩子:

if (Parentparent == nullptr)
{
	subR->_parent = nullptr;
	_root = subR;
}
else
{
	if (Parentparent->_left == parent)
	{
		Parentparent->_left = subR;
	}
	else
	{
		Parentparent->_right = subR;
	}
	subR->_parent = Parentparent;
}

        最后,根据抽象图更新平衡因子即可:

			subR->_bf = parent->_bf = 0;

左单旋整体代码逻辑:

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right, * subRL = subR->_left;
		Node* Parentparent = parent->_parent;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		parent->_parent = subR;

		if (Parentparent == nullptr)
		{
			subR->_parent = nullptr;
			_root = subR;
		}
		else
		{
			if (Parentparent->_left == parent)
			{
				Parentparent->_left = subR;
			}
			else
			{
				Parentparent->_right = subR;
			}
			subR->_parent = Parentparent;
		}
		subR->_bf = parent->_bf = 0;
	}

ii,右单旋(新节点插入较高左子树的左侧---左左:右单旋)

        右单旋与与左单旋,无论是插入位置,触发条件,还是旋转处理,都与左单旋类似(某一种对称)这里给出右单旋抽象图,不再赘述:

实现参考:

// 右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left, * subLR = subL->_right;
	Node* Parentparent = parent->_parent;

	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	subL->_right = parent;
	parent->_parent = subL;

	if (Parentparent == nullptr)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (Parentparent->_left == parent)
		{
			Parentparent->_left = subL;
		}
		else
		{
			Parentparent->_right = subL;
		}
		subL->_parent = Parentparent;
	}
	//更新平衡因子,下方的树的平衡因子没有影响
	subL->_bf = parent->_bf = 0;
}

        通过总结,我们发现:插入在较高的子树的同侧(较高的左子树的左侧,较高的右子树的右侧),只需要进行一次旋转就可以树平衡化;但是对于插入位置在较高的子树的异侧时,就需要换一种处理方法——双旋。

iii,左右双旋(新节点插入较高左子树的右侧---左右:先左单旋再右单旋)

        对于这棵子树(一般来说是子树,也可能是一整颗树),90节点为根,30节点为左子树,它相对于90的右子树更高。

        插入位置在较高的左子树(30这棵子树)的右侧;这时需要先对30这棵子树左单旋,再对90这棵子树右单旋,最终90这棵子树成为平衡树。 

        

在具体实现中,我们可以复用已经实现的左单旋和右单旋的函数:

void RotateLR(Node* parent)
{
	Node* subL = parent->_left, * subLR = subL->_right;
	

	RotateL(subL);
	RotateR(parent);

    //.....
}

        但是需要注意,在旋转后,需要手动更新平衡因子,因为单旋中更新的平衡因子是只适合单旋的情况,对于双旋,自己设计更新平衡因子:

        通过观察抽象图,我们发现,左右双旋的插入位置无非只有两个:

这两种情况我们可以通过观察60的平衡因子来判断,于是,在旋转之前,提前保存60这个节点的平衡因子:

int bf = subLR->_bf;

        接下来,根据60节点的平衡因子的情况,更新90这棵子树中,插入位置的父祖节点的平衡因子即可。

        特殊处理:60这个节点的平衡因子可能为0,这表示60这个节点本身就是插入的新节点。依然根据抽象图分类假设,更新平衡因子即可。

左右双旋代码实现:

// 左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left, * subLR = subL->_right;
	int bf = subLR->_bf;

	RotateL(subL);
	RotateR(parent);

	if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		parent->_bf = 0;
		subL->_bf = -1;
	}
	else if (bf == -1)
	{
		subLR->_bf = 0;
		parent->_bf = 1;
		subL->_bf = 0;
	}
	else
		assert(false);//一般逻辑不会进入,用于检验平衡因子的异常
}

iv,右左双旋(新节点插入较高右子树的左侧---右左:先右单旋再左单旋)

         基本思路与左右双旋一致,这两种满足某种对称性,这里仅仅给出抽象图和代码实现:

右左双旋参考代码:

// 右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right, * subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(subR);
	RotateL(parent);

	if (bf == 0)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else if (bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else
		assert(false);
}

(四)AVL树的验证

        到这里,AVL树的主要工作原理你已经十分清楚了,接下来可以通过一下这个程序来测试一下我们的AVL树的代码逻辑有没有问题:

bool IsAVLTree()
{
	return _IsBalanceTree(_root);
}

bool _IsBalanceTree(Node* root)
{
	// 空树也是AVL树
	if (nullptr == root)
		return true;
	// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差

	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;

	// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
	// pRoot平衡因子的绝对值超过1,则一定不是AVL树

	if (abs(diff) >= 2 || root->_bf != diff)
		return false;

	// pRoot的左和右如果都是AVL树,则该树一定是AVL树
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

size_t _Height(Node* root)
{
	if (root == nullptr)
		return 0;

	int Hl = _Height(root->_left), Hr = _Height(root->_right);
	return Hl > Hr ? Hl + 1 : Hr + 1;
}

 如果没有问题,那么在运行如下场景时,每一次插入后,检测都是满足AVL树的:

#include"AVLTree.h"
int main()
{
	ddsm::AVLTree<int,int> at;
	//int arr[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
	int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (const auto& e : arr)
	{
		at.insert({e,e});
	}
	at.inorder();
	cout<<at.IsAVLTree();
	return 0;
}

 运行结果:

4->1
2->1
6->1
1->1
3->1
5->1
15->1
7->1
16->1
14->1

 

(五)适用场景与性能分析

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。

        但是如果要对AVL树做一些结构修改的操作,性能非常低下;(比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。)

        因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合使用AVL树来做底层来实现。

回顾:

        二叉搜索树——模拟实现

 


完~

未经作者同意禁止转载 

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

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

相关文章

LeetCode面试150——189轮转数组

题目难度&#xff1a;中等 默认优化目标&#xff1a;最小化平均时间复杂度。 Python默认为Python3。 目录 1 题目描述 2 题目解析 3 算法原理及程序实现 3.1 暴力求解 3.2 循环链表 3.3 环状替代 3.4 数组翻转 4 题目难度 参考文献 1 题目描述 给定一个整数数组 nu…

运维.Linux.bash学习笔记.数组及其使用

运维专题 Bash Shell数组及其使用 此笔记当前仍在修改和编写。 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:http…

基于N32L406+Freertos+letter_shell终端开源库移植

移植教程 这里首先感谢作者的开源 https://gitee.com/zhang-ge/letter-shell) [Letter shell 3.0 全新出发 | Letter (nevermindzzt.github.io)](https://nevermindzzt.github.io/2020/01/19/Letter shell 3.0全新出发/) 1.复制代码 将litter_shell文件夹中的所有文件复制到…

本地使用Git同步、配合Gitee同步至仓库并下拉到本地(亲手调试,全能跑通)

这几天在公司&#xff0c;同事都在使用Gitee上传项目&#xff0c;进行同步&#xff0c;我也进行了简单学习了解了一下版本控制软件Git&#xff0c;挺不错的&#xff0c;故写个笔记记录一下。 本篇博文主要涉及的内容&#xff1a; 1&#xff0c;本地写代码&#xff0c;通过Git同…

软件测试_接口测试面试题

接口测试是软件测试中的重要环节&#xff0c;它主要验证系统不同模块之间的通信和数据交互是否正常。在软件开发过程中&#xff0c;各个模块之间的接口是实现功能的关键要素&#xff0c;因此对接口进行全面而准确的测试是确保系统稳定性和可靠性的关键步骤。 接口测试的核心目…

树上dp学习总结2

今天也是侥幸刷了两道树上dp的问题&#xff0c;第一个还算简单&#xff0c;但是第二个真的可以说是我碰到的蓝题之首&#xff0c;做了一个晚上我只能留下了不争气的口水&#xff08;太饿了&#xff0c;该吃夜宵了&#xff09; P1131 [ZJOI2007] 时态同步 思路&#xff1a;一开…

RK3568笔记四十九:W25Q64驱动开发(硬件SPI1)

若该文为原创文章&#xff0c;转载请注明原文出处。 一、SPI介绍 串行外设接口 (Serial Peripheral interface) 简称 SPI&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线&#xff0c;并 且在芯片的管脚上只占用四根线&#xff0c;节约了芯片的管脚。 …

Word如何设置表格内容的中文和英文字体

1、选中需要设置的表格内容。 2、CtrlD&#xff0c;分别设置中文和英文字体&#xff0c;点确定即可。 提升自己最好的方法就是改变坏习惯&#xff0c;改变坏习惯最好的方法找习惯替代它。坏习惯不改&#xff0c;你永远受到限制&#xff0c;只能原地踏步。To do list&#xf…

爬取指定的天气网站数据

目 录 一、引言 &#xff08;一&#xff09;项目背景 &#xff08;二&#xff09;目标与意义 二、数据获取与处理 &#xff08;一&#xff09;使用的库和模块 &#xff08;二&#xff09;获取天气信息的函数 &#xff08;三&#xff09;数据预处理 三、数据分析…

python np.max怎么用

python np.max的用法&#xff1a; 语法&#xff1a;np.max&#xff1a;(a, axisNone, outNone, keepdimsFalse) 求序列的最值&#xff1b; 最少接收一个参数&#xff1b; axis&#xff1a;默认为列向&#xff08;也即 axis0&#xff09;&#xff0c;axis 1 时为行方向的最…

SQL labs-SQL注入(七,sqlmap对于post传参方式的注入,2)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。参考&#xff1a;SQL注入之Header注入_sqlmap header注入-CSDN博客 序言&#xff1a; 本文主要讲解基于SQL labs靶场&#xff0c;sqlmap工具进行的post传参方式的SQL注入&#xff0c…

如何利用大语言模型进行半监督医学图像分割?这篇文章给出了答案

PS&#xff1a;写在前面&#xff0c;近期感谢很多小伙伴关注到我写的论文解读&#xff0c;我也会持续更新吖~同时希望大家多多支持本人的公主号~ 想了解更多医学图像论文资料请移步公主&#x1f478;号哦~~~后期将持续更新&#xff01;&#xff01; 关注我&#xff0c;让我们一…

大模型时代,编程已成为当代大中专学生的必备技能,如何选择编程语言的一些建议

目录 一、具体建议 1. 确定学习目标 &#xff08;1&#xff09;兴趣驱动 &#xff08;2&#xff09;职业规划 2. 评估市场需求 &#xff08;1&#xff09;行业趋势 &#xff08;2&#xff09;就业前景 3. 考虑应用领域 4. 学习资源 &#xff08;1&#xff09;查看官方文档…

idea 常用的快捷键大全 建议收藏!!

IDEA 一款非常优秀的开发工具&#xff0c;本篇博客总结一些在 IDEA 中常用的快捷键&#xff0c;旨在提高开发效率。点击File --> Settings --> keymap便可进入看到 IDEA 提供的快捷键&#xff0c;我们也可以搜索和自定义所有快捷键。下面给出的是IDEA常用操作归纳。 1、…

RK3568平台(触摸篇)串口触摸屏

一.什么是串口屏 串口屏&#xff0c;可组态方式二次开发的智能串口控制显示屏&#xff0c;是指带有串口通信的TFT彩色液晶屏显示控制模组。利用显示屏显示相关数据&#xff0c;通过触摸屏、按键、鼠标等输入单元写入参数或者输入操作指令&#xff0c;进而实现用户与机器进行信…

AI问答:理解CRLF和LF / 两者区别 / 在编程和文件处理中的影响

一、背景 vscode这里的CRLF&#xff0c;点击后有CRLF和LF的两个选项&#xff0c;本文我们理解CRLF 和 LF 二、理解CRLF和LF 2.1、CRLF&#xff1a;起源于早期的打字机和电传打字机&#xff0c;这些设备在打印完一行后&#xff0c;需要先将打印头移回到行首&#xff08;回车&…

【Java题解】杨辉三角—力扣

&#x1f389;欢迎大家收看&#xff0c;请多多支持&#x1f339; &#x1f970;关注小哇&#xff0c;和我一起成长&#x1f680;个人主页&#x1f680; ⭐目前主更 专栏Java ⭐数据结构 ⭐已更专栏有C语言、计算机网络⭐ 题目链接&#xff1a;杨辉三角 目录&#x1f451; ⭐题…

用60行python代码制作一个扫雷

扫雷游戏&#xff08;Minesweeper&#xff09;是一个经典的逻辑游戏&#xff0c;玩家需要在一个包含隐藏地雷的网格中标记出所有地雷的位置&#xff0c;同时避免触发它们。下面&#xff0c;我将提供一个简单的Python扫雷游戏实现&#xff0c;并附带详细的教程。 第一步&#x…

基于cubeMX的STM32的RTC实时时钟实现

1、在仪器仪表的项目开发中&#xff0c;时常需要设备显示当前的日期和时间&#xff0c;这时&#xff0c;可以使用STM32自带的RTC实时时钟模块来实现此功能。这里我们使用STM32F103RCT6单片机芯片为例。 2、cubeMX的设置 &#xff08;1&#xff09;RTC设置 &#xff08;2&…