深圳幻海软件技术有限公司 欢迎您!

【C++】用手搓的红黑树手搓set和map

2023-04-16

目录一、set/map的底层结构1、set/map的源码2、利用模板区分set/map3、利用仿函数控制比较大小二、set/map的迭代器(红黑树的迭代器)1、红黑树的begin、end迭代器2、红黑树迭代器的operator++3、红黑树迭代器的operator--三、set的const迭代器四、


目录

一、set/map的底层结构

1、set/map的源码

2、利用模板区分set/map

3、利用仿函数控制比较大小

二、set/map的迭代器(红黑树的迭代器)

1、红黑树的begin、end迭代器

2、红黑树迭代器的operator++

3、红黑树迭代器的operator--

三、set的const迭代器

四、map的const迭代器

五、迭代器类的拷贝构造

六、整体代码

1、RBTree.h

2、Set.h

3、map.h


本文相关往期内容,可按需查阅:
1、【C++】set/multiset、map/multimap的使用

2、【数据结构】二叉搜索树的实现

3、【数据结构】平衡二叉树

4、【数据结构】手撕红黑树

        本文难点:使用红黑树封装set和map,必须保证两种数据结构复用同一棵红黑树;且满足set和map的性质,set的value不可被改变,而map的value可以被改变。

一、set/map的底层结构

1、set/map的源码

        扒一扒STL库中set和map的底层结构,不难发现,set和map的底层用的都是红黑树且均为key/value模型。

        只不过set的key/value均为key值填充,而map的key/value使用key和pair<const Key,T>进行填充。因此,set和map中底层虽然都是红黑树,但这两种数据结构中的红黑树实例化类型并不相同

        那么使用同一颗红黑树的模板,如何实例化出适配set和/map的对象?

2、利用模板区分set/map

  1. template <class T>//T类型代表value
  2. struct RBTreeNode
  3. {
  4. RBTreeNode(const T& data)
  5. :_parent(nullptr)
  6. , _left(nullptr)
  7. , _right(nullptr)
  8. , _data(data)
  9. , _col(RED)
  10. {}
  11. RBTreeNode<T>* _parent;
  12. RBTreeNode<T>* _left;
  13. RBTreeNode<T>* _right;
  14. T _data;
  15. Color _col;
  16. };

        map和set的区别在于value的不同,红黑树模板参数T,代表value用以区分set和map。

3、利用仿函数控制比较大小

        我们会发现红黑树的插入等接口会对key值进行比较大小,像set直接对key进行比较,这没问题,但是map中的节点装的是pair<K,V>,pair的比较规则是first比完之后可能会再去比较second(而我们仅仅想要比较first,该比较规则不适用)。

        通过源码启发,我们可以对红黑树新增一个模板参数:仿函数KeyOfT,在set和map类中完善该仿函数的比较对象,用于区分set和map的比较:

  1. template <class K>
  2. class set
  3. {
  4. //仿函数用于比较大小
  5. struct SetKeyOfT
  6. {
  7. const K& operator()(const K& key)//传入节点的值
  8. {
  9. return key;//返回key
  10. }
  11. };
  12. private:
  13. RBTree<K, K, SetKeyOfT> _t;
  14. };
  15. class map
  16. {
  17. struct MapKeyOfT
  18. {
  19. const K& operator()(const pair<K, V>& kv)//传入节点的值
  20. {
  21. return kv.first;//返回kv.first
  22. }
  23. };
  24. private:
  25. RBTree<const K, pair<K,V>, MapKeyOfT> _t;
  26. };
  27. //利用模板确定传入对象是set还是map
  28. template <class K, class T,class KeyOfT>
  29. class RBTree//红黑树
  30. {};

        利用仿函数,传入节点的值,set将会返回key值,map将会的返回pair的first。这样就解决了比较大小的规则问题。

二、set/map的迭代器(红黑树的迭代器)

        因为红黑树的中序遍历是有序的,可以根据中序遍历作为迭代器++--的依据。

        STL源码采用下图结构,多搞了一个头结点。迭代器begin()可以指向header的左,迭代器end()指向header。

        不过本文采用无头结点的常规红黑树仿写红黑树的迭代器。

