[C++进阶] 19. map && set的使用

一. 序列式容器、关联式容器

1)序列式容器

  1. 序列式容器指的是逻辑结构为线性序列的数据结构,两个位置存储的值之间通常没有紧密的关系,随意交换一下,他们依旧是序列式容器。

  2. 顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。

  3. 比如我们前面已经了解过的一些容器:string, vector, list, forward_list, deque, array等。

2)关联式容器

  1. 也是用来存储数据的。

  2. 关联式容器的逻辑结构通常是非线性的,两个位置之间有紧密的关联关系,不能随意交换,否则会破坏他的存储结构。

  3. 关联容器中的元素是按关键字来保存和访问的。

  4. 比如map\set系列、unordered_map\unordered_set系列。

3) 1. map\set底层是红黑树,红黑树是一棵平衡二叉搜索树。

  1. set是key搜索场景的结构,map是key_value搜索场景的结构。

二. set 系列的使用

1)set类

  1. 类模板参数

① T就是set底层关键字的类型,即key

② set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数 传给第二个模版参数。

③ set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参

数。

④ 后两个参数有缺省值,通常不需要我们自己传。

2. 效率

set底层是用红黑树(平衡二叉搜索树)实现,增删查效率是O(logN)。

迭代器(set的迭代器是双向迭代器,不是随机迭代器)遍历走搜索树的中序,所以遍历结果是有序的。

3. 容器接口

set接口和之前的vector,list...的设计都是保持一致,高度相似的。我们就简单看一下个别重要接口的使用即可。set 去重+排序+默认升序。

insert

① 因为不允许冗余,所以set有去重 的功能。插入操作会检查每个插入的元素是否与容器中已有的某个元素等价,如果等价,则该元素不会被插入,而是返回一个指向此已存在元素的迭代器。

② set的迭代器不支持通过迭代器修改数据,因为修改关键字会破坏底层搜索树的结构。

cpp 复制代码
// 去重+降序排序
//set<int, greater<int>> s;

// 去重+升序排序
set<int> s;

s.insert(2);
s.insert(0);
s.insert(5);
s.insert(3);
s.insert(1);

// 迭代器遍历访问
set<int>::iterator it = s.begin();
while (it != s.end())
{
	//*it++; // 断言报错,无法解引用指针
	cout << *it << " ";
	it++;
}
cout << endl;

// 范围for遍历
for (const auto& e : s)
{
	cout << e << " ";
}
cout << endl;

③ 除了 s.insert(3); 这种普通的插入,还支持 initializer_list 插入一段数据。

④ string是按照ASCII码比较大小的。

cpp 复制代码
		// initializer_list插入
		set<int> s1;
		s1.insert({ 1,3,4,9,2,3,7,4 });

		for (const auto& e : s1)
		{
			cout << e << " ";
		}
		cout << endl;

		// set<string>中序遍历是按照字符串的ASCII码比较的
		set<string> s2 = { "hello", "world","laosi" };
		for (const auto& e : s2)
		{
			cout << e << " ";
		}
		cout << endl; // hello laosi world
erase && find && count

① 可以删除一个迭代器位置、删除一个值的位置、删除一段迭代器区间。

② 返回值部分,第一个和第三个返回的是一个迭代器,指向最后一个被移除元素后面的元素(如果最后一个元素被移除,则返回set::end);第二个返回的是正确删除的元素个数,val存在返回1,不存在返回0。

③ 第二个删除一个值的版本明明只有两种可能,删掉了或者没删掉(因为set不允许冗余)。返回值用bool就够了,为什么要返回一个整型呢?

其实是为了和multiset的接口保持一致,因为multiset允许冗余,可能会存在多个相同的值,返回值设计为整型,表示成功删除的个数。

① 在容器中搜索一个与 val 等价(equivalent)的元素。如果找到,返回指向该元素的迭代器;如果没找到,返回 set::end() 迭代器。

② set 是一个有序容器,它默认使用 less<key> 作为比较对象(即 < 运算符)。判断等价的标准是:对于两个元素 a 和 b,如果!(a<b) && !(b<a) 为真,即a 不小于 b并且 b 不小于 a,则 a 和 b 等价。也就意味着find匹配到了。

③ set自己实现的find和算法库里的find有什么区别?

算法库里的find是暴力查找,在迭代区间内挨个比对查找,效率是O(N);set自身实现的查找是平衡二叉搜索树中查找,效率是O(logN)。

