【C++修炼之路】18.map和set

news2024/11/13 14:51:32

在这里插入图片描述
每一个不曾起舞的日子都是对生命的辜负

map和set

  • map和set
  • 一.关联式容器
  • 二.set
    • 2.1 set的介绍
    • 2.2 set的使用
      • 1.set的模板参数列表
      • 2.set的构造
      • 3.set的迭代器
      • 4.set修改操作
      • 5.bound函数
  • 三.multiset
  • 四.map
    • 3.1 map的介绍
    • 3.2 map的使用
      • 1.map的模板参数说明
      • 2.pair的介绍
      • 3.map的[]重载
  • 五.multimap

map和set

本节目标:

    1. 关联式容器
    1. 键值对
    1. 树形结构的关联式容器

一.关联式容器

在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?

关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。

键值对:

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

树形结构的关联式容器:

根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。**树型结构的关联式容器主要有四种:map、set、multimap、multiset。**这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。

二.set

2.1 set的介绍

set的底层为二叉搜索树,也就是上一节中所实现的,不过在此之上,set作为容器来说,封装了和以前的vector、list、deque相关的迭代器、运算符重载等,方便我们去进行一系列的操作。

仍然是需要看文档:

set - C++ Reference (cplusplus.com)

可以发现,这与之前我们所学的容器的方法几乎差不多。只不过有了些许特征:

  1. set是按照一定次序存储元素的容器
  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
  3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
  4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
  5. set在底层是用二叉搜索树(红黑树)实现的。
  6. set没有[]重载。

注意:

  1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
  2. set中插入元素时,只需要插入value即可,不需要构造键值对。
  3. et中的元素不可以重复(二叉搜索树中重复则不会插入,并且返回false因此可以使用set进行去重)。
  4. 使用set的迭代器遍历set中的元素,可以得到有序序列
  5. set中的元素默认按照小于来比较
  6. set中查找某个元素,时间复杂度为: l o g 2 n log_2 n log2n (实际上是二叉搜索树的高度次)
  7. set中的元素不允许修改(为什么?)
  8. set中的底层使用二叉搜索树(红黑树)来实现。

2.2 set的使用

头文件当然是:#include<set>

1.set的模板参数列表

image-20230209173116553

T: set中存放元素的类型,实际在底层存储<value, value>的键值对。
**Compare:**set中元素默认按照小于来比较
**Alloc:**set中元素空间的管理方式,使用STL提供的空间配置器管理

对于Compare来说,利用仿函数默认小于,就是默认按照升序去处理的,即上一讲中的二叉搜索树中序从小到大。

2.set的构造

函数声明功能介绍
set (const Compare& comp = Compare(), const Allocator&= Allocator() );构造空的set
set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& =Allocator() );用[first, last)区间中的元素构造set
set ( const set<Key,Compare,Allocator>& x);set的拷贝构造

3.set的迭代器

对于set的迭代器,和之前的vector、list相同,有正向迭代器、反向迭代器、const迭代器。名称都是一样的。

容量的函数也是如此,就不一一展示了,看上面文档就行,如果知道vector、list的容量函数,也可以不看。

4.set修改操作

pair<iterator,bool> insert (const value_type& x )在set中插入元素x,实际插入的是<x, x>构成的键值对,如果插入成功,返回<该元素在set中的位置,true>,如果插入失败,说明x在set中已经存在,返回<x在set中的位置,false>
void erase ( iterator position )删除set中position位置上的元素
size_type erase ( constkey_type& x )删除set中值为x的元素,返回删除的元素的个数
void erase ( iterator first,iterator last )删除set中[first, last)区间中的元素
void swap (set<Key,Compare,Allocator>&st );交换set中的元素,实际上只需交换搜索树根节点的指针
iterator find ( constkey_type& x ) const返回set中值为x的元素的位置
size_type count ( constkey_type& x ) const返回set中值为x的元素的个数

对于第一个pair,实际上就是KV模型,下面会展示。erase的重载也没什么,交换函数值得一提的是由于底层是二叉搜索树,因此swap只需交换根节点的指针。对于find,count, 由于set是去重的元素,因此上面提到的返回位置以及元素个数并不需要在意,因为每一个元素都是唯一的。而也有不唯一的容器:multiset,multiset只排序不去重,下面讲这个的时候会知道返回位置是中序遍历的第一个。

演示1:

#include<iostream>
#include<set>//底层是二叉搜索树:自动进行排序去重
using namespace std;

void test_set1()
{
	set<int> s;//排序+去重
	s.insert(3);
	s.insert(1);
	s.insert(4);
	s.insert(7);
	s.insert(2);
	s.insert(1);

	set<int>::iterator it = s.begin();
	//auto it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : s)//范围for底层就是迭代器
	{
		cout << e << " ";
	}
	cout << endl;

	auto pos = s.find(3); //这个更快,因为借用搜索树的特性
	//auto pos = find(s.begin(), s.end(), 3);//底层实现不一样,这个是暴力查找
	if (pos != s.end())
	{
		s.erase(pos);
	}
	cout << s.erase(1) << endl;
	cout << s.erase(3) << endl;
	s.erase(1);
	s.erase(1);
	for (auto e : s)//范围for底层就是迭代器
	{
		cout << e << " ";
	}
	cout << endl;
}
int main()
{
	test_set1();
	return 0;
}

image-20230209181206177

演示2:


5.bound函数

在文档中发现,有两个函数:lower_boundupper_bound,这两个实际上也是迭代器类型,传入的只有一个参数,同样是左闭右开,传值之后可以标记对应的位置:

int main()
{
	set<int> myset;
	set<int>::iterator itlow, itup;
	for (int i = 1; i < 10; i++)
	{
		myset.insert(i * 10);//10 20 30 40 50 60 70 80 90 
	}
	itlow = myset.lower_bound(35);
	itup = myset.upper_bound(70);

	myset.erase(itlow, itup);
	for (set<int>::iterator it = myset.begin(); it != myset.end(); it++)
	{
		cout << " " << *it;
	}
	cout << endl;
	return 0;
}

image-20230209174746490

可见,对于erase也有迭代器为参数的重载函数,此外,通过bound类型的迭代器可以保存位置。即便没有该元素,同样可以标记,因为元素是有序。

如果把70变成75,itup同样会得到80的位置,所以upper_bound返回的是大于该位置的边界,可以对标end(),而lower_bound对标begin,返回的是大于等于的边界。这就非常方便获得一个左闭右开的范围。但这个东西实际上用的不多。

三.multiset

与set不同的是,multiset虽然会排序,但并不会进行去重,因此是由重复值的存在的。底层仍然是二叉搜索树。

#include<iostream>
#include<set>
#include<string>
using namespace std;
void test_set2()
{
	multiset<int> s;//单纯排序,没有去重
	s.insert(3);
	s.insert(1);
	s.insert(4);
	s.insert(7);
	s.insert(2);
	s.insert(1);
	s.insert(1);
	s.insert(1);
	s.insert(3);
	cout << s.count(1) << endl;//1的数量
	for (auto e : s)//范围for底层就是迭代器
	{
		cout << e << " ";
	}
	cout << endl;
}
int main()
{
	test_set2();

	return 0;
}

image-20230209211519444

需要注意的是查找的返回值是中序遍历的第一个。

四.map

3.1 map的介绍

map的底层当然也是二叉搜索树,但就好比set是K模型一样,map就是上篇二叉搜索树提到的KV模型,即一一对应的方式。

文档说明:

www.cplusplus.com

  1. . map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
  2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair: typedef pair<const key, T value_type>;
  3. 在内部,map中的元素总是按照键值key进行比较排序的
  4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
  5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
  6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

3.2 map的使用

头文件当然是:#include<map>

1.map的模板参数说明

image-20230209182014571

key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
**Alloc:**通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器

而对于map的构造,迭代器的一些,与之前的容器使用方式相同,就不展示了,需要可以自行查看文档。

2.pair的介绍

我们需要知道pair:

image-20230209182555053

first就是Key,second是Value。就是KV模型的结构。在搜索树定义的时候,我们是直接定义了两个变量类型:K,V。那为什么需要pair这个东西将两个值放在一起呢?实际上是为了map能够更方便的操作,举个例子,对于map,如果是迭代器访问,返回的时候不可能返回两个参数,这时候以pair为参数的函数就派上用场了,直接返回pair类型就好了。

image-20230209183231464

但pair类型也不支持流插入,所以就需要一个一个的访问

