• C++:红黑树


    目录

    一.红黑树介绍

    1.红黑树的概念

    2.红黑树的性质

    思考:如何保证最长的路径不超过最短路径的2倍?

    3.小练习

    4.黑高

    5.红黑树和AVL树比较

    4.新增节点给红色

    5.红黑树的插入操作

    情况一: 红叔:叔父爷变色,爷变新结点。(cur为红,p为红,g为黑,u存在且为红)

     (1)具体情况1

     (2)具体情况2:相当于重复具体情况1

    情况一: 黑叔LL型:右单旋,父,爷变色。(cur为红,p为红,g为黑,u存在且为红)

    情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑,但是cur是p的左

     (1)具体情况1:cur为红,p为红,g为黑,u不存在,但是cur是p的左

    (2)具体情况2:cur为红,p为红,g为黑,u存在且为黑,但是cur是p的左

    情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑,但是cur是p的右

     (1)具体情况1:cur为红,p为红,g为黑,u不存在,但是cur是p的右

    6.检查红黑树合法性

    红黑树总代码

    二. 插入红黑树的完整实例

    1.实例

    (1)黑叔LL型。右单旋,父,爷变色。

    (2)红叔:叔父爷变色,爷变新结点。

    (3)黑叔RR型。左单旋,父,爷变色。

    (3)黑叔LR型。左,右双旋,儿,爷变色

    2.与“黑高”相关的推论

    3.删除操作(不考)


     

     

    一.红黑树介绍

    a9ac02b872b2422395a320671b4184a4.png

    1.红黑树的概念

    红黑树,是一种 二叉搜索树,但 在每个结点上增加一个存储位表示结点的颜色,可以是Red或
    Black。 通过对 任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路
    径会比其他路径长出俩倍,因而是 接近平衡的。( 最长的路径不超过最短路径的2倍,近似平衡
     
     
    14d05351b42d48bcbf431a237eb932a1.png

     

    2.红黑树的性质

    1. 每个结点不是红色就是黑色
    2. 根节点是黑色的 
    3. 如果一个节点是红色的,则它的两个孩子结点是黑色的  (没有连续的红色节点)
    4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 ( 每条路径都包含相同数量黑色节点)
    5. 每个叶子结点都是黑色的(此处的叶子结点指的是 空结点)NULL节点
    9df6b6e9bccb40bcb7a4f88552023f60.png

    思考:如何保证最长的路径不超过最短路径的2倍?

    为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点 个数的两倍?
     
    最长的路径不超过最短路径的2倍
    最短:全黑
    最长:一黑一红间隔
     

    3.小练习

    1840b60eb3b94062a0ae91a7ce3080d5.png

     86388f4e729a471c8102bae8d442efa2.png

    730abf6765ac45bbb9669a71d2c46287.png 

    4.黑高

    ea9bce2f43954e629549f0b5e624a70c.png

    5.红黑树和AVL树比较

    100w数据
    AVL树高度: 20行左右
    红黑树高度: 40行左右,但旋转次数少很多
     

    4.新增节点给红色

    新增节点是红色,可能破坏3.(没有连续的红色节点
    新增节点是黑色,一定破坏4.(每条路径都包含相同数量黑色节点),规则4很难维护
    因此新插入的节点都染成红色,因为他下面有黑色的null叶节点,因此不是叶节点,不用给成黑色。

    5.红黑树的插入操作

    规则:(染色=变色)

    c44536917b954dd5b71a828fa1be8459.png

     叶子都是黑色的null节点,下面没有画出来而已。

    情况一: 红叔:叔父爷变色,爷变新结点。(cur为红,p为红,g为黑,u存在且为红)

    对应上面的 红叔 情况,叔父爷变色,爷变为新节点。即:叔父p,u变黑色,爷g变红。

    如果g是子树,则继续将g当成cur,继续向上调整;若如果g是,则“爷变新结点”,即:将爷g当作新节点,又回到上面判断新节点的条件:新结点是根—染为黑色,新结点非根—染为红色。则将g染为黑色即可。

    739ea3be9bc5453aa4a68161fd7cf768.png

     (1)具体情况1

    a、b、c、d、e都是空,则它们都是黑色的null叶节点。

    5a15f32658ce46c0b022c0c5c07293a5.png

     (2)具体情况2:相当于重复具体情况1

    cur是新增节点,将p,u改为黑,g改为红,把g当成cur,继续向上调整。此时g/cur 这个节点看成新增节点,把他的父pp亲和叔叔uu改成黑色,gg改成红色,然后如果是子树就还能继续像这样向上调整;如果是根

    bbf686d8f25d4d64b8cbc6272cc65c57.png

     

    d050c0eaa77a44fc95733c6b6da44bb5.png 

     

    情况一: 黑叔LL型:右单旋,父,爷变色。(cur为红,p为红,g为黑,u存在且为红)

    对应上面的 黑叔 情况,叔父爷变色,爷变为新节点。即:叔父p,u变黑色,爷g变红。

    如果g是子树,则继续将g当成cur,继续向上调整;若如果g是,则“爷变新结点”,即:将爷g当作新节点,又回到上面判断新节点的条件:新结点是根—染为黑色,新结点非根—染为红色。则将g染为黑色即可。

    情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑但是cur是p的左

     (1)具体情况1:cur为红,p为红,g为黑,u不存在,但是cur是p的左

    p,g右单旋后,把p改成黑色,g改成红色。因为这个局部子树的根节点p是黑色,所以跟上面的数颜色也没关系,所以不用继续向上调整

    861d6bf169bd46e0ae51d31ed33e6f49.png

    (2)具体情况2:cur为红,p为红,g为黑,u存在且为黑,但是cur是p的左

    通过上面的分析,u如果存在且为黑那么cur原来一定是黑色的,作为下面子树的祖父,由情况一变更过来,原来是黑色,现在变红。

    操作是右单旋p,g:p的右给g的左,把g再给p的右,p做根节点。最后p和g变色,p变黑,g变红

    p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反p为g的右孩子,cur为p的右孩子,则进行左单旋转

     

    9da98774cf0d4ea391740f221babef23.png

    x y m n几个位置中某个位置子树插入红节点,引发cur从黑变红
    xymnabd e等子树一定是包含相同数量黑色节点的红黑树子树

    ①到②是情况一cur(y位置)为红,p为红,g为黑,u存在且为红的变化,②到③才是这里的 cur为红,p为红,g为黑,u存在且为黑 的情况,操作是右单旋p,g,g变红,p变黑。

    83d0ca2326e34a80ba54767a4bf89c5a.png

    情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑但是cur是p的右

    情况二和情况三的区别

    情况二:p是g的左,cur是p的左 -- 单旋
    情况三:p是g的左,cur是p的右 -- 双旋

     

    p g 的左孩子, cur p 的右孩子,则针对 p 做左单旋转;相反,
    p g 的右孩子, cur p 的左孩子,则针对 p 做右单旋转
    则转换成了情况 2
    ①到②p和cur进行左单旋,②到③把cur和p交换,最终转换成了情况二
     

    c0b2353bd2a248768c28b9f904531b96.png

     (1)具体情况1:cur为红,p为红,g为黑,u不存在,但是cur是p的右

    双旋+变色

     577ac4f97a954ddf88366250ba4d9e17.png

     

     

      (2)具体情况2:cur为红,p为红,g为黑,u存在且为黑,但是cur是p的右

     258f09e128414879a81f0b54452cfb6b.png

     

     

    6.检查红黑树合法性

    1、遇到红色节点就检查孩子,但是建议改成遇到红色节点检查父亲,这样好实现。(遇到红色节点就检查孩子不好检查,因为孩子可能为空可能不为空)
    2、每条路径黑色节点如何检查?(下图有11条路径,走到空为一条路径)
    前序递归遍历树,求出每条路径黑色节点的数量

    8ae36be9c8ce435b84548268da323d69.png

    1. // 检测是否有连续红色节点和各路径黑节点个数是否相同 的支线函数
    2. bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount)
    3. {//k用来记录每条路径中黑色节点的个数, blackCount是主要函数传进来的“任意一个路径的黑色
    4. //节点个数”作基准值,只要有k和基准值比较不同就违反了性质四:每条路径中黑色节点的个数必须相同
    5. //走到null之后,k就是整个路径的黑色节点个数,判断k和black是否相等
    6. if (nullptr == pRoot)
    7. {
    8. if (k != blackCount)
    9. {
    10. cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
    11. return false;
    12. }
    13. return true;
    14. }
    15. // 统计黑色节点的个数
    16. if (BLACK == pRoot->_col)
    17. k++;
    18. // 顺便检测当前节点与其双亲是否都为红色
    19. if (RED == pRoot->_col && pRoot->_parent && pRoot->_parent->_col == RED)
    20. {
    21. cout << "违反性质三:存在连在一起的红色节点" << endl;
    22. return false;
    23. }
    24. return _IsValidRBTree(pRoot->_left, k, blackCount) &&
    25. _IsValidRBTree(pRoot->_right, k, blackCount);
    26. }
    27. bool IsBalanceTree() //检查红黑树合法性的主要函数
    28. {
    29. // 检查红黑树几条规则
    30. Node* pRoot = _root;
    31. // 1.只要是空树就是红黑树
    32. if (nullptr == pRoot)
    33. return true;
    34. // 2.检测根节点是否满足情况
    35. if (BLACK != pRoot->_col)
    36. {
    37. cout << "违反红黑树性质二:根节点必须为黑色" << endl;
    38. return false;
    39. }
    40. // 获取任意一条路径中黑色节点的个数 -- 比较基准值
    41. size_t blackCount = 0;
    42. Node* pCur = pRoot;
    43. while (pCur)
    44. {
    45. if (BLACK == pCur->_col)
    46. blackCount++;
    47. pCur = pCur->_left;
    48. }
    49. // 检测是否有连续红色节点和各路径黑节点个数是否相同
    50. size_t k = 0; //k用来记录路径中黑色节点的个数
    51. return _IsValidRBTree(pRoot, k, blackCount);
    52. }

     

     

     

    红黑树总代码

     

    1. #pragma once
    2. enum Colour
    3. {
    4. RED,
    5. BLACK,
    6. };
    7. template<class K, class V>
    8. struct RBTreeNode
    9. {
    10. RBTreeNode* _left;
    11. RBTreeNode* _right;
    12. RBTreeNode* _parent;
    13. pair _kv;
    14. Colour _col;
    15. RBTreeNode(const pair& kv)
    16. :_kv(kv)
    17. , _left(nullptr)
    18. , _right(nullptr)
    19. , _parent(nullptr)
    20. , _col(RED)
    21. {}
    22. };
    23. template<class K, class V>
    24. struct RBTree
    25. {
    26. typedef RBTreeNode Node;
    27. public:
    28. bool Insert(const pair& kv)
    29. {
    30. // 1、搜索树的规则插入
    31. // 2、看是否违反平衡规则,如果违反就需要处理:旋转
    32. if (_root == nullptr)
    33. {
    34. _root = new Node(kv);
    35. _root->_col = BLACK;
    36. return true;
    37. }
    38. Node* parent = nullptr;
    39. Node* cur = _root;
    40. while (cur)
    41. {
    42. if (cur->_kv.first < kv.first)
    43. {
    44. parent = cur;
    45. cur = cur->_right;
    46. }
    47. else if (cur->_kv.first > kv.first)
    48. {
    49. parent = cur;
    50. cur = cur->_left;
    51. }
    52. else
    53. {
    54. return false;
    55. }
    56. }
    57. cur = new Node(kv);
    58. cur->_col = RED;
    59. if (parent->_kv.first < kv.first)
    60. {
    61. parent->_right = cur;
    62. }
    63. else
    64. {
    65. parent->_left = cur;
    66. }
    67. cur->_parent = parent;
    68. // 存在连续红色节点
    69. while (parent && parent->_col == RED)
    70. {
    71. Node* grandfater = parent->_parent;
    72. assert(grandfater);
    73. if (grandfater->_left == parent)
    74. {
    75. Node* uncle = grandfater->_right;
    76. // 情况一:
    77. if (uncle && uncle->_col == RED) // 叔叔存在且为红
    78. {
    79. // 变色
    80. parent->_col = uncle->_col = BLACK;
    81. grandfater->_col = RED;
    82. // 继续往上处理
    83. cur = grandfater;
    84. parent = cur->_parent;
    85. }
    86. else // 叔叔不存在 或者 叔叔存在且为黑
    87. {
    88. if (cur == parent->_left) // 单旋
    89. {
    90. // g
    91. // p
    92. // c
    93. RotateR(grandfater);
    94. parent->_col = BLACK;
    95. grandfater->_col = RED;
    96. }
    97. else // 双旋
    98. {
    99. // g
    100. // p
    101. // c
    102. RotateL(parent);
    103. RotateR(grandfater);
    104. cur->_col = BLACK;
    105. grandfater->_col = RED;
    106. }
    107. break;
    108. }
    109. }
    110. else //(grandfater->_right == parent)
    111. {
    112. Node* uncle = grandfater->_left;
    113. // 情况一:
    114. if (uncle && uncle->_col == RED)
    115. {
    116. // 变色
    117. parent->_col = uncle->_col = BLACK;
    118. grandfater->_col = RED;
    119. // 继续往上处理
    120. cur = grandfater;
    121. parent = cur->_parent;
    122. }
    123. else
    124. {
    125. if (cur == parent->_right)
    126. {
    127. // g
    128. // p
    129. // c
    130. RotateL(grandfater);
    131. parent->_col = BLACK;
    132. grandfater->_col = RED;
    133. }
    134. else // 双旋
    135. {
    136. // g
    137. // p
    138. // c
    139. RotateR(parent);
    140. RotateL(grandfater);
    141. cur->_col = BLACK;
    142. grandfater->_col = RED;
    143. }
    144. break;
    145. }
    146. }
    147. }
    148. _root->_col = BLACK;
    149. return true;
    150. }
    151. vectorint>> levelOrder() {
    152. vectorint>> vv;
    153. if (_root == nullptr)
    154. return vv;
    155. queue q;
    156. int levelSize = 1;
    157. q.push(_root);
    158. while (!q.empty())
    159. {
    160. // levelSize控制一层一层出
    161. vector<int> levelV;
    162. while (levelSize--)
    163. {
    164. Node* front = q.front();
    165. q.pop();
    166. levelV.push_back(front->_kv.first);
    167. if (front->_left)
    168. q.push(front->_left);
    169. if (front->_right)
    170. q.push(front->_right);
    171. }
    172. vv.push_back(levelV);
    173. for (auto e : levelV)
    174. {
    175. cout << e << " ";
    176. }
    177. cout << endl;
    178. // 上一层出完,下一层就都进队列
    179. levelSize = q.size();
    180. }
    181. return vv;
    182. }
    183. void RotateL(Node* parent)
    184. {
    185. Node* subR = parent->_right;
    186. Node* subRL = subR->_left;
    187. parent->_right = subRL;
    188. if (subRL)
    189. subRL->_parent = parent;
    190. Node* ppNode = parent->_parent;
    191. subR->_left = parent;
    192. parent->_parent = subR;
    193. if (parent == _root)
    194. {
    195. _root = subR;
    196. _root->_parent = nullptr;
    197. }
    198. else
    199. {
    200. if (parent == ppNode->_left)
    201. {
    202. ppNode->_left = subR;
    203. }
    204. else
    205. {
    206. ppNode->_right = subR;
    207. }
    208. subR->_parent = ppNode;
    209. }
    210. }
    211. void RotateR(Node* parent)
    212. {
    213. Node* subL = parent->_left;
    214. Node* subLR = subL->_right;
    215. parent->_left = subLR;
    216. if (subLR)
    217. subLR->_parent = parent;
    218. Node* ppNode = parent->_parent;
    219. subL->_right = parent;
    220. parent->_parent = subL;
    221. if (parent == _root)
    222. {
    223. _root = subL;
    224. _root->_parent = nullptr;
    225. }
    226. else
    227. {
    228. if (ppNode->_left == parent)
    229. {
    230. ppNode->_left = subL;
    231. }
    232. else
    233. {
    234. ppNode->_right = subL;
    235. }
    236. subL->_parent = ppNode;
    237. }
    238. }
    239. int _maxHeight(Node* root)
    240. {
    241. if (root == nullptr)
    242. return 0;
    243. int lh = _maxHeight(root->_left);
    244. int rh = _maxHeight(root->_right);
    245. return lh > rh ? lh + 1 : rh + 1;
    246. }
    247. int _minHeight(Node* root)
    248. {
    249. if (root == nullptr)
    250. return 0;
    251. int lh = _minHeight(root->_left);
    252. int rh = _minHeight(root->_right);
    253. return lh < rh ? lh + 1 : rh + 1;
    254. }
    255. void _InOrder(Node* root)
    256. {
    257. if (root == nullptr)
    258. return;
    259. _InOrder(root->_left);
    260. cout << root->_kv.first << " ";
    261. _InOrder(root->_right);
    262. }
    263. bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount)
    264. {
    265. //走到null之后,判断k和black是否相等
    266. if (nullptr == pRoot)
    267. {
    268. if (k != blackCount)
    269. {
    270. cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
    271. return false;
    272. }
    273. return true;
    274. }
    275. // 统计黑色节点的个数
    276. if (BLACK == pRoot->_col)
    277. k++;
    278. // 检测当前节点与其双亲是否都为红色
    279. if (RED == pRoot->_col && pRoot->_parent && pRoot->_parent->_col == RED)
    280. {
    281. cout << "违反性质三:存在连在一起的红色节点" << endl;
    282. return false;
    283. }
    284. return _IsValidRBTree(pRoot->_left, k, blackCount) &&
    285. _IsValidRBTree(pRoot->_right, k, blackCount);
    286. }
    287. public:
    288. void InOrder()
    289. {
    290. _InOrder(_root);
    291. cout << endl;
    292. }
    293. void Height()
    294. {
    295. cout << "最长路径:" << _maxHeight(_root) << endl;
    296. cout << "最短路径:" << _minHeight(_root) << endl;
    297. }
    298. bool IsBalanceTree() //检查红黑树合法性的主要函数
    299. {
    300. // 检查红黑树几条规则
    301. Node* pRoot = _root;
    302. // 1.只要是空树就是红黑树
    303. if (nullptr == pRoot)
    304. return true;
    305. // 2.检测根节点是否满足情况
    306. if (BLACK != pRoot->_col)
    307. {
    308. cout << "违反红黑树性质二:根节点必须为黑色" << endl;
    309. return false;
    310. }
    311. // 获取任意一条路径中黑色节点的个数 -- 比较基准值
    312. size_t blackCount = 0;
    313. Node* pCur = pRoot;
    314. while (pCur)
    315. {
    316. if (BLACK == pCur->_col)
    317. blackCount++;
    318. pCur = pCur->_left;
    319. }
    320. // 检测是否有连续红色节点和各路径黑节点个数是否相同
    321. size_t k = 0; //k用来记录路径中黑色节点的个数
    322. return _IsValidRBTree(pRoot, k, blackCount);
    323. }
    324. private:
    325. Node* _root = nullptr;
    326. };
    327. void TestRBTree1()
    328. {
    329. //int a[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
    330. int a[] = { 30, 29, 28, 27, 26, 25, 24, 11, 8, 7, 6, 5, 4, 3, 2, 1 };
    331. RBTree<int, int> t;
    332. for (auto e : a)
    333. {
    334. t.Insert(make_pair(e, e));
    335. }
    336. t.levelOrder();
    337. t.InOrder();
    338. t.Height();
    339. }
    340. void TestRBTree2()
    341. {
    342. const size_t N = 1024 * 1024;
    343. vector<int> v;
    344. v.reserve(N);
    345. srand(time(0));
    346. for (size_t i = 0; i < N; ++i)
    347. {
    348. v.push_back(rand());
    349. //v.push_back(i);
    350. }
    351. RBTree<int, int> t;
    352. for (auto e : v)
    353. {
    354. t.Insert(make_pair(e, e));
    355. }
    356. //t.levelOrder();
    357. //cout << endl;
    358. cout << "是否平衡?" << t.IsBalanceTree() << endl;
    359. t.Height();
    360. //t.InOrder();
    361. }

    二. 插入红黑树的完整实例

    1.实例

    c44536917b954dd5b71a828fa1be8459.png

    (1)黑叔LL型。右单旋,父,爷变色。

    对应上面的 黑叔LL型 情况,右单旋,父,爷变色。

    5是新插入节点—儿子,此时父10是爷20的左L,儿子5是父10的左L,所以是黑叔LL型,父10和爷20右单旋,10的右给20的左边,把20给10的右,10去爷爷的位置。再变色,把父亲10和爷爷20变色。

    12c94ec71cb64165b4f255025396be80.png

    (2)红叔:叔父爷变色,爷变新结点。

    30是新插入节点—儿子,让爷10变红,叔5和父20变黑,爷10变新结点,如果爷10是根节点,就回到上面判断新节点的条件:新结点是根—染为黑色,新结点非根—染为红色。这里爷10是根,则将10染为黑色即可。

    8bb5c9982c61479bb533eb167a3bd544.png

    (3)黑叔RR型。左单旋,父,爷变色。

    对应上面的 黑叔RR型 情况,左单旋,父,爷变色。

    40是新插入节点—儿子,此时父30是爷20的右R,儿子40是父30的右R,并且叔叔是null黑节点,所以是黑叔RR型,父30和爷20左单旋,30的左给20的右,把20给30的左,30去爷爷的位置。再变色,把父亲30和爷爷20变色,20变红,30变黑。因为这个局部子树的根节点30是黑色,所以跟上面的数颜色也没关系,所以不用继续向上调整
    8ebdb63baf304b9c8b03988b6208f598.png

    又是红叔:叔父爷变色,爷变新结点。


    11563b00b83a44888c27148ad047c192.png
    当把“爷变为新结点”后,还需要继续按照红黑树的定义进行判断和调整。
    d216a07de4f9480099ee57aa8bc078b3.png
    6c52b9c912d2450594adb9dbf4c6e275.png
    05c62e075df7474bb8b9220118563288.png

    (3)​​​​​​​黑叔LR型。左,右双旋,儿,爷变色

    对应上面的 黑叔LR型 情况,左,右双旋,儿,爷变色

    23是新插入节点—儿子,此时父22是爷爷25的左L,儿子23是父22的右R,并且叔叔是null黑节点,所以是黑叔LR型,进行左右双旋


    127a7cd0d6e5489ab7b59bc819f0a804.png

    父22和儿子23先左单旋:把23的左给父亲22的右,把父亲22给儿子23的左,儿子23给爷爷25的左。
    8f2317ea600445408825cc8c73dff725.png

    儿子23和爷爷25右单旋:把23的右给25的左,把25给儿子23的右,儿子23给20的右。


    4321f75696dd43b0b54834ddc063c117.png

    再变色,把儿子23和爷爷25变色,爷爷25变红,儿子23变黑。因为这个局部子树的根节点23是黑色,所以跟上面的数颜色也没关系,所以不用继续向上调整。​​​​​​​
    72c67ca1a6284eeb9e28113b5a858e67.png


    其余情况都类似,就不赘述了。
    37ab56f67cd641e39012696e94f10cdc.png

    2.与“黑高”相关的推论

    699ec707b87c4206aac0594da2c8a5ae.png
    内部结点不包括叶子结点,即NULL那些结点。
    d8d1a372bd1e479d9ddf47dd76d55b1c.png
    81539f94e5cf4ebbbe875f0f80664e14.png

    3.删除操作(不考)

    ec448053b7eb4132a1dc7bd0e5c2ea03.png

     

     

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    uni-app_消息推送_华为厂商_unipush离线消息推送
    从ORB-SLAM2到DynaSLAM的编译 v1.0版本
    JavaScript内置对象 - Array数组(一)- 基础部分
    .NET开发工作效率提升利器 - CodeGeeX AI编程助手
    F. Alex‘s whims Codeforces Round 909 (Div. 3) 1899F
    8月4日第壹简报,星期四,农历七月初七,七夕节
    编程思考 : Java中如何用简单的数字描述更多的信息
    【python学习】-matplotlib绘制双坐标柱状图
    (09_22)【有奖体验】轻点鼠标,让古籍数字化“重生_
    Set集合源码分析
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/126453555