有序表(上)

news2024/12/24 7:05:22

文章目录

  • 1、引入
  • 2、左旋和右旋
  • 3、AVL树
    • 3.1 AVL 树的平衡条件
    • 3.2 搜索二叉树如何删除节点
    • 3.3 AVL树的平衡性被破坏的四种类型
    • 3.4 AVL 树平衡性如何检查?如何调整失衡?
      • 3.4.1 AVL树新增节点如何检查树的平衡性?
      • 3.4.2 AVL树删除节点如何检查树的平衡性?
    • 3.5 AVL 树代码实现
    • 3.6 有序表和 AVL 树的关系

1、引入

如果数据库中只是按照顺序存储数据,那么找数据的时候就必须遍历,整个过程非常慢。于是在数据库中有一个重要的优化——建索引。

可以建索引的前提是被索引的字段是可排序的。

那么排序是如何加速找数据库的数据的?

数据库中的数据可能在硬盘里就是顺序存储的,但是数据库一定会在这个底层的数据库上建立一棵索引树。

索引树就是搜索二叉树(左孩子数据 < 根节点数据 < 右孩子数据)或者叫做有序树,那么给定一个字段,在有序树中就能快速得找到该数据在物理结构中的真实位置,就能快速取出数据。注意,如果只是单纯地找某个数据,那么哈希表性能更好。但是现实世界中,通常查询的不单单是某个数据,更多的是范围查询,比如年龄在某个范围或者以ABC开头的姓名这种业务,这是哈希表无法完成的,但是有序结构可以。

实际上,搜索二叉树本身就能支持索引,增删改查都可以在这棵树上完成。但是有一个瓶颈——树的高度。如果用户的数据是顺序给的,那这棵树就变成了一个链表,则这棵树就没有效率可言了。这种时候的算法性能就是用户的输入数据决定的。

于是,就要涉及到 平衡搜索二叉树 的概念了。即保证一棵树是搜索二叉树的前提下(搜索性),还要保证它的平衡性。所谓的平衡性就是希望最高的高度是 l o g N logN logN(其中 N N N是数据量)。

2、左旋和右旋

先来讲讲 左旋右旋 的概念。

注意:左旋/右旋的时候一定要指明是以哪个节点为头结点进行左旋/右旋,往哪边旋就是头结点往哪边倒。

左旋和右旋并不会改变搜索二叉树的性质。
请添加图片描述
经典的有序表会有多种实现,AVL树、SB树、红黑树都有各自的实现,会有自己使用左旋和右旋的策略,但是不管是哪种平衡搜索二叉树,不管你的平衡性如何定义,让它变平衡的基本操作只有左旋和右旋。

不同的有序表的树(AVL树、SB树、红黑树、B+树等)的差别在于规定的平衡性不一样,相似点是不管是哪种平衡树都可以保证增删改查的时间复杂度为 O ( l o g N ) O(logN) O(logN),即它们在时间复杂度指标上没有区别。

3、AVL树

3.1 AVL 树的平衡条件

AVL树具有最严苛的平衡性:任意一个节点的左树高度和右树高度之差不超过2,即 ∣ H e i g h t 左 − H e i g h t 右 ∣ < 2 |Height_左 - Height_右| \lt 2 HeightHeight<2

3.2 搜索二叉树如何删除节点

搜索二叉树的增查改节点都比较简单,但是删除节点就稍微复杂一些,如下是搜索二叉树分情况删除节点:
请添加图片描述
请添加图片描述
请添加图片描述
AVL树的增删节点操作和搜索二叉树一样,但是区别在AVL树在增删节点后有平衡动作。

3.3 AVL树的平衡性被破坏的四种类型

  • LL型:左子树的左孩子导致了失衡,只需要做一次右旋
  • LR型:左子树的右子树高度过高,导致了失衡,需要进行做两次旋转,一次失衡节点的左子树左旋,一次失衡的节点右旋,也就是LR型先通过左旋变成LL型,再右旋
  • RR型:右子树的右孩子导致了失衡,只需要做一次左旋
  • RL型:同理LR型,先进行一次右旋,再进行一次左旋。即先通过右旋将RL变成RR型,再左旋

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

在这里插入图片描述

