set、map、multiset、multimap的介绍及使用以及区别,注意事项

简介: set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。


关联式容器
在介绍set、map、multiset、multimap之前我们先了解什么是关联式容器,

C++STL包含了序列式容器和关联式容器:

序列式容器:序列容器以线性序列的方式存储元素。它没有对元素进行排序,元素的顺序和存储它们的顺序相同。一般来说,有 5 种标准的序列容器,每种容器都具有不同的特性。

array (数组容器) :是一个长度固定的序列,有N个T类型的对象,不能增加或删除元素。
vector (向量容器) :
是一个长度可变的序列,用来存放T类型的对象。必要时,可以自动增加容量,但只能在序列的末尾高效地增加或删除元素。
deque (双向队列容器) :
是一个长度可变的、可以自动增长的序列,在序列的两端都不能高效地增加或删除元素。
list (链表容器) :
是一个长度可变的、由 T 类型对象组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素。访问容器中任意元素的速度要比前三种容器慢,这是因为
list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
forward list (正向链表容器) :是一个长度可变的、由 T 类型对象组成的序列,它以单链表的形式组织元素,是一类比链表容器快、更节省内存的容器,但是它内部的元素只能从第一个元素开始访问。

关联式容器: 里面存储的是结构的键值对,在数据检索时比序列式容器效率更高。比如:set、map、unordered_set、unordered_map等。

关联式容器存储的元素,都是一个一个的“键值对”( ),这是和序列式容器最大的不同。除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。

下面开始这篇的讲解set、map、multiset、multimap。

如果查阅相关资料可以得知,set、map、multiset、multimap的底层是用红黑树来实现的,关于红黑树是什么,后面会单独写一篇文章进行详细讲解,在这里只需要其底层是红黑树即可(红黑树是比二叉搜索树更有的树)。

键值对

比如说我们去买菜,每种菜都对应着不同的单价,这就是一一对应关系,可以通过其菜名知道其多少钱一斤。

在SGI-STL中关于键值对的定义如下:

template
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() : first(T1()), second(T2())
{}
pair(const T1& a, const T2& b) : first(a), second(b)
{}
};

set
set的介绍

set的构造函数与其定义
这里就不给官方的函数声明截图了,直接展示使用案例

bool fncomp(int lhs, int rhs)
{
return lhs < rhs;
}
struct classcomp
{
bool operator() (const int& lhs, const int& rhs) const
{
return lhs < rhs;
}
};
int main()
{
set first; // 最基本的定义,其里面为空
int myints[] = { 10,20,30,40,50 };
set second(myints, myints + 5); // 利用数组定义

set<int> third(second);                  // 拷贝构造

set<int> fourth(second.begin(), second.end());  // 使用迭代器构造

set<int, classcomp> fifth;                 // 使用仿函数定义set是升序还是降序
                                           // 小于是升序,大于是降序
bool(*fn_pt)(int, int) = fncomp;
set<int, bool(*)(int, int)> sixth(fn_pt);  // 利用函数指针的方式定义是升序还是降序

return 0;

}

set的常用的成员函数
set当中常用的成员函数如下:

成员函数 功能
insert 插入指定元素
erase 删除指定元素
find 查找指定元素//返回的是迭代器,没有找到返回end()
size 获取容器中元素的个数
empty 判断容器是否为空//是空返回真
clear 清空容器
swap 交换两个容器中的数据
count 获取容器中指定元素值的元素个数
erase

最常用的也就是第二种,第一种的传参可以传find的返回值。第三种是删除某个特定的区间,但着个通常还是与别的函数一起使用。比如下面这三个函数

lower_bound
upper_bound
equal_range
然而set函数还有几个成员函数是我们没有见过的几个函数

因为set是有序的,所以这三个函数就达到了返回大于/小于/等于某个值的部分;

就比如一个set函数内存储着 1,2,6,8,9,11;

如果想返回比6小的部分,就可以用 lower_bound,解释起来很简单,但是里面还是有很多的细节的,下面用代码来解释细节部分。

首先前两个函数的返回值为迭代器。其使用大多是与erase同用

