STL之mapset|AVL树

STL之map&set|AVL树

  • set&map
    • 搜索二叉树
      • 实现代码
    • set的使用
    • map的使用
    • set&map的模拟实现(见红黑树篇)
  • AVL树
    • AVL树的模拟实现

set&map

前言:stl库中set和map的底层都是红黑树,一种平衡搜索二叉树,是我下一篇博客的内容。map和set的模拟实现见我的下一篇博客红黑树。

vector/list/deque…之前学的容器称为序列式容器,数据线性存储
map/set…称为关联式容器,数据之间强关联。

搜索二叉树

无论AVL树,还是红黑树,它们都属于搜索二叉树。那么它们都遵守搜索二叉树的规则:左孩子的值比父亲的小/大,右孩子的值比父亲的大/小,注意对应。通过/前面的规则,可以得出一些结论,比如左子树的max比父亲的值小,右子树的min比父亲的大。

实现代码

namespace key
{template<class K>struct BSTreeNode{BSTreeNode* _left = nullptr;BSTreeNode* _right = nullptr;K _key;BSTreeNode(K key) :_key(key) {}};template<class K>class BSTree{typedef BSTreeNode<K> node;public:BSTree() = default;//强制生成默认构造BSTree(const BSTree<K>& t){Copy(_root, t._root);}BSTree<K>& operator=(BSTree<K> t){swap(_root, t._root);return *this;}bool Insert(const K& key)//参数缺省值不能给_root,一方面缺省值得是全局变量或常量;这里也不能用this指针{if (!_root){_root = new node(key);return true;}node* parent = _root;node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{return false;}}node* newnode = new node(key);if (key > parent->_key){parent->_right = newnode;}else{parent->_left = newnode;}}bool InsertR(const K& key){return _InsertR(_root, key);}void InOrder(){_InOrder(_root);cout << endl;}bool Find(const K& key){node* cur = _root;while (cur){if (key > cur->_key){cur = cur->_right;}else if (key < cur->_key){cur = cur->_left;}else return true;}return false;}bool FindR(const K& key){return _FindR(_root, key);}bool Erase(const K& key){node* parent = _root;node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{	//单子/null,托孤if (cur->_left == nullptr){if (cur == _root){_root = cur->_right;return true;}if (cur == parent->_left)parent->_left = cur->_right;elseparent->_right = cur->_right;delete cur;}else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;return true;}if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;delete cur;}else//双子,请保姆->左子树最右/右子树最左{/*node* pmaxLeft = cur;node* maxLeft = cur->_left;while (maxLeft->_right){pmaxLeft = maxLeft;maxLeft = maxLeft->_right;}if (pmaxLeft != cur)pmaxLeft->_right = maxLeft->_left;elsecur->_left = maxLeft->_left;cur->_key = maxLeft->_key;delete maxLeft;*/node* pminRight = cur;node* minRight = cur->_right;while (minRight->_left){pminRight = minRight;minRight = minRight->_left;}if (pminRight != cur)pminRight->_left = minRight->_right;elsecur->_right = minRight->_right;cur->_key = minRight->_key;delete minRight;}return true;}}return false;}bool EraseR(const K& key){return _EraseR(_root, key);}~BSTree(){Destroy(_root);}protected:void Copy(node*& myroot, const node* root){if (root == nullptr)return;myroot = new node(root->_key);Copy(myroot->_left, root->_left);Copy(myroot->_right, root->_right);}void _InOrder(node* root){if (!root)return;_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}bool _FindR(node* root, const K& key){if (root == nullptr)return false;if (root->_key == key)return true;if (key > root->_key)_FindR(root->_right, key);else _FindR(root->_left, key);}bool _InsertR(node*& root, const K& key)//引用の妙用,这里的root就是根节点或父亲的left/right,直接就链接上了{if (root == nullptr){root = new node(key);}if (root->_key == key)return false;if (key > root->_key)_InsertR(root->_right, key);else _InsertR(root->_left, key);}bool _EraseR(node*& root, const K& key){if (root == nullptr)return false;if (key > root->_key)return _EraseR(root->_right, key);else if (key < root->_key)return _EraseR(root->_left, key);else{if (!root->_left){node* tmp = root;root = root->_right;delete tmp;}else if (!root->_right){node* tmp = root;root = root->_left;delete tmp;}else{node* maxLeft = root->_left;while (maxLeft->_right){maxLeft = maxLeft->_right;}swap(root->_key, maxLeft->_key);return _EraseR(root->_left, key);}return true;}}void Destroy(node*& root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}node* _root = nullptr;};
}
namespace key_value
{template<class K, class V>struct BSTreeNode{BSTreeNode* _left = nullptr;BSTreeNode* _right = nullptr;K _key;V _val;BSTreeNode(const K& key, const V& val) :_key(key), _val(val) {}};template<class K, class V>class BSTree{typedef BSTreeNode<K, V> node;public:BSTree() = default;//强制生成默认构造bool Insert(const K& key, const V& val)//参数缺省值不能给_root,一方面缺省值得是全局变量或常量;这里也不能用this指针{if (!_root){_root = new node(key, val);return true;}node* parent = _root;node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{return false;}}node* newnode = new node(key, val);if (key > parent->_key){parent->_right = newnode;}else{parent->_left = newnode;}}void InOrder(){_InOrder(_root);cout << endl;}node* Find(const K& key){node* cur = _root;while (cur){if (key > cur->_key){cur = cur->_right;}else if (key < cur->_key){cur = cur->_left;}else return cur;}return nullptr;}bool Erase(const K& key){node* parent = _root;node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{	//单子/null,托孤if (cur->_left == nullptr){if (cur == _root){_root = cur->_right;return true;}if (cur == parent->_left)parent->_left = cur->_right;elseparent->_right = cur->_right;delete cur;}else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;return true;}if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;delete cur;}else//双子,请保姆->左子树最右/右子树最左{/*node* pmaxLeft = cur;node* maxLeft = cur->_left;while (maxLeft->_right){pmaxLeft = maxLeft;maxLeft = maxLeft->_right;}if (pmaxLeft != cur)pmaxLeft->_right = maxLeft->_left;elsecur->_left = maxLeft->_left;cur->_key = maxLeft->_key;delete maxLeft;*/node* pminRight = cur;node* minRight = cur->_right;while (minRight->_left){pminRight = minRight;minRight = minRight->_left;}if (pminRight != cur)pminRight->_left = minRight->_right;elsecur->_right = minRight->_right;cur->_key = minRight->_key;delete minRight;}return true;}}return false;}protected:void _InOrder(node* root){if (!root)return;_InOrder(root->_left);cout << root->_key << ":" << root->_val << endl;_InOrder(root->_right);}node* _root = nullptr;};
}
int main()
{key::BSTree<int>b;int a[] = { 8,3,1,10,6,4,7,14,13 };for (auto& x : a){b.InsertR(x);}b.InOrder();key::BSTree<int>bb(b);bb.InOrder();b.EraseR(4);b.InOrder();b.EraseR(14);b.InOrder();b.EraseR(3);b.InOrder();b.EraseR(8);b.InOrder();for (auto& x : a){b.InOrder();b.EraseR(x);}/*key_value::BSTree<string, int>dict;string arr[] = { "a","a","b","c","d","d","d" };for (auto& s : arr){auto ret = dict.Find(s);if (!ret)dict.Insert(s, 1);elseret->_val++;}dict.InOrder();*/
}

但是由于搜索二叉树在数据接近有序的情况下,会退化成单支树,时间复杂度也降级为O(N)。所以库中map/set底层使用红黑树,一种自动平衡的搜索二叉树。

set的使用

set作为key模型,插入同样的key会失败,所以其主要用途有去重+排序。