有个细节需要注意:失衡有可能是(LL型 && LR型) 或者 (RR型 && RL)型。比如:
在这里插入图片描述
假设右子树上删除了一个节点导致右子树的高度变成了5,而左子树高度仍然是7,且左子树的左右孩子的高度均为6,这时候就对于A来说就同时出现了 LL 和 LR 型违规,这时候一定要按照 LL 型来调整,即 LL & LR 默认为 LL型即可。

再举个例子:
在这里插入图片描述

小结

假设 X 为根节点的树失衡

  • 如果是 LL 型失衡,则 X 节点进行一次右旋;
  • 如果是 LR 型失衡,则 X 节点的左孩子进行一次左旋,然后 X 节点进行一次右旋;
  • 如果是 RR 型失衡,则 X 节点进行一次左旋;
  • 如果是 RL 型失衡,则 X 节点的右孩子进行一次右旋,然后 X 节点进行一次左旋;
  • 如果是 LL & LR 型失衡,则按照 LL 型调整;
  • 如果是 RR & RL 型失衡,则按照 RR 型调整。

无论是上述哪种类型的失衡,都能在 O ( 1 ) O(1) O(1) 时间复杂度内调整平衡。


3.4 AVL 树平衡性如何检查?如何调整失衡?

3.4.1 AVL树新增节点如何检查树的平衡性?

如果 X 节点挂在了树的某个具体位置,先检查 X 节点中了上述的哪种失衡类型;然后检查 X 节点的父节点是哪种失衡类型;接着检查 X 节点的父节点的父节点是哪种失衡类型,一直到头结点。即从 X 节点到根节点沿途的所有节点全部都要检查平衡性。

AVL树的调整不是只对一个节点或者只对头结点的平衡性进行检查,而是只要有节点加入,沿途的每个节点都要检查。

如果 AVL 树在加入节点之前最大高度是 l o g N logN logN,那么加入一个节点后,即便沿途到头结点的节点都检查,一共最多也就 l o g N logN logN 个节点,而每对一个节点做四种失衡类型的检查并调整的时间复杂度是 O ( 1 ) O(1) O(1),所以加入节点后对沿途的每个节点都检查调整也就是 O ( l o g N ) O(logN) O(logN) 的代价。

3.4.2 AVL树删除节点如何检查树的平衡性?

  • 如果删除的 X 节点是叶子节点,则检查从删除节点开始一直到头结点沿途的所有节点全部都要检查是否平衡;

  • 如果删除的 X 节点有右孩子,无左孩子,直接用右孩子替代X节点的环境,则要检查从右孩子来到的位置开始往上到头结点沿途的所有节点的平衡性;有左孩子,无右孩子同理;

  • 如果删除的 X 节点既有左孩子,又有右孩子。如下:
    在这里插入图片描述

3.5 AVL 树代码实现

public class AVLTreeMap {
	//AVL树的节点
	//AVL树是有序表中用到的,所以Key必须是可比较的,所以此处的 K 一定要实现为一个可比较的类
	public static class AVLNode<K extends Comparable<K>, V> {
		public K k;
		public V v;
		public AVLNode<K, V> l; //指向左孩子
		public AVLNode<K, V> r; //指向右孩子
		public int h; //平衡因子:高度,整棵树的高度

		public AVLNode(K key, V value) {
			k = key;
			v = value;
			h = 1;
		}
	}

	//AVL树实现的有序表
	public static class AVLTreeMap<K extends Comparable<K>, V> {
		private AVLNode<K, V> root; //根节点
		private int size; //节点的数量

		public AVLTreeMap() {
			root = null;
			size = 0;
		}
		//右旋:对cur这棵树右旋
		private AVLNode<K, V> rightRotate(AVLNode<K, V> cur) {
			AVLNode<K, V> left = cur.l; //记录cur的左孩子
			cur.l = left.r; //左孩子的右孩子 成为 cur的左孩子
			left.r = cur; //左孩子的右孩子 变成 cur
			//接管高度
			//先更新 cur 的高度
			cur.h = Math.max((cur.l != null ? cur.l.h : 0), (cur.r != null ? cur.r.h : 0)) + 1;
			//再更新 left 的高度
			left.h = Math.max((left.l != null ? left.l.h : 0), (left.r != null ? left.r.h : 0)) + 1;
			return left; //以谁做头,就返回谁
		}
		//左旋
		private AVLNode<K, V> leftRotate(AVLNode<K, V> cur) {
			AVLNode<K, V> right = cur.r;
			cur.r = right.l;
			right.l = cur;
			cur.h = Math.max((cur.l != null ? cur.l.h : 0), (cur.r != null ? cur.r.h : 0)) + 1;
			right.h = Math.max((right.l != null ? right.l.h : 0), (right.r != null ? right.r.h : 0)) + 1;
			return right;
		}
		
