【C++】数据结构的恶龙set和map来了~(上)

简介: 【C++】数据结构的恶龙set和map来了~(上)

前言



1.关联式容器


在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、

forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?

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


2.键值对


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


3.树形结构的关联式容器


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


一、set的介绍



1. set是按照一定次序存储元素的容器

2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。

3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。

4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。

5. set在底层是用二叉搜索树(红黑树)实现的。


set的注意事项:


1. 与map/multimap不同,map/multimap中存储的是真正的键值对,set中只放value,但在底层实际存放的是由构成的键值对。

2. set中插入元素时,只需要插入value即可,不需要构造键值对。

3. set中的元素不可以重复(因此可以使用set进行去重)。

4. 使用set的迭代器遍历set中的元素,可以得到有序序列

5. set中的元素默认按照小于来比较。

6. set中查找某个元素,时间复杂度为:O(n)

7. set中的元素不允许修改(为什么?)

8. set中的底层使用二叉搜索树(红黑树)来实现


下面我们来使用一下set:

void test_set1()
{
  set<int> st;
  st.insert(1);
  st.insert(2);
  st.insert(3);
  st.insert(1);
  st.insert(3);
  st.insert(4);
  set<int>::iterator it = st.begin();
  while (it != st.end())
  {
  cout << *it << " ";
  ++it;
  }
}
int main()
{
  test_set1();
  return 0;
}


set的使用与我们之前学的容器相差不大,大家只需要记住set的作用是排序+去重,当然set既然支持迭代器那么也肯定支持范围for了。


2d0877809fbd42aebd5fe6630ee11714.png


在这里我们要注意,set是不支持修改里面的值的,因为一旦修改了就不能保持搜索二叉树的特性了。下面我们演示一下set中的find和count接口:


void test_set2()
{
  set<int> st;
  st.insert(1);
  st.insert(2);
  st.insert(3);
  st.insert(1);
  st.insert(3);
  st.insert(4);
  auto it = st.find(4);
  if (it != st.end())
  {
  cout << "要查询的值存在:" << *it << endl;
  }
  else
  {
  cout << "不存在" << endl;
  }
}
int main()
{
  //test_set1();
  test_set2();
  return 0;
}
void test_set2()
{
  set<int> st;
  st.insert(1);
  st.insert(2);
  st.insert(3);
  st.insert(1);
  st.insert(3);
  st.insert(4);
  if (st.count(4))
  {
  cout << "要查询的值存在" << endl;
  }
  else
  {
  cout << "不存在" << endl;
  }
}


在set中我们确认一个值用count接口会比find更方便,因为如果存在那么count一定是1,如果不存在则是0.count这个接口是为了和multiset中的count对应。


下面我们来看看multiset:


void test_set3()
{
  multiset<int> st;
  st.insert(1);
  st.insert(2);
  st.insert(3);
  st.insert(1);
  st.insert(3);
  st.insert(4);
  for (auto& e : st)
  {
  cout << e << " ";
  }
}


fbaafaf43dc74720af43b3577a07b28a.png


通过结果我们可以看到multiset的功能仅仅是排序,并不会去重,下面我们看看刚刚说到的multiset中的count:


void test_set3()
{
  multiset<int> st;
  st.insert(1);
  st.insert(2);
  st.insert(3);
  st.insert(1);
  st.insert(3);
  st.insert(4);
  for (auto& e : st)
  {
  auto it = st.find(e);
  if (it != st.end())
  {
    cout << e << "的个数为:" << st.count(e) << endl;
  }
  else
  {
    cout << "找不到" << endl;
  }
  }
}

78368d60a0e64e339d0b6d80c94068eb.png


我们可以看到在multiset中count可以准确的计算元素的数量,下面我们看看find接口:


void test_set3()
{
  multiset<int> st;
  st.insert(1);
  st.insert(1);
  st.insert(1);
  st.insert(1);
  st.insert(1);
  st.insert(1);
  auto it = st.find(1);
  while (it != st.end()&&*it == 1)
  {
  cout << *it << " ";
  ++it;
  }
}


c679a823907f47eeadbecc9001172f3a.png


对于find接口来讲,查找到的元素一定是中序遍历的第一个元素,如果不是这样的原则我们上面的打印是不会将所有1打印出来的。


set的其他接口我们就不再演示了,因为与其他STL容易相差并不大,只需要记住几个有差异的接口就可以了。


