zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++STL剖析(七)—— map和multimap的概念和使用

C++Map概念 剖析 STL 使用
2023-09-27 14:19:51 时间


1. map的介绍和使用

map 的介绍:

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

🍑 map的模板参数列表

如下图所示:

在这里插入图片描述

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

🍑 map的构造

主要有下面 3 种构造方式:

在这里插入图片描述

(1)指定 key 和 value 的类型构造一个空容器

// 构造一个key为string类型,value为int类型的空容器
map<string, int> m1;

(2)拷贝构造某类型容器

// 拷贝构造key为string类型,value为int类型的m1容器的复制品
map<string, int> m2(m1); 

(3)使用迭代器区间进行初始化构造

// 使用迭代器拷贝构造m2容器某段区间的复制品
map<string, int> m3(m2.begin(), m2.end());

🍑 map的使用

map 的接口虽然比较多,但是常用的也就那么几个。

🍅 insert

在 map 中插入键值对 x,注意 x 是一个键值对,返回值也是键值对

iterator 代表新插入元素的位置,bool 代表是否插入成功

在这里插入图片描述

在 map 的内部,key 与 value 通过成员类型 value_type 绑定在一起,为其取别名称为 pair

typedef pair<const key, T> value_type;

下面对于 insert 的插入有两种方式。

(1)使用 pair 直接来构造键值对

void testmap()
{
	map<string, string> dict;

	// 调用pair的构造函数,构造一个匿名对象插入
	dict.insert(pair<string, string>("sort", "排序"));
	dict.insert(pair<string, string>("root", "根"));
	dict.insert(pair<string, string>("left", "左边"));

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

可以看到插入的英文是按照升序进行排列的。

在这里插入图片描述

(2)使用 make_pair 函数来构造键值对

构造一个第一个元素设为 x,第二个元素设为 y 的pair对象。

模板类型可以从传递给 make_pair 的参数隐式推导出来。

如果各自的类型可以隐式转换,则可以从包含不同类型的其他 Pair 对象构造 Pair 对象。

在这里插入图片描述

代码示例

void testmap()
{
	map<string, string> dict;

	// 调用make_pair的构造函数,构造一个匿名对象插入
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("root", "根"));
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("up", "上面"));

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

推荐使用这种方式插入数据

在这里插入图片描述

(3)统计水果出现的次数

还记得在二叉查找树这篇文章种我们写了一个统计水果出现次数的代码吗?那么现在就可以用 map 来实现了

代码示例

void testmap()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

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

	// 遍历
	for (const auto& kv : countFruit)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
}

可以看到水果出现的次数已经被打印出来了

在这里插入图片描述

但是代码还可以进行优化,我们知道 insert 的返回值是 pair<iterator, bool>

  • 其中第一个成员 iteratorpair::first) 是指向新插入元素或映射中具有等效键的元素的迭代器。
  • 第二个成员 boolpair::second)是返回插入成功与否的结果
    • 若待插入元素的键值 key 在 map 当中不存在,则 insert 函数插入成功,并返回插入后元素的迭代器和 true。
    • 若待插入元素的键值 key 在 map 当中已经存在,则 insert 函数插入失败,并返回 map 当中键值为 key 的元素的迭代器和 false。

代码示例

void testmap()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	map<string, int> countFruit;
	for (auto& str : arr)
	{
		pair<map<string, int>::iterator, bool> ret = countFruit.insert(make_pair(str, 1));
		// auto ret = countFruit.insert(make_pair(str, 1)); // 也可以写成auto
		if (ret.second == false)
		{
			ret.first->second++; // ret.first是插入位置的迭代器,通过迭代器去访问second
		}
	}

	// 遍历
	for (const auto& kv : countFruit)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
}

运行可以看到是一样滴

在这里插入图片描述

🍅 operator[ ]

返回 key 对应的 value:

  • 如果 k 与容器中元素的键匹配,则函数返回对其映射值的引用。
  • 如果 k 与容器中任何元素的键不匹配,该函数将插入一个具有该键的新元素,并返回对其映射值的引用。

在这里插入图片描述

对这个函数的调用相当于:

(*((this->insert(make_pair(k,mapped_type()))).first))

operator[] 的原理就是:

  • <key, T()> 构造一个键值对,然后调用 insert() 函数将该键值对插入到 map 中
  • 如果 key 已经存在,插入失败,insert 函数返回该 key 所在位置的迭代器
  • 如果 key 不存在,插入成功,insert 函数返回新插入元素所在位置的迭代器
  • operator[] 函数最后将 insert 返回值键值对中的 value 返回

代码示例一

void testmap()
{
	map<string, string> dict;
	
	// 用make_pair函数来构造键值对
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("root", "根"));
	dict.insert(make_pair("left", "左边"));
	
	dict["up"] = "向上"; // up不存在,那么就插入up元素并修改(插入+修改)
	dict["left"] = "剩余"; // left存在,那么只修改(查找+修改)
	dict["erase"]; // erase不存在,那么插入
	
	// 遍历
	for (auto e : dict)
	{
		cout << e.first << ":" << e.second << endl;
	}
}