		//平衡调整:以cur为头的这棵树上进行平衡调整
		private AVLNode<K, V> maintain(AVLNode<K, V> cur) {
			if (cur == null) {
				return null;
			}
			int leftHeight = cur.l != null ? cur.l.h : 0; //左树的高度
			int rightHeight = cur.r != null ? cur.r.h : 0; //右树的高度
			if (Math.abs(leftHeight - rightHeight) > 1) { //失衡
				if (leftHeight > rightHeight) { //左树高度 > 右树高度
					//左树的左树高度
					int leftLeftHeight = cur.l != null && cur.l.l != null ? cur.l.l.h : 0;
					//左树的右树高度
					int leftRightHeight = cur.l != null && cur.l.r != null ? cur.l.r.h : 0;
					//=号表示 LL & LR型,归为LL型
					if (leftLeftHeight >= leftRightHeight) { //LL型 或者 既是LL又是LR型 ,进行右旋
						cur = rightRotate(cur);
					} else { //LR型,先cur的左孩子进行左旋,然后cur整体右旋
						cur.l = leftRotate(cur.l);
						cur = rightRotate(cur);
					}
				} else { //左树高度 < 右树高度
					int rightLeftHeight = cur.r != null && cur.r.l != null ? cur.r.l.h : 0;
					int rightRightHeight = cur.r != null && cur.r.r != null ? cur.r.r.h : 0;
					if (rightRightHeight >= rightLeftHeight) { //RR型 或者 既是RR型又是RL型,左旋
						cur = leftRotate(cur);
					} else { //RL型,cur的右孩子先右旋,然后cur整体左旋
						cur.r = rightRotate(cur.r);
						cur = leftRotate(cur);
					}
				}
			}
			return cur; //返回调整后的头结点
		}
		
		
		// 找到 <=key 离 key 最近的 (与平衡性无关)
		private AVLNode<K, V> findLastIndex(K key) {
			AVLNode<K, V> pre = root;
			AVLNode<K, V> cur = root;
			while (cur != null) {
				pre = cur;
				if (key.compareTo(cur.k) == 0) {
					break;
				} else if (key.compareTo(cur.k) < 0) {
					cur = cur.l;
				} else {
					cur = cur.r;
				}
			}
			return pre;
		}
		
		// 找到 >= key 离 key 最近的
		private AVLNode<K, V> findLastNoSmallIndex(K key) {
			AVLNode<K, V> ans = null;
			AVLNode<K, V> cur = root;
			while (cur != null) {
				if (key.compareTo(cur.k) == 0) {
					ans = cur;
					break;
				} else if (key.compareTo(cur.k) < 0) {
					ans = cur;
					cur = cur.l;
				} else {
					cur = cur.r;
				}
			}
			return ans;
		}

		private AVLNode<K, V> findLastNoBigIndex(K key) {
			AVLNode<K, V> ans = null;
			AVLNode<K, V> cur = root;
			while (cur != null) {
				if (key.compareTo(cur.k) == 0) {
					ans = cur;
					break;
				} else if (key.compareTo(cur.k) < 0) {
					cur = cur.l;
				} else {
					ans = cur;
					cur = cur.r;
				}
			}
			return ans;
		}
		
