C++ 哈希表(unordered_map与unordered_set)

文章目录

  • unordered_map 与 unordered_set
  • 哈希表 (Hash Table)
    • 哈希函数
    • 哈希冲突
    • 模拟实现
    • 封装
  • 补充:unordered_map 与 unordered_set 的使用

unordered_map 与 unordered_set

就和名字一样,这是 map、set 的无序版本(数据遍历出来是无序的),其底层不是红黑树,而是哈希表

● 为什么要设计这两个容器?
答案很简单:效率高

//我们用以下代码比较set和unordered_set的效率(测效率编译器用release版本)
#include <iostream>
#include <set>
#include <map>
#include<unordered_map>
#include<unordered_set>
#include<time.h>
#define N 1000000
int main(){set<int> s;unordered_set<int> us;srand(time(0));vector<int> v;v.reserve(N);for (int i = 0; i < N; i++) {//v.push_back(rand());//有大量重复数据时//v.push_back(rand() + i);//重复数据较少时 -- rand只能生成差不多 30000 个不同的数,通过 +i 减少重复数据v.push_back(i);//有序数据时}int begin1 = clock();for (auto e : v) {s.insert(e);}int end1 = clock();cout << "set插入用时:" << end1 - begin1 << endl;int begin2 = clock();for (auto e : v) {us.insert(e);}int end2 = clock();cout << "unordered_set插入用时:" << end2 - begin2 << endl << endl;int begin3 = clock();for (auto e : v) {s.find(e);}int end3 = clock();cout << "set查找(所有数据)用时:" << end3 - begin3 << endl;int begin4 = clock();for (auto e : v) {us.find(e);}int end4 = clock();cout << "unordered_set查找(所有数据)用时:" << end4 - begin4 << endl << endl;cout << "数据总数:" << N << endl;cout << "不同数据个数:" << s.size() << endl << endl;int begin5 = clock();for (auto e : v) {s.erase(e);}int end5 = clock();cout << "set删除(所有数据)用时:" << end5 - begin5 << endl;int begin6 = clock();for (auto e : v) {us.erase(e);}int end6 = clock();cout << "unordered_set删除(所有数据)用时:" << end6 - begin6 << endl;return 0;	
}

结果:

这是VS2022版本的测试结果
在这里插入图片描述
可以看见,除了有序以及查找所有数据的情况,unordered_set的效率都是比set高的

这是VS2019版本的测试结果:
在这里插入图片描述
nnd,我还以为换了个环境就能得到我想要的结果,按理说根据哈希表的特点无论是2022还是2019应该测出来unordered_set的查找应该比set要快才对,但即便我把数据增多到10000000也仍然是两个0……

也不算是一无所获……至少让我再一次明白了编译器的底层到底是什么是不能用我的常理去理解的……

这是老师的演示的结果(我们就以这个为准):
请添加图片描述

哈希表 (Hash Table)

也称散列表,其思想为让其中存储的 key 与存储位置(哈希地址)建立映射关系,如此其查找速度将会变得相当快

就比如想要统计某文章中26个字母各出现的次数,C语言期间我们可以通过 int arr[26] 数组去记录次数,下标 0 对应 a,25 对应z,如此建立起映射关系

哈希函数

哈希表建立映射关系所依赖的函数,这种转换函数称为 哈希(散列)函数

  1. 直接定址法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
  2. 除留余数法
    %得余数
  3. 其他(之后如果发现有需要再补充)

哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞

解决哈希冲突两种常见的方法是:闭散列和开散列

闭散列
也称开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的 “下一个” 空位置中去。
寻找空位置可以用线性探测(挨个或挨几个地往后找,但这样数据容易堆成一片降低效率)、二次探测(方法如其名)

这地方有人了?我给你往后找个最近的空位置坐

开散列
也称链地址法(开链法),哈希表不存储单个元素而是存储一条单链,各链表的头结点存储在哈希表中(显然每条单链上都是发生冲突的元素)

这根链子上有人挂着了?那你也挂这吧

模拟实现

简单功能的实现倒没什么难的

//实现方式一:闭散列 / 开放地址法 + 除留余数法
//开放定址法(闭散列)
namespace OpenAddress {enum State{EMPTY,EXIST,DELETE};template<class K, class V>struct HashData {pair<K, V> _kv;//一步步来,先认为是给map用的State _state = EMPTY;};template<class K, class V>class HashTable {public:bool Insert(pair<K, V> kv) {//●判断是否需要扩容 -- 依据负载因子(当前数据量/最大数据量 一般 < 0.7 或 0.8)//因为我们一开始没有给_table开空间,所以这里要加个判断if (_size == 0 || _size * 10 - _table.size() * 10 >= 7) {扩容方法一:创建新表(代码有些冗余)//int newsize = (_size == 0 ? 10 : _size * 2);//vector<HashDate<K, V>> newtable;//newtable.resize(newsize);转移旧数据(遍历旧表,按新的映射关系插入到新表中)//for (auto& e : _table) {//	if (e._state == EXIST) {//		int index = (e._kv).first % newsize;//		while (newtable[index]._state == EXIST) {//			index = (index + 1) % newtable.size();//		}//		newtable[index]._kv = e._kv;//	}//}//_table.swap(newtable);//扩容方法二:创建新哈希表,复用Insertint newsize = (_size == 0 ? 10 : _size * 2);HashTable<K, V> newhashtable;newhashtable._table.resize(newsize);for (auto e : _table) {if (e._state == EXIST)newhashtable.Insert(e._kv);}_table.swap(newhashtable._table);}//●插入int index = kv.first % _table.size();//有负载因子在不怕没空间while (_table[index]._state == EXIST) {index = (index + 1) % _table.size();//线性探测(往后一个一个找)}_table[index]._kv = kv;_table[index]._state = EXIST;_size++;return true;}HashData<K, V>* Find(const K& key) {if (_table.size() == 0)return nullptr;int hashi = key % _table.size();int index = hashi;//线性查找:从hashi下标开始往后一个一个找,遇到EMPTY状态结束,如果都找一圈了(全是删除状态)也返回while (_table[index]._state != EMPTY){if (_table[index]._state == EXIST && _table[index]._kv.first == key)return &_table[index];index = (index + 1) % _table.size();// 如果已经查找一圈,那么说明全是存在+删除if (index == hashi)break;}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_size;return true;}elsereturn false;}private:vector<HashData<K, V>> _table;//哈希表(.size()表示)int _size = 0;				   //哈希表中实际存在的元素个数public:};void test_insert() {HashTable<int, int> ht;ht.Insert(make_pair(1, 5));ht.Insert(make_pair(2, 6));ht.Insert(make_pair(3, 7));cout << ht.Find(1)->_kv.second << endl;ht.Erase(1);if (ht.Find(1))cout << "1在" << endl;elsecout << "1不在" << endl;}
}
//哈西桶、链地址法(开散列)(不想手写了,所以借用了老师写的代码)
namespace HashBucket {template<class K,class V>struct HashNode{HashNode<K, V>* _next;pair<K, V> _kv;HashNode(const pair<K, V>& kv):_next(nullptr), _kv(kv){}};template<class K, class V>class HashTable {typedef HashNode<K, V> Node;public://析构函数,闭散列不写是因为vector自己就释放了,而这里不行~HashTable(){for (auto& cur : _tables){while (cur){Node* next = cur->_next;delete cur;cur = next;}cur = nullptr;}}Node* Find(const K& key){if (_tables.size() == 0)return nullptr;size_t hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;// 负载因因子==1时扩容if (_n == _tables.size()){//扩容方法一:按开放定址法的方法二一样Insert(但这里问题在于每次Insert都会开空间)/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size()*2;HashTable<K, V> newht;newht.resize(newsize);for (auto cur : _tables){while (cur){newht.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newht._tables);*///扩容方法二:改变旧表数据指向,转移到新表中size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newtables(newsize, nullptr);//for (Node*& cur : _tables)for (auto& cur : _tables){while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first % newtables.size();// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}size_t hashi = kv.first % _tables.size();// 头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}private:vector<Node*> _tables; // 指针数组size_t _n = 0; // 存储有效数据个数};
}

封装

我们选择用哈希桶去进行封装

封装步骤

  1. 改进哈希表:修改、增加模板参数

首先,我们知道map、set 肯定离不开key-value键值对

● 像Find这样的函数需要根据 key 去进行查找,所以Key的类型是必须知道的
——模板参数 K

● 以上是针对存储pair数据实现的哈希表,而现在你要存储不同数据
—— 模板参数 T 表示

● unordered_map存储pair<int ,char>这种 key-value 键值对,想要获取key则需要从存储的T中取出first;unordered_set存储int之类的类型,存储的T就是key,能直接取到
——获取key的方法不同,需要仿函数 KeyOfT 去指导获取key

● 哈希表里存储数据和位置之间的关系是计算来的,上面存储pair,我们就其中的first去计算,但我们这里默认了first就是整形,如果这是string呢?
——模板参数 HashiFunc ,用于将key转换为整形,用于计算数据在哈希表中映射的位置

  1. 增添哈希表迭代器
    模板参数:const迭代器模板参数三兄弟 T Ref Ptr,此外哈希表的迭代器++操作时是需要用 key 去计算数据当前所处位置的
    —— K、KeyOfT、HashiFunc

还要注意迭代器的成员变量

  1. 其他注意事项
    函数返回值、unordered_set与unordered_map默认支持整形和string类型转换去计算哈希表映射位置、unordered_set与unordered_map的普通迭代器与const迭代器……

Hash.h

#include<vector>
#include<iostream>
using namespace std;
template<class T>
struct HashNode{HashNode<T>* _next;T _data;HashNode(const T& data):_next(nullptr), _data(data){}
};
//因为迭代器里需要哈希表的指针,所以需要前置声明哈希表
template<class K, class T, class KeyOfT, class Hash>
class HashTable;//老师似乎并没有给哈希表设置const迭代器,是因为这么写模板参数太多了吗?
template<class K, class Ref, class Ptr, class T, class KeyOfT, class HashiFunc>
// ++时需要计算当前哈希位置,故需要得知哈希表的大小
//可以整一个函数去访问哈希表的里的_tables,但这里我们试试用友元解决这个问题
struct __HashIterator {typedef __HashIterator<K, Ref, Ptr, T, KeyOfT, HashiFunc> Self;typedef HashNode<T> Node;//! ++计算时肯定需要_table里的元素,因此你需要让迭代器能访问到所处的哈希表typedef HashTable<K, T, KeyOfT, HashiFunc> HT;Node* _pnode;HT* _pht;__HashIterator(Node* pnode, HT* pht)//迭代器的初始化一般就是begin这些函数了,到时候传参数构造就行:_pnode(pnode), _pht(pht){}typedef __HashIterator<K, T&, T*, T, KeyOfT, HashiFunc> Iterator;__HashIterator(const Iterator& it):_pnode(it._pnode),_pht(it._pht){}bool operator!=(const Self& s) {return _pnode != s._pnode;}T& operator*() {return _pnode->_data;}T* operator->() {return &_pnode->_data;//自己的碎碎念:记得之前学过这么写是因为编译器会优化,使用时看似一个->实则两个,但这里写的……真的是两个->而不是一个->加一个*吗}Self& operator++() {if (_pnode->_next != nullptr) {_pnode = _pnode->next;}//去寻找下一个有数据的哈希桶else {KeyOfT kot;size_t hashi = kot(_pnode->data) % _pht->_tables.size();while (hashi < _pht->_tables.size()) {if (_pht->_tables[hashi] == nullptr) {hashi++;}else {_pnode = _pht->_tables[hashi];return *this;}}//后面没数据了_pnode = nullptr;return *this;}}
};
template<class K>
struct hashifunc {size_t operator()(const K& key) {return key;}
};
//类模板特化(string)
template<>
struct hashifunc<string> {size_t operator()(const string& s) {size_t hashi = 0;for (auto ch : s) {hashi += ch;hashi *= 31;//只是将每个字符的ASCII码值相加岂不是换个顺序就哈希冲突了?通过乘一个数解决这个问题(为什么是31?我不到啊)}return hashi;}
};template<class K, class T,class KeyOfT,class HashiFunc>//HashiFunc为仿函数,用于将传入的K转换为整形,以供hashi的计算
class HashTable {template<class K, class Ref, class Ptr, class T, class KeyOfT, class HashiFunc>friend struct __HashIterator;typedef HashNode<T> Node;
public://析构函数,闭散列不写是因为vector自己就释放了,而这里不行~HashTable(){for (auto& cur : _tables){while (cur){Node* next = cur->_next;delete cur;cur = next;}cur = nullptr;}}bool Erase(const K& key){HashiFunc hash;//一定记住仿函数得先实例化才能用啊淦size_t hashi = hash(key) % _tables.size();KeyOfT kot;Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;_n--;return true;}else{prev = cur;cur = cur->_next;}}return false;}typedef __HashIterator<K, K&, K*, T, KeyOfT, HashiFunc> iterator;typedef __HashIterator<K, const K&, const K*, T, KeyOfT, HashiFunc> const_iterator;iterator begin() {//先找到有数据的哈希桶for (auto e : _tables) {if (e) {return iterator(e, this);}}return iterator(nullptr, this);}iterator end() {return iterator(nullptr, this);}const_iterator begin() const{//先找到有数据的哈希桶for (auto e : _tables) {if (e) {return iterator(e, this);}}return const_iterator(nullptr, this);}const_iterator end() const{return const_iterator(nullptr, this);}iterator Find(const K& key){if (_tables.size() == 0)return end();HashiFunc hash;size_t hashi = hash(key) % _tables.size();KeyOfT kot;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key) {return iterator(cur, this);}cur = cur->_next;}return end();}pair<iterator, bool> Insert(const T& data){HashiFunc hash;KeyOfT kot;iterator it = Find(kot(data));if (it != end()){return make_pair(it, false);}// 负载因因子==1时扩容if (_n == _tables.size()){//扩容方法一:按开放定址法的方法二一样Insert(但这里问题在于每次Insert都会开空间)/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size()*2;HashTable<K, V> newht;newht.resize(newsize);for (auto cur : _tables){while (cur){newht.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newht._tables);*///扩容方法二:改变旧表数据指向,转移到新表中size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newtables(newsize, nullptr);//for (Node*& cur : _tables)for (auto& cur : _tables){while (cur){Node* next = cur->_next;size_t hashi = hash(kot(cur->_data)) % newtables.size();//HashFunc用于这种求数据在哈希表映射位置的情况// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}size_t hashi = hash(kot(data)) % _tables.size();// 头插Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return make_pair(iterator(newnode, this), true);}
private:vector<Node*> _tables; // 指针数组size_t _n = 0; // 存储有效数据个数
};

unordered_map.h

#pragma once
template<class K, class V, class HashiFunc = hashifunc<K>>
class unordered_map {
public:struct MapKeyOfT {const K& operator()(const pair<const K,V>& kv) {return kv.first;}};typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, HashiFunc>::iterator iterator;typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, HashiFunc>::const_iterator const_iterator;iterator begin() {return _ht.begin();}iterator end() {return _ht.end();}const_iterator begin() const{return _ht.begin();}const_iterator end() const{return _ht.end();}pair<iterator, bool> Insert(const pair<const K,V>& kv) {return _ht.Insert(kv);}bool Erase(const K& key) {return _ht.Erase(key);}iterator Find(const K& key) {return _ht.Find(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;}
private:HashTable<K, pair<const K, V>, MapKeyOfT, HashiFunc> _ht;
};
void test_unordered_map() {unordered_map<string, int> um;um.Insert(make_pair("好耶", 4));um.Insert(make_pair("不好", 5));um.Insert(make_pair("我去", 6));um.Erase("好耶");if (um.Find("好耶") != um.end()) cout << "存在" << endl;else cout << "不存在" << endl;cout << (*um.Find("我去")).second << endl;
}

unordered_set.h

#pragma once
template<class K, class HashiFunc = hashifunc<K>>
class unordered_set {
public:struct MapKeyOfT {const K& operator()(const K& key) {return key;}};//typedef typename HashTable<K, const K, MapKeyOfT, HashiFunc>::iterator iterator;//这么写要搭配HashTable<K, const K, MapKeyOfT, HashiFunc> _ht;食用typedef typename HashTable<K, K, MapKeyOfT, HashiFunc>::const_iterator iterator;typedef typename HashTable<K, K, MapKeyOfT, HashiFunc>::const_iterator const_iterator;iterator begin() {return _ht.begin();//和封装set时遇到的问题一样}iterator end() {return _ht.end();}const_iterator begin() const{return _ht.begin();}const_iterator end() const{return _ht.end();}pair<iterator, bool> Insert(const K& key) {return _ht.Insert(key);}bool Erase(const K& key) {return _ht.Erase(key);}iterator Find(const K& key) {return _ht.Find(key);}
private:HashTable<K, K, MapKeyOfT, HashiFunc> _ht;
};
void test_unordered_set() {unordered_set<int> um;um.Insert(1);um.Insert(3);um.Insert(4);um.Erase(1);if (um.Find(1) != um.end()) cout << "存在" << endl;else cout << "不存在" << endl;cout << *um.Find(3) << endl;
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Hash.h"
#include"unordered_map.h"
#include"unordered_set.h"
#include<string>
int main() {test_unordered_map();test_unordered_set();return 0;
}

补充:unordered_map 与 unordered_set 的使用

● 查找或是使用[]向unordered_map 中插入 key,如果成功了返回相应位置的迭代器,那失败了该如何确定呢?
就像我们上面实现的一样,失败了返回end()

● 想要查看某数据是否存在且不用 find 函数?

size_type count ( const key_type& k ) const;//有则返回 1,无则返回 0

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

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

相关文章

机器学习 - 代价函数

场景 上次简单学习了支持向量机的概念。概念如下&#xff1a; 支持向量机&#xff08;SVM&#xff09;&#xff1a;SVM是一种监督学习算法&#xff0c;常用于分类问题。它的目标是找到一个超平面&#xff08;在二维空间中是一条线&#xff0c;在更高维空间中是一个面&#xf…

把网页打包成app(简单) 2024

文章目录 **01-准备好要打包的网页文件&#xff0c;一般包含HTML-CSS-JS-静态资源文件&#xff1a;****02-下载HBuilderX&#xff0c;注册一个账号-必须****注****册账号****(免费&#xff09;****03-新建一个H5项目&#xff1a;****04-然后把以下红框里面的这些文件都删掉&…

2024年【T电梯修理】报名考试及T电梯修理考试报名

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 T电梯修理报名考试考前必练&#xff01;安全生产模拟考试一点通每个月更新T电梯修理考试报名题目及答案&#xff01;多做几遍&#xff0c;其实通过T电梯修理复审考试很简单。 1、【多选题】TSGT7001-2009《检规(简称)…

C语言内存函数:memcpy、memcat、memmove介绍和模拟实现(实用性高,建议三连收藏)

目录 1.memcpy函数 1.1函数介绍 1.2函数示范使用 1.3函数的模拟实现 1.4补充 2.memmove函数 2.1函数介绍 2.2函数的使用示范 2.3函数的模拟实现 3.memcmp(内存比较函数&#xff09; 3.1函数介绍 3.2函数的示范使用&#xff0c;有趣的例子 4.函数补充memset(内存…

Pandas教程11:关于pd.DataFrame.shift(1)数据下移的示例用法

---------------pandas数据分析集合--------------- Python教程71&#xff1a;学习Pandas中一维数组Series Python教程74&#xff1a;Pandas中DataFrame数据创建方法及缺失值与重复值处理 Pandas数据化分析&#xff0c;DataFrame行列索引数据的选取&#xff0c;增加&#xff0c…

【RT-DETR有效改进】UNetv2提出的一种SDI多层次特征融合模块(细节高效涨点)

👑欢迎大家订阅本专栏,一起学习RT-DETR👑 一、本文介绍 本问给大家带来的改进机制是UNetv2提出的一种多层次特征融合模块(SDI)其是一种用于替换Concat操作的模块,SDI模块的主要思想是通过整合编码器生成的层级特征图来增强图像中的语义信息和细节信息。包括皮肤…

数据库管理-第144期 深入使用EMCC-01(20240204)

数据库管理144期 2024-02-04 数据库管理-第144期 深入使用EMCC-01&#xff08;20240204&#xff09;1 用户管理2 配置告警动作3 配置意外事件规则总结 数据库管理-第144期 深入使用EMCC-01&#xff08;20240204&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xff08;尹海文&am…

redis(6)

文章目录 一、redis clusterRedis Cluster 工作原理Redis cluster 基本架构Redis cluster主从架构Redis Cluster 部署架构说明部署方式介绍 原生命令手动部署原生命令实战案例&#xff1a;利用原生命令手动部署redis cluster 实战案例&#xff1a;基于Redis 5 的redis cluster部…

Matplotlib绘制炫酷柱状图的艺术与技巧【第60篇—python:Matplotlib绘制柱状图】

文章目录 Matplotlib绘制炫酷柱状图的艺术与技巧1. 簇状柱状图2. 堆积柱状图3. 横向柱状图4. 百分比柱状图5. 3D柱状图6. 堆积横向柱状图7. 多系列百分比柱状图8. 3D堆积柱状图9. 带有误差线的柱状图10. 分组百分比柱状图11. 水平堆积柱状图12. 多面板柱状图13. 自定义颜色和样…

2024.2.5日总结(小程序开发2)

小程序的宿主环境 宿主环境 宿主环境指的是程序运行所必须的依赖环境。 Android系统和iOS系统是两个不同的宿主环境。安卓版的微信App不能再iOS环境下运行。Android是安卓软件的宿主环境&#xff0c;脱离了宿主环境的软件是没有意义的。 小程序的宿主环境 手机微信是小程序…

vue全家桶之状态管理Pinia

一、Pinia和Vuex的对比 1.什么是Pinia呢&#xff1f; Pinia&#xff08;发音为/piːnjʌ/&#xff0c;如英语中的“peenya”&#xff09;是最接近pia&#xff08;西班牙语中的菠萝&#xff09;的词&#xff1b; Pinia开始于大概2019年&#xff0c;最初是作为一个实验为Vue重新…

【ArcGIS微课1000例】0102:面状要素空洞填充

文章目录 一、实验描述二、实验数据三、实验步骤1. 手动补全空洞2. 批量补全空洞四、注意事项一、实验描述 在对地理数据进行编辑时,时常会遇到面数据中存在个别或大量的空洞,考虑实际情况中空洞的数量多少、分布情况,填充空洞区域可以采用逐个填充的方式,也可以采用快速大…