① 如果只是单纯想看看某个值在不在set容器中,利用count是最快、最简单的方法。如果在count返回1,否则返回0。

cpp 复制代码
set<int> s1 = { 1,2,5,8,3,9,7,31 };
for (const auto& e : s1)
	cout << e << " ";
cout << endl; // 1 2 3 5 7 8 9 31

// 删除最小值
s1.erase(s1.begin()); // begin返回最左(即最小)结点,因为迭代器遍历走的是中序
for (const auto& e : s1)
	cout << e << " ";
cout << endl; // 2 3 5 7 8 9 31

// 直接给值删除
s1.erase(8);
for (const auto& e : s1)
	cout << e << " ";
cout << endl; // 2 3 5 7 9 31

// 边查边删,先查找值,再利用迭代器删除
int num = 0;
cout << "输入想要查找并删除的值: ";
cin >> num;
auto pos = s1.find(num);
if (pos != s1.end())
{
	// 说明找到了,删除它
	s1.erase(pos);
}
else  cout << num << " 不存在" << endl;
for (const auto& e : s1)
	cout << e << " ";
cout << endl;

set<int> s2 = { 1,2,5,8,3,9,7,31 };
// 算法库find: O(N)
auto pos1 = find(s2.begin(), s2.end(), 31);
cout << *pos1 << " ";
// set自己实现的find: O(logN)
auto pos2 = s2.find(5);
cout << *pos2 << endl;

// count实现快速查找是否存在
cout << "输入想确认是否存在的元素:";
cin >> num;
if (s1.count(num))
	cout << "存在" << endl;
else
	cout << "不存在" << endl;
lower_bound && upper_bound

lower_bound 和 upper_bound 常用于寻找适合大部分算法的左闭右开区间[first, last)的边界first和last。

cpp 复制代码
set<int> s1 = { 10,20,30,40,50,60,70,80,90 };
for (const auto& e : s1)
	cout << e << " ";
cout << endl; // 10 20 30 40 50 60 70 80 90

set<int>::iterator lowit = s1.lower_bound(32);
set<int>::iterator upit = s1.upper_bound(90);

s1.erase(lowit, upit);
for (const auto& e : s1)
	cout << e << " ";
cout << endl; // 10 20 30

算法库中也有lower_bound和upper_bound,但是使用前要先排序。

2)multiset类

  1. multiset没有自己的头文件,被包含在<set>中。

  2. multiset和set的使用基本完全类似,主要区别在于multiset支持值冗余 ,接口比如

insert/find/count/erase等也都围绕着支持值冗余有所差异。
3. 接口差异

① multiset只排序,不去重。

② find 时,可能存在多个匹配的值。find返回的是中序遍历的第一个值。

怎么算找到了中序的第一个值?搜索时找到第一个匹配的值就到他的左子树继续找还有没有,如果他的左子树在没有了,则这个位置为中序遍历的第一个匹配值。

③ count 此时就不只有看在不在的功能了,他返回的是容器中关键值实际的个数。

④ erase 在给值删除的时候,删除的是所有匹配的值(不是只删第一个)。如果想只删除中序遍历的第一个匹配值,可以先用find找到他的位置,再调用erase的指定迭代器位置删除,这个位置必须有效,否则程序崩溃。

cpp 复制代码
multiset<int> ms1;

// 只排序,不去重
ms1.insert({ 1,3,4,9,2,3,7,4,4 });
for (const auto& e : ms1)
	cout << e << " ";
cout << endl; // 1 2 3 3 4 4 4 7 9

// find查找中序遍历的第一个
int num = 0;
cout << "输入想查找的值:";
cin >> num; // 4
multiset<int>::iterator pos1 = ms1.find(num);
// 如果find找到的是中序遍历的第一个,那后续所有的num都能找出来
while (pos1 != ms1.end() && *pos1 == num)
{
	cout << *pos1 << " ";
	++pos1;
}
cout << endl; // 4 4 4 

// count返回实际个数
cout << num << "的个数:" << ms1.count(num) << endl; // 3

// 给值删除时,erase删除所有num
ms1.erase(num);
for (const auto& e : ms1)
	cout << e << " ";
cout << endl; // 1 2 3 3 7 9