1、红黑树的begin、end迭代器

2、红黑树迭代器的operator++

        1、如果当前节点的右不为空,迭代器++返回右子树的最左节点

        2、如果当前节点的右为空,迭代器++返回祖先(当前节点是父亲的左)(end()-1迭代器++返回nullptr即end())

  1. template <class T>
  2. struct __RBTreeIterator
  3. {
  4. typedef RBTreeNode<T> Node;
  5. typedef __RBTreeIterator<T> Self;
  6. Node* _node;
  7. __RBTreeIterator(Node* node)
  8. :_node(node)
  9. {}
  10. //1、右不为空,下一个节点是右树的最小节点
  11. //2、右为空,去找右是父亲左的最近祖先
  12. Self& operator++()//找中序的下一个节点,即根的右树的最左节点,返回值是一个迭代器的对象
  13. {
  14. if (_node->_right != nullptr)
  15. {
  16. Node* min = _node->_right;
  17. while (min->_left != nullptr)
  18. {
  19. min = min->_left;
  20. }
  21. _node = min;
  22. }
  23. else
  24. {
  25. Node* cur = _node;
  26. Node* parent = cur->_parent;
  27. while (parent != nullptr && cur == parent->_right)
  28. {
  29. cur = cur->_parent;
  30. parent = parent->_parent;
  31. }
  32. _node = parent;
  33. }
  34. return *this;
  35. }
  36. bool operator!=(const Self& s)
  37. {
  38. return _node != s._node;
  39. }
  40. };

3、红黑树迭代器的operator--

        1、如果当前节点的左不为空,迭代器--返回左子树的最右节点

        2、如果当前节点的左为空,迭代器--返回祖先(当前节点是父亲的右)

  1. template <class T>
  2. struct __RBTreeIterator
  3. {
  4. typedef RBTreeNode<T> Node;
  5. typedef __RBTreeIterator<T> Self;
  6. Node* _node;
  7. __RBTreeIterator(Node* node)
  8. :_node(node)
  9. {}
  10. Self& operator--()
  11. {
  12. if (_node->_left!=nullptr)
  13. {
  14. Node* max = _node;
  15. while (max->_right)
  16. {
  17. max = max->_right;
  18. }
  19. _node = max;
  20. }
  21. else
  22. {
  23. Node* cur = _node;
  24. Node* parent = cur->_parent;
  25. while (parent != nullptr && cur == parent->_left)
  26. {
  27. cur = cur->_parent;
  28. parent = parent->_parent;
  29. }
  30. _node = parent;
  31. }
  32. return *this;
  33. }
  34. };

三、set的const迭代器

        对于set和map,它们的key都是不能改的。set的value不能修改,map的value可以修改。

        因为set的value是不能改的,所以它的底层将普通迭代器和const迭代器全部封装成const迭代器来“解决”:

  1. //自己实现的,不代表STL
  2. typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
  3. typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;

        封装之后又会出现新问题,set使用迭代器其实都是在使用const迭代器,但是自己实现的红黑树的迭代器接口返回普通类型的迭代器,在Set.h中对this加上const“解决”:

  1. iterator begin()const
  2. {
  3. return _t.begin();
  4. }
  5. iterator end()const
  6. {
  7. return _t.end();
  8. }

        这时使用迭代器调用上方函数会发现红黑树返回了普通迭代器类型的迭代器,类型不匹配。在红黑树中补齐const版本的迭代器函数解决:

  1. const_iterator begin()const//找红黑树最左节点
  2. {
  3. Node* left = _root;
  4. while (left != nullptr && left->_left != nullptr)
  5. {
  6. left = left->_left;
  7. }
  8. return const_iterator(left);
  9. }
  10. const_iterator end()const
  11. {
  12. return const_iterator(nullptr);
  13. }

四、map的const迭代器

        map的value是可以改的,所以要分别设计普通迭代器和const迭代器。

  1. typedef typename RBTree<const K, pair<const K, V>, MapKeyOfT>::iterator iterator;
  2. typedef typename RBTree<const K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;
  3. iterator begin()
  4. {
  5. return _t.begin();
  6. }
  7. iterator end()
  8. {
  9. return _t.end();
  10. }
  11. const_iterator begin()const
  12. {
  13. return _t.begin();
  14. }
  15. const_iterator end()const
  16. {
  17. return _t.end();
  18. }

