Bootstrap

map与set的介绍使用

作者:清风玉骨

专栏:C++进阶

        昨夜松边醉倒,问松我醉何如。只疑松动要来扶,以手推曰:去! 《西江月·遣兴》

目录

关联式容器

键值对

set的介绍使用

时间复杂度

类模板

成员函数

Modifiers: insert -- 插入

iterator -- 迭代器

Modifiers:erase

Operations:find

Modifiers:swap

Modifiers:count

multiset

Operations:lower_bound & upper_bound

测试代码

map的使用介绍

map

pair

键值对

Modifiers:insert

map:operator[ ]

multimap

测试代码

在OJ中的使用


关联式容器

        在前面,我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。
        关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是

<key, value>结构的键值对,在数据检索时比序列式容器效率更高。

键值对

        用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。

        比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

set的介绍使用

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在底层是用二叉搜索树(红黑树)实现的

注意:
1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放
value,但在底层实际存放的是由<value, value>构成的键值对。
2. set中插入元素时,只需要插入value即可,不需要构造键值对。
3. set中的元素不可以重复(因此可以使用set进行去重)。
4. 使用set的迭代器遍历set中的元素,可以得到有序序列
5. set中的元素默认按照小于来比较
6. set中查找某个元素,时间复杂度为:logN
7. set中的元素不允许修改(为什么?)

解释:

set 容器内的元素会被自动排序,set 与 map 不同,set 中的元素即是键值又是实值,set 不允许两个元素有相同的键值。

不能通过 set 的迭代器去修改 set 元素,原因是修改元素会破坏 set 组织


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

时间复杂度

        set作为一个检索容器来说,是非常厉害的,它的检索速度可以达到 logN 的恐怖阶段。可能有些人对这时间复杂度不太敏感,打一个比方,在十亿(大约2的三十次方)的数据中查找到某一个数据,我们只需要大概30次就可以找到!也就是说假如有十亿的人口通过该容器,利用身份证号码去找到具体某一个人的话,只需要30次!

类模板

成员函数

        观看下面的图,我们发现有种似曾相识的感觉,通过前面几个容器的学习,我们大致知道有什么成员函数以及如何使用,至于最下方的几个在实际使用中较少出现,所以这里就不再详细介绍了,简单介绍一些函数

set - C++ Reference (cplusplus.com) -- 具体内容参考该链接

Modifiers: insert -- 插入

我们发现它是支持插入一段区间的(迭代器区间),不过哦,这个插入方式,谨慎使用,容易破坏树的结构,至于为什么,可以联想搜索二叉树的原理

iterator -- 迭代器

        可以根据上图知晓迭代器的使用,当然其他的迭代器就不一一介绍了,前面的容器介绍过很多次

        可以使用范围for进行遍历,范围for的本质是利用迭代器直接替换的,所以通过这一点可以解释为什么set可以使用范围for,同理其他的容器也是如此,可以根据这一点判断是否支持范围for的使用。

Modifiers:erase

        也可以直接使用erase直接删除,不过这样是无法知道它是否删除了没,我们得用另外的方法观察,可以看到是有返回值的

  

Operations:find

 没找到返回 end() 迭代器

        有一点需要知道的是,我们也可以使用库里面实现的find,来找到我们想要的数据位置,因为库里面的定义是一个模板,传什么可以直接推出来

 

        不过这两个的查找原理是不相同的,set时间复杂度可以达到 O(logN),而库里面实现的只能达到O(N),

库里面的实现

set里面的实现借助了数据结构的优势,所以要快上许多

Modifiers:swap

利用set里面实现的swap的代价是很小的,只需要交换根节点 

Modifiers:count

        很简单,容器内有就返回1,没有就返回 0。不过我们会发现相比find,count的用处并不是很突出,那么为什么会单独设置这么一个函数呢?这里可以提及set的另一个,multiset

multiset  -- 链接

 

multiset

multiset -- 多样的set

可以支持多样元素的插入

        这一个容器和set的使用是完全一样的,这里就不再赘述了,上面说的 count 在这样的一个容器中就凸显出了它的用处了

        那么利用它遍历一个元素的时候找到的是哪一个呢?答案是,中序遍历的第一个元素,下图可以求证

Operations:lower_bound & upper_bound

        可以找到某一区间,注意是左开右闭,因为从erase(删除处理方式为左边右开)中可以看到,不过这个使用较少 

余下的就不一一介绍了,可以自行查看<set> - C++ Reference (cplusplus.com)

测试代码

#include<iostream>
#include<set>
using namespace std;