		// 添加节点:以cur为根节点的树上添加新的记录(key,value),返回整棵树的头结点
		// 如果 key<cur.k,则新节点添加到cur的左边
		// 如果 key > cur.k,则新节点添加到cur的右边
		// 如果 key = cur.k,直接将cur的v值更新为value,搜索二叉树不接收重复的K
		private AVLNode<K, V> add(AVLNode<K, V> cur, K key, V value) { //该方法的潜台词就是不会有相同的key插入
			if (cur == null) { //空树
				return new AVLNode<K, V>(key, value);
			} else {
				if (key.compareTo(cur.k) < 0) { //key < cur.k,去cur的左树上添加
					cur.l = add(cur.l, key, value); //添加调整后可能换头,所以必须用cur.l接住该头
				} else { 
					cur.r = add(cur.r, key, value);
				}
				cur.h = Math.max(cur.l != null ? cur.l.h : 0, cur.r != null ? cur.r.h : 0) + 1;
				return maintain(cur); //对cur这棵树平衡性的检查,调整后可能换头,所以返回的是一个节点
			}
		}

		// 删除节点:在cur这棵树上,删掉key所代表的节点,返回cur这棵树的新头部
		private AVLNode<K, V> delete(AVLNode<K, V> cur, K key) { //潜台词是存在key所代表的节点
			if (key.compareTo(cur.k) > 0) {
				cur.r = delete(cur.r, key);
			} else if (key.compareTo(cur.k) < 0) {
				cur.l = delete(cur.l, key);
			} else {
				if (cur.l == null && cur.r == null) { //既无左孩子,也无右孩子
					cur = null;
				} else if (cur.l == null && cur.r != null) { //无左孩子,有右孩子
					cur = cur.r;
				} else if (cur.l != null && cur.r == null) { //无右孩子,有左孩子
					cur = cur.l;
				} else { //既有左孩子,也有右孩子
					AVLNode<K, V> des = cur.r;
					while (des.l != null) { //找到右子树上最左节点
						des = des.l;
					}
					cur.r = delete(cur.r, des.k);  //在右子树上删除 des 这个节点,并进行平衡性的调整
					//用des节点替换cur节点
					des.l = cur.l; 
					des.r = cur.r;
					cur = des;
					//注意:此时,cur的右子树的平衡性已经调整完毕,然后从cur开始向上的节点进行平衡性检查与调整,与上文中讲到的平衡性的调整略有不同,此处时进行了分离
				}
			}
			if (cur != null) {
				cur.h = Math.max(cur.l != null ? cur.l.h : 0, cur.r != null ? cur.r.h : 0) + 1;
			}
			return maintain(cur);//每次递归都会检查一遍自己,所有受到影响的节点全部都会检查一遍
		}

		public int size() {
			return size;
		}

		public boolean containsKey(K key) {
			if (key == null) {
				return false;
			}
			AVLNode<K, V> lastNode = findLastIndex(key);
			return lastNode != null && key.compareTo(lastNode.k) == 0 ? true : false;
		}

		public void put(K key, V value) {
			if (key == null) {
				return;
			}
			AVLNode<K, V> lastNode = findLastIndex(key);
			if (lastNode != null && key.compareTo(lastNode.k) == 0) {
				lastNode.v = value;
			} else {
				size++;
				root = add(root, key, value);
			}
		}

		public void remove(K key) {
			if (key == null) {
				return;
			}
			if (containsKey(key)) {
				size--;
				root = delete(root, key);
			}
		}

		public V get(K key) {
			if (key == null) {
				return null;
			}
			AVLNode<K, V> lastNode = findLastIndex(key);
			if (lastNode != null && key.compareTo(lastNode.k) == 0) {
				return lastNode.v;
			}
			return null;
		}

		public K firstKey() {
			if (root == null) {
				return null;
			}
			AVLNode<K, V> cur = root;
			while (cur.l != null) {
				cur = cur.l;
			}
			return cur.k;
		}

		public K lastKey() {
			if (root == null) {
				return null;
			}
			AVLNode<K, V> cur = root;
			while (cur.r != null) {
				cur = cur.r;
			}
			return cur.k;
		}

		public K floorKey(K key) {
			if (key == null) {
				return null;
			}
			AVLNode<K, V> lastNoBigNode = findLastNoBigIndex(key);
			return lastNoBigNode == null ? null : lastNoBigNode.k;
		}

		public K ceilingKey(K key) {
			if (key == null) {
				return null;
			}
			AVLNode<K, V> lastNoSmallNode = findLastNoSmallIndex(key);
			return lastNoSmallNode == null ? null : lastNoSmallNode.k;
		}

	}
}

3.6 有序表和 AVL 树的关系