五、迭代器类的拷贝构造

        STL库中的普通迭代器都可以转换为const迭代器,这是迭代器类的拷贝构造所支持的。

这个拷贝构造有点特殊:

  1. //红黑树的迭代器
  2. template <class T,class Ref,class Ptr>//key/value、T&、T*
  3. struct __RBTreeIterator
  4. {
  5. typedef RBTreeNode<T> Node;
  6. typedef __RBTreeIterator<T, Ref, Ptr> Self;
  7. typedef __RBTreeIterator<T, T&, T*> iterator;
  8. Node* _node;
  9. __RBTreeIterator(Node* node)
  10. :_node(node)
  11. {}
  12. __RBTreeIterator(const iterator& it)//const iterator本质还是普通迭代器
  13. :_node(it._node)
  14. {}
  15. };

        1、当这个模板的的Ref和PTR被实例化为T&和T*时,__RBTreeIterator(const iterator& it)就是一个拷贝构造(没啥意义)

        2、当这个模板的的Ref和PTR被实例化为const T&和const T*时,__RBTreeIterator(const iterator& it)就是一个构造函数,支持用普通迭代器去构造const迭代器。此时const迭代器的拷贝构造函数则由编译器自动生成,刚好满足迭代器值拷贝的特点。

六、整体代码