int main()
{
	map<string, string> dict;
	dict.insert(pair<string, string>("排序", "sort"));//匿名对象
	dict.insert(pair<string, string>("左边", "left"));
	dict.insert(pair<string, string>("右边", "right"));
	dict.insert(make_pair("字符串", "string"));//make_pair是一个函数模板

	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();//这样方便
	while (it != dict.end())
	{
		//cout << *it << endl;//pair不支持流插入,因此这样不行
		//cout << (*it).first << ":" << (*it).second << endl; //这样可以
		cout << it->first << ":" << it->second << endl;//返回的是数据的地址/指针
		++it;
	}
	cout << endl;
	for (const auto& kv : dict)//注意加引用,不加就是拷贝构造,代价大
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	return 0;
}

image-20230209192002746

3.map的[]重载

如果想通过map统计水果出现的次数,可以这样:

int main()
{
	//map统计水果操作的次数
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "西瓜",
		"苹果", "苹果","西瓜","苹果",  "香蕉", "苹果", "香蕉" };

	map<string, int> countMap;
	for (auto& e : arr)
	{
		auto it = countMap.find(e);
		if (it == countMap.end())//没有就插入
		{
			countMap.insert(make_pair(e, 1));
		}
		else
		{
			it->second++;
		}
	}

	for (const auto& kv : countMap)//注意加引用,不给就是拷贝构造,代价大
	{
		cout << kv.first << ":" << kv.second << endl;
	}

	return 0;
}

image-20230209194401586

但这样看起来麻烦很多,实际上可以通过这种方式:

int main()
{
	
	//map统计水果操作的次数
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "西瓜",
		"苹果", "苹果","西瓜","苹果",  "香蕉", "苹果", "香蕉" };

	map<string, int> countMap;
	for (auto& e : arr)
	{
		countMap[e]++;
	}
	for (const auto& kv : countMap)//注意加引用,不加就是拷贝构造,代价大
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	return 0;
}

image-20230209195234968

方括号支持的是随机访问,而对于这个括号来说,括号里的实际上是key,countMap[e]就是value。

image-20230209202858039

上面所标注的就是map[]重载的返回值,发现还有insert的操作,所以我们应该先知道insert的返回值:image-20230209202446853

发现insert返回类型为pair<iterator, bool>类型,看看文档是如何说明的:image-20230209202558711

因此,我们可以将最上面的map重载的返回值进行解释了,可以转化成这样了:

V& operator[](const K& k)
{
    pair<iterator, bool> ret = insert(make_pair(k, V()));
    return ret.first->second;
}

这样,我们就知道,operator返回的就是map<string, int>的第二个参数,并且是引用返回,所以map的映射关系就是这样的。

可以看出,C++map的[]有很多功能:

  1. 插入
  2. 修改
  3. 查找

因此,了解底层之后我们也可以直接这样写:

int main()
{
	map<string, string> dict;
	dict.insert(pair<string, string>("排序", "sort"));//匿名对象
	dict.insert(pair<string, string>("左边", "left"));
	dict.insert(pair<string, string>("右边", "right"));
	dict.insert(make_pair("字符串", "string"));//make_pair是一个函数模板
	dict["迭代器"] = "iterator"; // 插入+修改
	dict["insert"];//单纯的插入,默认构造的va为""
	//dict.insert(pair<string, string>("左边", "XXX"));//这样会插入失败,因为已经有了"左边"
	dict["insert"] = "插入";//修改
	cout << dict["左边"] << endl;// 查找

	//key在就是查找,不在就是插入
    return 0;
}

image-20230209205833862

image-20230209205905366

因此,用[]代替insert很方便,而且对于已有的key,如果想再插入相同的key,无论对应的value是否相同,都不能成功,因为pair<iterator, bool>对应的bool会返回false。如果想修改指定key的value,就用[]。

对于词典来说,由于一词多义,key和value也可能不是一一映射的关系,有可能是一个key对应多个value,这个时候,可以这样map<string, vector<string>>,除此之外,也可以利用multimap,下面将会提到。

再次说明:map中用pair保存值。

五.multimap

和multset一样,multimap与map相比也是不会进行去重,只是有序。

那继续看一下,用multimap完成上面说的”一词多义“:

#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
	multimap<string, string> dict;
	dict.insert(pair<string, string>("left", "左边"));
	dict.insert(pair<string, string>("left", "剩余"));
	dict.insert(pair<string, string>("string", "字符串"));
	dict.insert(pair<string, string>("left", "xxx"));
	for (const auto& kv : dict)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	return 0;
}

image-20230209214342546

multimap是不支持[]重载的,文档里面也没有:

multimap - C++ Reference (cplusplus.com)

但multimap同样可以统计次数,因为下面的方式是先find,再加入或者值++。

#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "西瓜",
		"苹果", "苹果","西瓜","苹果",  "香蕉", "苹果", "香蕉" };

	multimap<string, int> countMap;
	for (auto& e : arr)
	{
		auto it = countMap.find(e);
		if (it == countMap.end())//没有就插入
		{
			countMap.insert(make_pair(e, 1));
		}
		else
		{
			it->second++;
		}
	}

	for (const auto& kv : countMap)//注意加引用,不给就是拷贝构造,代价大
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	return 0;
}

image-20230209215950465

这样也可以统计次数。

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

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

相关文章

如何构建微服务架构?

相信很多人对微服务架构都会产生这样一些疑问&#xff0c;例如我要何时使用微服务架构?又如何将应用程序分解为微服务?分解后&#xff0c;要如何去搭建微服务架构?同时&#xff0c;在微服务架构中&#xff0c;因为会涉及到多个组件&#xff0c;那么这些组件又可以使用什么技…

[软件工程导论(第六版)]第9章 面向对象方法学引论(复习笔记)

文章目录9.1 面向对象方法学概述要点9.2 面向对象的概念对象9.3 面向对象建模9.4 对象模型9.5 动态模型9.6 功能模型9.7 3种模型之间的关系9.1 面向对象方法学概述要点 面向对象方法学的出发点和基本原则&#xff0c;是尽可能模拟人类习惯的思维方式&#xff0c;使开发软件的方…

CS144-Lab3

概述 在实验0中&#xff0c;你实现了流控制字节流&#xff08;ByteStream&#xff09;的抽象。 在实验1和2中&#xff0c;你实现了将不可靠数据报中的段转换为传入字节流的工具&#xff1a;StreamReassembler和TCPReceiver。 现在&#xff0c;在实验3中&#xff0c;你将实现…

【STM32笔记】低功耗模式配置及避坑汇总

【STM32笔记】低功耗模式配置及配置汇总 文章总结&#xff1a;&#xff08;后续更新以相关文章为准&#xff09; 【STM32笔记】__WFI()&#xff1b;进入不了休眠的可能原因 【STM32笔记】HAL库低功耗模式配置&#xff08;ADC唤醒无法使用、低功耗模式无法烧录解决方案&#x…

kanban系统wekan安装

看板类开源项目排名第一的wekan项目安装比较友好的leantime 下载windows 版本 wekan 进入官网 https://wekan.github.io/ , 留意最新版依赖的技术栈, 比如 WeKan v6.74 依赖的是 Meteor 2.10.0, Node.js 14.21.2, MongoDB 6.0.4。 点击 Offline Window LAN 链接进入 github wik…

「7」线性代数(期末复习)

&#x1f680;&#x1f680;&#x1f680;大家觉不错的话&#xff0c;就恳求大家点点关注&#xff0c;点点小爱心&#xff0c;指点指点&#x1f680;&#x1f680;&#x1f680; 目录 第五章 相似矩阵及二次型 &4&#xff09;对称阵的对角化 &5二次型及其标准型 …

【C++】类与对象理解和学习(中)

专栏放在【C知识总结】&#xff0c;会持续更新&#xff0c;期待支持&#x1f339;六大默认成员函数前言每个类中都含有六大默认成员函数&#xff0c;也就是说&#xff0c;即使这个类是个空类&#xff0c;里面什么都没有写&#xff0c;但是编译器依然会自动生成六个默认成员函数…

【SSD 代码精读】之 model (Backbone) loss

model1、Backbone1&#xff09;ResNet-502&#xff09;截取 ResNet-50 的前半部分作为 backbone2、Module3、Loss Function1&#xff09;location loss2&#xff09;confidence loss3&#xff09;整体 loss4&#xff09;loss 代码1、Backbone 这里介绍使用 ResNet-50 作为 bac…

思维经验 | ​如何刻意练习提升用户思维?

小飞象交流会哪里有什么捷径&#xff0c;那些个“一步登天”的哪个不是在前面就打好了“地基”的。内部交流│20期思维经验分享如何刻意练习提升用户思维&#xff1f;data analysis●●●●分享人&#xff1a;大江老师‍数据部门和运营部门做了大量的用户标签和用户分层工作。为…

基于GIS的地下水脆弱性评价