  • insert最常用的是第一个。
    在这里插入图片描述

pair是标准库里的一个类,用来封装两个变量。经常使用make_pair函数来创造pair。在这里插入图片描述

  • find返回迭代器,end表示失败。返回的迭代器不允许修改key,否则会破坏搜索二叉树的规则find
  • count找到返回1,失败返回0。set的count显得鸡肋,但是multiset中的count可就有用了。后者的count成功返回key的个数。
    count

multiset是可以存多个相同key的set,即允许键值冗余。在存入多个相同key时,第二个key会在第一个key的下面插入。在find时,返回的是中序的第一个key的迭代器。其余接口与set基本类似,不再赘述。

map的使用

map作为key-value模型,主要用途是。

  • insert常用第一个,要插入的value_type是pair<const Key, T>,我们常用make_pair插入。返回值pair的第一个是封装了value_type的迭代器可供外界修改,第二个bool用来判断是否插入成功。在这里插入图片描述
  • 最强大的功能当属[ ]重载了。如果传入的key不存在,就插入并采用默认值初始化;如果存在就返回对应value的引用,允许外界修改它。几乎可以平替insert和find了。实现代码比较精简,return (insert(make_pair(k,V())->first)->value。可见,[ ]支持修改,支持插入,支持插入+修改,支持查找。在这里插入图片描述
  • map自然也有multimap版本,但后者没有重载[ ]。

另外,map和set有迭代器,那就支持范围for遍历,无需多言。

set&map的模拟实现(见红黑树篇)

set&map的模拟实现,等我发布了红黑树篇就过来贴链接奥。

AVL树

AVL树,名字是取它的发明者–两个俄罗斯数学家的名字首字母而来。它通过增加平衡因子+旋转来控制每个结点的左右子树高度差的绝对值不超过1。本文中平衡因子balance factor采用右子树高度-左子树高度。

AVL树的模拟实现

下图是旋转的详细解析图,插入和删除都会用到。
旋转的目的,一是保持二叉树搜索规则,二是保持平衡,降低高度。
旋转

代码:

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K, V>* _left = nullptr;AVLTreeNode<K, V>* _right = nullptr;AVLTreeNode<K, V>* _parent = nullptr;//方便起见,采取三叉链pair<K, V>_kv;int _bf = 0;//平衡因子,此处定义为右子树高-左子树高AVLTreeNode(const pair<K, V>& kv) :_kv(kv) {}
};
template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> node;
public:bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new node(kv);return true;}node* parent = nullptr;node* cur = _root;while (cur){if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else return false;}cur = new node(kv);cur->_parent = parent;if (parent->_kv.first > kv.first)parent->_left = cur;elseparent->_right = cur;while (parent)//更新平衡因子,如果大于1,需要旋转{if (cur == parent->_left)parent->_bf--;elseparent->_bf++;if (parent->_bf == 0)break;//插入前1/-1,插入后0,说明该子树平衡/高度不变,不必向上else if (parent->_bf == -1 || parent->_bf == 1)//插入前0,插入后1/-1,左/右多出的节点必然影响祖先的bf,须迭代{cur = parent;parent = cur->_parent;}else if (parent->_bf == -2 || parent->_bf == 2)//旋转,解析请看jpg{if (parent->_bf == 2){if (cur->_bf == 1)//左单旋{RotateL(parent);//parent->_right = cur->_left;//cur->_left = parent;//cur->_left->_parent = parent;//cur->_parent = parent->_parent;//if (parent->_parent)//{//	if (parent->_parent->_left == parent)//		parent->_parent->_left = cur;//	else//		parent->_parent->_right = cur;//}//else _root = cur;//parent->_parent = cur;break;//break很关键,因为旋转完parent都变了,不能再循环了。}else if (cur->_bf == -1){RotateRL(parent);break;//没写break,改bug改了半天。。。}else assert(false);}else{if (cur->_bf == -1)//右单旋{RotateR(parent);break;}else if (cur->_bf == 1){RotateLR(parent);break;}else assert(false);}}else assert(false);//防bug}return true;}bool isAVLTree(){return _isAVLTree(_root);}void Inorder(){_Inorder(_root);}
private:int _Height(node* root){if (!root)return 0;int LeftH = _Height(root->_left) + 1;int RightH = _Height(root->_right) + 1;return LeftH > RightH ? LeftH : RightH;}bool _isAVLTree(node* root){if (!root)return true;if (root->_bf > 1 || root->_bf < -1){cout << root->_kv.first << ":" << root->_bf << endl;return false;}int leftH = _Height(root->_left);int rightH = _Height(root->_right);if (rightH - leftH != root->_bf){cout << root->_kv.first << ":" << root->_bf << " true:" << rightH - leftH << endl;return false;}return _isAVLTree(root->_left) && _isAVLTree(root->_right);}void _Inorder(node* root){if (!root)return;_Inorder(root->_left);cout << root->_kv.first << " ";_Inorder(root->_right);}void RotateL(node* parent){node* subR = parent->_right;node* subRL = subR->_left;node* pparent = parent->_parent;parent->_right = subRL;subR->_left = parent;subR->_parent = pparent;if (subRL)subRL->_parent = parent;parent->_parent = subR;if (pparent){if (pparent->_left == parent)pparent->_left = subR;else pparent->_right = subR;}else _root = subR;subR->_bf = parent->_bf = 0;}void RotateR(node* parent){node* subL = parent->_left;node* subLR = subL->_right;node* pparent = parent->_parent;parent->_left = subLR;subL->_right = parent;subL->_parent = pparent;if (subLR)subLR->_parent = parent;parent->_parent = subL;if (pparent){if (parent == pparent->_left)pparent->_left = subL;else pparent->_right = subL;}else _root = subL;subL->_bf = parent->_bf = 0;}void RotateLR(node* parent){node* subL = parent->_left;node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == -1)//subLR的左新增{parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1)//subLR的右新增{parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == 0)//subLR本身即新增{parent->_bf = subL->_bf = subLR->_bf = 0;}else assert(false);}void RotateRL(node* parent){node* subR = parent->_right;node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else assert(false);}
private:node* _root = nullptr;
};
int main()
{//AVLTree<int, int>t;//int arr[] = { 4,2,6,1,3,5,15,7,16,14 };//for (int i = 0; i < 9; i++)//{//	t.Insert(make_pair(arr[i], i));//	//cout << "第" << i << ":";//调试//	t.isAVLTree();//	//cout << endl;//}//t.Inorder();srand(time(0));const size_t N = 1000000;AVLTree<int, int>t;for (size_t i = 0; i < N; i++){size_t x = rand();t.Insert(make_pair(x, x));}cout << t.isAVLTree() << endl;
}