int main()
{
set myset;
set::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(30);              //
itlow = myset.lower_bound(25);                // >= val值位置的iterator 30
itup = myset.upper_bound(70);                 // >  val值位置的iterator 80

// 删除区间:[30,80)
myset.erase(itlow, itup);                     // 10 20 80 90

for (auto e : myset)
{
    cout << e << " ";
}
cout << endl;


//运行结果:
//10 20 80 90
return 0;

}

第三个成员函数的返回值略有不同

pair的first与second所接收的是两个不同的迭代器,但也有可能相同

first接收>=val的迭代器
second接收>val的迭代器
int main()
{
set myset;
set::iterator itlow, itup;

for (int i = 1; i <= 5; i++) myset.insert(i * 10);   // myset: 10 20 30 40 50


auto ret = myset.equal_range(35);

cout << "the lower bound points to: " << *ret.first << '\n';   // >= val
cout << "the upper bound points to: " << *ret.second << '\n';  // > val

//运行结果:
//  the lower bound points to : 40
//    the upper bound points to : 40
return 0;

}

multiset
multiset容器与set容器的底层实现一样,都是红黑树,其次,multiset容器和set容器所提供的成员函数的接口都是基本一致的,但set是有序,并且没有重复的,那么与之对应的就是multiset无序,并且有重复的。用官方的话来说就是multiset允许键值冗余,即multiset容器当中存储的元素是可以重复的。

用法与set几乎完全相同,这里不在举例说明。

但还是有很多细节要注意的,就比如说因为multiset是无序存在重复元素的,那么find如果要查找重复的元素找到了要返回哪一个呢?

因此两个容器中成员函数find和count的意义也有所不同:

find
成员函数find 功能
set对象 返回值为val的元素的迭代器
multiset对象 返回底层搜索树中序的第一个值为val的元素的迭代器
count
成员函数count 功能
set对象 值为val的元素存在则返回1,不存在则返回0(find成员函数可代替)
multiset对象 返回值为val的元素个数(find成员函数不可代替)
erase

对于第二个同样的erase也会删除多个,然后返回值删除的个数。

count同样是如此,找在不在,并且返回再次对象中有几个这样的值

map
map的介绍
底层同样是用红黑树来写的。

set的构造函数与其定义
这里就不给官方的函数声明截图了,直接展示使用案例

bool fncomp(char lhs, char rhs) { return lhs < rhs; }

struct classcomp {
bool operator() (const char& lhs, const char& rhs) const
{
return lhs < rhs;
}
};

int main()
{
map first;// 空,也是最简单的定义

first['a'] = 10;
first['b'] = 30;
first['c'] = 50;
first['d'] = 70;

map<char, int> second(first.begin(), first.end());// 用迭代器,使用一个区间定义

map<char, int> third(second); // 拷贝构造定义

map<char, int, classcomp> fourth;                  // 使用仿函数定义set是升序还是降序
                                                   // 小于是升序,大于是降序

bool(*fn_pt)(char, char) = fncomp;
map<char, int, bool(*)(char, char)> fifth(fn_pt); // 利用函数指针的方式定义是升序还是降序

return 0;

}

以下的知识在set中同样适用,只需要进行转化为set的特点即可。

insert

返回值
用的最多的也就是这个了。其返回值的pair是返回的迭代器,其产生的效果与set相似,如果插入成功就返回这个位置的迭代器,反之如果这个数已经存在,那么也是返回这个数位置的迭代器。

pair的second为一个bool类型。插入成功返回true,失败为false。

参数列表(map)
insert函数的参数显示是value_type类型的,实际上value_type就是pair类型的别名:

typedef pair value_type;

因此,我们向map容器插入元素时,需要用key和value构造一个pair对象,然后再将pair对象作为参数传入insert函数。

方式一: 构造匿名对象插入。

int main()
{
map m;

m.insert(pair<int, string>(2, "two"));
m.insert(pair<int, string>(1, "one"));
m.insert(pair<int, string>(3, "three"));
for (auto e : m)
{
    cout << "<" << e.first << "," << e.second << ">" << " ";
}
cout << endl; //<1,one> <2,two> <3,three>
return 0;

}