// 删除中序第一个3
multiset<int>::iterator pos2 = ms1.find(3);
if (pos2 != ms1.end())
{
	// pos2必须是有效的迭代器
	ms1.erase(pos2);
}
for (const auto& e : ms1)
	cout << e << " ";
cout << endl; // 1 2 3 7 9

三. set相关算法题

  1. 两个数组的交集:https://leetcode.cn/problems/intersection-of-two-arrays/

  2. 环形链表Ⅱ:https://leetcode.cn/problems/linked-list-cycle-ii/

1)两个数组的交集

1. 题目描述

2. 解题思路

要求输出结果中每个元素都是唯一的,示例一中的情况如果一个一个对比,2会在结果集中出现四次,是不合理的。所以首先要对nums1和nums2去重,可以用算法库中的算法unique(要求被去重的数组有序,使用前要先对数组排序),我们这里使用set。

去重之后,遍历其中一个数组,到另一数组中查找是否存在,如果存在插入到我们的结果集中,不存在继续遍历,直到遍历结束。

3. 代码实现

cpp 复制代码
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());

        set<int>::iterator it = s2.begin();
        vector<int> v;

        while(it != s2.end())
        {
            auto pos = s1.find(*it);
            if(pos != s1.end())
            {
                // 找到了,是交集
                v.push_back(*pos);
            }
            it++;
        }
        return v;
    }
};

使用范围for会更简洁:

cpp 复制代码
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());

        vector<int> v;
        for(const auto& e : s1)
        {
            if(s2.count(e))
            {
                v.push_back(e);
            }
        }
        return v;
    }
};

2)环形链表Ⅱ

1. 题目描述

2. 解题思路

3. 代码实现

快慢指针

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

ListNode* detectCycle(ListNode* head) 
{
    // 寻找相遇点
    ListNode *fast, *slow;
    fast = slow = head;

    while (fast && fast->next) 
    {
        fast = fast->next->next;
        slow = slow->next;

        if (fast == slow) 
        {
           ListNode* newhead = head;
           while (fast != newhead) 
           {
                newhead = newhead->next;
                fast = fast->next;
           }
           return newhead;
        }
    }
    return NULL;
}
cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */

class Solution 
{
public:
    ListNode *detectCycle(ListNode *head) 
    {
        // 方法一:快慢指针

        // 方法二:向容器中插入结点的指针,边插入边查找是否已经存在这个结点
        // 如果已经存在那么就是有环并且是入口点
        ListNode* cur = head;
        set<ListNode*> s;

        while(cur)
        {
            set<ListNode*>::iterator it = s.find(cur);
            if(it != s.end())
            {
                // 找到了
                return *it;
            }
            else
            {
                s.insert(cur);
                cur = cur->next;
            }
        }
        return nullptr;

        // 方法三:set不允许冗余,定义一个指针遍历链表,持续向set中插入
        // 如果有环一定会有重复值,就会插入失败
        // pair<iterator, bool>的第二个参数返回false
    }
};

先到后面pair类部分把pair的内部结构了解清楚再看这个方法。

cpp 复制代码
class Solution 
{
public:
    ListNode *detectCycle(ListNode *head) 
    {
        // 方法一:快慢指针

        // 方法二:向容器中插入结点的指针,边插入边查找是否已经存在这个结点
        // 如果已经存在那么就是有环并且是入口点
        
        // 方法三:set不允许冗余,定义一个指针遍历链表,持续向set中插入
        // 如果有环一定会有重复值,就会插入失败
        // pair<iterator, bool>的第二个参数返回false
        ListNode* cur = head;
        set<ListNode*> s;
        while(cur)
        {
            pair<set<ListNode*>::iterator, bool> ref = s.insert(cur);
            if(ref.second == false) return cur;
            cur = cur->next;
        }
        return nullptr;
    }
};

四. map系列的使用

1)pair类

  1. map底层红黑树的结点中存储的就是键值对数据pair<Key, T>。

  2. 类内有两个成员变量,first就是键值对的第一个值,second是第二个。

  3. pair类型不支持流插入流提取。pair类使用struct定义的,全公有。如果想打印直接解引用提取first和second即可。