运行结果

在这里插入图片描述

对于上面统计水果的次数,其实比较常用的就是 operator[]

void testmap()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

	map<string, int> countFruit;
	for (auto& str : arr)
	{
		countFruit[str]++; 
	}

	// 遍历
	for (const auto& kv : countFruit)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
}

运行结果

在这里插入图片描述

如果水果是第一次出现,那么就先插入,因为是第一次出现,所以水果的次数为0,插入成功以后,会把插入的这个水果所在的节点里面的次数进行引用返回,也就是返回 0 的引用,那么此时进行 ++,变成 1

如果水果是第二次出现,那么不会插入成功,那么就直接返回这个水果所在的节点迭代器里面的次数,然后再进行 ++ 变成 2。

注意:这里的 [] 不支持随机访问!

🍅 find

在容器中搜索键值等于 k 的元素,如果找到则返回该元素的迭代器,否则返回 map::end 的迭代器。

在这里插入图片描述

代码示例

void testmap()
{
	map<string, string> dict;
	
	// 用make_pair函数来构造键值对
	dict["sort"] = "排序";
	dict["up"] = "向上";;
	dict["left"] = "左边";
	dict["root"] = "根";
	
	auto pos = dict.find("root");
	if (pos != dict.end())
	{
		cout << "找到了" << endl;
		cout << pos->first << ":" << pos->second << endl;
	}
}

运行结果

在这里插入图片描述

🍅 erase

从 map 容器中移除单个元素或一组元素。

在这里插入图片描述

(1)从 map 容器中移除单个元素

void testmap()
{
	map<string, string> dict;

	// 用make_pair函数来构造键值对
	dict["sort"] = "排序";
	dict["up"] = "向上";;
	dict["left"] = "左边";
	dict["root"] = "根";

	auto pos = dict.find("root");
	if (pos != dict.end())
	{
		dict.erase(pos);
		cout << "删除成功" << endl;
	}

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

运行结果

在这里插入图片描述

(1)从 map 容器中移除一组元素([first, last))

void testmap()
{
	map<string, string> dict;

	// 用make_pair函数来构造键值对
	dict["sort"] = "排序";
	dict["up"] = "向上";;
	dict["left"] = "左边";
	dict["root"] = "根";

	auto pos = dict.find("root");
	if (pos != dict.end())
	{
		dict.erase(pos, dict.end()); // 删除从pos位置开始后面所有的元素
		cout << "删除成功" << endl;
	}

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

可以看到 root 后面的元素已经被删掉了。

在这里插入图片描述

🍅 swap

交换 map 容器中的元素

在这里插入图片描述

代码示例

void testmap()
{
	map<string, string> dict1;
	dict1["sort"] = "排序";
	dict1["up"] = "向上";;
	dict1["left"] = "左边";
	dict1["root"] = "根";

	map<string, string> dict2;
	dict2["size"] = "大小";
	dict2["erase"] = "删除";
	dict2["clear"] = "清除";
	dict2["insert"] = "插入";

	dict1.swap(dict2); // 交换两个对象中的元素

	for (auto e1 : dict1)
	{
		cout << e1.first << ":" << e1.second << endl;
	}
	cout << endl;

	for (auto e2 : dict2)
	{
		cout << e2.first << ":" << e2.second << endl;
	}
}

可以看到两个对象的元素已经被交换

在这里插入图片描述

🍅 empty

检测 map 中的元素是否为空,是返回 true,否则返回 false。

在这里插入图片描述

代码示例

void testmap()
{
	map<string, string> dict1;
	dict1["sort"] = "排序";
	dict1["up"] = "向上";
	dict1["left"] = "左边";
	dict1["root"] = "根";

	map<string, string> dict2;

	cout << dict1.empty() << endl; // 不为空,返回false

	cout << dict2.empty() << endl; // 为空,返回true
}

运行结果

在这里插入图片描述

🍅 size

返回 map 中有效元素的个数

在这里插入图片描述

代码示例

void testmap()
{
	map<string, string> dict;
	dict["sort"] = "排序";
	dict["up"] = "向上";
	dict["left"] = "左边";
	dict["root"] = "根";

	cout << dict.size() << endl;
}

运行结果

在这里插入图片描述

🍅 count

获取 map 容器中指定 k 值的元素个数

在这里插入图片描述

代码示例

void testmap()
{
	map<string, string> dict;
	dict["sort"] = "排序";
	dict["up"] = "向上";
	dict["left"] = "左边";
	dict["root"] = "根";

	cout << dict.count("root") << endl; // root的个数
	cout << dict.count("hello") << endl; // hello的个数
}

运行结果

在这里插入图片描述

这个接口对于 map 容器其实没有太大用处,因为 map 当中的每个键值 key 和值 value 都是唯一的。

🍑 总结

  1. map 中的的元素是键值对
  2. map 中的 key 是唯一的,并且不能修改
  3. 默认按照小于的方式对 key 进行比较
  4. map 中的元素如果用迭代器去遍历,可以得到一个有序的序列
  5. map 的底层为平衡搜索树(红黑树),查找效率比较高 O ( l o g N ) O(logN) O(logN)
  6. 支持 [] 操作符,operator[] 中实际进行插入查找。

2. multimap的介绍和使用

multimap 的介绍:

  • multimaps 是关联式容器,它按照特定的顺序,存储由 key 和 value 映射成的键值对 <key, value>,其中多个键值对之间的 key 是可以重复的。
  • 在 multimap 中,通常按照 key 排序和唯一地标识元素,而映射的 value 存储与 key 关联的内容。key 和 value的类型可能不同,通过 multimap 内部的成员类型 value_type 组合在一起,value_type 是组合 key 和 value 的键值对:typedef pair<const Key, T> value_type;
  • 在内部,multimap 中的元素总是按照其内部比较对象(类型为 Compare)所指示的特定严格弱排序标准对 key 进行排序的。
  • multimap 通过 key 访问单个元素的速度通常比 unordered_multimap 容器慢,但是使用迭代器直接遍历 multimap 中的元素可以得到关于 key 有序的序列。
  • multimap 在底层用二叉搜索树(红黑树)来实现。

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

multimap 的模板参数列表如下:

在这里插入图片描述

🍑 multimap的使用

multimap 允许键值 key 冗余,即 multimap 容器当中存储的元素是可以重复的。

代码示例

void testmultimap()
{
	multimap<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("up", "向上"));
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("root", "根"));
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("root", "原因"));
	dict.insert(make_pair("hello", "您好"));

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

