C++——STL之map和set

👀先看这里👈
😀作者:江不平
📖博客:江不平的博客
📕学如逆水行舟,不进则退
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
❀本人水平有限,如果发现有错误的地方希望可以告诉我,共同进步👍

🏐序列式容器和关联式容器

C++ 容器大致分为 2 类,序列式容器和关联式容器。

  1. 序列式容器,其存储的都是 C++ 基本数据类型(诸如 int、double、float、string 等)或使用结构体自定义类型的元素。数据之间没有很强的关联性,就是挨着排。
  2. 关联式容器存储的元素,都是一个一个的“键值对”( <key,value> )。如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式,具有很强的关联性。也是因为这个特性,数据的增删查改相比于序列式容器来说效率更高。

🏀什么是键值对

是一种具有一一对应关系的结构,这种结构有两个成员变量,一个是键值key,另一个是与key对应的value

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

C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset。他们的底层结构都是红黑树,下面来看一下这些关联式容器吧。

🏐什么是set

以前学的一般都是两个参数,set是三个
在这里插入图片描述
可以看到这里给了一个Compare,和以前的学习优先级队列时的仿函数差不多,可以自己去写定规则去比较,比如T是自定义类型的时候,或者类如Date*这样的,自己需要去写一个比较规则,一个仿函数。
来看一下它的构造:
在这里插入图片描述
有一个全缺省构造,可以看到在构造这里也传这个仿函数进去,还有一个迭代区间构造,一个拷贝构造
这里拷贝构造是深拷贝,要拷贝一棵树,代价极大,所以如果不少特别需要,要谨慎拷贝

key模型支持增删查不支持改,因为本质是一棵搜索树,修改了就乱了。

void test_set()
{
	/*set<int> s;
	s.insert(4);
	s.insert(2);
	s.insert(1);*/
	//我们还可以迭代器区间初始化,如果底层是连续的物理空间,那么原生指针就可以当作天然的迭代器

    //当然还可以像下面这么写,list,vector都可以这么写,这是C++11里增加的列表初始化
	/*set<int> s = { 3, 2, 1, 6, 3, 7, 5 };*/
	
	我们可以用迭代器区间初始化如下面:
	int a[] = { 3, 2, 1, 6, 3, 7, 5 };
	set<int> s(a, a + sizeof(a) / sizeof(int));

这里set直接完成了排序和去重的功能,去重是间接作用出来的,因为这个地方3在第二次插入的时候发现前面插入过了,所以就没有继续插入。

// 排序 + 去重
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : s)//可以使用迭代器就可以使用范围for
	{
		cout << e << " ";
	}
	cout << endl;