代码中只实现了插入。删除在原理上类似,删节点后更新平衡因子。
综上可见,AVL树查找效率近似O(log2N),因为它近似平衡二叉树。但是由于种种原因,现实中(库中)更常用的是红黑树,我下一篇博客的主题。而现实工作中,也不需要手撕这些高级二叉树,因为你写的大概不会有库中实现的完美。用现成的就好,但原理也要懂。

总结时刻,两天肝出此博客,累,但手撕AVL树的插入之后的成就感是无与伦比的。话不多说,铁子们,红黑树见~😘

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

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

相关文章

使用阿里云快速搭建 DataLight 平台

使用阿里云快速搭建 DataLight 平台 本篇文章由用户 “闫哥大数据” 分享&#xff0c;B 站账号&#xff1a;https://space.bilibili.com/357944741?spm_id_from333.999.0.0 注意&#xff1a;因每个人操作顺序可能略有区别&#xff0c;整个部署流程如果出现出入&#xff0c;以…

OceanBase 分区表详解

1、分区表的定义 在OceanBase数据库中&#xff0c;普通的表数据可以根据预设的规则被分割并存储到不同的数据区块中&#xff0c;同一区块的数据是在一个物理存储上。这样被分区块的表被称为分区表&#xff0c;而其中的每一个独立的数据区块则被称为一个分区。 如下图所示&…

