C++STL详解--set和map

674 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天

零.前言

在我们之前的学习中,已经了解了STL的部分容器,比如:vector,list,deque,forward_list等等,这些容器统称为序列式容器,而我们今天要将的map和set称之为关联式容器,序列式容器和关联式容器的区别就在于,序列式容器单纯为了存储数据而存在。而关联式容器不仅仅存储数据还对结构进行了存储。 在了解set和map之前,我们需要了解搜索二叉树是什么,因为set和map的底层就是一棵搜索二叉树。

1.set

(1)概念

set本质上是搜索二叉树的key模型。set的底层依然使用到了模板,因此我们这样定义set类型的对象:

set<int> s;

(2)遍历

迭代器法遍历

    set<int>::iterator it = s.begin();//定义迭代器it为s.begin()
    while (it != s.end())//依次遍历s中的数据
    {
        cout << *it << " ";
        it++;
    }

范围for遍历

    for (auto& e: s)
    {
        cout << e << " ";
    }

(3)插入与删除

insert()

使用insert函数进行插入数据,具有去重的作用,这是因为set的底层就是一个key模型的搜索二叉树。是没有相同的元素的。

    s.insert(3);
    s.insert(5);
    s.insert(1);
    s.insert(1);
    s.insert(7);
    s.insert(0);
    s.insert(6);
    s.insert(9);
    s.insert(9);

当向s中插入这一组元素的时候,遍历后打印出来的结果是: 在这里插入图片描述 可以发现,并没有录入重复的元素。 并且我们会发现,迭代器走的是中序遍历的路线。

erase()

与其他容器的erase一样,set的erase可以传入值,迭代器或者迭代器区间。 当我们要传入迭代器的时候,需要检测迭代器位置是否存在,否则会发生崩溃:

    set<int> ::iterator pos = s.find(3);
    if (pos != s.end())
    {
        s.erase(pos);
    }

而当我们向erase中传入值的时候,即使这个值不存在,程序也不会崩溃,这是因为传入值的底层就是传入迭代器并且进行了检测。 在这里插入图片描述 我们发现当我们传入100的时候程序并没有崩溃。 我们也可以通过查阅文档来观察erase函数的返回值: 在这里插入图片描述 返回一个size_type类型的变量: 在这里插入图片描述 它的含义是被删除节点的个数。 可能你会问,这不是一棵搜索树吗,节点只有一个,当然size_type的值就是1啊。

(4)multiset

通过上述对set的解释我们可以知道,set是一棵搜索树,通过它可以实现数据的插入去重和排序,那么如果我们不想去重呢?STL提供了一棵基于set的搜索树multiset,它没有对数据进行去重的操作。用法和set类似:

    multiset<int> s;
    s.insert(3);
    s.insert(5);
    s.insert(1);
    s.insert(1);
    s.insert(1);
    s.insert(1);
    s.insert(6);
    s.insert(9);
    s.insert(9);
    set<int>::iterator it = s.begin();
    while (it != s.end())
    {
        cout << *it << " ";
        it++;
    }

此时进行插入操作之后,再进行遍历我们发现没有发生去重操作: 在这里插入图片描述 我们发现遍历它依然是经过中序遍历。那么对于相同元素来说,使用find查找,找到的是哪一个值呢? 我们可以通过下面这段代码来进行尝试:

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

我们可以对比这两段代码。,第一段代码是从头开始遍历整个set,第二段代码是从find(1)的位置开始遍历整个set: 在这里插入图片描述 两者打印的结果是相同的,因此find(1)的位置其实就是set中序遍历的起始位置。 当我们使用erase函数来进行删除的时候,它删除的是所有该元素,比如我们可以测试一个erase(1),它删除了所有1: 在这里插入图片描述

(5)set总结

注意set只有增删查,没有修改的函数。 这是因为一旦修改key的值,set就不是一棵搜索二叉树了。set底层的普通迭代器的实现和const迭代器的实现是一样的。*pos是一个常量。 在这里插入图片描述 此时会发生报错。

2.map

(1)概念

map的本质其实就是搜索二叉树的key/value模型map的每一个节点都是一个名为pair的结构体,pair有两个元素,其中first元素存放的是key的值,second元素存放的是value的值。

    map<string, string> dict;
    pair<string, string> kv1("sort", "排序");

其中dict为一棵key,value的搜索二叉树,kv1为它的一个节点。向pair的构造函数中传入key:sort和value:排序。 map的key是不支持做修改的,但是它的value是支持修改的。