如果说我们想实现降序的排序,我们怎么可以实现呢?第一种我们可以用反向迭代器,第二种我们可以直接去改变树,我们在初始化插入的时候,迭代器区间去初始化的时候我们这么写(用greater,但是在前面我们要加上#include< function > ):

	set<int, greater<int>> s(a, a + sizeof(a) / sizeof(int));

我们可以看到迭代器在此处的作用,这是一种设计模式,它访问list,map,set,vector容器,没有暴露容器的底层结构,迭代器的优点在于不需要关心底层实现,这是一种封装的体现,你不需要去关心它是链表,顺序表还是说是树,用一种方式做到全都访问。

构造主要支持的核心方式是
那我们现在遍历它主要支持的核心方式是什么呢

🏀erase

erase往往与find分不开,那么下面这两种删除方式有区别吗?

    s.erase(3);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	set<int>::iterator pos = s.find(3);
	s.erase(pos);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

答:都是删除3的话在这个序列s上是没区别的。但是如果是下面这样就得出的效果就不一样了

    s.erase(30);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	set<int>::iterator pos = s.find(30);
	s.erase(pos);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

第一个呢,仍然不会报错,但是第二个用迭代器的会报错,这是什么原因呢,原来find在没有找到目标值时会返回end,所以删除出了问题。那么我们可以改进一下代码,在查找后加入判断,如果没有找到目标则不进行删除,找到了再进行删除。如下:

    s.erase(30);
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	set<int>::iterator pos = s.find(30);
	if(pos!=s.end())
	{
	    s.erase(pos);
	}
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

传值传位置都可以,有什么区别呢?感觉传位置还麻烦一些啊,不如传值
确认值在不在就可以用第二种,不然就用删除值的,根据你的需求去选择合适的

🏀count

在这里插入图片描述

考虑这个数在不在的时候可以用count,不过不是为了这里设计的,为的是multiset,允许key值冗余,在这设计的原因是为了保持兼容一致。

🏀lower_bound&&upper_bound

lower_bound低边界的意思,要的是≥这个边界的值,upper_bound是高边界的意思,要的是<这个边界的值,可以说是一个左闭右开区间。测试代码如下:

void test_set2()
{
	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(25); // >= val
	itup = myset.upper_bound(60);  // > val             //
	cout << "["<<*itlow <<","<<*itup<<"]"<<endl;

	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';
	}

效果如下:
在这里插入图片描述

🏀equal_range

这里用到了pair键值对,详见下文里map部分内容。
如果所给数是35的话,则得到的是[40,40],如果是40的话,得到的是[40,50],由此可见区间左值要≥所给数,区间右值要>所给数。

void test_set3()
{
	std::set<int> myset;

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

	std::pair<std::set<int>::const_iterator, std::set<int>::const_iterator> ret;
	ret = myset.equal_range(40); // xx <= val < yy

	std::cout << "the lower bound points to: " << *ret.first << '\n';
	std::cout << "the upper bound points to: " << *ret.second << '\n';
}

🏐multipleset

真正的唯一区别就只有一个,允许键值冗余,用法上完全没有区别,会用set就会用multipleset

存储结构怎么存呢?
这里就不是遍历加去重了,单纯的排序

同时count和find也有点区别
例如find这里当有多个重复值时返回的是中序的第一个,count也不再只有0和1了,会显示个数,

🏐什么是map

在这里插入图片描述
底层存储的结构已经是pair了,pair是一个模板的键值对
pair的定义
在这里插入图片描述
第一个成员叫first第二个叫second
这两个的value type 的typedef是不一样的,pair的key对应的是const的key
在这里插入图片描述

make_pair就是构造一个匿名的pair返回,这样的话就可以去推导不用我们去写类型了,能方便一些。
拷贝和析构与set一样,底层是个树,代价还是挺大的
只要key有了就不允许插入,看key不看val,
make_pair的优势就是自动推导,好处就是不用显式的去写模板参数,那我们为什么不直接构造还调用这个函数,建立栈帧呢?不用担心,这个函数一般会定义成inline。

map的遍历
我们会发现报错,一个函数不会有两个返回值,当里面的数据是一个结构时
统计次数有这么几种方法
常规方法:
方括号:
这里的方括号与以前有些不同,以前相当于是给数组用的,用下标来随机访问,得益于底层空间是连续的,而这里是树形结构,肯定就不是来访问第几个了,这里是给一个key返回对应的value的引用,方括号一定程度上也代表了插入,如果说没有对应的映射类型,将会调用它的默认构造 (the element is constructed using its default constructor)

  • 1.map中有这个key,返回value的引用(查找,修改)
  • map中没有这个key,插入一个pair(key,value()),返回value的引用。(插入+修改)

跟at不一样,at只是查找加修改,如果没找到,就抛异常,而方括号的修改更加多元化。
以前我们写insert都是插入成功返回true,失败返回false,包括写二叉树搜索树都是,这里呢,是返回了个pair,
方括号的底层实现,相当于调用(*((this->insert(make_pair(k,mapped_type()))).first)).
在这里插入图片描述
上面那一大坨看起来很麻烦,我们先来看一下这里的insert
在这里插入图片描述
这里有个bool,就是说判断是否有这个key的,如果没有就插入

  • 1.key已经在map中,返回pair(key_iterator,false);
  • 2.key不在map中,返回pair(new_key_iterator,true);

了解了这些之后就可以把上面那个一大坨看起来很麻烦的简化一下

V& operator[const K & key]
{
	pair<iterator,bool> ret = insert(make_pair(key,valur());
    return  ret.first->second;
}

所以我们更喜欢用方括号,功能多样,增改都有了,比起insert更喜欢用方括号来插入

🏐multimap

与map相比,没有方括号了,是因为允许键值冗余。find查找的是中序的第一个,其他基本差不多。
auto 用的前提是推导,右边的值推到左边,如果不能就别用auto

🏐相关问题

🏀topk问题(前k个高频单词)

前k个高频单词
思考:这个问题可以看到要求双排序,我们可以先统计次数。统计完之后我们要考虑topk,很多人都会想到堆,就是优先级队列,建一个大堆,依次去取,取出来后放进去,或者说小堆,但是小堆主要应对大量数据,这地方没必要,这里用优先级队列的话,有一个问题是不能保证出现次数相同的谁在前谁在后,重要的是字典序排序也很难。用优先级队列可不可以搞,可以,但还要上其他的东西或者说搞仿函数。优先级队列默认传的是小于,碰到次数相等尝试去比较key。

class Solution {
public:
        struct Less
        {
            bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
            {
                if(kv1.second<kv2.second)
                {
                    return true;
                }
                if(kv1.second==kv2.second&&kv1.first>kv2.first)
                {
                    return true;
                }
                return false;
            }
        };
    vector<string> topKFrequent(vector<string>& words, int k) {
        //统计次数
        map<string,int> countMap;
        for(auto&str: words)
        {
            countMap[str]++;
        }
        //topk
        // priority_queue<pair<string,int>,vector<pair<string,int>>,Less<pair<string,int>>> maxHeap;
        // for<auto& kv: countMap)
        // {
        //     maxHeap.push(kv);
        // }
        typedef priority_queue<pair<string,int>,vector<pair<string,int>>,Less> MaxHeap;
        MaxHeap mh(countMap.begin(),countMap.end());

        vector<string> v;
        while(k--)
        {
            v.push_back(mh.top().first);
            mh.pop();
        }
        return v;

    }
};

发现通过仿函数的方式用优先级队列也可以完成。
那我们用其他的方法呢?sort是不稳定的,我们改变它的比较方法让其稳定了,和上面那个优先级队列没啥区别都用到了仿函数,只是下面这个地方我们用到了算法

class Solution {
public:
        struct Greater
        {
            bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
            {
                if(kv1.second>kv2.second)
                {
                    return true;
                }
                if(kv1.second==kv2.second&&kv1.first<kv2.first)
                {
                    return true;
                }
                return false;
            }
        };
    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int> countMap;
        for(auto&str: words)
        {
            countMap[str]++;
        }
        for(auto&kv:countMap)
        {
            cout<<kv.first<<":"<<kv.second<<endl;
        }
        vector<pair<string,int>> sortv(countMap.begin(),countMap.end());
        sort(sortv.begin(),sortv.end(),Greater());

        vector<string> v;
        for(size_t i=0;i<k;++i)
        {
            v.push_back(sortv[i].first);
        }
        return v;

    }
};

1.这个地方sort也可以换成stable_sort,这样仿函数就不用多比较一下。2.算法需要随机迭代器所以这个地方用vector转了一下。
那如果不用仿函数可以做吗?我们要进行排序,除了考虑排序算法我们还可以考虑map和set,只不过是对key排序,但是这里不能用map是因为,map是去重+排序,所以要用multimap

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
    //统计次数,默认按string去排序
        map<string,int> countMap;
        for(auto&str: words)
        {
            countMap[str]++;
        }
        multimap<int,string,greater<int>> sortMap;
        for(auto&kv:countMap)
        {
            sortMap.insert(make_pair(kv.second,kv.first));
        }

        vector<string> v;
        multimap<int,string,greater<int>>::iterator it=sortMap.begin();
        for(size_t i=0;i<k;++i)
        {
            v.push_back(it->second);
            ++it;
        }
        return v;

    }
};