1、RBTree.h

  1. #pragma once
  2. #include <iostream>
  3. #include <map>
  4. #include <set>
  5. #include <string>
  6. using namespace std;
  7. enum Color
  8. {
  9. RED,
  10. BLACK,
  11. };
  12. template <class T>//T类型代表value
  13. struct RBTreeNode
  14. {
  15. RBTreeNode(const T& data)
  16. :_parent(nullptr)
  17. , _left(nullptr)
  18. , _right(nullptr)
  19. , _data(data)
  20. , _col(RED)
  21. {}
  22. RBTreeNode<T>* _parent;
  23. RBTreeNode<T>* _left;
  24. RBTreeNode<T>* _right;
  25. T _data;
  26. Color _col;
  27. };
  28. //红黑树的迭代器
  29. // key/value T& T*
  30. template <class T,class Ref,class Ptr>
  31. struct __RBTreeIterator
  32. {
  33. typedef RBTreeNode<T> Node;
  34. typedef __RBTreeIterator<T, Ref, Ptr> Self;
  35. typedef __RBTreeIterator<T, T&, T*> iterator;
  36. Node* _node;
  37. __RBTreeIterator(Node* node)
  38. :_node(node)
  39. {}
  40. __RBTreeIterator(const iterator& it)
  41. :_node(it._node)
  42. {}
  43. Ref operator*()
  44. {
  45. return _node->_data;
  46. }
  47. Ptr operator->()//返回类型的地址
  48. {
  49. return &_node->_data;
  50. }
  51. //1、右不为空,下一个节点是右树的最小节点
  52. //2、右为空,去找右是父亲左的最近祖先
  53. Self& operator++()//找中序的下一个节点,即根的右树的最左节点,返回值是一个迭代器的对象
  54. {
  55. if (_node->_right != nullptr)
  56. {
  57. Node* min = _node->_right;
  58. while (min->_left != nullptr)
  59. {
  60. min = min->_left;
  61. }
  62. _node = min;
  63. }
  64. else
  65. {
  66. Node* cur = _node;
  67. Node* parent = cur->_parent;
  68. while (parent != nullptr && cur == parent->_right)
  69. {
  70. cur = cur->_parent;
  71. parent = parent->_parent;
  72. }
  73. _node = parent;
  74. }
  75. return *this;
  76. }
  77. Self& operator--()
  78. {
  79. if (_node->_left!=nullptr)
  80. {
  81. Node* max = _node;
  82. while (max->_right)
  83. {
  84. max = max->_right;
  85. }
  86. _node = max;
  87. }
  88. else
  89. {
  90. Node* cur = _node;
  91. Node* parent = cur->_parent;
  92. while (parent != nullptr && cur == parent->_left)
  93. {
  94. cur = cur->_parent;
  95. parent = parent->_parent;
  96. }
  97. _node = parent;
  98. }
  99. return *this;
  100. }
  101. bool operator!=(const Self& s)const
  102. {
  103. return _node != s._node;
  104. }
  105. bool operator==(const Self& s)const
  106. {
  107. return _node == s._node;
  108. }
  109. };
  110. //pair的比较是如果first小还要比second,我们只要比first,所以加了仿函数KeyOfT,用以取出first进行比较
  111. //set->RBTree<K, K, SetKeyOfT>
  112. //map->RBTree<const K, pair<K,V>, MapKeyOfT>
  113. template <class K, class T,class KeyOfT>
  114. class RBTree
  115. {
  116. public:
  117. typedef __RBTreeIterator<T,T&,T*> iterator;
  118. typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
  119. iterator begin()//找红黑树最左节点
  120. {
  121. Node* left = _root;
  122. while (left!=nullptr&&left->_left!=nullptr)
  123. {
  124. left = left->_left;
  125. }
  126. return iterator(left);
  127. }
  128. iterator end()
  129. {
  130. return iterator(nullptr);
  131. }
  132. const_iterator begin()const//找红黑树最左节点
  133. {
  134. Node* left = _root;
  135. while (left != nullptr && left->_left != nullptr)
  136. {
  137. left = left->_left;
  138. }
  139. return const_iterator(left);
  140. }
  141. const_iterator end()const
  142. {
  143. return const_iterator(nullptr);
  144. }
  145. typedef RBTreeNode<T> Node;
  146. pair<iterator,bool> Insert(const T& data)//对于map来说data是pair
  147. {
  148. if (_root == nullptr)
  149. {
  150. _root = new Node(data);
  151. _root->_col = BLACK;//根节点给黑色
  152. return make_pair(iterator(_root), true);//返回插入的节点
  153. }
  154. KeyOfT kot;//搞一个仿函数对象
  155. //_root不为空
  156. Node* parent = nullptr;
  157. Node* cur = _root;
  158. while (cur)
  159. {
  160. if (kot(cur->_data) < kot(data))
  161. {
  162. parent = cur;
  163. cur = cur->_right;
  164. }
  165. else if (kot(cur->_data) > kot(data))
  166. {
  167. parent = cur;
  168. cur = cur->_left;
  169. }
  170. else//相等说明元素相同,插入失败
  171. return make_pair(iterator(cur),false);//插入失败,说明找到了,返回被查找节点的迭代器
  172. }
  173. //开始插入
  174. cur = new Node(data);
  175. Node* newNode = cur;//记录cur的地址,make_pair返回插入节点的地址
  176. cur->_col = RED;//新插入节点给红色,可能违反规则。如果给黑色会导致其他路径的黑色节点数量不相同,必定违反规则。
  177. if (kot(parent->_data) < kot(data))
  178. {
  179. parent->_right = cur;
  180. cur->_parent = parent;//维护cur的父指针
  181. }
  182. else
  183. {
  184. parent->_left = cur;
  185. cur->_parent = parent;
  186. }
  187. //调整
  188. while (parent && parent->_col == RED)
  189. {
  190. Node* grandfather = parent->_parent;//找到祖父
  191. if (grandfather->_left == parent)//如果父亲是祖父的左孩子
  192. {
  193. Node* uncle = grandfather->_right;//找到叔叔
  194. //情况一:叔叔存在且为红
  195. if (uncle != nullptr && uncle->_col == RED)
  196. {
  197. //变色
  198. parent->_col = uncle->_col = BLACK;
  199. grandfather->_col = RED;
  200. cur = grandfather;
  201. parent = cur->_parent;
  202. }
  203. else//情况二或情况三
  204. {
  205. if (cur == parent->_left)//情况二,直线
  206. {
  207. RotateRight(grandfather);//右单旋
  208. parent->_col = BLACK;
  209. grandfather->_col = RED;
  210. }
  211. else//情况三,折线
  212. {
  213. RotateLeft(parent);//左单旋
  214. RotateRight(grandfather);//右单旋
  215. cur->_col = BLACK;
  216. grandfather->_col = RED;
  217. }
  218. break;
  219. }
  220. }
  221. else//如果父亲是祖父的右孩子
  222. {
  223. Node* uncle = grandfather->_left;
  224. if (uncle != nullptr && uncle->_col == RED)
  225. {
  226. parent->_col = uncle->_col = BLACK;
  227. grandfather->_col = RED;
  228. cur = grandfather;
  229. parent = cur->_parent;
  230. }
  231. else
  232. {
  233. if (cur == parent->_right)//情况二,直线
  234. {
  235. //g
  236. // p
  237. // c
  238. RotateLeft(grandfather);//左单旋
  239. parent->_col = BLACK;
  240. grandfather->_col = RED;
  241. }
  242. else//情况三,折线
  243. {
  244. //g
  245. // p
  246. //c
  247. RotateRight(parent);//右单旋
  248. RotateLeft(grandfather);//左单旋
  249. cur->_col = BLACK;
  250. grandfather->_col = RED;
  251. }
  252. break;
  253. }
  254. }
  255. }
  256. _root->_col = BLACK;
  257. return make_pair(iterator(newNode), true);//返回插入的节点
  258. }
  259. void Inorder()
  260. {
  261. _Inorder(_root);
  262. }
  263. bool IsBalance()
  264. {
  265. return _IsBalance();
  266. }
  267. private:
  268. void _Inorder(Node* root)
  269. {
  270. if (root == nullptr)
  271. return;
  272. _Inorder(root->_left);
  273. cout << kot(root->_data) << ":" << root->_data.second << endl;
  274. _Inorder(root->_right);
  275. }
  276. bool Check(Node* root, int blackNum, const int ref)//检查有没有连续红节点
  277. {
  278. if (root == nullptr)
  279. {
  280. if (blackNum != ref)
  281. {
  282. cout << "路径上黑节点数量不一致" << endl;
  283. return false;
  284. }
  285. return true;
  286. }
  287. if (root->_col == BLACK)
  288. {
  289. ++blackNum;
  290. }
  291. if (root->_col == RED && root->_parent->_col == RED)
  292. {
  293. cout << "违反规则,父子均为红" << endl;
  294. return false;
  295. }
  296. return Check(root->_left, blackNum, ref) && Check(root->_right, blackNum, ref);
  297. }
  298. bool _IsBalance()
  299. {
  300. if (_root == nullptr)
  301. return true;
  302. if (_root->_col != BLACK)
  303. {
  304. return false;
  305. }
  306. //数一下一条路径黑色节点数量
  307. int ref = 0;//统计一条路上黑色节点的数量
  308. Node* left = _root;
  309. while (left != nullptr)
  310. {
  311. if (left->_col == BLACK)
  312. {
  313. ++ref;
  314. }
  315. left = left->_left;
  316. }
  317. return Check(_root, 0, ref);
  318. }
  319. void RotateLeft(Node* parent)//左单旋
  320. {
  321. Node* grandfather = parent->_parent;
  322. Node* cur = parent->_right;
  323. if (parent == _root)
  324. {
  325. _root = cur;
  326. cur->_parent = nullptr;
  327. }
  328. else
  329. {
  330. if (grandfather->_left == parent)//需要判定parent原来属于grandfather的哪一边
  331. grandfather->_left = cur;
  332. else
  333. grandfather->_right = cur;
  334. cur->_parent = grandfather;
  335. }
  336. parent->_right = cur->_left;
  337. if (cur->_left != nullptr)
  338. cur->_left->_parent = parent;
  339. cur->_left = parent;
  340. parent->_parent = cur;
  341. }
  342. void RotateRight(Node* parent)//右单旋
  343. {
  344. Node* grandfather = parent->_parent;
  345. Node* cur = parent->_left;
  346. if (parent == _root)
  347. {
  348. _root = cur;
  349. cur->_parent = nullptr;
  350. }
  351. else
  352. {
  353. if (grandfather->_left == parent)
  354. {
  355. grandfather->_left = cur;
  356. cur->_parent = grandfather;
  357. }
  358. else
  359. {
  360. grandfather->_right = cur;
  361. cur->_parent = grandfather;
  362. }
  363. }
  364. parent->_parent = cur;
  365. parent->_left = cur->_right;
  366. if (cur->_right != nullptr)
  367. cur->_right->_parent = parent;
  368. cur->_right = parent;
  369. }
  370. private:
  371. Node* _root = nullptr;
  372. };

        迭代器的begin(),end()接口放在红黑树这个类中,而operator++--放在迭代器这个类中,自己写一下循环遍历红黑树的代码就知道为什么这样设计了。