代码随想录算法训练营第三十八天 | 322.零钱兑换 279.完全平方数 139.单词拆分 多重背包以及背包总结

LeetCode 322.零钱兑换&#xff1a; 文章链接 题目链接&#xff1a;322.零钱兑换 思路&#xff1a; 首先分析题目&#xff0c;每种硬币的数量是无限的&#xff0c;因此为完全背包问题&#xff1b;又要求返回的是最少硬币个数&#xff0c;因此与组合数/排列数无关&#xff0c…

计算机网络WebSocket——针对实习面试

目录 计算机网络WebSocket什么是WebSocket&#xff1f;WebScoket和HTTP协议的区别是什么?说明WebSocket的优势和使用场景&#xff1f;说明WebSocket的建立连接的过程&#xff1f; 计算机网络WebSocket 什么是WebSocket&#xff1f; WebSocket是一个网络通信协议&#xff0c;提…

在Ubuntu 24.04 LTS上安装飞桨PaddleX

前面我们介绍了《在Windows用远程桌面访问Ubuntu 24.04.1 LTS》本文接着介绍安装飞桨PaddleX。 PaddleX 3.0 是基于飞桨框架构建的一站式全流程开发工具&#xff0c;它集成了众多开箱即用的预训练模型&#xff0c;可以实现模型从训练到推理的全流程开发&#xff0c;支持国内外多…

LM2 : A Simple Society of Language Models Solves Complex Reasoning

文章目录 题目摘要简介相关工作方法论实验结果结论局限性 题目 LM2&#xff1a;简单的语言模型社会解决复杂推理问题 论文地址&#xff1a;https://aclanthology.org/2024.emnlp-main.920/ 项目地址&#xff1a; https://github.com/LCS2-IIITD/Language_Model_Multiplex 摘要…

(三十三)队列(queue)

文章目录 1. 队列&#xff08;queue&#xff09;1.1 定义1.2 函数1.3 习题1.3.1 例题&#xff08;周末舞会&#xff09; 2. 双向队列&#xff08;deque&#xff09;2.1 定义2.2 函数2.3 题目2.3.1 例题&#xff08;打BOSS&#xff09; 1. 队列&#xff08;queue&#xff09; 队…

web——upload-labs——第二关

MIME验证 MIME&#xff08;Multipurpose Internet Mail Extensions&#xff09;验证是指在互联网传输中&#xff0c;通过检查数据的MIME类型来确保数据格式的正确性和安全性。MIME最初是为了扩展电子邮件的功能&#xff0c;让邮件支持多种格式&#xff0c;如文本、图片、音频等…

