set和map的封装
目录
介绍
红黑树代码
set
insert的迭代器转换问题
为什么会有这样的问题?
如何解决
代码
map
注意点
代码
介绍
set和map的底层都是红黑树,所以我们可以在自己实现的红黑树(简易版)的基础上,进行封装,成为简易的set和map
红黑树代码
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <cassert>
#include <cstdlib>
#include <utility>// 有迭代器的红黑树
namespace my_RB_Tree
{enum colour{black,red};template <class T>struct RBTreeNode // 结点{RBTreeNode(const T& data): _left(nullptr),_right(nullptr),_parent(nullptr),_col(red),_data(data){}RBTreeNode* _left;RBTreeNode* _right;RBTreeNode* _parent;colour _col;T _data;};template <class T, class Ptr, class Ref> // T是元素类型,ptr是指针类型,ref是引用类型(后两种会有const类型)struct RBTreeIterator // 迭代器{typedef RBTreeNode<T> Node;typedef RBTreeIterator<T, Ptr, Ref> Self;//为了可以能让普通迭代器初始化const迭代器,需要来一个普通迭代器对象typedef RBTreeIterator<T, T*, T&> iterator;Node* _pNode;RBTreeIterator(Node* pNode): _pNode(pNode){}RBTreeIterator(const iterator& it) // const迭代器时,它是一个初始化;普通迭代器时,它是一个拷贝: _pNode(it._pNode){}// 让迭代器具有类似指针的行为Ref operator*(){return _pNode->_data;}Ptr operator->(){return &(_pNode->_data);}// 让迭代器可以移动:前置/后置++Self& operator++(){Increament();return *this;}Self operator++(int){Self tmp(*this);Increament();return tmp;}// 让迭代器可以移动:前置/后置--Self& operator--(){DeIncreament();return *this;}Self operator--(int){Self tmp(*this);DeIncreament();return tmp;}// 让迭代器可以比较bool operator!=(const Self& s) const{return _pNode != s._pNode;}bool operator==(const Self& s) const{return _pNode == s._pNode;}private:void Increament();void DeIncreament();};// 为了后序封装map和set,本代码的红黑树会有一个作为哨兵位的头结点template <class K, class T, class KeyOfT> // K是关键字的类型,T是元素类型(区分这两个的原因:会用该红黑树封装成set和map,而map是key_value的)// keyofT是返回关键字类型的值(否则map无法返回)class RBTree // 红黑树{public:typedef RBTreeNode<T> Node;typedef RBTreeIterator<T, T*, T&> iterator;typedef RBTreeIterator<T, const T*, const T&> const_iterator;public:RBTree(){_pHead = new Node(T());_pHead->_left = _pHead;_pHead->_parent = nullptr;_pHead->_right = _pHead;}// 在红黑树中插入值为data的节点,插入成功返回true,否则返回falsestd::pair<iterator, bool> Insert(const T& data);// 检测红黑树中是否存在值为data的节点,存在返回该节点的地址,否则返回nullptrNode* Find(const K& data);// 获取红黑树最左侧节点Node* LeftMost() const;// 获取红黑树最右侧节点Node* RightMost() const;iterator begin(){return iterator(LeftMost());}iterator end(){return iterator(_pHead);}const_iterator begin() const{return const_iterator(LeftMost());}const_iterator end() const{return const_iterator(_pHead);}// 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测bool IsValidRBTRee(){Node* root = _pHead->_parent;if (root->_col == red){return false;}int count = 0;find_blacknode(count, _pHead->_parent);return _IsValidRBTRee(_pHead->_parent, count, 0);}private:bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack);// 左单旋void RotateL(Node* pParent);// 右单旋void RotateR(Node* pParent);// 为了操作树简单起见:获取根节点Node*& GetRoot(){return _pHead->_parent;}void find_blacknode(int& count, Node* root){if (root == nullptr){return;}if (root->_col == black){++count;}find_blacknode(count, root->_left);find_blacknode(count, root->_right);}private:Node* _pHead = nullptr;};template <class K, class T, class KeyOfT>void RBTree<K, T, KeyOfT>::RotateL(Node* pParent){Node* cur = pParent->_right, * curleft = cur->_left;// 连接p和cur左树,因为该位置被p占据pParent->_right = curleft;if (curleft){curleft->_parent = pParent;}// 连接父结点if (pParent->_parent != _pHead){Node* ppnode = pParent->_parent;if (ppnode->_left == pParent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}else{_pHead->_parent = cur;cur->_parent = _pHead;}// 连接p和curpParent->_parent = cur;cur->_left = pParent;}template <class K, class T, class KeyOfT>void RBTree<K, T, KeyOfT>::RotateR(Node* pParent){Node* cur = pParent->_left, * curright = cur->_right;// 连接p和cur右树,因为该位置被p占据pParent->_left = curright;if (curright){curright->_parent = pParent;}// 连接父结点if (pParent->_parent != _pHead){Node* ppnode = pParent->_parent;if (ppnode->_left == pParent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}else{_pHead->_parent = cur;cur->_parent = _pHead;}// 连接p和curpParent->_parent = cur;cur->_right = pParent;}template <class K, class T, class KeyOfT>typename RBTree<K, T, KeyOfT>::Node* RBTree<K, T, KeyOfT>::LeftMost() const{Node* cur = _pHead->_parent;while (cur->_left){cur = cur->_left;}return cur;}template <class K, class T, class KeyOfT>typename RBTree<K, T, KeyOfT>::Node* RBTree<K, T, KeyOfT>::RightMost() const{Node* cur = _pHead->_parent;while (cur->_right){cur = cur->_right;}return cur;}template <class K, class T, class KeyOfT>typename RBTree<K, T, KeyOfT>::Node* RBTree<K, T, KeyOfT>::Find(const K& data) // 注意这里,{Node* cur = _pHead->_parent;KeyOfT kot;while (cur){if (data > kot(cur->_data)){cur = cur->_right;}else if (data < kot(cur->_data)){cur = cur->_left;}else{return cur;}}return nullptr;}template <class K, class T, class KeyOfT>std::pair<typename RBTree<K, T, KeyOfT>::iterator, bool> RBTree<K, T, KeyOfT>::Insert(const T& data) // 为了和map适配,要返回pair类型//(first是插入元素所在的迭代器,second是bool值,判断是否成功插入){KeyOfT kot;Node* newnode = nullptr;if (_pHead->_parent == nullptr){newnode = new Node(data);newnode->_col = black;_pHead->_parent = newnode;newnode->_parent = _pHead;return std::make_pair(iterator(newnode), true);}else{Node* cur = _pHead->_parent, * parent = cur;while (cur){if (kot(data) > kot(cur->_data)){parent = cur;cur = cur->_right;}else if (kot(data) < kot(cur->_data)){parent = cur;cur = cur->_left;}else{return std::make_pair((iterator)cur, false);}}newnode = new Node(data);cur = newnode;cur->_parent = parent;if (kot(parent->_data) > kot(cur->_data)){parent->_left = cur;}else{parent->_right = cur;}Node* grandfather = nullptr;while (parent != _pHead && parent->_col == red){grandfather = parent->_parent; // 因为父结点是红色,所以肯定有爷爷结点(注意红黑树规则:根结点必须是黑色)if (grandfather->_left == parent) // 确定父亲位置{Node* uncle = grandfather->_right; // 也就能确定叔叔位置if (uncle && uncle->_col == red){parent->_col = uncle->_col = black;grandfather->_col = red;}else // 如果uncle不存在/为黑,就需要旋转+变色了{// 需要先判断旋转类型(也就是判断 -- parent和cur的相对位置)if (parent->_left == cur){// 一条偏右的直线,需要右旋RotateR(grandfather);// 旋转完后parent成为根结点// 更改完结点指向后,就可以改颜色了(都是根结点为黑,另外两个为红)parent->_col = black;cur->_col = grandfather->_col = red; // 和cur一层}else{// 拐角在左边,也就是先左旋,再右旋RotateL(parent);RotateR(grandfather);// cur成为根结点// 改颜色cur->_col = black;parent->_col = grandfather->_col = red;}break;}}else // parent在grandfather的右树{Node* uncle = grandfather->_left;if (uncle && uncle->_col == red){parent->_col = uncle->_col = black;grandfather->_col = red;}else // 如果uncle不存在/为黑,就需要旋转+变色了{// 需要先判断旋转类型(也就是判断 -- parent和cur的相对位置)if (parent->_right == cur){// 一条偏左的直线,需要左旋RotateL(grandfather);parent->_col = black;cur->_col = grandfather->_col = red; // 和cur一层}else{// 拐角在右边,也就是先右旋,再左旋RotateR(parent);RotateL(grandfather);// 改颜色cur->_col = black;parent->_col = grandfather->_col = red;}break;}}cur = grandfather; // 注意,这里会改cur的指向,但返回值需要返回插入位置的迭代器,所以需要另外保存parent = cur->_parent;}(_pHead->_parent)->_col = black; // 根结点必须为黑(防止它在上面的循环中被修改)}_pHead->_left = LeftMost();_pHead->_right = RightMost();//std::cout << (_pHead->_left)->_data << " " << (_pHead->_right)->_data << std::endl;return std::make_pair(iterator(newnode), true);}template <class K, class T, class KeyOfT>bool RBTree<K, T, KeyOfT>::_IsValidRBTRee(Node* cur, size_t blackCount, size_t pathBlack){if (cur == nullptr){// 到空结点后,就说明一条路径已经走通了,可以用得到的黑色结点数与基准数对比,不一样就说明红黑树错误if (pathBlack != blackCount){return false;}else{return true;}}if (cur->_parent){Node* ppnode = cur->_parent;if (cur->_col == red && ppnode->_col == red){return false;}}if (cur->_col == black){++pathBlack;}return _IsValidRBTRee(cur->_left, blackCount, pathBlack) && _IsValidRBTRee(cur->_right, blackCount, pathBlack);}template <class T, class Ptr, class Ref>void RBTreeIterator<T, Ptr, Ref>::Increament(){Node* cur = _pNode, * parent = _pNode->_parent;if (cur->_right){// 找到右子树的最小结点Node* curright = cur->_right;while (curright->_left){curright = curright->_left;}_pNode = curright;}else{while (parent->_parent != cur && parent->_right == cur) // 找到cur是parent的左结点的位置,这样parent的位置就是下一个位置{cur = parent;parent = parent->_parent;}_pNode = parent;}}template <class T, class Ptr, class Ref>void RBTreeIterator<T, Ptr, Ref>::DeIncreament(){Node* cur = _pNode, * parent = _pNode->_parent;if (cur->_left){// 找到左子树的最大结点Node* curleft = cur->_left;while (curleft->_right){curleft = curleft->_right;}_pNode = curleft;}else{while (parent->_parent != cur && parent->_left == cur) // 找到cur是parent的左结点的位置,这样parent的位置就是下一个位置{cur = parent;parent = parent->_parent;}_pNode = parent;}}
}
set
set我们只实现它的插入和迭代器部分,大概可以看到效果就行
insert的迭代器转换问题
不考虑别的,因为insert返回的都是pair类型的,都是迭代器+布尔值,所以set直接调用红黑树的插入即可
但是,编译过不去!
大概就是说,普通迭代器无法转换为const迭代器
为什么会有这样的问题?
注意,set中,无论是普通迭代器还是const迭代器,其实都封装的是红黑树的const迭代器
stl源码中就是这么定义的:
- 但是,tree的insert返回的是普通迭代器,而set的insert要返回的是const迭代器
- 这就存在一个普通迭代器向const迭代器转换的过程
如何解决
所以我们需要在红黑树的迭代器类中增加这一功能
typedef RBTreeNode<T> Node; typedef RBTreeIterator<T, Ptr, Ref> Self; //为了可以能让普通迭代器初始化const迭代器,需要来一个普通迭代器对象 typedef RBTreeIterator<T, T*, T&> iterator; Node* _pNode;RBTreeIterator(Node* pNode): _pNode(pNode) {} RBTreeIterator(const iterator& it) // const迭代器时,它是一个初始化;普通迭代器时,它是一个拷贝: _pNode(it._pNode) {}
代码
#include "RB_Tree.hpp"namespace my_set
{template <class K>class set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename my_RB_Tree::RBTree<K, K, SetKeyOfT>::const_iterator iterator;typedef typename my_RB_Tree::RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;const_iterator begin() const{return _t.begin();}const_iterator end() const{return _t.end();}std::pair<iterator, bool> insert(const K& data) {//return _t.Insert(data);//这里在构建时,set的insert调用tree的insert//而tree中insert的返回值,返回的pair中,第一个成员是tree的普通迭代器//然后回到该函数,该函数返回的pair的第一个成员是set中的普通迭代器(实质上是tree中的const迭代器)//所以我们本质上是用不同类型的pair在赋值//所以要先转换std::pair<typename my_RB_Tree::RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _t.Insert(data); //这里是tree的普通迭代器iterator it(ret.first);return std::pair<iterator, bool>(it,ret.second); //这里是要用普通迭代器初始化一个const迭代器,所以需要在tree迭代器中增加这个功能}private:my_RB_Tree::RBTree<K, K, SetKeyOfT> _t;};
}
map
注意点
map的重点就在insert和[ ]的重载上
也没啥别的了,就需要自己先构建一个pair类型,其他的就注意返回值和接收值到底是谁
K:key值类型 V:value类型 T:map的元素类型
代码
#include "RB_Tree.hpp"namespace my_map
{template <class K, class V>class map{public:typedef std::pair<const K, V> T; // map中key不能变,value可以变struct MapKeyOfT{const V &operator()(const T &data){return data.second;}};typedef typename my_RB_Tree::RBTree<K, T, MapKeyOfT>::iterator iterator;typedef typename my_RB_Tree::RBTree<K, T, MapKeyOfT>::const_iterator const_iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}const_iterator begin() const{return _t.begin();}const_iterator end() const{return _t.end();}std::pair<iterator, bool> insert(const T &data){return _t.Insert(data);}V &operator[](const K &data){auto ret = insert(std::make_pair(data,V()));return (ret.first)->second;}private:my_RB_Tree::RBTree<K, T, MapKeyOfT> _t;};
}
相关文章:

set和map的封装
目录 介绍 红黑树代码 set insert的迭代器转换问题 为什么会有这样的问题? 如何解决 代码 map 注意点 代码 介绍 set和map的底层都是红黑树,所以我们可以在自己实现的红黑树(简易版)的基础上,进行封装,成为简易的set和map 红黑树代码 #pragma once#include <…...
java基础练习--基础语法
预备知识:Java基本语法、分支、循环、数组和字符串 7-1 累加器 请你实现一个累加器。输入n个非负整数,输出他们的和。 1<n<1000,而每个数则<10000。 输入格式: 输入包括两行。 第一行:包括一个整数n,表示总共有n个数。 第二行:包…...
Android12 OTA编译差分包报错问题
前言 在Ubuntu 20.04.4 LTS系统中编译Android12 OTA差分包的时候提示如下报错log: Warning: releasetools script should be invoked as hermetic Python executable -- build and run ota_from_target_files directly. Traceback (most recent call last):File "./bu…...
现代c++手撸2309神经网络最简化版230901
用c++输入数据:vector<vector<float>> inputs = { {1, 1}, {1, 0} };数据targets={0,1}; 测试:vector<vector<float>> inputs22 = { {1, 0}, {1,1} }; 构建神经网络,例如:NeuralNetwork nn({ 2, 4, 1 }); 则网络有四层、输入层2个节点、输出层1个节…...

Qt之显示PDF文件
之前使用过mupdf库,能够成功显示pdf,但是我用着有BUG,不太理解它的代码,搞了好久都不行。后面又试了其他库,如pdfium、popler、下载了很多例程,都跑不起来!后面偶然得知xpdf库,看起来…...

[极客大挑战 2019]FinalSQL - 异或盲注
1、这题的关键是找注入点,如果选择用户名、密码作为输入点就麻烦了 2、注入点:按钮,点击就传id;当id1时,提示Click others 可以利用id的特性,构造异或匹配 payload: f"1^(ord(substr((select…...

【Go语言实战】(25) 分布式算法 MapReduce
MapReduce 写在前面 身为大数据专业的学生,其实大学我也多多少少接触过mapreduce,但是当时觉得这玩意太老了,觉得这和php一样会被时代淘汰。只能说当时确实太年轻了,没有好好珍惜那时候的学习资源… 现在回过头来看mapreduce&a…...

【网络安全-信息收集】网络安全之信息收集和信息收集工具讲解(提供工具)
工具下载百度网盘链接(包含所有用到的工具): 百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固,支持教育网加速,支持手机端。注册使用百度网盘即可享受免费存储空间https://pan.…...

战火使命ssr排名,战火使命角色强度排行
在战火使命中,很多玩家都在关注SSR角色的强度排行,那么,下面就为大家分享一下小编整理的最新战火使命ssr排名,一起来看看吧。 关注【娱乐天梯】,获取内部福利号 一、SSR角色排名榜: 1. 克拉拉、艾蕾娜、杰西…...
CSS之linear-gradient( ) 函数—背景颜色渐变设计
目录 linear-gradient( ) 函数 简介: 语法: 详解: 例如: linear-gradient( ) 函数 简介: linear-gradient 函数是 CSS 中用于创建线性渐变的函数。它接受一个或多个参数,并使用这些参数创建一个渐变。…...
[Unity]未能加载一个或多个断点问题
【背景】 大家2023国庆快乐,虽然是假期,我还是继续码些文章。 今天写项目时遇到个环境问题,新建脚本时双击调起VS编辑器,忽然提示无法加载一个或多个断点(当时忘记截图了,现在已解决,就不上图了…...

Qt中的基础数据类型
1.基础类型 因为Qt是一个C++ 框架, 因此C++中所有的语法和数据类型在Qt中都是被支持的, 但是Qt中也定义了一些属于自己的数据类型, 下边给大家介绍一下这些基础的数类型 QT基本数据类型定义在#include <QtGlobal> 中,QT基本数据类型有: 类型名称注释备注qint8signed ch…...

2023阿里云域名优惠口令大全
2023年阿里云域名优惠口令,com域名续费优惠口令“com批量注册更享优惠”,cn域名续费优惠口令“cn注册多个价格更优”,cn域名注册优惠口令“互联网上的中国标识”,阿里云优惠口令是域名专属的优惠码,可用于域名注册、续…...

湖南软件测评公司简析:软件功能测试和非功能测试的联系和区别
一、软件功能测试 软件功能测试旨在验证软件是否按照需求规格说明书的要求正常工作。具体而言,功能测试会对软件的所有功能进行测试,以确保其满足用户的需求和预期。在进行功能测试时,根据需求规格说明书编写测试用例,并在测试…...
HuggingFace Transformers教程(1)--使用AutoClass加载预训练实例
知识的搬运工又来啦 ☆*: .。. o(≧▽≦)o .。.:*☆ 【传送门>原文链接:】https://huggingface.co/docs/transformers/autoclass_tutorial 🚗🚓🚕🛺🚙🛻🚌Ƕ…...
Qt获取当前所用的Qt版本、编译器、位数等信息
//详细的Qt版本编译器位数 QString compilerString "<unknown>"; { #if defined(Q_CC_CLANG)QString isAppleString; #if defined(__apple_build_version__)isAppleString QLatin1String(" (Apple)"); #endifcompilerString QLatin1String("…...

《C和指针》笔记31:多维数组的数组名、指向多维数组的指针、作为函数参数的多维数组
文章目录 1. 指向多维数组的数组名2. 指向多维数组的指针3. 作为函数参数的多维数组 1. 指向多维数组的数组名 我们知道一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。那么多维数组的数组名代表什么呢&#x…...

【伪彩色图像处理】将灰度图像转换为彩色图像研究(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

Go Gin Gorm Casbin权限管理实现 - 2. 使用Gorm存储Casbin权限配置以及`增删改查`
文章目录 0. 背景1. 准备工作2. 权限配置以及增删改查2.1 策略和组使用规范2.2 用户以及组关系的增删改查2.2.1 获取所有用户以及关联的角色2.2.2 角色组中添加用户2.2.3 角色组中删除用户 2.3 角色组权限的增删改查2.3.1 获取所有角色组权限2.3.2 创建角色组权限2.3.3 修改角色…...

DNDC模型的温室气体排放分析
DNDC(Denitrification-Decomposition,反硝化-分解模型)是目前国际上最为成功的模拟生物地球化学循环的模型之一,自开发以来,经过不断完善和改进,从模拟简单的农田生态系统发展成为可以模拟几乎所有陆地生态…...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

麒麟系统使用-进行.NET开发
文章目录 前言一、搭建dotnet环境1.获取相关资源2.配置dotnet 二、使用dotnet三、其他说明总结 前言 麒麟系统的内核是基于linux的,如果需要进行.NET开发,则需要安装特定的应用。由于NET Framework 是仅适用于 Windows 版本的 .NET,所以要进…...
深入浅出JavaScript中的ArrayBuffer:二进制数据的“瑞士军刀”
深入浅出JavaScript中的ArrayBuffer:二进制数据的“瑞士军刀” 在JavaScript中,我们经常需要处理文本、数组、对象等数据类型。但当我们需要处理文件上传、图像处理、网络通信等场景时,单纯依赖字符串或数组就显得力不从心了。这时ÿ…...

Java在word中指定位置插入图片。
Java使用(Poi-tl) 在word(docx)中指定位置插入图片 Poi-tl 简介Maven 依赖配置Poi-tl 实现原理与步骤1. 模板标签规范2.完整实现代码3.效果展示 Poi-tl 简介 Poi-tl 是基于 Apache POI 的 Java 开源文档处理库,专注于…...

vb监测Excel两个单元格变化,达到阈值响铃
需求 在Excel中实现监控两个单元格之间的变化范围,当达到某个设定的值的范围内时,实现自动响铃提示。 实现: 首先设置Excel,开启宏、打开开发者工具,点击visual Basic按钮,然后在左侧双击需要监测的shee…...