二、map的介绍



1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。

2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型

value_type绑定在一起,为其取别名称为pair:

typedef pair value_type;

3. 在内部,map中的元素总是按照键值key进行比较排序的。

4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序

对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。

5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。

6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))

bb1f18b5d2b94878a084e0c05ade4d5d.png

我们前面已经说过了,map里面存的是键值对,下面我们演示一下map的使用:


void test_map1()
{
  map<string, string> mp;
  mp.insert(make_pair("string", "字符串"));
  mp.insert(make_pair("left", "左边"));
  mp.insert(make_pair("right", "右边"));
  mp.insert(make_pair("erase", "删除"));
  mp.insert(make_pair("string", "字符串2"));
  auto it = mp.begin();
  while (it != mp.end())
  {
  cout << (*it).first << ":" << (*it).second << endl;
  ++it;
  }
}


41afb17b69104296b4e908e7955d4466.png


我们可以看到map同样是排序+去重,当然如果看不懂make_pair是什么那么我们写的再原始一点:


eb9042822fe546d9b1f1f6bf7d6bfe43.png


如上图所示绿色的是原始的pair插入,对于打印为什么是first和second呢?因为pair这个结构里存的就是K,V键值对,并且first代表key,second代表value.当然还记得我们在写反向迭代器的时候重载的那个->符号吗,在这里也有哦:


af4af66ac6484653b09c90e4ae8cb89e.png


在这里->本来要用两次,因为首先访问到结构体,其次再访问到结构体里的first或者second,但是现在一个->就搞定了因为重载了->符号。下面我们讲一下在map中最重要的[]符号:


void test_map2()
{
  map<string, int> mp;
  string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", 
  "苹果", "香蕉" };
  for (auto& e : arr)
  {
  auto it = mp.find(e);
  if (it == mp.end())
  {
    mp.insert(make_pair(e, 1));
  }
  else
  {
    it->second++;
  }
  }
  for (auto& m : mp)
  {
  cout << m.first << ":" << m.second << endl;
  }
}


34e230fb1cf5432d9b6b1127fb4211f7.png


还记得我们学二叉搜索树的时候用的水果的例子吗?当我们树中已经存在这个水果了我们就让水果的count++,如果不存在这个水果我们就让插入并且count值为1,如今有了方括号我们1行代码就能搞定:


void test_map2()
{
  map<string, int> mp;
  string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", 
  "苹果", "香蕉" };
  for (auto& e : arr)
  {
  mp[e]++;
  }
  for (auto& m : mp)
  {
  cout << m.first << ":" << m.second << endl;
  }
}


beddc1ebf81c41b783150c8431649280.png

是不是很神奇呢?下面我们来重点介绍一个[]运算符:

4a5088a49437459daf0ed30c5c57367d.png


从文档中我们可以知道[]有多个作用,首先我们要看懂[]的返回值,如果要插入的值在当前树中不存在我们就插入,如果存在(如何判断是否存在是因为插入有一个bool值)就返回second的值(准确来说是引用),这也就解释了我们刚刚那个代码,因为当要插入的水果不存在的时候就直接帮我们插入了,并且插入后返回了此种水果的次数,然后我们后置++让这个水果变成了1次,后面如果发现这种水果已经被插入了就不在插入了但是返回值返回的是水果的次数的引用所以我们会再次++让次数变成2,然后重复。也就是说[]有四个作用:1.插入  2.修改  3.插入+修改  4.查找


void test_map1()
{
  map<string, string> mp;
  mp.insert(make_pair("string", "字符串"));
  mp.insert(make_pair("right", "右边"));
  mp.insert(make_pair("erase", "删除"));
  mp["left"];   //仅仅插入
  mp["second"] = "第二";   //插入加修改 因为返回值是second,我们将默认的second修改为"第二"
  mp["string"] = "字符串2";   //修改
  cout << mp["string"] << endl;  //查找
  auto it = mp.begin();
  while (it != mp.end())
  {
  cout << it->first << ":" << it->second << endl;
  ++it;
  }
}

adf28cbdf3df423b9d1a6ae57d528852.png


