C++进阶---Map和Set使用及模拟实现

1)引入

关联式容器与序列式容器: 关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高

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

pair

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

make_pair

make_pair:参考 make_pair

template <class T1,class T2>
pair<T1,T2> make_pair (T1 x, T2 y)
{
	return ( pair<T1,T2>(x,y) );
}

2)树形结构的关联式容器(STL分为树形结构和哈希结构)

①set

注意

注意

  1. 元素不可重复
  2. 在Set中key就是value,value也是key,set中只放value,但在底层实际存放的是由<value, value>构成的键值对
  3. RBTree作为底层容器
  4. 不允许迭代器修改Set的key/value,底层将其设置为了const(修改会造成迭代器失效),但是可以删除
  5. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则(默认按照小于)进行排序
  6. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代
  7. set中查找某个元素,时间复杂度为log2N

迭代器

与前面容器大同小异,参考Set
迭代器遍历:

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

构造 容量(略)

与前面容器大同小异,参考Set

修改

函数功能
insert插入元素
erase删除元素

关联式容器都是使用insert/erase进行操作而非push/pop
与前面容器大同小异,参考Set

操作

函数功能
find获取元素的迭代器
count计算具有特定值的元素的个数
lower_bound将迭代器返回到下限
upper_bound将迭代器返回到上限
equal_range获取相等元素的范围

find()返回值:如果找到元素,则返回该元素的迭代器,否则返回end()
count()只能返回0或1


相当于库中的swap和容器swap区别: 参考lower_bound upper_bound equal_range

在从小到大排序数组中:
upper_bound(begin,end,num)
lower_bound(begin,end,num)
在从大到小排序数组中(重载:需要传一个比较函数):
upper_bound(begin,end,num, greater< type>())
lower_bound(begin,end,num, greater< type>())


②multiset

注意

注意:

  1. 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
  2. 其他与set一致

迭代器 构造 容量 修改(略)

参考:multiset

操作

函数功能
find拿到查找元素的迭代器

由于允许重复 multiset的find找到的是中序遍历的第一个元素(底层为RBTree) 找到此元素以后,要继续看此元素的左孩子是不是同值元素,如果不是,就返回当前这个元素,如果是,继续取左边的同值元素,继续刚才的判断

验证
在这里插入图片描述
由于元素可以重复 这里用count可以计算个数

③map

注意

注意:

  1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素
  2. 在内部,map中的元素总是按照键值key进行比较排序的
  3. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)
  4. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value
  5. typedef pair<const key_type,mapped_type> value_type
  6. 底层是RBTree

构造 迭代器 容量 操作(略)

参考:map

修改

函数功能
insert插入元素

pair<iterator,bool> insert (const value_type& val)
insert返回值:

  1. 如果插入成功,将pair<iterator,bool>中的iterator设为该插入元素的迭代器,bool设为true
  2. 如果插入失败说明已存在此元素,将pair<iterator,bool>中的iterator设为已存在元素的迭代器,bool设为false

参考:insert

元素访问

函数功能
operator[]访问元素
mapped_type& operator[] (const key_type& k)
{
	pair<iterator, bool> ret = insert(make_pair(k, mapped_type()));
	return ret.first->second;
}

使用[]相当于: (*((this->insert(make_pair(k,mapped_type()))).first)).second
参考insert返回值,很好理解,这里[]重载已经改变了[]的意思了
这里的mapped_type()是value的匿名对象(缺省)(相当于第一次给0),如果value是string就会调用string的匿名对象,类比


返回值可以理解为:

  1. k不存在,插入默认构造函数生成缺省值的value的pair<K, V()>
  2. k存在,返回k对应的value值

例子:

map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
dict["string"] = "字符串"; // 先插入("string", ""),再修改value
dict["map"];              // 插入("map", "")
dict["map"] = "地图,映射"; // 查找+修改

统计一个数组中每个字符串出现的次数(三种)

1.普通方法

string str[] = { "sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" };
map<string, int> countMap;
for (auto& e : str)
{
	auto ret = countMap.find(e);
	if (ret != countMap.end())
	{
		//(*ret).second++;
		ret->second++; // ret->->second++;
	}
	else
	{
		countMap.insert(make_pair(e, 1));
		//countMap.insert(pair<string, int>(e, 1));
	}
}

2.使用insert

string str[] = { "sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" };
map<string, int> countMap;
for (auto& e : str)
{
	//pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(e, 1));
	auto ret = countMap.insert(make_pair(e, 1));
	// 插入失败,表示e对应字符串已经在map中了,++次数
	if (ret.second == false)
		ret.first->second++;
}

3. 使用operator[]

string str[] = { "sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" };
map<string, int> countMap;
for (auto& e : str)
	countMap[e]++;

迭代打印结果都一样
在这里插入图片描述

③multimap

注意

注意:

  1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的
  2. 注意multimap没有[]重载
  3. 其他和map相似

3)题目

leetcode: 前K个高频单词
题目:
给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字典顺序 排序。
分析:

  1. 容易想到用sort库函数进行对value,也就是次数,加入一个仿函数进行排序,但值得注意的是这里要求按照字典序(相对顺序不能改变),所以不稳定排序(sort是快排,不稳定)一律不能用,我们只能用冒泡,归并等稳定排序