2、Set.h

  1. #pragma once
  2. #include "RBTree.h"
  3. namespace jly
  4. {
  5. template <class K>
  6. class set
  7. {
  8. struct SetKeyOfT
  9. {
  10. const K& operator()(const K& key)//传入value
  11. {
  12. return key;
  13. }
  14. };
  15. public:
  16. typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
  17. typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;
  18. pair<iterator, bool> insert(const K& key)
  19. {
  20. pair<typename RBTree<K, K, SetKeyOfT>::iterator,bool> ret= _t.Insert(key);
  21. return pair<iterator, bool>(ret.first, ret.second);
  22. }
  23. iterator begin()const
  24. {
  25. return _t.begin();
  26. }
  27. iterator end()const
  28. {
  29. return _t.end();
  30. }
  31. private:
  32. RBTree<K, K, SetKeyOfT> _t;
  33. };
  34. void test2()
  35. {
  36. //int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
  37. //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
  38. int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
  39. //int a[] = { 9,8,7,6,5,4,3,2,1};
  40. set<int> s;
  41. for (auto e : a)
  42. {
  43. s.insert(e);
  44. }
  45. set<int>::iterator it = s.begin();
  46. while (it != s.end())
  47. {
  48. cout << *it << " ";
  49. ++it;
  50. }
  51. }
  52. }

