【C++】使用红黑树封装map与set

文章目录

  • 1. 源码分析
  • 2. 调整红黑树的结构搭建map、set
  • 3. 红黑树的迭代器
    • 3.1 普通迭代器
    • 3.2 const迭代器
    • 3.3 map的operator[ ]
  • 4. 完整代码
    • 4.1 RBTree
    • 4.2 MyMap
    • 4.3 MySet

在这里插入图片描述

对于map与set,它们一个是KV模型,一个是K模型,那我们要写两个红黑树吗?
我们来看一下源码

1. 源码分析

在这里插入图片描述
我很疑惑为什么它们两个都传了两个参数给底层的红黑树,那再看一下红黑树的源码

在这里插入图片描述

我们可以发现,红黑树在底层不知道你传给它的是什么,为了兼容map与set它搞了两个参数。

但是搞了两个参数后,第一个模板参数Key是不是有点多余呢?

  • 对于set而言,两个K确实是有点多余;对于map来说,pair里面也有K,好像也有点多余
  • 如果只留第二个模板参数,_rb_tree_node< Value >,为set时 Value为K;为 map时 Value为pair<K,V>,好像也没问题。
  • 对于插入操作,set是k,map是pair还挺好;但是对于查找操作时,set使用Value没问题;map查找时也要使用Value,但map的Value是一个pair(有K,V),那我既然知道了K和V,我还查找什么呢?所以第一个模板参数不多于,反而是必须

但还有一个问题,在插入操作时,涉及数据比较大小。但是map的Value是一个pair,还得取里面的key;set的Value就是key,此时就比较麻烦。pair默认的比较规则是first比完second比,不符合我们的预期。因此我们还要给红黑树传递一个仿函数,以便获取key。

2. 调整红黑树的结构搭建map、set

那我们之前实现的红黑树的结构就得做一下调整了
在这里插入图片描述
除了上面呈现出来的模板参数以外,还可以传递自定义的比较规则模板Compare。

下面是map与set的基本框架

原来红黑树中涉及数据比较大小的地方,都得回调传递过来的KeyOfT获取Key后再比较
在这里插入图片描述

3. 红黑树的迭代器

3.1 普通迭代器

红黑树的迭代器无非也就是包含一个节点的指针,然后重载指针的各种操作即可。
在这里插入图片描述

一般迭代器的实现都是按照中序遍历,遍历完是有序的。所以红黑树迭代器的begin(),应该是树最左侧的节点;对于迭代器的end(),我们就按照NULL来处理。

在这里插入图片描述
那红黑树迭代器的++与- -操作是如何实现的呢?

对于++操作:
在这里插入图片描述

对于- -操作:为了方便_node == nullptr时找树最右侧的节点,我们再给迭代器加一个指针root记录树的根。
在这里插入图片描述

在这里插入图片描述