Vue3 -- 集成sass【项目集成5】

集成sass&#xff1a; 看过博主的 配置styleLint工具应该已经安装过 sass sass-loader 了&#xff0c;所以我们只需要加上我们的 lang"scss"即可。 <style scoped lang"scss"></style>给项目添加全局样式文件&#xff1a; 在src文件夹下创建…

【Web前端】Promise的使用

Promise是异步编程的核心概念之一。代表一个可能尚未完成的操作&#xff0c;并提供了一种机制来处理该操作最终的成功或失败。具体来说&#xff0c;Promise是由异步函数返回的对象&#xff0c;能够指示该操作当前所处的状态。 当Promise被创建时&#xff0c;它会处于“待定”&a…

EI检索!2024年大数据与数据挖掘会议(BDDM 2024)全解析!

第二届大数据与数据挖掘国际会议&#xff08;BDDM 2024&#xff09;将于2024年12月13-15日在武汉举行&#xff0c;已启动第二轮征稿&#xff0c;截稿2024年11月30日。邀请学者探讨大数据与数据挖掘进展&#xff0c;可在线投稿及AC学术中心查看详情。 大会官网&#xff1a;www.i…

基于java+ssm+Vue的校园美食交流系统设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven mysql5.7或8.0等等组成&#x…

关于 MSVCP110.dll 缺失的解决方案

背景&#xff1a;之前使用 PR&#xff08;Adobe Premiere&#xff09; 从来没有遇到过这样的问题。今天重装系统后&#xff08;window 10&#xff09;&#xff0c;想要重新安装以前的软件时&#xff0c;遇到了以下 DLL 文件缺失的错误。 解决方案&#xff1a; 可以到微软官网的…

Python小游戏27——飞翔的小鸟

首先&#xff0c;你需要确保已经安装了Pygame库。如果还没有安装&#xff0c;可以通过以下命令进行安装&#xff1a; 【bash】 pip install pygame 游戏的代码&#xff1a; 【python】 import pygame import random # 初始化Pygame pygame.init() # 设置屏幕大小和标题 screen_…

【Three.js基础学习】24. shader patterns

前言 课程回顾: ShaderMaterial 这里用的是着色器材质 所以顶点和片段着色器就不需要像原始着色器那样添加需要的属性 然后写 片段着色器需要属性 &#xff1a; 顶点 属性 -》变化 -》 片段中 顶点中的属性不需要声明 只需要声明传送的变量 例如 varying vec vUv; vUv uv; 补充…

构建客服知识库:企业效率提升的关键步骤

客服知识库是企业提升客户服务效率和质量的重要工具。它不仅帮助客服团队快速准确地回答客户问题&#xff0c;还能通过数据分析来优化服务流程和提升客户满意度。 1. 明确知识库的目标和范围 构建客服知识库的第一步是明确其目标和范围。这包括确定知识库的主要用户群体、需要…

运动汇 专业的比赛管理平台数据获取

在获取到运动汇的网站链接后&#xff0c;界面如图所示: 右键检查&#xff0c;我们会发现没有任何数据&#xff0c;只有当我们点开这些"第一单元"、"第二单元"等&#xff0c;数据才会加载出来&#xff1b; 由于我们只需要分析这一个网页并获取其中的数据&a…

免费送源码:Java+Springboot+MySQL Springboot多租户博客网站的设计 计算机毕业设计原创定制

Springboot多租户博客网站的设计 摘 要 博客网站是当今网络的热点&#xff0c;博客技术的出现使得每个人可以零成本、零维护地创建自己的网络媒体&#xff0c;Blog站点所形成的网状结构促成了不同于以往社区的Blog文化&#xff0c;Blog技术缔造了“博客”文化。本文课题研究的“…

Docker环境搭建Cloudreve网盘服务(附shell脚本一键搭建)

Docker搭建Cloudreve Cloudreve介绍&#xff1a; Cloudreve 是一个基于 ThinkPHP 框架构建的开源网盘系统&#xff0c;旨在帮助用户以较低的成本快速搭建起既能满足个人也能满足企业需求的网盘服务。Cloudreve 支持多种存储介质&#xff0c;包括但不限于本地存储、阿里云OSS、…

Macs Fan Control - 控制 Apple 计算机上的风扇

免费下载 提供 macOS 和 Windows &#xff08;Boot Camp&#xff09; 版本 https://apsgo.cn/joN0WG Mac 风扇控制 监视和控制 Apple 计算机上的风扇 实时监控风扇速度和温度传感器&#xff0c;包括第三方 HDD/SSD&#xff08;使用 S.M.A.R.T.&#xff09;。设置自定义 RP…