但是这种方式会使得我们的代码变得很长,尤其是没有直接展开命名空间的情况下,因此我们最常用的是方式二。

方式二: 调用make_pair函数模板插入。
在库当中提供以下make_pair函数模板

template
pair make_pair(T1 x, T2 y)
{
return (pair(x, y));
}
我们只需向make_pair函数传入key和value,该函数模板会根据传入参数类型进行自动隐式推导,最终构造并返回一个对应的pair对象。

int main()
{
map m;
//方式二:调用函数模板make_pair,构造对象插入
m.insert(make_pair(2, "two"));
m.insert(make_pair(1, "one"));
m.insert(make_pair(3, "three"));
for (auto e : m)
{
cout << "<" << e.first << "," << e.second << ">" << " ";
}
cout << endl; //<1,one> <2,two> <3,three>
return 0;
}

find

返回值
查找函数是根据所给key值在其中当中进行查找,若找到了,则返回对应元素的迭代器,若未找到,则返回容器中最后一个元素下一个位置的正向迭代器,即也就是end()。

int main()
{
map m;
m.insert(make_pair(2, "two"));
m.insert(make_pair(1, "one"));
m.insert(make_pair(3, "three"));
//获取key值为2的元素的迭代器
map::iterator pos = m.find(2);
if (pos != m.end())
{
cout << pos->second << endl; //two
}
return 0;
}

erase

用的最多的也就是第二个,这个返回值没有什么特的,返回值为实际删除的元素个数。

int main()
{
map m;

m.insert(make_pair(2, "two"));
m.insert(make_pair(1, "one"));
m.insert(make_pair(3, "three"));

auto it = m.begin();
m.erase(it); // 删除了  2, "two"

m.erase(1); // 删除了   1, "one"
//   返回值为1

m.erase(m.begin(), m.end());// 删除了   3, "three"
return 0;

}

map的[ ]运算符重载
这个成员函数也是map与set与众不同的一点,set中没有这个函数,其的功能特别的强大。

其在官方中的函数原型如下:

mapped_type& operator[] (const key_type& k);
[ ]运算符重载函数的参数就是一个key值,而这个函数的返回值如下:

(*((this->insert(make_pair(k, mapped_type()))).first)).second
就这样看着不太好理解,我们整理一下,实际上[ ]运算符重载实现的逻辑实际上就是以下三个步骤:

调用insert函数插入键值对。
拿出从insert函数获取到的迭代器。
返回该迭代器位置元素的值value。
对应分解代码如下:

mapped_type& operator[] (const key_type& k)
{
//1、调用insert函数插入键值对
pair ret = insert(make_pair(k, mapped_type()));
//2、拿出从insert函数获取到的迭代器
iterator it = ret.first;
//3、返回该迭代器位置元素的值value
return it->second;
}

那么这个函数的价值体现在哪里呢?我们来看看下面这段代码:

统计出现次数:

int main()
{
string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map ht;
for (auto& e : arr)
{
ht[e]++;
}

for (auto& kv : ht)
{
    cout << kv.first << ":" << kv.second << endl;
}
cout << endl;

//打印结果:
//苹果 : 6
//甜瓜 : 1
//西瓜 : 3
//香蕉 : 3
return 0;

}

说明一下:

如果k不在map中,则先插入键值对,然后返回该键值对中V对象的引用。其中的v()是默认构造,比如说如果v为int类型,那么就会默认构造为0,char为’0‘,指针为nullptr.
如果k已经在map中,则返回键值为k的元素对应的V对象的引用。注意是引用!!!
同样还可以修改其中的value:

int main()
{
string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map ht;
for (auto& e : arr)
{
ht[e]++;
}

for (auto& kv : ht)
{
    cout << kv.first << ":" << kv.second << endl;
}
cout << endl;

//打印结果:
//苹果 : 6
//甜瓜 : 1
//西瓜 : 3
//香蕉 : 3

ht["苹果"] = 100;

//打印结果:
//苹果 : 100
//甜瓜 : 1
//西瓜 : 3
//香蕉 : 3
return 0;

}