cpp 复制代码
	//typedef pair<const Key, T> value_type;

	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)
		{}

		// 拷贝构造模板化,实现类型转换
		template<class U, class V>
		pair(const pair<U, V>& pr) : first(pr.first), second(pr.second)
		{}
	};

	// make_pair函数,便捷地创建pair对象,并自动推导类型
	template <class T1, class T2>
	inline pair<T1, T2> make_pair(T1 x, T2 y)
	{
		return (pair<T1, T2>(x, y));
	}

2)map


  1. map是一种关联容器,它存储由键值Key和映射值T组合而成的元素pair<const Key, T>,并遵循特定的顺序Compare。

  2. 在map中,键值通常用于对元素进行排序和唯一标识(键值不允许冗余,映射值可以),而映射值则存储与该键关联的内容。

键和映射值的类型可以不同,它们被组合在成员类型 value_type 中,该类型是一个组合了两者的键值对(pair)类型:typedef pair<const Key, T> value_type;

  1. 在内部,map中的元素始终根据其键的大小进行排序,排序依据内部比较对象(类型为 Compare)指定的排序准则。默认要求Key支持小于比较,如果不支持,或者默认的比较逻辑不符合需求的话,可以自己实现仿函数并传给第三个模板参数。

  2. map底层 存储数据的内存是从空间配置器申请的。

  3. 一般情况下,我们不需要传后两个参数。

  4. map底层是用红黑树实现的,增删查改(Key不允许修改,但是T可以)的效率都是O(logN)。

  5. 迭代器(双向迭代器)遍历走的是中序,是按key有序顺序遍历的。

8. 接口

insert && 迭代器
cpp 复制代码
// 简单字典
map<string, string> dict;

// 定义有名对象插入
pair<string, string> pa1("sort", "排序");
dict.insert(pa1);

// 匿名对象
dict.insert(pair<string, string>("left", "左边"));

// 隐式类型转换,花括号里的内容直接转换成pair<const string,string>
// 因为insert这个接口要的是pair<const string,string>,不是pair<string,string>
dict.insert({ "right", "右边" });

// 迭代器遍历
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
	// map没有重载流插入和流提取,但可以直接提取pair的成员
	cout << (*it).first << " : " << (*it).second << endl;
	// 迭代器对象能直接用->访问成员,是因为迭代器类重载了operator->
	// 返回指向元素的指针,然后编译器自动用这个指针去访问成员
	cout << it->first << " : " << it->second << endl; // 省略一个->
	it++;
}
cout << endl;

// 范围for遍历
for (const auto& e : dict)
{
	cout << e.first << " : " << e.second << endl;
}
cout << endl;
※ operator[]
  1. 功能

① 传Key,返回value的引用 (说明可修改)

② 若Key不存在,先插入并把value用他的默认构造初始化

  1. operator[] 内部实现

operator[]是借助insert实现的:

返回值:返回一个 pair。

① iterator:其成员 pair::first 是一个迭代器,指向新插入的元素 或 你要插入的键map中已经存在了,就指向他;

② bool:如果插入了新元素,键值对中的 pair::second 成员被设置为 true;否则,如果已存在等效键,则设置为 false。

③ 一定要分清这部分有两个pair,一个是我们要插入的pair<key,value>。另一个是insert的返回值pair<iterator,bool>

① 如果k不在map中,insert会插入k和mapped_type默认值。同时[ ]返回结点中存储的

mapped_type值的引用,那么我们可以通过引用修改映射值。所以[ ]具备了插入+修改的功能。

② 如果k在map中,insert会插入失败,但是insert返回的 pair对象的first(iterator)是指向key结点的迭代器,返回值同时返回结点中存储mapped_type值的引用,所以[ ]具备了查找+修改的功能。

cpp 复制代码
	mapped_type& operator[] (const key_type& k)
	{
		pair<iterator, bool> ret = insert({ k, mapped_type() });
		iterator it = ret.first;
		return it->second;
	}
  1. make_pair

① make_pair是一个函数模板,相比于定义有名、匿名对象,make_pair的参数是自动推导出来的,不用传。

② 它的作用就是构造一个 pair 对象,将其第一个元素设置为 x,第二个元素设置为 y。

模板参数类型可以从传递给 make_pair 的参数中推断出来。如果相应的类型可以隐式转换,则可以从包含不同类型的对对象构造对对象。


  1. 应用 -- 插入+修改的功能、统计次数