&#xff08;一&#xff09;行政边界数据、土地利用数据和土壤类型数据 本文所用到的河北唐山行政边界数据、土地利用数据和土壤类型数据均来源于中国科学院资源环境科学与数据中心&#xff08;https://www.resdc.cn/Default.aspx&#xff09;。 &#xff08;二&#xff09;地…

小孩扁桃体肿大3度能自愈吗?6岁小孩扁桃体肥大怎么治效果好?

12月7日&#xff0c;四川眉山市民唐先生说&#xff0c;他刚出生的儿子在妇产医院分娩中心住了20天后感染了败血症。据唐先生介绍&#xff0c;哈子出院时各项指标正常。他在分娩中心住了半个月左右&#xff0c;孩子喝牛奶很生气&#xff0c;第二天就开始发烧了。同一天&#xff…

新版bing(集成ChatGPT)申请通过后在谷歌浏览器(Chrome)上的使用方法

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,科大讯飞比赛第三名,CCF比赛第四名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

Flink-运行时架构介绍

文章目录Flink 运行时架构系统架构整体架构作业管理器&#xff08;JobManager&#xff09;任务管理器&#xff08;TaskManager&#xff09;作业提交流程高层级抽象视角独立模式&#xff08;Standalone&#xff09;YARN 集群数据流图&#xff08;Dataflow Graph&#xff09;并行…

用户画像——如何构建用户画像系统

为什么需要用户画像 如果你是用户,当你使用抖音、今日头条的时候,如果平台推荐给你的内容都是你感兴趣的,能够为你节省大量搜索内容的时间。 如果你是商家,当你投放广告的时候,如果平台推送的用户都是你的潜在买家,能够使你花更少的钱,带来更大的收益。 这两者背后都…

Linux内核驱动之efi-rtc

Linux内核驱动之efi-rtc1. UEFI与BIOS概述1.1. BIOS 概述1.1.1. BIOS缺点&#xff1a;1.1.2. BIOS的启动流程1.2 UEFI 概述1.2.1 Boot Sevices&#xff1a;1.2.2. Runtime Service&#xff1a;1.2.3. UEFI优点&#xff1a;1.2.4. UEFI启动过程&#xff1a;1.3 Legacy和UEFI1.4 …

【第31天】SQL进阶-写优化- 插入优化(SQL 小虚竹)

回城传送–》《31天SQL筑基》 文章目录零、前言一、练习题目二、SQL思路&#xff1a;SQL进阶-写优化-插入优化解法插入优化禁用索引语法如下适用数据库引擎非空表&#xff1a;禁用索引禁用唯一性检查语法如下适用数据库引擎禁用外键检查语法如下适用数据库引擎批量插入数据语法…

W806(一)模拟IIC驱动0.96OLED[移植]

前言平头哥内核的国产开发板&#xff0c;资源丰富&#xff0c;按照官方的描述是能够吊打STM32F103C8T6的&#xff0c;22年刚发布的时候就买了&#xff0c;但是当时忙于考研&#xff0c;而且开发板的SDK不够完善&#xff0c;所以23年来填一下坑&#xff0c;今年我在官方群里找到…

ChatGPT原理与技术演进剖析

—— 要抓住一个风口&#xff0c;你得先了解这个风口的内核究竟是什么。本文作者&#xff1a;黄佳 &#xff08;著有《零基础学机器学习》《数据分析咖哥十话》&#xff09; ChatGPT相关文章已经铺天盖地&#xff0c;剖析&#xff08;现阶段或者只能说揣测&#xff09;其底层原…

为啥程序会有bug?

这是一篇半娱乐性的吐槽文章&#xff0c;权当给广大技术人员解解闷&#xff1a;&#xff09;。哈哈哈&#xff0c;然后我要开始讲一个经常在发生的事实了。&#xff08;程序员们可能会感到一些不适&#xff09;99.999999999%做技术的都会被问到或者被吐槽到&#xff1a;“你的程…

PPT和回放来了 | 中国PostgreSQL数据库生态大会圆满落幕

2月17-19日&#xff0c;中国PostgreSQL数据库生态大会在北京中科院软件所和CSDN平台以线下线上结合方式盛大召开&#xff01;本届大会由中国开源软件推进联盟PostgreSQL分会主办。作为自2017年后我们举办的第六届年度行业大会&#xff0c;延续了传播技术&#xff0c;发展产业生…