迭代器
迭代器的遍历其实是比较麻烦的,因为set,map的底层使用红黑树来写的,不是简单的进行++/--就可以了,走起来还是不叫麻烦的,这里就不说了,会在红黑树中解释。迭代器使用起来还是没难度的,也不举例了。

再次提醒: 编译器在编译时会自动将范围for替换为迭代器的形式,因此支持了迭代器实际上就支持了范围for。

map的其他成员函数
成员函数 功能
size 获取容器中元素的个数
empty 判断容器是否为空
clear 清空容器
swap 交换两个容器中的数据
count 获取容器中指定key值的元素个数

include

include

include

using namespace std;

int main()
{
map m;
m.insert(make_pair(2, "two"));
m.insert(make_pair(1, "one"));
m.insert(make_pair(3, "three"));
//获取容器中元素的个数
cout << m.size() << endl; //3
//容器中key值为2的元素个数
cout << m.count(2) << endl; //1
//清空容器
m.clear();
//容器判空
cout << m.empty() << endl; //1
//交换两个容器中的数据
map tmp;
m.swap(tmp);
return 0;
}

multimap
multimap容器与map容器的底层实现一样,也都是平衡搜索树(红黑树),其次,multimap容器和map容器所提供的成员函数的接口都是基本一致的,这里也就不再列举了,multimap容器和map容器的区别与multiset容器和set容器的区别一样,multimap允许键值冗余,即multimap容器当中存储的元素是可以重复的。

同样其因为允许存在重复的K,所以erase还是与set的操作是相似的,不在说明,有问题可以看set部分,也可以评论,看见必解释。

由于multimap容器允许键值冗余,因此两个容器中成员函数find和count的意义也有所不同:

成员函数find 功能
map对象 返回值为键值为key的元素的迭代器
multimap对象 返回底层搜索树中序的第一个键值为key的元素的迭代器
成员函数count 功能
map对象 键值为key的元素存在则返回1,不存在则返回0(find成员函数可代替)
multimap对象 返回键值为key的元素个数(find成员函数不可代替)

目录
相关文章
|
5天前
|
存储 缓存 JavaScript
Set和Map有什么区别?
Set和Map有什么区别?
19 1
|
5天前
|
存储 缓存 JavaScript
在JavaScript中,Set和Map的性能有什么区别?
在JavaScript中,Set和Map的性能有什么区别?
27 0
|
1月前
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
56 0
|
1月前
|
存储 C++ 容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap的介绍及使用
unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。在unordered_set中,元素的值同时也是唯一地标识它的key。在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。它的迭代器至少是前向迭代器。前向迭代器的特性。
75 0
|
1月前
|
编译器 C++ 容器
用一棵红黑树同时封装出map和set
再完成上面的代码后,我们的底层代码已经完成了,这时候已经是一个底层STL的红黑树了,已经已符合库里面的要求了,这时候我们是需要给他穿上对应的“衣服”,比如穿上set的“衣服”,那么这个穿上set的“衣服”,那么他就符合库里面set的要求了,同样map一样,这时候我们就需要实现set与map了。因此,上层容器map需要向底层红黑树提供一个仿函数,用于获取T当中的键值Key,这样一来,当底层红黑树当中需要比较两个结点的键值时,就可以通过这个仿函数来获取T当中的键值了。我们就可以使用仿函数了。
30 0
|
4月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
115 2
|
5月前
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set
|
5月前
|
编译器 测试技术 计算机视觉
红黑树模拟封装map和set
红黑树模拟封装map和set
|
7月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
137 18
你对Collection中Set、List、Map理解?
|
7月前
|
存储 缓存 安全
只会“有序无序”?面试官嫌弃的List、Set、Map回答!
小米,一位热衷于技术分享的程序员,通过与朋友小林的对话,详细解析了Java面试中常见的List、Set、Map三者之间的区别,不仅涵盖了它们的基本特性,还深入探讨了各自的实现原理及应用场景,帮助面试者更好地准备相关问题。
135 20