有序表是接口名,AVL树是具体的类或者说实现,AVL树可以实现有序表,Size-Blanced树、红黑树、跳表、B树和B+树也可以实现有序表,具体的实现细节不同。但是有序表应该有的功能,每种树都可以实现;有序表要求的性能,无论用哪种树实现都是 O ( l o g N ) O(logN) O(logN)

有序表中要求的所有功能和平衡性的调整毫无关系,只要能够玩转搜索二叉树就能改出有序表要求的所有功能。任何树关于平衡性的调整都只是一个补丁。

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

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

相关文章

学习笔记:Java 并发编程⑥_并发工具_JUC

若文章内容或图片失效&#xff0c;请留言反馈。 部分素材来自网络&#xff0c;若不小心影响到您的利益&#xff0c;请联系博主删除。 视频链接&#xff1a;https://www.bilibili.com/video/av81461839配套资料&#xff1a;https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw&am…

4. 寻找两个正序数组的中位数(数组)

文章目录题目描述方法一,重组排序方法二,调用系统函数题目描述 给定两个大小分别为 m 和 n 的正序&#xff08;从小到大&#xff09;数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (mn)) 。 示例 1&#xff1a; 输入&#…

最全面的SpringBoot教程(五)——整合框架

前言 本文为 最全面的SpringBoot教程&#xff08;五&#xff09;——整合框架 相关知识&#xff0c;下边将对SpringBoot整合Junit&#xff0c;SpringBoot整合Mybatis&#xff0c;SpringBoot整合Redis等进行详尽介绍~ &#x1f4cc;博主主页&#xff1a;小新要变强 的主页 &…

回归预测 | MATLAB实现NGO-LSTM北方苍鹰算法优化长短期记忆网络多输入单输出

回归预测 | MATLAB实现NGO-LSTM北方苍鹰算法优化长短期记忆网络多输入单输出 目录回归预测 | MATLAB实现NGO-LSTM北方苍鹰算法优化长短期记忆网络多输入单输出预测效果基本介绍程序设计参考资料预测效果 基本介绍 Matlab实现NGO-LSTM北方苍鹰算法优化长短期记忆网络多输入单输出…

聚观早报 |阿里清空印度支付宝Paytm股票;Meta终于成功收购Within

今日要闻&#xff1a;全球多所学校禁止学生使用ChatGPT&#xff1b;阿里清空印度支付宝Paytm股票&#xff1b;Meta终于成功收购Within&#xff1b;极氪完成 7.5 亿美元 A 轮融资&#xff1b;现代汽车在美电动汽车销量突破10万全球多所学校禁止学生使用ChatGPT 2月12日消息&…

关于北京君正:带ANC的2K网络摄像头用户案例

如果远程办公是您的未来&#xff0c;或者您经常通过视频通话与远方的朋友和亲戚交谈&#xff0c;那么您可以考虑购买网络摄像头以显著改善您的沟通。Anker PowerConf C200是个不错的选择。 Anker PowerConf C200专为个人工作空间而设计&#xff0c;能够以每秒30帧的速度拍摄2K…

Python图像卡通化animegan2-pytorch实例演示

先看下效果图&#xff1a; 左边是原图&#xff0c;右边是处理后的图片&#xff0c;使用的 face_paint_512_v2 模型。 项目获取&#xff1a; animegan2-pytorch 下载解压后 cmd 可进入项目地址的命令界面。 其中 img 是我自己建的&#xff0c;用于存放图片。 需要 torch 版本 …

【Selenium学习】Selenium 中常用的基本方法

1&#xff0e;send_keys 方法模拟键盘键入此方法类似于模拟键盘键入。以在百度首页搜索框输入“Selenium”为例&#xff0c;代码如下&#xff1a;# _*_ coding:utf-8 _*_ """ name:zhangxingzai date:2023/2/13 form:《Selenium 3Python 3自动化测试项目实战》 …

React Native(一)