void test_set1() {
	set<int> s;	//注意的是set并没有提供push函数,而是insert插人式
	s.insert(3);
	s.insert(1);
	s.insert(4);
	s.insert(7);
	s.insert(2);
	s.insert(1);

	//set的功能为 排序 + 去重 
	
	//set<int>::iterator it = s.begin();	//迭代器
	auto it = s.begin();
	while (it != s.end()){
		cout << *it << ' ';
		it++;
	}
	cout << endl;

	//使用auto与范围for也是当然也是可以的,注意范围for在底层原理是直接替换成迭代器的
	for (auto e : s) {
		cout << e << ' ';
	}
	cout << endl;

	//erase 有就删除没有也不会报错, find 字面意思
	//auto pos = s.find(3);

	auto pos = find(s.begin(),s.end(),3);	//使用库里面的也是可以的,迭代器区间
	if (pos != s.end()) {
		s.erase(pos);
	}
	for (auto e : s) {
		cout << e << ' ';
	}
	cout << endl;

	//也可以直接使用erase直接删除,不过这样是无法知道它是否删除了没,我们得用另外的方法观察
	cout<<s.erase(1)<<endl;		//成功返回该值
	cout<<s.erase(3)<<endl;		//失败返回 0 
	for (auto e : s) {
		cout << e << ' ';
	}
	cout << endl;

	if (s.count(3))
	{
		//...
	}
}

void test_set2() {
	multiset<int> s;	//注意的是set并没有提供push函数,而是insert插人式
	s.insert(3);
	s.insert(1);
	s.insert(4);
	s.insert(7);
	s.insert(2);
	s.insert(1);
	s.insert(2);
	s.insert(3);
	s.insert(1);
	s.insert(3);
	s.insert(2);
	s.insert(3);
	s.insert(1);

	//multiset的功能为 排序

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

	//使用auto与范围for也是当然也是可以的,注意范围for在底层原理是直接替换成迭代器的
	for (auto e : s) {
		cout << e << ' ';
	}
	cout << endl;


	//erase -- 有就删除没有也不会报错, find -- 字面意思
	auto pos = s.find(3);
	while(pos != s.end()) {
		cout << *pos << " ";
		++pos;
	}
	cout << endl;
	
	//erase -- 有就删除没有也不会报错, find -- 字面意思
	//auto pos = s.find(3);
	//auto pos = find(s.begin(), s.end(), 3);	//使用库里面的也是可以的,迭代器区间
	//if (pos != s.end()) {
	//	s.erase(pos);
	//}
	//for (auto e : s) {
	//	cout << e << ' ';
	//}
	//cout << endl;
	

	//cout << s.count(1) << endl;	//返回元素的数量
	//也可以直接使用erase直接删除,不过这样是无法知道它是否删除了没,我们得用另外的方法观察
	//cout << s.erase(1) << endl;		//成功返回该元素数量
	//cout << s.erase(3) << endl;		//失败返回 0 
	//for (auto e : s) {
	//	cout << e << ' ';
	//}
	//cout << endl;
}

void test_set3() {
// set::lower_bound/upper_bound
	std::set<int> myset;
	std::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(30);            //返回大于等于的边界,假如是35的话30就会保留下来
	itup = myset.upper_bound(60);             //返回大于的边界,这里是80

	myset.erase(itlow, itup);                     // 10 20 70 80 90

	std::cout << "myset contains:";
	for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
		std::cout << ' ' << *it;
	std::cout << '\n';
}



int main() {
	//test_set1();
	//test_set2();
	test_set3();

	return 0;
}

map的使用介绍

map

map - C++ Reference (cplusplus.com)

 解释

key: 键值对中key的类型


T: 键值对中value的类型


Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比
较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户
自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)


Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的
空间配置器


注意:在使用map时,需要包含头文件,map是一个典型的KV结构的容器,通过它的模板可以看到一个独特例行,pair结构,下面再详细解释

文章大致翻译 

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通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))

成员函数与set几乎一模一样,这里就不再详细介绍了,下面介绍几个不同之处

pair

pair - C++ Reference (cplusplus.com)  

pair的构造

键值对

        用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。

        比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。


SGI-STL中关于键值对的定义大致如下

template <class T1, class T2>
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)
{}
};

通常我们会使用first来当作key,second来当作value

Modifiers:insert

 

进一步简化可以写成下图

make_pair

make_pair - C++ Reference (cplusplus.com)

作用自动识别参数并返回相应的pair 

实际使用例子

  