可以看到是存在多个相同元素的。

在这里插入图片描述

另外,它和 map 容器所提供的成员函数的接口都是基本一致的,所以就不全部列举了,只列举几个稍微有点小差别的函数接口。

🍅 find

在容器中搜索 val 元素,如果找到,则返回中序位置的第一个迭代器,否则返回 multimap::end 的迭代器。

在这里插入图片描述

代码示例

void testmultimap()
{
	multimap<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("up", "向上"));
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("root", "根"));
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("root", "原因"));
	dict.insert(make_pair("hello", "您好"));
	dict.insert(make_pair("root", "路径"));

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

	auto pos = dict.find("root"); // 多个root的话,返回中序第一个root

	// 打印pos位置后面的所有元素
	while (pos != dict.end())
	{
		cout << pos->first << ":" << pos->second << endl;
		++pos;
	}
}

可以看到确实是从第一个 root 开始打印的

在这里插入图片描述

🍅 count

在容器中搜索键值等于 k 的元素,并返回匹配的个数。

在这里插入图片描述

代码示例

void testmultimap()
{
	multimap<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("up", "向上"));
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("root", "根"));
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("root", "原因"));
	dict.insert(make_pair("hello", "您好"));
	dict.insert(make_pair("root", "路径"));

	// 统计root的元素个数
	cout << dict.count("root") << endl;

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

可以看到 root 出现了 3 次

在这里插入图片描述

🍑 总结

  1. multimap 中的 key 是可以重复的。
  2. multimap 中的元素默认将 key 按照小于来比较
  3. multimap中没有重载 operator[] 操作。
  4. 使用时与 map 包含的头文件相同。

3. 前K个高频单词

题目描述

在这里插入图片描述

解题思路

首先定义一个 countMap,用来统计每种单词出现的次数。

再使用一个 SortMap,按照单词出现次数 int 进行降序排列(multimap默认是按照从小到大进行排序,传了一个 greater 仿函数,就是按照从大到小进行排序)。

在这里插入图片描述

由于 map 的排序是稳定的,所以排序后如果不同的单词有相同出现频率,会自动按字典顺序排列,就不会改变相对位置。

代码实现

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        // 把单词和出现的次数放进map中
        // string就是words 
        // int就是words出现的次数
        map<string, int> countMap; 
        for (auto& str : words)
        {
            countMap[str]++;
        }

        // 排序
        // multimap默认是升序,传了仿函数,对int排降序
        multimap<int, string, greater<int>> sortMap;
        for (auto& kv : countMap)
        {
            // 因为是对int排序,所以此时int就是key,而words就是value
            // 那么此时单词出现的次数就按照从高到低排好了
            sortMap.insert(make_pair(kv.second, kv.first));
        }

        vector<string> ret; // 用来存放单词
        multimap<int, string>::iterator it = sortMap.begin();
        for (size_t i = 0; i < k; ++i)
        {
            // sortMap里面的first存放的是次数,second才是单词
            // 然后把words尾插进vector里面
            ret.push_back(it->second); 
            ++it;
        }
        return ret; // 返回结果
    }
};