如果我们仅仅想要用[]插入,这个时候插入的是key模型,value会使用缺省值,比如我们方面的插入left,最后打印发现left的value值为空,这就可以验证当value的类型为字符串时缺省值为空字符串,当我们想要插入加修改时,因为[]的返回值为value的引用,所以相当于我们把缺省值修改为自己想要的value值,如果仅仅要对某个key的value值修改我们可以借助[]的返回值进行修改,以上就是map的一些知识,下面我们讲解multimap的知识:


1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对,其中多个键值对之间的key是可以重复的。

2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,

value_type是组合key和value的键值对: typedef pair value_type;

3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的。

4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。

5. multimap在底层用二叉搜索树(红黑树)来实现。

注意: multimap 和 map 的唯一不同就是: map 中的 key 是唯一的,而 multimap 中 key 是可以重复的。

782b622feb5046119506bf39417062c4.png


multimap与multiset一样,都是支持键值冗余,也可以理解为仅仅排序不去重,与map不同的是,由于multimap的键值不再是一一对应的关系了,所以multimap没有了[]运算符重载。下面我们演示一下:


void test_map1()
{
  multimap<string, string> mp;
  mp.insert(make_pair("string", "字符串1"));
  mp.insert(make_pair("string", "字符串2"));
  mp.insert(make_pair("string", "字符串3"));
  mp.insert(make_pair("left", "左边"));
  mp.insert(make_pair("right", "右边"));
  for (auto& e : mp)
  {
  cout << e.first << ":" << e.second << endl;
  }
}

abfae04bc94e479d99ec430160f59bbc.png


当然multimap虽然没有了[]运算符,但是count还是比较有用的:


void test_map1()
{
  multimap<string, string> mp;
  mp.insert(make_pair("string", "字符串1"));
  mp.insert(make_pair("string", "字符串2"));
  mp.insert(make_pair("string", "字符串3"));
  mp.insert(make_pair("left", "左边"));
  mp.insert(make_pair("right", "右边"));
  if (mp.count("string"))
  {
  cout << "string的个数:" << mp.count("string") << endl;
  }
}

91eb9a341a4d413a9f1413f8146afe01.png


在multimap和multiset中count都可以计算出某个key的个数,以上就是map和set的基本用法以及一些重要功能的讲解,下面我们讲一道题练习一下map和set。


目录
相关文章
|
3天前
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set
|
3天前
|
编译器 测试技术 计算机视觉
红黑树模拟封装map和set
红黑树模拟封装map和set
|
1月前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
142 77
|
1月前
|
存储 人工智能 算法
【C++数据结构——图】最短路径(头歌教学实验平台习题) 【合集】
任务描述 本关任务:编写一个程序,利用Dijkstra算法,实现带权有向图的最短路径。 相关知识 为了完成本关任务,你需要掌握:Dijkst本关任务:编写一个程序,利用Dijkstra算法,实现带权有向图的最短路径。为了完成本关任务,你需要掌握:Dijkstra算法。带权有向图:该图对应的二维数组如下所示:Dijkstra算法:Dijkstra算法是指给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径。Dijkstra算法的具体步骤如下:(1)初始时,S只包含源点,即S={v},v的距离为0。
61 15
|
1月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
46 10
|
1月前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
61 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
1月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
48 12
|
1月前
|
算法 C++
【C++数据结构——图】最小生成树(头歌实践教学平台习题) 【合集】
【数据结构——图】最小生成树(头歌实践教学平台习题)目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:【合集】任务描述 本关任务:编写一个程序求图的最小生成树。相关知识 为了完成本关任务,你需要掌握:1.建立邻接矩阵,2.Prim算法。建立邻接矩阵 上述带权无向图对应的二维数组,根据它建立邻接矩阵,如图1建立下列邻接矩阵。注意:INF表示无穷大,表示整数:32767 intA[MAXV][MAXV];Prim算法 普里姆(Prim)算法是一种构造性算法,从候选边中挑
44 10
|
1月前
|
存储 算法 C++
【C++数据结构——图】图的邻接矩阵和邻接表的存储(头歌实践教学平台习题)【合集】
本任务要求编写程序实现图的邻接矩阵和邻接表的存储。需掌握带权有向图、图的邻接矩阵及邻接表的概念。邻接矩阵用于表示顶点间的连接关系,邻接表则通过链表结构存储图信息。测试输入为图的顶点数、边数及邻接矩阵,预期输出为Prim算法求解结果。通关代码提供了完整的C++实现,包括输入、构建和打印邻接矩阵与邻接表的功能。
49 10