【C++进阶:map和set】

本节涉及到的所有代码见以下链接,欢迎参考指正!

​​​​​​​

practice: 课程代码练习 - Gitee.comhttps://gitee.com/ace-zhe/practice/tree/master/map%E5%92%8Cset

​​​​​​​

关联式容器

在C++初阶阶段,已经学习并总了STL中的部分容器,比如:vector、list、deque、forward_list等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是结构的键值对,在数据检索时比序列式容器效率更高。

键值对:

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代 表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
SGI-STL中定义一个pair结构来定义键值对,如下:
template 
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)
   {}
};

树形结构的关联式容器:

根据应用场景的不同,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结
构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一 个器。

set

翻译:
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中存储的是真正的键值对,set中只放
value,但在底层实际存放的是由构成的键值对。
2. set中插入元素时,只需要插入value即可,不需要构造键值对。
3. set中的元素不可以重复(因此可以使用set进行去重),其功能理解为排序+去重
4. 使用set的迭代器遍历set中的元素,可以得到有序序列
5. set中的元素默认按照小于来比较
6. set中查找某个元素,时间复杂度为:log_2 n
7. set中的元素不允许修改,修改之后将会影响原搜索二叉树的结构

set的用法:

有之前使用容器的基础,上手使用set其实并不困难,这里就不详细说明,有需要可以阅读set相关文档,链接如下:

https://cplusplus.com/reference/set/set/?kw=sethttps://cplusplus.com/reference/set/set/?kw=set这里给出几个较为重要的用法:

set迭代器的使用:

set的迭代器是双向迭代器,利用迭代器可实现元素的遍历,也可利用范围for(本质一样)如下:

void set_test1()
{
	std::set s1;
	s1.insert(2);
	s1.insert(4);
	s1.insert(3);
	s1.insert(6);
	s1.insert(6);
	s1.insert(1);
	std::set::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << endl;
		it++;
//搜索树key值不可变,即不能对*it进行修改
	}
	cout << endl;

	for (auto ch : s1)
	{
		cout << ch << endl;
	}
}

set实际上的功能可描述为:排序+去重 ,测试结果如下:

【C++进阶:map和set】_第1张图片

 特殊成员函数count的用法:

与之前容器相比,set有一个新增的特殊成员函数count(),set中可以利用其判断某个元素是否存在,相较于用迭代器更加方便,如下:

void set_test2()
{
	std::set s1;
	s1.insert(2);
	s1.insert(4);
	s1.insert(3);
	s1.insert(6);
	s1.insert(6);
	s1.insert(1);
	std::set::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	int x = 0;
	while (cin >> x)
	{
//使用迭代器+find的方法
		/*auto ret = s1.find(x);
		if (ret != s1.end())
		{
			cout << "在" << endl;
		}
		else
		{
			cout << "不在" << endl;
		}
		cout << endl;*/
//使用count的方法
		if (s1.count(x))
		{
			cout << "在" << endl;
		}
		else
		{
			cout << "不在" << endl;
		}
		cout << endl;
    }
}

使用方法是,如果存在某个元素,函数返回1,否则返回0,测试结果如下: 

【C++进阶:map和set】_第2张图片

 multiset

与set的用法几乎一样,唯一的区别在于,multiset允许键值冗余,即允许相同元素存在,相同元素怎么插入取决于底层的实现,后面讲到底层原理会详细说明:

multiset的用法:

multiset迭代器的使用:

multiset迭代器的使用与set一样

void multiset_test1()
{
	std::multiset s1;
	s1.insert(2);
	s1.insert(4);
	s1.insert(3);
	s1.insert(6);
	s1.insert(6);
	s1.insert(1);
	std::multiset::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << endl;
		it++;
	}
	cout << endl;
}

set实际上的功能可描述为:排序 ,测试结果如下:

【C++进阶:map和set】_第3张图片

特殊成员函数count的用法:

与set不同的是:multiset的count不仅可以用于判断某一元素是否存在,还可以得到相应元素的个数,如下:

void multiset_test2()
{
	std::multiset s1;
	s1.insert(2);
	s1.insert(4);
	s1.insert(3);
	s1.insert(6);
	s1.insert(6);
	s1.insert(1);
	s1.insert(1);
	s1.insert(1);

	std::multiset::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	int x = 0;
	while (cin >> x)
	{
		if (s1.count(x))
		{
			cout << "在" << endl;
		}
		else
		{
			cout << "不在" << endl;
		}
		cout << s1.count(x) << endl;

		cout << endl;
	}
}