思路1:

  1. 写一个稳定排序(较麻烦 略)

思路2:

  1. 上面讲的第三种统计次数,形成一个CountMap,
  2. 再创建一个SortMultiMap(用multimap是因为次数会可能相同),不用默认的less<Key>,加上greater<Key>形成逆序,(底层由于平衡树的旋转不会改变次数相同元素的相对顺序),
  3. 再压入ret前k个SortMultiMap中的元素
vector<string> topKFrequent(vector<string>& words, int k) {
	map<string, int> CountMap;
	 for(auto& e: words)
	     CountMap[e]++;
	 multimap<int, string, greater<int>> SortMultiMap;
	 for(auto& e : CountMap)
	 {
	     SortMultiMap.insert(make_pair(e.second, e.first));
	 }
	 vector<string> ret;
	 for(auto& e: SortMultiMap)
	 {
	     if(k==0)
	         break;
	     ret.push_back(e.second);
	     k--;
	 }
	 return ret;
}

4)模拟实现

set和map底层都是用红黑树封装的
在STL底层set的key_type是key,value_type也是key而map的value_type是pair< const Key T>

在这里插入图片描述在这里插入图片描述
红黑树模板参数Value统一接收se和map的Value
在这里插入图片描述
我们模拟的结构如下图:
在这里插入图片描述


①迭代器实现

本质就是一个树节点指针:Node* _node

template<class T, class Ref, class Ptr>
class RBTreeIterator
{
public:
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T, Ref, Ptr> Self;
	Node* _node;

重载* ->

Ref operator*()
{
	return _node->_data;
}

注意使用的时候,编译器会省略一个->

Ptr operator->()
{
	return &_node->_data;
}

重载++ - -

++it是找下一个比*it大的节点

  1. 如果it指向节点的右子树不为空间,下一个就+ +到右子树中序第一个节点,也就是右子树的最左节点
  2. 如果it指向节点右子树为空,下一个+ +要访问节点是,沿着it指向节点到根节点的路径中,孩子是父亲左的那个父亲节点
Self& operator++()
{
	if (_node->_right != nullptr)//右子树不为空,找右子树最左边的那个节点
	{
		Node* cur = _node->_right;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		_node = cur;
	}
	else//右子树为空,向上找 直到祖父节点的左节点是父节点
	{
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && parent->_right == cur)
		{
			cur = parent;
			parent = parent->_parent;
		}
		_node = parent;
	}
	return *this;
}

--it同理:略

②红黑树中的修改

节点

为了兼容map和set,pair<K,V>全部替换为T

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;

	Color _col;
	T _data;

	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(RED)
		, _data(data)
	{}
};

插入

在进行插入的时候,由于比较的是RBTree中的T,map传入的是pair<const K, V>,set传入的是K,不能进行比较,所以要在map和set的类里分别实现一个仿函数,同时传给RBTree的KetOfT


在map中:

//针对map和set T分别返回pair<K,V>中的K 或 直接返回K,这里是pair,返回比较的内容是pair的K
struct MapKeyOfT {
	const K& operator()(const pair<const K,V>& kv)
	{
		return kv.first;
	}
};

在set中:

struct SetKeyOfT
{
	const K& operator()(const K& key)
	{
		return key;
	}
};

先在insert函数里定义一个kot

KetOfT kot

在所有的节点大小比较时需要改为如下:

kot(cur->_data) < kot(data)

注意需要改变返回值,insert的返回值是一个pair<iterator,bool>

加入begin()和end()函数

begin找最左边的那个节点

iterator begin()
{
	Node* cur = _root;
	while (cur && cur->_left)
	{
		cur = cur->_left;
	}
	return iterator(cur);
}

end就是nullptr

iterator end()
{
	return iterator(nullptr);
}

③map

结构:

template<class K, class V >
class map {
	//针对map和set T分别返回pair<K,V>中的K 或 直接返回K,这里是pair,返回比较的内容是pair的K
	struct MapKeyOfT {
		const K& operator()(const pair<const K,V>& kv)
		{
			return kv.first;
		}
	};
public:
	//iterator
private:
	RBTree<K, pair<const K, V>,MapKeyOfT> _t;//私有一个RBTree类型
};

迭代器

直接复用RBTreeIterator迭代器,需要使用typename防止实例化,这里只是typedef声明类型

//直接复用RBTreeIterator迭代器,需要使用typename防止实例化,这里只是typedef声明类型
typedef typename RBTree<K, pair<const K, V>,MapKeyOfT>::iterator iterator;

iterator begin()
{
	return _t.begin();//复用RBTreeIterator的begin()
}

iterator end()//复用RBTreeIterator的end()
{
	return _t.end();
}

pair<iterator, bool> insert(const pair<const K, V>& kv)
{
	return _t.Insert(kv);
}

重载[]

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

注意:

  1. V()构造函数,自动调用V的默认构造函数初始化,int就会初始化为0
  2. second是pair<iterator, bool>中的iterator里的value (ret.first还是指针 需要用->,*(ret.first)才能用.)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值