更简洁的写法

         这里的方括号需要进一步解释,我们都知道方括号是为了支持随机访问的,但是这里的却是根据key值来进行访问的

map:operator[ ]

map::operator[] - C++ Reference (cplusplus.com)

map重载的方括号使用

实际例子

multimap

multimap - C++ Reference (cplusplus.com)

        multimap与map没有什么很大的差别,参考set与multiset,不过map与multiset有几个需要注意的点,multiset是没有提供方括号的重载的,因为multiset的原理会允许它有相同key值的存在,所以没有提供该项功能,还有一点它的find找的是中序的第一个和set一样,区别是它允许键值重复

统计水果次数

测试代码

#include<iostream>
#include<map>
#include<string>
using namespace std;


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"));	
	//dict["迭代器"] = "iterator";	//插入 + 修改
	//dict["insert"];	//key不在就是插入,不在就是查找
	//dict.insert(pair<string, string>("左边", "xxx"));
	//dict["insert"] = "插入";//修改的作用
	//cout << dict["左边"] << endl;//key不在就是插入,不在就是查找
	//

	//map<string, string>::iterator it = dict.begin();
	//while (it != dict.end())
	//{
	//	//cout << (*it).first<< ":"<<(*it).second << endl;	//注意pair是不支持流插入的,我们得直接访问
	//	//按道理需要两个->不过太难看了,所以编译器就直接省略掉了一个
	//	cout << it->first << ":" << it->second << endl;		//struct没有访问限定符,C++中有个不成文的原则
	//	it++; 												//当一个类成员都是公用,那么直接使用struct
	//}
	//cout << endl;

	//for (const auto kv : dict) {
	//	cout << kv.first << ":" << kv.second << endl;
	//}
	//cout << endl;


	// 统计水果出现的次数
	//string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果",
	//				"苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//map<string, int> countMap;

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

	//for (auto e : arr) {
	//	countMap[e]++;	//这里的方括号需要进一个解释
	//}

	//for (const auto kv : countMap) {
	//	cout << kv.first << ":" << kv.second << endl;
	//}


	multimap<string, string> dict;
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("left", "剩余"));
	dict.insert(make_pair("string", "字符串"));
	dict.insert(make_pair("left", "xxx"));

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

	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果",
					"苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	multimap<string, int> countMap;

	for (auto e : arr) {
		multimap<string, int>::iterator it = countMap.find(e);
		//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;
}

在OJ中的使用
 

692. 前K个高频单词 - 力扣(LeetCode)

经典的稳定性与排序问题

        至于为何不使用greater(可以对pair类型进行排序)进行比较,而直接自己写一个compare比较,这是基于pair的比较方式是与这题有所出路的

 

class Solution {
public:

    //方法二:直接sort进行排序

    //仿函数 -- 比较函数 需要进行改进
    struct Compare{
        bool operator()(const pair<int,string>& l, const pair<int, string>& r){
            return (l.first > r.first) ||  ( (l.first == r.first) && l.second < r.second );
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) {

        //1.把数据放入map中
        map<string, int> CountMap;
        for(auto& str : words){
            CountMap[str]++;
        }

        //2.因为sort算法不可以直接排序map(得是随机迭代器),其次sort算法并不是一个稳定的算法,联系快排的原理,要进行处理
        vector<pair<int,string>> v;
        for(auto& kv : CountMap){
            v.push_back(make_pair(kv.second, kv.first));
        }

        //3.利用stable_sort可以解决第一个问题
        //稳定的排序
        //stable_sort(v.begin(), v.end(), Compare()); 

        sort(v.begin(), v.end(), Compare());

        //4.创建一个返回的值
        vector<string> ret;
        for(int i = 0; i<k; ++i){
            ret.push_back(v[i].second);
        }

        return ret;
    }
};

349. 两个数组的交集 - 力扣(LeetCode)

解析 

比对算法逻辑

先排序 + 去重

相等就是交集值,然后同时++

否则就小的++,向后找

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        set<int> s1(nums1.begin(), nums1.end());
        set<int> s2(nums2.begin(), nums2.end());

        //比对算法逻辑
        //先排序 + 去重
        //相等就是交集值,然后同时++
        //否则就小的++,向后找
        auto it1 = s1.begin();
        auto it2 = s2.begin();


        vector<int> ret;
        while(it1 != s1.end() && it2 != s2.end()){
            if(*it1 > *it2)
                it2++;
            else if(*it1 < *it2)
                it1++;
            else{
                ret.push_back(*it1);
                it1++;
                it2++;
                }
        }

        return ret;
    }
};
;