cpp 复制代码
	void test02() // 统计次数 -- find(查找)+ 迭代器(修改)
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", 
			"苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
		map<string, int> countmap;

		for (const auto& str : arr)
		{
			map<string, int>::iterator pos = countmap.find(str);
			if (pos != countmap.end())
				// 已经存在,++其value
				++pos->second;
			else
				// 还不存在,插入并将值初始化为1
				countmap.insert({ str, 1 });
		}

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

	void test03() // 统计次数 -- operator[]
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果",
	"苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
		map<string, int> countmap;

		for (const auto& str : arr)
		{
			countmap[str]++; // 这种写法更简洁
		}

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

3)multimap

  1. mutlimap没有自己的头文件,和map都在<map>中。

  2. multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余 ,那么

insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全一样。⽐如find时,有多个key,返回中序第⼀个。

3. 和map的接口差异

① 不支持[],因为支持key冗余,[]就只能支持插入,不能支持修改。无法实现。

② insert可以插入多个Key值相同的元素。

③ count:传Key值,返回有几个。

④ erase:给key,把所有的都删了。

⑤ equal_range(用的很少):找一段Key相等的区间,并将[begin, end)这段区间的起始和结束迭代器组合到一个键值对pair<iterator, iterator>中返回。

cpp 复制代码
multimap<string, string> dict;

dict.insert({ "end", "结束" });
dict.insert({ "sort", "排序0" });
dict.insert({ "sort", "排序1" });
dict.insert({ "sort", "排序2" });
dict.insert({ "start", "开始" });
dict.insert({ "sort", "排序3" });
dict.insert({ "sort", "排序4" });
dict.insert({ "left", "左边" });
dict.insert({ "right", "右边" });

//dict.erase("sort");

//pair<multimap<string, string>::iterator, multimap<string, string>::iterator> itpair = dict.equal_range("sort");
//multimap<string, string>::iterator it = itpair.first; // 相当于拿到迭代区间的begin()
auto itpair = dict.equal_range("sort");
auto it = itpair.first;

while (it != itpair.second) // equal_range返回的迭代器指向pair<key,value>
{
	cout << it->first << " " << it->second << endl;
	++it;
}
cout << endl;

五. map相关算法题

  1. 随机链表的复制:https://leetcode.cn/problems/copy-list-with-random-pointer/

  2. 前K个高频单词:https://leetcode.cn/problems/top-k-frequent-words/

1)随机链表的复制

1. 题目描述

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        
    }
};

2. 解题思路

① 通过map建立原链表与拷贝链表之间的映射关系,只要找到原链表中结点就能找到其对应的拷贝结点。

② 那么设置random时,以cur->random作为key,就能得到copycur->random。(cur是原链表中当前节点,copycur是当前节点在拷贝链表中对应位置的拷贝节点)。

3. 代码实现

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution 
{
public:
    // 通过map建立原链表与拷贝链表之间的映射关系
    // 只要找到原链表中结点就能找到其对应的拷贝节点
    // 那么cur->random作为key,就能得到copycur->random
    Node* copyRandomList(Node* head) 
    {
        // 先遍历一遍:建立链表,建立映射关系
        Node* cur = head;
        Node* phead = nullptr;
        Node* ptail = nullptr;
        map<Node*, Node*> NNmap;

        while(cur)
        {
            // 复制结点
            if(ptail == nullptr)
            {
                phead = new Node(cur->val);
                ptail = phead;
            }
            else
            {
                ptail->next = new Node(cur->val);
                ptail = ptail->next;
            }
            // 建立映射
            NNmap[cur] = ptail; // []:插入+修改
            cur = cur->next;
        }

        // 遍历第二遍:设置random指针
        cur = head;
        Node* pcur = NNmap[cur];

        while(cur)
        {
            pcur->random = NNmap[cur->random];
            cur = cur->next;
            pcur = pcur->next;
        }

        return phead;
    }
};

2)前K个高频单词

1. 题目描述

cpp 复制代码
class Solution 
{
public:
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        
    }
};

2. 解题思路

① 在map的operator[]部分我们就了解过 [] 统计个数非常方便,所以我们直接用map的[]统计单词出现频率,map底层是红黑树,一种二叉平衡搜索树,所以在插入的过程中他就已经根据Key值,也就是string类型的单词按ASCII码,也就是字典序,排过序了。

② 这意味着如果几个单词出现的频率是相同的他们之间的相对顺序不能在改变,此刻就已经是我们期望的:

③ 将map中已经按字典序排好的数据导入到vector中排序,排序需要自己实现仿函数:按照value值比较,排降序(大的在前面)。并且如果有频率相等的单词,不改变原本的字典序(sort不稳定,会改变原本的相对顺序,想不改变又想用sort就只能在仿函数中控制)。

  1. sort需要随机迭代器,但是map是双向迭代器,不能用,解决方法就是把map的数据导入到vector中排序。

  2. sort可以为vector<pair<string, int>>类型排序,因为std::pair已经定义了 < 运算符,其比较规则是:

① 先比较first元素

② 如果first相等,再比较second元素

因此sort默认会按照字符串优先,然后整数的字典序排序。不符合我们的需求,我们是想按second(出现频率)比较,所以必须自己通过仿函数控制比较逻辑。

④ 排序部分如果不想在仿函数中控制字典序问题,可以使用一种稳定的排序方法,算法库中实现了稳定的排序:stable_sort。

⑤ 排好序后,频率最高(如果频率相同按字典序排列)的前K个单词已经在最前面了,取出来返回即可:循环k次,取前k个依次尾插到一个vector中返回。

3. 代码实现

① 用仿函数控制字典序

cpp 复制代码
class Solution 
{
public:
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        // 用map的[]直接统计次数
        map<string, int> countmap;
        for(const auto& word : words)
            countmap[word]++;

        // 将map中已经按字典序排好的数据导入到vector中排序
        // 排序需要自己实现仿函数:
        // 按照value值比较排降序(大的在前面),并且不改变原本的字典序
        class compare
        {
        public: // 千万别忘加,class内部默认私有,或者直接用struct定义
            bool operator()(const pair<string, int>& x, const pair<string, int>& y) const
            {
                return x.second > y.second || (x.second == y.second && x.first < y.first); 
            }
        };

        vector<pair<string, int>> v_kv(countmap.begin(), countmap.end());
        sort(v_kv.begin(), v_kv.end(), compare()); // sort不稳定,且默认排升序

        // 循环k次,取前k个 -- value值也就是出现频率最高的k个
        // 依次尾插到一个vector中返回即可
        vector<string> v_str;
        for(size_t i = 0; i < k; i++)
        {
            v_str.push_back(v_kv[i].first);
        }
        return v_str;
    }
};

② 用stable_sort控制字典序

cpp 复制代码
class Solution 
{
public:
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        // 用map的[]直接统计次数
        map<string, int> countmap;
        for(const auto& word : words)
            countmap[word]++;

        // 将map中已经按字典序排好的数据导入到vector中排序
        // 排序需要自己实现仿函数:
        // 按照value值比较排降序(大的在前面),并且不改变原本的字典序
        struct compare
        {
            bool operator()(const pair<string, int>& x, const pair<string, int>& y) const
            {
                return x.second > y.second; // stable_sort版
            }
        };

        vector<pair<string, int>> v_kv(countmap.begin(), countmap.end());
        stable_sort(v_kv.begin(), v_kv.end(), compare()); // 稳定,不改变原本的的相对顺序

        // 循环k次,取前k个 -- value值也就是出现频率最高的k个
        // 依次尾插到一个vector中返回即可
        vector<string> v_str;
        for(size_t i = 0; i < k; i++)
        {
            v_str.push_back(v_kv[i].first);
        }
        return v_str;
    }
};
相关推荐
阿贵---2 小时前
模板编译期循环展开
开发语言·c++·算法
ysa0510302 小时前
二分+前缀(预处理神力2)
数据结构·c++·笔记·算法
2401_833197732 小时前
嵌入式C++电源管理
开发语言·c++·算法
灰色小旋风2 小时前
力扣22 括号生成(C++)
开发语言·数据结构·c++·算法·leetcode
寒月小酒2 小时前
3.23 OJ
数据结构·c++·算法
2501_924952692 小时前
模板编译期哈希计算
开发语言·c++·算法
xiaoye-duck2 小时前
C++ STL map 系列深度解析:从底层原理、核心接口到实战场景
开发语言·c++·stl
2201_758642642 小时前
嵌入式C++开发注意事项
开发语言·c++·算法
小龙报3 小时前
【数据结构与算法】栈和队列的综合应用:1.用栈实现队列 2.用队列实现栈 3.设计循环队列
c语言·数据结构·数据库·c++·redis·算法·缓存