测试结果如下: 

【C++进阶:map和set】_第4张图片在查找的元素不止一个时,找到的是中序遍历结果中第一个该元素,验证如下:

void multiset_test3()
{
	std::multiset s1;
	s1.insert(2);
	s1.insert(4);
	s1.insert(3);
	s1.insert(6);
	s1.insert(6);
	s1.insert(1);
	s1.insert(1);
	s1.insert(1);

	std::multiset::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	int x = 0;
	while (cin >> x)
	{
		auto ret = s1.find(x);
		while (ret != s1.end() && *ret == x)
		{
			cout << *ret << " ";
			ret++;
		}
		cout << endl;
		cout << endl;
	}
}

 测试结果如下:

【C++进阶:map和set】_第5张图片

 map

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

 map的用法:

map同样与其它容器的使用无异,这里只对重要特殊的用法进行说明,更详细的用法见以下文档链接:

https://cplusplus.com/reference/map/map/?kw=maphttps://cplusplus.com/reference/map/map/?kw=map

map的插入:

我们知道,map的键值对以结构pair的形式存在,因此插入的时候应该插入一个pair结构,可以插入一个pair的匿名对象,但写完整类型比较麻烦,这里直接用一个模板函数make_pair来替代,实际上,make_pair就是对构造匿名对象的封装,它可以自动识=根据传入的参数类型来实例化对应的函数对象,不用自己写清楚这里绝大多数情况会被编译器处理为内联函数,因此不用考虑效率的问题,具体如下:

void map_test1()
{
	std::map m1;
    //m1.insert(pair("world", "世界"));
	m1.insert(make_pair("world", "世界"));
	m1.insert(make_pair("char", "字符"));
	m1.insert(make_pair("string", "字符串"));
	m1.insert(make_pair("girl", "女孩"));
	auto dit = m1.begin();
	while (dit != m1.end())
	{
		//cout << (*dit).first << ":" << (*dit).second << endl;
		cout << dit->first << ":" << dit->second << endl;
		dit++;
	}

}

这里返回迭代器类型通过箭头可以指向一个pair结构类型的指针,访问结构体成员有两种方法,其一:解引用得到结构体通过.访问,其二: 直接用结构体指针通过箭头访问

【C++进阶:map和set】_第6张图片

 方括号[ ]的用法:

map的[ ]重载是其重要的用法之一,通过它能实现元素的插入、修改、插入并修改以及查找等功能,如下:

void map_test2()
{
	std::map m1;
	m1.insert(make_pair("world", "世界"));
	m1.insert(make_pair("char", "字符"));
	m1.insert(make_pair("string", "字符串"));
	m1.insert(make_pair("girl", "女孩"));
	m1.insert(make_pair("string", "(字符串)"));//key已经存在,插入失败
	auto dit = m1.begin();
	while (dit != m1.end())
	{
		//cout << (*dit).first << ":" << (*dit).second << endl;
		cout << dit->first << ":" << dit->second << endl;
		dit++;
	}
	cout << endl;
	m1["boy"];//插入
	m1["cup"] = "杯子";//插入+修改
	m1["string"] = "(字符串)";//修改
	cout << endl;
	cout << m1["string"]<

测试结果如下:

【C++进阶:map和set】_第7张图片

掌握[ ]的用法,就要学习其底层是如何实现的,查阅文档,重载[ ]具体实现分析如下:

 【C++进阶:map和set】_第8张图片

应用实例:统计水果次数,代码如下:

void map_test3()
{
	string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" };
	map countMap;

	//for (auto& e : arr)
	//{
	//	auto ret = countMap.find(e);
	//	if (ret == countMap.end())
	//	{
	//		countMap.insert(make_pair(e, 1));
	//	}
	//	else
	//	{
	//		ret->second++;
	//	}
	//}

	for (auto& e : arr)
	{
		countMap[e]++;//一句就等效于上述注释部分的代码功能
	}

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

}

 测试结果如下:

【C++进阶:map和set】_第9张图片

multimap

multimap中的接口可以参考map,功能都是类似的。
注意:
1. multimap中的key是可以重复的
2. multimap中的元素默认将key按照小于来比较
3. multimap中没有重载operator[]操作,因为multimap中的key值和value值不是一 一对应的
4. 使用时与map包含的头文件相同

multimap的用法:

除了方括号用法几乎与map用法一致,这里只做简单举例:

multimap的插入:

void multimap_test1()
{
	std::multimap m1;
	m1.insert(make_pair("world", "世界"));
	m1.insert(make_pair("char", "字符"));
	m1.insert(make_pair("string", "字符串"));
	m1.insert(make_pair("girl", "女孩"));
	m1.insert(make_pair("string", "(字符串)"));//即使key已经存在,也能成功插入,允许键值冗余
	auto dit = m1.begin();
	while (dit != m1.end())
	{
		//cout << (*dit).first << ":" << (*dit).second << endl;
		cout << dit->first << ":" << dit->second << endl;
		dit++;
	}
	cout << endl;
}

运行测试结果如下:

【C++进阶:map和set】_第10张图片

相关编程题练习

1.前K个高频单词:

题目链接:

力扣

题目描述:

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

示例:

【C++进阶:map和set】_第11张图片

 

题目分析:大思路可以分为两步,第一步为给次数排序,找出次数最多的前k个,第二步为给字符串排序,以实现出现相等次数字符串的字典序排序,由此推测我们可能会用到和排序相关的算法和容器。

题目实现:

1.用map定义一个map容器可以完成字符串出现次数的统计

2.用set,Compare>定义一个set容器通过自己实现Compare完成按出现次数从多到少排序(ps:Compare默认是按pair升序即字符串升序来排,但我们想要其按次数降序来排,因此要自己实现)

3.完成以上两步,我们就能得到次数最多的前k个字符串,而次数相等时按字符串字典序排序可以在Compare中控制

4.定义数组将multiset中的前k个pair的key值即字符串值尾插即可

class Solution {
public:
    vector topKFrequent(vector& words, int k) {
      //定义map统计各字符串出现的次数
      map countmap;
      for(auto& str:words)
      {
          countmap[str]++;
      }

      //定义multiset排序
      class Compare{
         public:
         bool operator()(const pair& left,const pair& right) const
         {
             return left.second>right.second||left.second==right.second&&left.first,Compare> sortset(countmap.begin(),countmap.end());
      //定义vector完成尾插
      vector result;
      auto it=sortset.begin();
      while(k--)
      {
        result.push_back(it->first);
        it++;
      }
      return result;
    }
};

除了选用具有排序功能的set容器来进行排序,还可以用库里已有的排序算法,但需要注意排序的稳定性(相同数据的处理),这里补充一个稳定的排序算法为stable_sort,用法如下:


class Solution {
public:
struct Compare{
         bool operator()(const pair& left,const pair& right) const
         {
             return left.second>right.second;
         }
      };
    vector topKFrequent(vector& words, int k) {
      //定义map统计各字符串出现的次数
      map countmap;
      for(auto& str:words)
      {
          countmap[str]++;
      }
      vector> v(countmap.begin(),countmap.end());
      stable_sort(v.begin(),v.end(),Compare());
      //定义vector完成尾插
      vector result;
      auto it=v.begin();
      while(k--)
      {
        result.push_back(it->first);
        it++;
      }
      return result;
    }
};

2.两数组交集问题

题目链接:

力扣

题目描述:

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以不考虑输出结果的顺序 。

示例:

【C++进阶:map和set】_第12张图片

题目分析:

题目整体分为两步,第一步:排序去重,第二步:同时依次遍历判断

题目实现:

首先用具有排序及去重功能set来对两数组排序去重,得到两组从小到大排序的数据,定义一个结果数组,再同时逐次相比较,两数组都从第一个数据开始比较,比较过程中,若相等则为交集,该数字入结果数组,下标均++;若不相等,则较小数字所在数组下标++,直至其中一个数组走完,实现如下:

class Solution {
public:
    vector intersection(vector& nums1, vector& nums2) {
      set s1(nums1.begin(),nums1.end());
      set s2(nums2.begin(),nums2.end());
      auto it1=s1.begin();
      auto it2=s2.begin();
      vector ret;
      while(it1!=s1.end()&&it2!=s2.end())
      {
        if(*it1==*it2)
        {
            ret.push_back(*it1);
            it1++;
            it2++;
        }
        else if(*it1<*it2)
        {
            it1++;
        }
        else{
            it2++;
        }
      }
      return ret;
    }
};

拓展:两数组差集问题

分析后发现找差集方法与交集正好相反,同样的,首先用具有排序及去重功能set来对两数组排序去重,得到两组从小到大排序的数据,定义一个结果数组,再同时逐次相比较,两数组都从第一个数据开始比较,比较过程中,若相等则一定不是,下标均++;若不相等,则较小数字一定为差集,入结果数组,所在数组下标++,直至其中一个数组走完,剩下一个数组中的剩余元素一定都是差集元素。

底层结构

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个 共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此 map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

AVL树:

内容较多且重要,单独整理了一篇文章,见以下链接:

http://t.csdn.cn/Evckrhttp://t.csdn.cn/Evckr

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合,因此引入红黑树。

红黑树:

内容较多且重要,单独整理了一篇文章,见以下链接:

http://t.csdn.cn/JyZk8http://t.csdn.cn/JyZk8

红黑树模拟实现STL中的mapset

STL库中是怎么用红黑树封装map和set的,我们参考STL源码中核心部分发现:封装map和set时用的是同一颗红黑树,这与我们之前练习时的做法不同,即没有设计出两种红黑树模板,而是通过增加模板参数在实际运用中只需传不同的模板参数就可用一份模板来实例化出相应的红黑树类型进而封装成不同类型的容器,实际上就是为了复用,如下图(不完整,仅是核心代码的截取):

【C++进阶:map和set】_第13张图片  说明:给红黑树设置两个模板参数,key_type和value_type,key_type就是两种容器的key值,仅用于拿到key值,用于find()和erase()等以key为参数的函数,而value_type,set依然指key值,map指的是pair结构,用于决定树的结点中存储的数据类型。

我们进一步来看库里的红黑树究竟是怎么实现的,set和map内部又分别是怎么样得到相应的红黑树,见下图(不完整,仅为核心代码的截取):

【C++进阶:map和set】_第14张图片

 map和set的封装:

首先要做的的就是根据源码中的设计来修改我们自己实现的红黑树结构,首先修改红黑树结点和红黑树的插入部分,修改后代码如下:

enum Color
{
	RED,
	BLACK
};
template//T就表示结点存值的类型
//红黑树结点的定义
struct RBTreeNode
{
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;//红黑树也涉及到旋转,因此给出父节点
	T _data;//表示结点值
	Color _col;

	//给一个构造函数
	RBTreeNode(T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)//为什么结点颜色默认给红色?
	{}

};
//红黑树的定义
template
class RBTree
{
	typedef RBTreeNode Node;
private:
	Node* _root = nullptr;
public:
bool Insert(const T& data)
	{
		//按照搜索二叉树的规则插入结点

		//如果根节点为空,说明第一次插入,那么构造一个新结点让根节点指向它,将其颜色设为BLACK,返回true即插入成功
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return true;
		}

		//根节点不为空,按照搜索二叉树规则插入数据
		Node* cur = _root;//cur是为了向下找到插入的位置
		Node* parent = nullptr;//parent是为了找到插入位置后与原树链接
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		//找到了插入的位置
		cur = new Node(data);
		if (cur->_data > parent->_data)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		//到此结点按照搜索二叉树的性质插入成功了,接下来就是调整为红黑树结构
//...下面的判断调整部分就先不再赘述...//
    }
};

这样改完,我们发现了一个问题:在比较_data、data等值时,由于红黑树不知道上一层的数据具体是什么,因此我们不能单纯的只是实现一种数据类型的比较,参考源码发现,它的解决方式是运用仿函数,红黑树不知道数据类型,但上一层的map和set知道,因此可以在上一层就取出需要参与比较的key值,定义相应红黑树的时候同时传参,红黑树再通过使用仿函数来实现相应的比较,具体修改如下:

//Set.h
namespace wz
{
	template
	class Set
	{
		struct SetKeyofT
		{
			const K& operator () (const K& key)
			{
				return key;
			}
	};
	public:
		bool Insert(const K& key)
		{
			return _t.Insert(key);
		}
	private:
		RBTree _t;
	};

	void settest()
	{
		Set s1;
		s1.Insert(2);
		s1.Insert(4);
		s1.Insert(3);
		s1.Insert(6);
		s1.Insert(6);
		s1.Insert(1);
		int x = 0;

	}
}

//Mpa.h
namespace wz
{
template
class Map
{
		struct MapKeyofT
		{
			const K& operator()(const pair& kv)
			{
				return kv.first;
			}
		};
public:
	bool Insert(const pair& kv)
	{
		return _t.Insert(kv);
	}
private:
	RBTree,MapKeyofT> _t;
};

void maptest()
{
	Map m1;
	m1.Insert(make_pair(2,2));
	m1.Insert(make_pair(4,4));
	m1.Insert(make_pair(3,3));
	m1.Insert(make_pair(6,6));
	m1.Insert(make_pair(6,6));
	m1.Insert(make_pair(1,1));
	int x = 0;
}
}

//RBTree.h
template
class RBTree
{
	typedef RBTreeNode Node;
public:
	//成员函数
	//析构
	~RBTree()
	{
		_Destory(_root);
		_root = nullptr;
	}
	//查找
	Node* Find(const K& key)
	{
		Node* cur = _root;
		KeyofValue kov;
		while (cur)
		{
			if (kov(cur->_data)< key)
			{
				cur = cur->_right;
			}
			else if (kov(cur->_data) > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}
	//插入
	bool Insert(T data)
	{
		//按照搜索二叉树的规则插入结点

		//如果根节点为空,说明第一次插入,那么构造一个新结点让根节点指向它,将其颜色设为BLACK,返回true即插入成功
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return true;
		}

		//根节点不为空,按照搜索二叉树规则插入数据
		Node* cur = _root;//cur是为了向下找到插入的位置
		KeyofValue kov;
		Node* parent = nullptr;//parent是为了找到插入位置后与原树链接
		while (cur)
		{
			if (kov(cur->_data) < kov(data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kov(cur->_data)>kov(data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		//找到了插入的位置
		cur = new Node(data);
		if (kov(cur->_data) > kov(parent->_data))
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		//到此结点按照搜索二叉树的性质插入成功了,接下来就是调整为红黑树结构
//。。。此处调整部分省略。。。
};

测试插入的功能如下,说明调整到目前为止是成功地:

settest():

【C++进阶:map和set】_第15张图片

maptest():

【C++进阶:map和set】_第16张图片 总结,我们发现C++中很喜欢把一些类型不确定的问题,用仿函数来解决,库里当然增加了一个仿函数Compare,与之前总结过的一样,可以自行用来控制比较方式,因此不作为重点再次总结,有需要的可以参考我之前的总结。

 map和set的迭代器如何实现

观察源码可以知道,map和set的迭代器其实就是对红黑树迭代器的封装,因此我们这里主要先实现一下红黑树的迭代器,实现方法其实和链表的迭代器类似,这里不再详细说明,直接提供代码,如有需要可以参考本人总结链表时相关的分析,红黑树迭代器实现的代码如下,重点部分都有注释标记:

红黑树迭代器的实现:

template
struct __RBTreeIterator
{
	typedef RBTreeNode Node;
	typedef __RBTreeIterator Self;

	Node* _node;

	__RBTreeIterator(Node* node)
		:_node(node)
	{}
	//  支持普通迭代器构造const迭代器的构造函数
	__RBTreeIterator(const __RBTreeIterator& it)
		:_node(it._node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}
	Ptr operator->()
	{
		return &_node->_data;
	}
	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	Self& operator++()
	{
//如果右树不为空,则说明下一个一定是右树的最左节点
		if (_node->_right)
		{
			Node* subleft = _node->_right;
			while (subleft->_left)
			{
				subleft = subleft->_left;
			}
			_node= subleft;
		}
//如果右树为空,则不断向上寻找孩子为左树的父节点
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent&&parent->_right == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	 }
	Self& operator--()
	{
//如果左树为空,则不断向上寻找孩子为右树的父节点
		if (_node->_left)
		{
			Node* subright = _node->_right;
			while (subright->_left)
			{
				subright = subright->_right;
			}
			_node = subright;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_left == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}
	

};

红黑树begin()、end()、const_begin()、const_end()的实现:

typedef __RBTreeIterator iterator;
typedef __RBTreeIterator const_iterator;

	iterator begin()
	{
		Node* cur = _root;
		while (cur&&cur->_left)
		{
			cur = cur->_left;
		}
		return iterator(cur);
	}
	iterator end()
	{
		return iterator(nullptr);
	}
	const_iterator begin()const
	{
		Node* cur = _root;
		while (cur && cur->left)
		{
			cur = cur->_left;
		}
		return const_iterator(cur);
	}
	const_iterator end()const
	{
		return const_iterator(nullptr);
	}

Map.h:

包括具体实现以及测试代码,都已经调试过,测试代码均能正常运行

namespace wz
{
template
class Map
{
public:
		struct MapKeyofT
		{
			const K& operator()(const pair& kv)
			{
				return kv.first;
			}
		};
		typedef typename RBTree, MapKeyofT>::iterator iterator;

	iterator begin()
	{
		return _t.begin();
	}

	iterator end()
	{
		return _t.end();
	}
	pair Insert(const pair& kv)
	{
		return _t.Insert(kv);
	}
	V& operator[](const K& key)
	{
		pair ret = _t.Insert(make_pair(key, V()));
		return ret.first->second;
	}
private:
	RBTree,MapKeyofT> _t;
};

void maptest1()
{
	Map m1;
	m1.Insert(make_pair(2,2));
	m1.Insert(make_pair(4,4));
	m1.Insert(make_pair(3,3));
	m1.Insert(make_pair(6,6));
	m1.Insert(make_pair(6,6));
	m1.Insert(make_pair(1,1));
	Map::iterator it = m1.begin();
	while (it != m1.end())
	{
		cout << it->first << ":" << it->second << endl;
		/*it->first = "1111";
		it->second = "111";*/

		++it;
	}
	cout << endl;

	for (auto& kv : m1)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;
}
void maptest2()
{
	string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" };
	map countMap;
	//for (auto& e : arr)
	//{
	//	auto ret = countMap.find(e);
	//	if (ret == countMap.end())
	//	{
	//		countMap.insert(make_pair(e, 1));
	//	}
	//	else
	//	{
	//		ret->second++;
	//	}
	//}

	for (auto& e : arr)
	{
		countMap[e]++;
	}

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

Set.h:

包括具体实现以及测试代码,都已经调试过,测试代码均能正常运行

namespace wz
{
	template
	class Set
	{
	public:
		
		struct SetKeyofT
		{
			const K& operator () (const K& key)
			{
				return key;
			}
	    };
		typedef typename RBTree::const_iterator iterator;
		typedef typename RBTree::const_iterator const_iterator;
		iterator begin()
		{
			return _t.begin();
		}

		iterator end()
		{
			return _t.end();
		}
		pair Insert(const K& key)
		{
			return _t.Insert(key);
		}
		
	private:
		RBTree _t;
	};

	void settest()
	{
		Set s1;
		s1.Insert(2);
		s1.Insert(4);
		s1.Insert(3);
		s1.Insert(6);
		s1.Insert(6);
		s1.Insert(1);
		Set::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

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

RBTree.h:

#pragma once
#include

using namespace std;
//红黑树基本结构--红黑树结点的定义
//因为相较于普通的搜索二叉树,红黑树每个节点增加了颜色的属性,且不是红色就是黑色,为此我们定义一个枚举结构来表示颜色
enum Color
{
	RED,
	BLACK
};
//类比之前的AVL树和搜索二叉树,我们仍然设置两个模板参数分别方便表示Key,和value的类型
template//T就表示结点存值的类型
//红黑树结点的定义
struct RBTreeNode
{
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;//红黑树也涉及到旋转,因此给出父节点
	T _data;
	Color _col;

	//给一个构造函数
	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)//为什么结点颜色默认给红色?
	{}

};
template
struct __RBTreeIterator
{
	typedef RBTreeNode Node;
	typedef __RBTreeIterator Self;

	Node* _node;

	__RBTreeIterator(Node* node)
		:_node(node)
	{}
	__RBTreeIterator(const __RBTreeIterator& it)
		:_node(it._node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}
	Ptr operator->()
	{
		return &_node->_data;
	}
	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	Self& operator++()
	{
		if (_node->_right)
		{
			Node* subleft = _node->_right;
			while (subleft->_left)
			{
				subleft = subleft->_left;
			}
			_node= subleft;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent&&parent->_right == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	 }
	Self& operator--()
	{
		if (_node->_left)
		{
			Node* subright = _node->_right;
			while (subright->_left)
			{
				subright = subright->_right;
			}
			_node = subright;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_left == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}
	

};

template
class RBTree
{
public:
	typedef RBTreeNode Node;
	typedef __RBTreeIterator iterator;
	typedef __RBTreeIterator const_iterator;

	iterator begin()
	{
		Node* cur = _root;
		while (cur&&cur->_left)
		{
			cur = cur->_left;
		}
		return iterator(cur);
	}
	iterator end()
	{
		return iterator(nullptr);
	}
	const_iterator begin()const
	{
		Node* cur = _root;
		while (cur && cur->left)
		{
			cur = cur->_left;
		}
		return const_iterator(cur);
	}
	const_iterator end()const
	{
		return const_iterator(nullptr);
	}
	//成员函数
	//析构
	~RBTree()
	{
		_Destory(_root);
		_root = nullptr;
	}
	//查找
	Node* Find(const K& key)
	{
		Node* cur = _root;
		KeyofValue kov;
		while (cur)
		{
			if (kov(cur->_data)< key)
			{
				cur = cur->_right;
			}
			else if (kov(cur->_data) > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}
	//插入
	pair Insert(const T& data)
	{
		//按照搜索二叉树的规则插入结点

		//如果根节点为空,说明第一次插入,那么构造一个新结点让根节点指向它,将其颜色设为BLACK,返回true即插入成功
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root),true);
		}

		//根节点不为空,按照搜索二叉树规则插入数据
		Node* cur = _root;//cur是为了向下找到插入的位置
		KeyofValue kov;
		Node* parent = nullptr;//parent是为了找到插入位置后与原树链接
		while (cur)
		{
			if (kov(cur->_data) < kov(data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kov(cur->_data)>kov(data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return make_pair(iterator(cur),false);
			}
		}
		//找到了插入的位置
		cur = new Node(data);
		Node* newnode = cur;
		if (kov(cur->_data) > kov(parent->_data))
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		//到此结点按照搜索二叉树的性质插入成功了,接下来就是调整为红黑树结构

		//parent一定存在,当parent不为空且是红色结点,说明它不是根节点,就需要向上更新,是parent是黑色则跳过循环跳过直接返回true即可
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//分parent是grandfather的左孩子和右孩子两种情况来讨论
			if (grandfather->_left == parent)
			{
				//定义grandfather的右孩子为uncle
				Node* uncle = grandfather->_right;
				//情况一
				if (uncle && uncle->_col == RED)
				{
					//变色
					uncle->_col = BLACK;
					parent->_col = BLACK;
					grandfather->_col = RED;
					//更新
					cur = grandfather;
					parent = cur->_parent;
				}
				//情况二+三
				else//uncle不存在或uncle存在且为黑
				{
					//情况二
					if (cur == parent->_left)
					{
						//右单旋+变色
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					//情况3
					else
					{
						//针对p左单旋,针对g右单旋
						RotateL(parent);
						RotateR(grandfather);
						//更新结点颜色
						cur->_col = BLACK;
						parent->_col = RED;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else//(grandfather->_right == parent)
			{
				//定义grandfather的左孩子为uncle
				Node* uncle = grandfather->_left;
				//情况一
				if (uncle && uncle->_col == RED)
				{
					//变色
					uncle->_col = BLACK;
					parent->_col = BLACK;
					grandfather->_col = RED;
					//更新
					cur = grandfather;
					parent = cur->_parent;
				}
				//情况二+三
				else//uncle不存在或uncle存在且为黑
				{
					//情况二
					if (cur == parent->_right)
					{
						//左单旋+变色
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					//情况3
					else
					{
						//针对p右单旋,针对g左单旋
						RotateR(parent);
						RotateL(grandfather);
						//更新结点颜色
						cur->_col = BLACK;
						parent->_col = RED;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		//有可能更新到了根节点,如果到了根节点就要把根节点置黑
		_root->_col = BLACK;
		return make_pair(iterator(newnode),true);
	}
	//中序遍历
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
	//判断是否是红黑树
	bool IsBalance()
	{
		//性质1:结点的颜色不是红色就是黑色,这点其实不用验证,因为我们定义的枚举类型就能约束这一点
		//性质2:根节点是黑色,又因为空树也可看做红黑树,故若根节点存在且为红色,则不满足,返回false
		if (_root && _root->_col == RED)
		{
			cout << "根节点颜色是红色的" << endl;
			return false;
		}
		//性质3:不能有连续的红结点;
		//检查是否有连续红结点只要遍历所有节点同时检查该节点是否和它的父节点同为红色即可
		// 【检查当前结点与孩子结点不太好,因为一个结点可能没有孩子,但一定有父亲,走到这里已经不需要考虑整棵树的根节点了,因为前面已经判断过了】
		// 
		//性质4:每个节点到该后代所有叶节点的所有简单路径上黑色结点数相等,		
		//设置一个参考值,后面每遍历完一条路径就把得出的黑色节点数和这个基准值比较,不相等就返回false
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				benchmark++;
			}
			cur = cur->_left;
		}
		//递归判断是否有连续的红结点以及每条路径黑色结点是否相等
		return _check(_root, 0, benchmark);
	}
private:
	void _Destory(Node* _root)
	{
		if (_root == nullptr)
		{
			return;
		}
		_Destory(_root->_left);
		_Destory(_root->_right);
		delete _root;

	}
	bool _check(Node* root, int blacknum, int benchmark)
	{
		//到空节点,说明一条路径走完了,此时判断一次黑色结点数量和基准值是否相等,相等则直接返回true,不等返回false
		if (root == nullptr)
		{
			if (blacknum != benchmark)
			{
				cout << "某条路径黑色节点的数量不相等" << endl;
				return false;
			}
			return true;
		}
		//不是空节点且颜色是黑色,这条路径的黑色结点就加1
		if (root->_col == BLACK)
		{
			blacknum++;
		}
		//是红色结点,就判断与父节点颜色关系,都为红色直接返回false
		if (root->_parent && root->_col == RED && root->_parent->_col == RED)
		{
			cout << "出现连续的红色结点" << endl;
			return false;
		}
		//继续向左向右递归判断,当前的黑结点数量以及基准值都要往下传
		return _check(root->_left, blacknum, benchmark)
			&& _check(root->_right, blacknum, benchmark);
	}
	void _Inorder(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Inorder(root->_left);
		cout << root->_kv.first << " ";
		_Inorder(root->_right);
	}
	//右单旋
	void RotateR(Node* parent)
	{
		//定义两个变量分别标记将作为替补根的父节点的左,以及后面要改变父亲的替补结点的右
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;//就是分析过程中的b树
		//为了和上层的树链接,还要先记录一下当前parent的父亲
		Node* pparent = parent->_parent;
		//按照分析改变链接关系
		SubL->_right = parent;
		parent->_parent = SubL;
		parent->_left = SubLR;
		//b树不是空树,则更新SubLR的父指针
		if (SubLR)
		{
			SubLR->_parent = parent;
		}
		//如果来之前parent就是根节点了,那直接将根节点更新为SubL,并让其父亲指向空
		if (parent == _root)
		{
			_root = SubL;
			_root->_parent = nullptr;
		}
		//否则就让pparent指向parent的指针指向SubL
		else
		{
			if (parent == pparent->_left)
			{
				pparent->_left = SubL;
			}
			else
			{
				pparent->_right = SubL;

			}
			SubL->_parent = pparent;
		}
		//红黑树没有平衡因子的概念,无需更新平衡因子!!!!
		//SubL->_bf = parent->_bf = 0;
	}
	//左单旋
	void RotateL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		Node* pparent = parent->_parent;
		//改链接关系
		SubR->_left = parent;
		parent->_parent = SubR;
		parent->_right = SubRL;

		if (SubRL)
		{
			SubRL->_parent = parent;
		}

		if (parent == _root)
		{
			_root = SubR;
			_root->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = SubR;
			}
			else
			{
				pparent->_right = SubR;
			}
			SubR->_parent = pparent;
		}
		//红黑树没有平衡因子的概念,无需更新平衡因子!!!!
		//SubL->_bf = parent->_bf = 0;
	}
	Node* _root = nullptr;
};

综上我们基于对红黑树的改造对map和set完成了模拟封装,基本功能和思想与库里是基本一样的,库里考虑到更全面的问题,肯定还会有很多细节问题,但是不需要我们去挖得太深,同时我们模拟实现的内容并不全面,向Find()、sor()等函数都没实现,但在这里不算是重点,把我总结到的内容都理解,对于map和set的学习就很深入了,足够日后的理解使用。

本节涉及到的所有代码见以下链接,欢迎参考指正!

practice: 课程代码练习 - Gitee.comhttps://gitee.com/ace-zhe/practice/tree/master/map%E5%92%8Cset

你可能感兴趣的:(c++,开发语言)