(2)插入

insert的使用

map中依然有insert元素,不过和set不同的是,map插入的是一个pair类型的对象: 使用insert函数进行插入有三种方法:

	map<string, string> dict;
	pair<string, string> kv1("sort", "排序");
	dict.insert(kv1);//插入pair类型对象kv1
	dict.insert(pair<string, string>("string", "字符串"));//使用匿名对象进行插入
	dict.insert(make_pair("test", "测试"));//使用make_pair函数进行插入

其中使用make_pair函数进行插入最为常见,它返回一个pair的匿名对象。本质上都是向map中插入带有key和value的pair。 我们可以通过map再来实现水果的例子:

	string arr[] = { "苹果","橘子","鸭梨","水蜜桃","热带风味","苹果","水蜜桃","苹果" };
	map<string, int> countMap;
	for (auto& str : arr)
	{
		auto ret = countMap.find(str);
		if (ret == countMap.end())
		{
			countMap.insert(make_pair(str, 1));
		}
		else
		{
			ret->second++;
		}
	}
	for (auto& e : countMap)
    {
	cout << e.first << ":" << e.second << endl;
    }

此时我们就可以使用map的insert函数来进行插入操作了。当插入失败的时候,就对ret的second进行++操作。

insert的返回值

我们可以通过查文档来进行返回值的查看: 在这里插入图片描述 我们发现它返回的是一个迭代器类型,并对其进行了解释: 在这里插入图片描述 大致的意思是,insert返回的是一个pair,其中它的first是一个迭代器类型,它的second是一个bool类型。

插入元素之前没有存在:first指向新插入的元素,second的值为true。 当插入元素已经存在:first指向已经存在的元素,second的值为false。

了解了insert的返回值我们就可以改良一下上述代码:

	for (auto& str : arr)
	{
		auto kv = countMap.insert(make_pair(str, 1));
		if (!kv.second)
		{
			kv.first->second++;
		}
	}

[]的重载使用

[]的引入使我们插入,删除,查找更加方便:

	for (auto& str : arr)
	{
		countMap[str]++;
	}

这段代码使我们进行插入更加方便,我们可以查阅一下[]重载的返回值: 在这里插入图片描述 我们来分析一下这一长条,其中mapped_type()表示的是一个匿名的value。k表示的是我们要插入的str。 分为插入成功以及插入不成功两种情况来进行讨论:

成功:当插入成功的时候,insert返回一个pair,对象调用该pair的first(即插入成功的位置)再解引用得到插入成功位置处的pair,然后再调用它的second(即value)。 失败:当插入失败的时候,insert返回一个pair,对象调用该pair的first(即相同元素的位置)解引用得到相同元素处的pair,再调用该pair的second,再对其进行操作。

使用[]来进行插入时最常用的一种方式。它可以进行修改,插入和查找:

	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("sort", "排序"));
	dict["left"] = "剩余";//修改
	dict["test"] = "测试";//测试
	cout << dict["sort"] << endl;//查找

(3)遍历

迭代器遍历

	map<string, string> ::iterator it = dict.begin();
	while (it != dict.end())
	{
		//cout << it->first << ":" <<it->second<< endl;
		cout << (*it).first << ":" << (*it).second << endl;
		it++;
	}

map的每一个元素是一个pair,it就是一个指向pair的指针,要遍历出key和value需要使用指针解引用的形式来调用pair中的first和second。 注意使用.方式进行遍历的时候,需要加(),这是因为.的优先级比*高。

范围for遍历

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

范围for进行遍历也同理,这里的每一个e代表的是一个pair。

(4)删除

同set,可以向erase函数传入,迭代器,值或者迭代器区间来进行删除,注意传入的是key的值: 在这里插入图片描述

(5)multimap

同set,map也有multimap,当使用multimap的时候,二叉树的节点可以重复进行插入:

    multimap<string, string> dict;
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("left", "剩余"));

此时left将不会被覆盖,遍历的结果是: 在这里插入图片描述 注意:multimap是不支持[]来进行访问的。 对于mutimap来说,我们还可以通过count函数来观察key的元素的个数: 在这里插入图片描述

3.总结

map和set的区别在于,map可以使用下标来进行访问,这是因为map比set多了一个value,而存放key和value的方式也很特别,不是在节点中直接进行定义,而是建立一个pair来对key(first)和value(second)来进行存储。set和map公用一棵红黑树。