template<class Value>class RBTreeIterator{typedef TreeNode<Value> Node;typedef RBTreeIterator<Value> Self;private:Node* _node;Node* _root;public:RBTreeIterator(Node* data,Node* root):_node(data),_root(root){}Self& operator++(){//右树存在,找右树的最左侧节点if (_node->_right){Node* leftMost = _node->_right;while (leftMost && leftMost->_left){leftMost = leftMost->_left;}_node = leftMost;//直接跳转至}else  //右树不存在,继续向上{Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_right == cur){cur = parent;parent = cur->_parent;}//parent不存在或者该遍历根了_node = parent;}return *this;}Self& operator--(){//特殊处理end(),找整棵树的最右侧节点if (_node == nullptr){Node* rightMost = _root;while (rightMost && rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}//左子树存在,找左子树最右侧节点else if (_node->_left){Node* rightMost = _node->_left;while (rightMost && rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else{//左子树不存在,当前树遍历完,继续向上Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_left == cur){cur = parent;parent = cur->_parent;}//parent不存在或者该遍历根了_node = parent;}return *this;}Value& operator*(){return _node->_data;}Value* operator->(){return &_node->_data;}bool operator==(const Self& it){return _node == it._node;}bool operator!=(const Self& it){return _node != it._node;}};

3.2 const迭代器

对于const迭代器而言,无非就是数据不能被修改,那也就是针对迭代器的operator* 与operator->了。

那就先改造底层的迭代器
在这里插入图片描述

然后map与set在上层封一下就行了

在这里插入图片描述
通过上面的我们可以看到,使用const迭代器以后确实都不能修改了;但是普通迭代器存在一个问题:
在这里插入图片描述

所以上层在传递给下层时,我们可以将k设置为const
在这里插入图片描述
到这里我们的迭代器就完美了。

3.3 map的operator[ ]

我们都知道map重载的[ ]既可以充当插入,也可以充当修改。那么在底层它用的就是insert。(点击可看map的使用)
那我们就需要改变一下insert的返回值了。
在这里插入图片描述
在这里插入图片描述

到这里我们map与set的封装就结束了。

4. 完整代码

4.1 RBTree

#pragma once
namespace my
{//颜色enum Color{RED = 0,BLACK};template<class Value>struct TreeNode{Value _data;TreeNode<Value>* _left;TreeNode<Value>* _right;TreeNode<Value>* _parent;//前驱节点Color _col;TreeNode(const Value& data): _data(data), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED)//默认新节点为红色的{}};template<class Value,class Ref,class Ptr>class RBTreeIterator{typedef TreeNode<Value> Node;typedef RBTreeIterator<Value,Ref,Ptr> Self;private:Node* _node;Node* _root;public:RBTreeIterator(Node* data,Node* root):_node(data),_root(root){}Self& operator++(){//右树存在,找右树的最左侧节点if (_node->_right){Node* leftMost = _node->_right;while (leftMost && leftMost->_left){leftMost = leftMost->_left;}_node = leftMost;//直接跳转至}else  //右树不存在,继续向上{Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_right == cur){cur = parent;parent = cur->_parent;}//parent不存在或者该遍历根了_node = parent;}return *this;}Self& operator--(){//特殊处理end(),找整棵树的最右侧节点if (_node == nullptr){Node* rightMost = _root;while (rightMost && rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}//左子树存在,找左子树最右侧节点else if (_node->_left){Node* rightMost = _node->_left;while (rightMost && rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else{//左子树不存在,当前树遍历完,继续向上Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_left == cur){cur = parent;parent = cur->_parent;}//parent不存在或者该遍历根了_node = parent;}return *this;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator==(const Self& it){return _node == it._node;}bool operator!=(const Self& it){return _node != it._node;}};template<class K,class Value,class KeyOfT>class RBTree{typedef TreeNode<Value> Node;public:typedef RBTreeIterator<Value,Value&,Value*> Iterator;typedef RBTreeIterator<Value,const Value&,const Value*> ConstIterator;Iterator Begin(){Node* leftMost = _root;//找最左侧的节点while (leftMost && leftMost->_left){leftMost = leftMost->_left;}return Iterator(leftMost,_root);}Iterator End(){return Iterator(nullptr,_root);} ConstIterator Begin() const{Node* leftMost = _root;//找最左侧的节点while (leftMost && leftMost->_left){leftMost = leftMost->_left;}return ConstIterator(leftMost,_root);}ConstIterator End() const{return ConstIterator(nullptr,_root);}RBTree() = default;RBTree(const RBTree<K, Value,KeyOfT>& tree){this->_root = Copy(tree._root);}//类里面直接写类名,无需传递模板参数也可。RBTree& operator=(RBTree tree){swap(this->_root, tree._root);//现代写法return *this;}~RBTree(){Destroy(_root);}pair<Iterator,bool> insert(const Value& data){if (_root == nullptr){_root = new Node(data);//如果插入的节点是头节点,需要遵守规则2(根是黑的)//需要默认的红色改为黑_root->_col = BLACK;return make_pair(Iterator(_root,_root),true);}KeyOfT kot;Node* cur = _root;Node* parent = cur->_parent;while (cur){if (kot(cur->_data) < kot(data)){parent = cur;cur = cur->_right;}else if (kot(cur->_data) > kot(data)){parent = cur;cur = cur->_left;}elsereturn make_pair(Iterator(cur, _root), false);}//孩子连父亲cur = new Node(data);Node* newnode = cur;cur->_parent = parent;//父亲连孩子if (kot(parent->_data) < kot(data))parent->_right = cur;elseparent->_left = cur;//检查是否违反红黑树的规则//如果父亲存在且为红,则违反规则while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (grandfather->_left == parent){//根据爷爷节点找叔叔Node* uncle = grandfather->_right;//如果叔叔存在且为红,仅需要变色if (uncle && uncle->_col == RED){parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;//然后继续向上调整cur = grandfather;parent = cur->_parent;}//如果叔叔不存在 或者存在且为黑,需要旋转+变色else{//判断单旋(直树)还是双旋(弯树)//单旋if (parent->_left == cur){	//       g//     p   u//	cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}//双旋else{//        g//     p     u//	     cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;//旋转后已经符合规则,无需再调整了}}//grandfather->_right == parentelse{Node* uncle = grandfather->_left;//如果叔叔存在且为红,仅需要变色if (uncle && uncle->_col == RED){parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;//继续调整cur = grandfather;parent = cur->_parent;}//如果叔叔不存在 或者存在且为黑,需要旋转+变色else{//    g// u     p//           cif (parent->_right == cur){RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}//      g//   u      p//	      celse{RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}//父亲不存在,或者父亲为黑时,无需判断,直接将根变黑_root->_col = BLACK;return make_pair(Iterator(newnode, _root), true);}Iterator* find(const K& key){Node* cur = _root;KeyOfT kot;while (cur){if ( kot(cur->_data)< key)cur = cur->_right;else if (kot(cur->_data) > key)cur = cur->_left;elsereturn  Iterator(cur,_root);}return End();}void InOrder(){_InOrder(_root);cout << endl;}bool IsRBTree(){Node* root = _root;if (root == nullptr)//空树也是return true;if (root->_col == RED){cout << "根节点为红色,违反规则" << endl;return false;}//先统计最左侧路径上有多少黑节点作为标准Node* cur = root;int blackNum = 0;while (cur){if (cur->_col == BLACK)blackNum++;cur = cur->_left;}//k用来记录路径中黑节点的个数int k = 0;//在依据标准黑节点的个数,比较其它路径return _IsRBTree(root, blackNum, k);}int Size(){return _Size(_root);}int Height(){return _Height(_root);}private:int _Height(Node* root){if (root == nullptr)return 0;int leftH = _Height(root->_left);int rightH = _Height(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;}int _Size(Node* root){if (root == nullptr)return 0;return 1 + _Size(root->_left) + _Size(root->_right);}bool _IsRBTree(Node* root, int blackNum, int k){//一条路径走完,比较黑节点的个数if (root == nullptr){if (blackNum != k){cout << "路径黑节点个数不相等,违反规则" << endl;return false;}return true;}Node* parent = root->_parent;if (parent && root->_col == RED && parent->_col == RED){cout << "存在连续的红节点,违反规则" << endl;return false;}//统计黑节点if (root->_col == BLACK)k++;//统计完根,在依次统计左右子树return _IsRBTree(root->_left, blackNum, k)&& _IsRBTree(root->_right, blackNum, k);}void RotateR(Node* parent){Node* SubL = parent->_left;Node* SubLR = SubL->_right;//降级连兵parent->_left = SubLR;if (SubLR)SubLR->_parent = parent;Node* parentParent = parent->_parent;//升级连将SubL->_right = parent;parent->_parent = SubL;//将连将SubL->_parent = parentParent;if (parentParent == nullptr){_root = SubL;}else{if (parentParent->_left == parent)parentParent->_left = SubL;elseparentParent->_right = SubL;}}void RotateL(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;//降级连兵parent->_right = SubRL;if (SubRL)SubRL->_parent = parent;Node* parentParent = parent->_parent;//升级连将SubR->_left = parent;parent->_parent = SubR;//将连将SubR->_parent = parentParent;if (parentParent == nullptr)_root = SubR;else{if (parentParent->_left == parent)parentParent->_left = SubR;elseparentParent->_right = SubR;}}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;_root->_left = Copy(root->_left);_root->_right = Copy(root->_right);return _root;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}Node* _root;};
}

4.2 MyMap

#pragma once
#include"RBTree.h"namespace my
{template<class K, class V>class map{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename RBTree<K, pair<const K, V>,MapKeyOfT>::Iterator iterator;typedef typename RBTree<K, pair<const K, V>,MapKeyOfT>::ConstIterator const_iterator;const_iterator begin()const{return _t.Begin();}const_iterator end() const{return _t.End();}iterator begin(){return _t.Begin();}iterator end(){return _t.End();}pair<iterator, bool> insert(const pair<K, V> kv){return _t.insert(kv);}bool find(const K& key){return _t.find(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key,V()));//如果key存在,则返回其对应的value//如果key不存在,则插入它,其value为类型的默认值return ret.first->second;}private:RBTree<K, pair<const K, V>, MapKeyOfT> _t;};//void MapPrint(const map<string, string>& m)//{//	map<string, string>::const_iterator it = m.end();//	while (it != m.begin())//	{//		it->first += "x";//		it->second += "y";//		--it;//		cout << it->first << " " << it->second << endl;//	}//}void testmap(){map<string, string> m;m.insert({ "right","右"});m.insert({ "up","上"});m.insert({ "down","下"});m.insert({ "left","左"});m["left"] = "修改左";m["offer"] = "新增";//cout << m.find(3) << endl;//for (auto& e : m)//{//	cout << e.first << " " << e.second << endl;//}//MapPrint(m);map<string,string>::iterator it = m.end();while (it != m.begin()){//it->second += "y";//it->first += "x";--it;cout << it->first << " " << it->second << endl;}cout << endl;}
}

4.3 MySet

#pragma once
#include"RBTree.h"namespace my
{template<class K>class set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename RBTree<K, const K,SetKeyOfT>::Iterator iterator;typedef typename RBTree<K, const K,SetKeyOfT>::ConstIterator const_iterator;const_iterator begin() const{return _t.Begin();}const_iterator end() const{return _t.End();}iterator begin(){return _t.Begin();}iterator end(){return _t.End();}pair<iterator, bool> insert(const K& key){return _t.insert(key);}bool find(const K& key){return _t.find(key);}private:RBTree<K, const K, SetKeyOfT> _t;};//void SetPrint(const set<int>& s)//{//	set<int>::const_iterator it = s.end();//	while (it != s.begin())//	{//		//*it = 20;//		--it;//		cout << *it << " ";//	}//	cout << endl;//}void testset(){set<int> s;pair<set<int>::iterator,bool> ret = s.insert(5);cout << *(ret.first) << endl;//查看insert的返回值s.insert(2);s.insert(8);s.insert(15);s.insert(2);s.insert(9);set<int>::iterator it = s.end();while (it != s.begin()){//*it = 10;--it;cout << *it <<" ";}cout << endl;//SetPrint(s);}
}

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

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

相关文章

虚幻5|角色武器装备的数据库学习(不只是用来装备武器,甚至是角色切换也很可能用到)

虚幻5|在连招基础上&#xff0c;给角色添加武器并添加刀光|在攻击的时候添加武器并返回背后&#xff08;第一部分&#xff0c;下一部分讲刀光&#xff09;_unreal 如何给角色添加攻击-CSDN博客 目的&#xff1a;捡起各种不同的武器&#xff0c;捡起的武器跟装备的武器相匹配 …

【Hot100】LeetCode—234. 回文链表

目录 1- 思路快慢指针链表拆分反转链表 2- 实现⭐234. 回文链表——题解思路 3- ACM 实现 原题连接&#xff1a;234. 回文链表 1- 思路 快慢指针链表拆分反转链表 思路 ①将链表拆分前后两个部分——>找拆分点、②反转后面部分、③根据反转结果&#xff0c;同时利用两个指…

MySQL笔记01: MySQL入门_1.3 MySQL启动停止与登录

1.3 MySQL启动停止与登录 1.3.1 MySQL启动与停止 MySQL数据库分为客户端和服务器端&#xff0c;只有服务器端服务开启以后&#xff0c;才可以通过客户端登录MySQL服务端。 首先&#xff0c;以管理员身份运行“命令提示符”&#xff1a; &#xff08;1&#xff09;启动MySQL服务…

python井字棋游戏设计与实现

python实现井字棋游戏 游戏规则&#xff0c;有三个井字棋盘&#xff0c;看谁连成的直线棋盘多谁就获胜 棋盘的展现形式为 棋盘号ABC和位置数字1-9 输入A1 代表在A棋盘1号位数下棋 效果图如下 部分源码如下&#xff1a; 卫星工纵浩 白龙码程序设计&#xff0c;点 代码获取 …

海外短剧平台的局限性与优势:做平台还是选择CPS?

随着国内短剧市场的蓬勃发展&#xff0c;越来越多的目光开始聚焦在海外市场。不少企业和个人都看到了“文化输出”的巨大潜力&#xff0c;希望通过短剧这一形式&#xff0c;吸引海外的观众。然而&#xff0c;在进入海外市场时&#xff0c;我们面临着两种主要的选择&#xff1a;…

STM32 定时器 输入捕获

用于测频率测占空比 IC(Input Capture)输入捕获 输入捕获模式下&#xff0c;当通道输入引脚出现指定电平跳变&#xff08;上升沿/下降沿&#xff09;时&#xff0c;会让当前CNT的值将被锁存到CCR中&#xff0c;可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数…

2024年入职/转行网络安全,该如何规划?_网络安全职业规划

前言 前段时间&#xff0c;知名机构麦可思研究院发布了 《2022年中国本科生就业报告》&#xff0c;其中详细列出近五年的本科绿牌专业&#xff0c;其中&#xff0c;信息安全位列第一。 网络安全前景 对于网络安全的发展与就业前景&#xff0c;想必无需我多言&#xff0c;作为…

制造企业为什么要数字化转型?面临哪些困难?

如何界定制造企业 制造业&#xff08;Manufacturing Industry&#xff09;是指机械工业时代利用某种资源&#xff08;物料、能源、设备、工具、资金、技术、信息和人力等&#xff09;&#xff0c;按照市场要求&#xff0c;通过制造过程&#xff0c;转化为可供人们使用和利用的…

坐牢第二十七天(聊天室)

基于UDP的网络聊天室 一.项目需求&#xff1a; 1.如果有用户登录&#xff0c;其他用户可以收到这个人的登录信息 2.如果有人发送信息&#xff0c;其他用户可以收到这个人的群聊信息 3.如果有人下线&#xff0c;其他用户可以收到这个人的下线信息 4.服务器可以发送系统信息…

8月16日笔记

只有DNS协议出网场景 DNS 协议是一种请求、应答协议&#xff0c;也是一种可用于应用层的隧道技术。DNS 隧道的工作原理很简单&#xff0c;在进行 DNS 查询时&#xff0c;如果查询的域名不在 DNS 服务器本机缓存中&#xff0c;就会访问互联网进行查询&#xff0c;然后返回结果。…

JavaScript基础知识(三)

样式修改 元素.style是对象的一种格式,用于通过设置元素的相关行内样式来设置css,也可以选择相关关联的样式来修改元素相关的样式. 要注意的是,选择相关的样式的时候,样式名是采用小驼峰写法而非是全部小写的方式 类名 添加类名: 元素.classList.add("classname") …

FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer

GSYVideoPlayer是一个国产的移动端视频播放器&#xff0c;它采用了IJKPlayer、Media3(EXOPlayer)、MediaPlayer、AliPlayer等四种播放器内核&#xff0c;支持弹幕、滤镜、广告等多项功能。 GSYVideoPlayer的Github主页为https://github.com/CarGuo/GSYVideoPlayer&#xff0c;截…

『Z-Workshop』 The Graph workshop mini hackathon活动

Community Meetup In Hangzhou ZJUBCA 2024 求是 创新 概述 / OVERVIEW The Graph作为一个去中心化的查询协议&#xff0c;为区块链数据的索引和查询提供了强大的支持。我们希望通过这场黑客松&#xff0c;激发大家对区块链技术更深层次的探索和应用&#xff0c;共同推动这一…

Facebook与区块链:社交网络如何融入去中心化技术

随着区块链技术的飞速发展&#xff0c;去中心化理念逐渐渗透到各个领域&#xff0c;社交网络也不例外。作为全球领先的社交平台&#xff0c;Facebook在这一趋势下开始积极探索区块链技术的潜力&#xff0c;希望利用这一前沿技术来提升平台的安全性、透明度和用户控制权。本文将…

Linux网络:基于OS的网络架构

Linux网络&#xff1a;OS视角下的网络架构 网络分层模型OSI 七层模型TCP/IP 五层模型 协议操作系统与网络网络相关命令ifconfigpingnetstat 本博客将基于操作系统&#xff0c;讲解计算机网络的设计理念&#xff0c;帮助大家理解操作系统与网络之间的关系。 网络分层模型 网络…

【AI安防】YOLOv8 + OpenVINO2023 + QT5 电子围栏预警系统

引言 电子围栏是一种利用无线通信技术和地理信息系统实现的虚拟边界&#xff0c;用于监控和控制被监控对象的位置。它可以帮助我们实现对特定区域内的自定义对象进行实时检测、定位与跟踪。本文介绍了一种基于YOLOv8 OpenVINO2023 QT5 联合打造的实时高效、多线程、自定义对…

Java使用Graphics绘制图片文字边缘出现粗糙的锯齿问题解决

为什么会出现锯齿问题 文字出现锯齿的现象通常是由于显示设备的分辨率有限&#xff0c;无法完美地表现出字符的曲线和斜线的原因。 怎么解决 可以通过Graphics2D设置抗锯齿效果 // 打开抗锯齿效果g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VAL…

【备战蓝桥杯青少组】第二天 奇特的砖墙

真题 第十四届省赛 编程题 第5题 工人砌了一面奇特的砖墙&#xff0c;该墙由N列砖组成&#xff08;1≤N≤1e6&#xff09;&#xff0c;且每列砖的数量为Ki&#xff08;1≤Ki≤1e4&#xff0c;相邻砖块之间无缝隙&#xff09;&#xff0c;每块砖的长宽高都为1。小蓝为了美化这面…

网络安全简介(入门篇)

目录 前言 一、什么是网络安全&#xff1f; 二、网络安全的重要性 1、保护数据安全和隐私 2、防止服务中断和数据丢失 3、防止经济损失和法律责任 4、维护公共安全和国家安全 5、提升技术发展和创新 三、网络安全等级保护 1、第一级&#xff08;自主保护级&#xff0…

解密!抖音百万粉丝博主三维地图视频都用到了什么GIS数据和技术

引言 在抖音上有许多诸如三维地图科普局、三维地图看世界和三维地图鉴赏等百万粉丝博主靠着三维地图科普城市、景区、人文和地理视频获赞百万&#xff0c;在我们浏览视频时犹如身临其境一般&#xff0c;那么制作这些视频需要什么GIS技术呢&#xff1f;如何利用MapMost技术自己…