移动端触摸事件example1:<ButtononPress{() > {Alert.alert(你点击了按钮&#xff01;);}}title"点我&#xff01;" />Touchable 系列组件TouchableHighlight 此组件的背景会在用户手指按下时变暗TouchableNativeFeedback 会在用户手指按下时形成类似墨水涟…

Java基础常见面试题(四)

反射 什么是反射&#xff1f; 反射是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对于任意一个对象&#xff0c;都能够调用它的任意一个方法和属性&#xff1b;这种动态获取的信息以及动态调用对象的方法的功能称为 Jav…

大数据技术架构(组件)34——Spark:Spark SQL--Optimize

2.2.3、Optimize2.2.3.1、SQL3.3.1.1、RB1、Join选择在Hadoop中&#xff0c;MR使用DistributedCache来实现mapJoin。即将小文件存放到DistributedCache中&#xff0c;然后分发到各个Task上&#xff0c;并加载到内存中&#xff0c;类似于Map结构&#xff0c;然后借助于Mapper的迭…

【半监督医学图像分割 2021 CVPR】CVRL

文章目录【半监督医学图像分割 2021 CVPR】CVRL摘要1. 介绍1.1 总览1.2 无监督对比学习2. 实验3. 总结【半监督医学图像分割 2021 CVPR】CVRL 论文题目&#xff1a;Momentum Contrastive Voxel-wise Representation Learning for Semi-supervised Volumetric Medical Image Seg…

ThinkPHP多语言模块文件包含RCE复现详细教程

免责声明 本文章只用于技术交流&#xff0c;若使用本文章提供的技术信息进行非法操作&#xff0c;后果均由使用者本人负责。 漏洞描述&#xff1a; ThinkPHP在开启多语言功能的情况下存在文件包含漏洞&#xff0c;攻击者可以通过get、header、cookie等位置传入参数&#xff…

Transformer机制学习笔记

学习自https://www.bilibili.com/video/BV1J441137V6 RNN&#xff0c;CNN网络的缺点 难以平行化处理&#xff0c;比如我们要算b4b^4b4&#xff0c;我们需要一次将a1a^1a1~a4a^4a4依次进行放入网络中进行计算。 于是有人提出用CNN代替RNN 三角形表示输入&#xff0c;b1b^1b1的…

【数据结构】算法的复杂度分析:让你拥有未卜先知的能力

&#x1f451;专栏内容&#xff1a;数据结构⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;日拱一卒&#xff0c;功不唐捐 文章目录一、前言二、时间复杂度1、定义2、大O的渐进表示法3、常见的时间复杂度三、空间复杂度1、定义2、常见的空间复杂度一、前…

微信小程序删除list指定列表项

一、删除效果展示&#xff1a; // 重要代码片段async deleteListItem(e) {const sureResult await wx.showModal({title: "提示",content: "确定要删除这项吗",});if (sureResult.confirm) {const { index } e.currentTarget.dataset;setTimeout(()>{…

Centos7安装kvm WEB管理工具kimchi

参考&#xff1a;虚拟化技术之kvm WEB管理工具kimchi一、kimchi安装1.1 下载wok安装包和kimchi安装包# 1. wok下载地址&#xff1a; wget https://github.com/kimchi-project/kimchi/releases/download/2.5.0/wok-2.5.0-0.el7.centos.noarch.rpm # 2. kimchi下载地址&#xff1…

Springmvc补充配置

Controller配置总结 控制器通常通过接口定义或注解定义两种方法实现 在用接口定义写控制器时&#xff0c;需要去Spring配置文件中注册请求的bean;name对应请求路径&#xff0c;class对应处理请求的类。 <bean id"/hello" class"com.demo.Controller.HelloCo…

【Spark分布式内存计算框架——Spark SQL】2. SparkSQL 概述(上)

第二章 SparkSQL 概述 Spark SQL允许开发人员直接处理RDD&#xff0c;同时可以查询在Hive上存储的外部数据。Spark SQL的一个重要特点就是能够统一处理关系表和RDD&#xff0c;使得开发人员可以轻松的使用SQL命令进行外部查询&#xff0c;同时进行更加复杂的数据分析。 2.1 前…

蓝桥杯模块学习16——PCF8591(深夜学习——单片机)

一、硬件电路&#xff1a;1、蓝桥杯板子上的电路&#xff1a;&#xff08;1&#xff09;AIN0-3&#xff1a;四种模拟量的输入口&#xff0c;AIN1为光敏电阻控制电压输入&#xff0c;AIN3为电位器控制电压输入&#xff08;2&#xff09;A0-2&#xff1a;决定设备的地址码&#x…