注意这里它是中序来比较,有的人觉得我这里排降序,然后用反向迭代器也可以,但是在multimap时已经按字典序排过了,倒着取就不对了。
这个地方同stablesort是一样的,保证稳定是保证次数相同的稳定。

🏀两个数组的交集

两个数组的交集
判断在不在另一个数组里呢?肯定是用set更好,因为它快啊,比我们直接去用find快,算法里也有find但是是暴力查找,复杂度为O(N),另一个是O(lgN)。
放进set去重,记住!!搞交集一定要去重,因为如下两个数组nums1 = [1,2,2,1], nums2 = [2,2],当2去第一个数组里查时发现重复所以放到vector里,那当第二个2去查时也会发现重复又放到vector里,所以一定要去重!!
当然算法里也有个东西可以做到去重,叫unique,这是去重算法,你可以先sort一下,再unique。这个是没有删除元素的,是通过覆盖的方式,最后resize一下实现的,这也在刚学习数据结构时顺序表部分有类似操作。
这里我们搞个新思路,不用在不在的思想,假设已经去过重了,交集差集怎么得到呢?这里有个算法思想可以很好的解决这个问题。
两个下标位置依次走

找交集
1.小的++
2.相等的就是交集,同时++
3.其中一个结束了,交集就找完了
找差集
1.小的就是差集
2.相等就同时++
3.其中一个结束了,另一个剩下的部分就是差集

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> v;
        while(it1!=s1.end()&&it2!=s2.end())
        {
            if(*it1<*it2)
            {
                ++it1;
            }
            else if(*it2<*it1)
            {
                ++it2;
            }
            else
            {
                v.push_back(*it1);
                ++it1;
                ++it2;
            }
        }
        return v;

    }
};

💬总结

  • 找交集时一定要记得去重!!

在这里插入图片描述

觉得还不错的铁汁点赞关注一下吧😀
  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江不平

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值