3、map.h

  1. #pragma once
  2. #include "RBTree.h"
  3. namespace jly
  4. {
  5. template <class K,class V>
  6. class map
  7. {
  8. struct MapKeyOfT
  9. {
  10. const K& operator()(const pair<K, V>& kv)//传入value
  11. {
  12. return kv.first;
  13. }
  14. };
  15. public:
  16. //typename是C++中用于指定一个类的类型的关键字。
  17. //通常用于表示某个类型是一个类类型,而不是其他类型,如int等。
  18. //这里不加typedef编译器无法区分iterator是一个类型还是一个静态变量。因为他俩都可以这么写。。
  19. //所以从类模板取出内嵌类型就需要加typedef
  20. typedef typename RBTree<const K, pair<const K, V>, MapKeyOfT>::iterator iterator;
  21. typedef typename RBTree<const K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;
  22. pair<iterator,bool> insert(const pair<K, V>& kv)
  23. {
  24. return _t.Insert(kv);
  25. }
  26. iterator begin()
  27. {
  28. return _t.begin();
  29. }
  30. iterator end()
  31. {
  32. return _t.end();
  33. }
  34. const_iterator begin()const
  35. {
  36. return _t.begin();
  37. }
  38. const_iterator end()const
  39. {
  40. return _t.end();
  41. }
  42. V& operator[](const K& key)//传入key值
  43. {
  44. pair<iterator,bool> ret= _t.Insert(key,V());
  45. return ret.first->second;//找到ret(make_pair<iterator,bool>)的first,解引用找到节点value
  46. }
  47. private:
  48. RBTree<const K, pair<const K,V>, MapKeyOfT> _t;
  49. };
  50. void test1()
  51. {
  52. int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
  53. //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
  54. //int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
  55. //int a[] = { 9,8,7,6,5,4,3,2,1};
  56. map<int,int> m;
  57. for (auto e : a)
  58. {
  59. m.insert(make_pair(e,e));
  60. }
  61. map<int, int>::iterator it = m.begin();
  62. while (it != m.end())
  63. {
  64. cout << (* it).first << " ";
  65. ++it;
  66. }
  67. cout << endl;
  68. for (auto& e : m)
  69. {
  70. cout << e.first<<" ";
  71. }
  72. }
  73. }
文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览44207 人正在系统学习中
创作推广/买腾讯云产品找我可打折
微信名片