• 算法实战:亲自写红黑树之五 删除erase的平衡


            本文承接自:

            算法实战:亲自写红黑树之一-CSDN博客

            算法实战:亲自写红黑树之二 完整代码-CSDN博客

            算法实战:亲自写红黑树之三 算法详解-CSDN博客

            算法实战:亲自写红黑树之四 插入insert的平衡-CSDN博客

    目录

    一、入口

    二、删除

    三、转换中间节点(左右子树都存在)

    四、简单情形

    五、复杂情形


    一、入口

            有几个不同参数的删除入口:

    1. bool erase(const_iterator it)
    2. {
    3. T_COMP comp;
    4. return erase(it, comp);
    5. }
    6. //删除并指向下一个
    7. iterator& DeleteAndMoveNext(iterator& it)
    8. {
    9. if (end() == it)return it;
    10. iterator& ret = it;
    11. iterator tmp = ret;
    12. ++ret;
    13. erase(tmp);
    14. return ret;
    15. }
    16. bool erase(const_iterator it, T_COMP& comp)
    17. {
    18. if (it.handle < 0)return true;
    19. //DEBUG_LOG<<"要删除节点"<
    20. bool ret = _erase(it.handle);
    21. return ret;
    22. }
    23. bool erase(T_DATA const& data)
    24. {
    25. T_COMP comp;
    26. return erase(data, comp);
    27. }
    28. bool erase(T_DATA const& data, T_COMP& comp)
    29. {
    30. const_iterator it = find(data, comp);
    31. if (it.handle < 0)return true;
    32. return erase(it, comp);
    33. }

            最终都进入_erase(h),直接删除一个节点。

            在到达这一步之前要先找到要删除的节点,这个属于二叉树的标准算法,很简单。

    二、删除

            删除的控制代码:

    1. //删除
    2. bool _erase(T_SHM_SIZE position)
    3. {
    4. string report;
    5. //DEBUG_LOG<<"开始删除...."<
    6. bool vLeft;//删除的节点是左子节点
    7. bool vRed;//删除的节点是红色
    8. T_SHM_SIZE p = TREE_NODE::at(position).hParent;//父节点
    9. T_SHM_SIZE u;//替换节点
    10. bool uRed;//替换节点是红色(-1为黑色)
    11. if (TREE_NODE::at(position).hLeft != -1 && TREE_NODE::at(position).hRight != -1)
    12. {
    13. debug_log << "左右都存在,替换为左子树最大值" << endi;
    14. if (!__erase_change_middle(position, vLeft, vRed, u, uRed, p))return false;
    15. debug();
    16. return _erase(position);
    17. }
    18. else if (-1 == TREE_NODE::at(position).hLeft && -1 == TREE_NODE::at(position).hRight)
    19. {
    20. vLeft = TREE_NODE::at(position).isLeft();
    21. vRed = TREE_NODE::at(position).bColorRed;
    22. u = -1;
    23. uRed = false;
    24. if (-1 == p)
    25. {
    26. debug_log << "最后一个节点" << endi;
    27. tree_head->hHead = -1;
    28. vRed = true;//阻止后面继续处理,删除红色无需额外处理
    29. }
    30. else
    31. {
    32. debug_log << "叶子节点" << endi;
    33. if (TREE_NODE::at(position).isLeft())TREE_NODE::at(p).hLeft = -1;
    34. else TREE_NODE::at(p).hRight = -1;
    35. }
    36. }
    37. else
    38. {
    39. vLeft = TREE_NODE::at(position).isLeft();
    40. vRed = TREE_NODE::at(position).bColorRed;
    41. if (-1 != TREE_NODE::at(position).hLeft)
    42. {
    43. debug_log << "左子树存在" << endi;
    44. u = TREE_NODE::at(position).hLeft;
    45. }
    46. else
    47. {
    48. debug_log << "右子树存在" << endi;
    49. u = TREE_NODE::at(position).hRight;
    50. }
    51. if (-1 == p)
    52. {
    53. debug_log << "根节点" << endi;
    54. tree_head->hHead = u;
    55. }
    56. else
    57. {
    58. if (vLeft)TREE_NODE::at(p).hLeft = u;
    59. else TREE_NODE::at(p).hRight = u;
    60. }
    61. TREE_NODE::at(u).hParent = p;
    62. uRed = TREE_NODE::at(u).bColorRed;
    63. p = TREE_NODE::at(u).hParent;
    64. }
    65. bool ret = true;
    66. if (vRed)
    67. {
    68. debug_log << "删除红色节点,不用调整" << endi;
    69. }
    70. else if (!vRed && uRed)
    71. {
    72. debug_log << "删除黑色节点,替换节点红色改为黑色" << endi;
    73. TREE_NODE::at(u).bColorRed = false;
    74. }
    75. else
    76. {
    77. debug_log << "删除双黑节点================================================" << endi;
    78. ret = _RB_erase_Balance(p, u);
    79. }
    80. debug();
    81. if (ret)__erase_node(position);
    82. if (-1 != tree_head->hHead)TREE_NODE::at(tree_head->hHead).bColorRed = false;
    83. return ret;
    84. }

            这个函数的逻辑分为两部分:

    1. 分析要删除的节点的分支情况,将左右节点都存在的情形改为删除叶子数据节点(不是红黑树意义的叶子节点,指一般意义的叶子节点),然后确定删除节点和替换节点。
    2. 根据删除节点和替换节点的颜色进行处理,黑节点减少则需要平衡。

    三、转换中间节点(左右子树都存在)

            很容易把中间节点替换掉,跟左子树最大值或者右子树最小值交换即可。

            本代码用__erase_change_middle(h)函数做这件事,将颜色也做了交换(逻辑上相当于不改变结构数据,只交换用户数据,但是我们的用户数据一般都很大,所以反过来操作,改变了除用户数据以外的所有结构数据),执行完毕后红黑树结构仍然正确,删除节点变成了最多只有一个子树的节点。

    1. //删除中间节点,将中间节点替换为左子树最大值(数据不动,改变树结构)
    2. bool __erase_change_middle(T_SHM_SIZE const position, bool& vLeft, bool& vRed, T_SHM_SIZE& u, bool& uRed, T_SHM_SIZE& p)
    3. {
    4. string tmp;
    5. T_SHM_SIZE new_position = TREE_NODE::at(position).hLeft;
    6. if (-1 != new_position)
    7. {
    8. while (-1 != TREE_NODE::at(new_position).hRight)
    9. {
    10. new_position = TREE_NODE::at(new_position).hRight;
    11. }
    12. }
    13. debug_log << "position " << position << " new_position " << new_position << endi;
    14. debug_log << "position " << position << TREE_NODE::at(position).toString(tmp) << endi;
    15. debug_log << "new_position " << new_position << TREE_NODE::at(new_position).toString(tmp) << endi;
    16. vLeft = false;
    17. vRed = TREE_NODE::at(new_position).bColorRed;
    18. p = TREE_NODE::at(new_position).hParent;
    19. debug_log << "p " << TREE_NODE::at(p).toString(tmp) << endi;
    20. if (p != position)
    21. {
    22. debug_log << "非直接对调 p " << TREE_NODE::at(p).toString(tmp) << ende;
    23. TREE_NODE t;
    24. t._CopyWithoutData(TREE_NODE::at(new_position));
    25. TREE_NODE::at(new_position)._CopyWithoutData(TREE_NODE::at(position));
    26. TREE_NODE::at(position)._CopyWithoutData(t);
    27. debug_log << "position " << TREE_NODE::at(position).toString(tmp) << endi;
    28. debug_log << "new_position " << TREE_NODE::at(new_position).toString(tmp) << endi;
    29. //注意,这里无法用isLeft来判断,因为父节点的hLeft和hRight是旧数据
    30. if (TREE_NODE::at(new_position).hParent != -1)
    31. {
    32. if (TREE_NODE::at(TREE_NODE::at(new_position).hParent).hLeft == position)TREE_NODE::at(TREE_NODE::at(new_position).hParent).hLeft = new_position;
    33. else TREE_NODE::at(TREE_NODE::at(new_position).hParent).hRight = new_position;
    34. }
    35. if (TREE_NODE::at(position).hParent != -1)
    36. {
    37. if (TREE_NODE::at(TREE_NODE::at(position).hParent).hLeft==new_position)TREE_NODE::at(TREE_NODE::at(position).hParent).hLeft = position;
    38. else TREE_NODE::at(TREE_NODE::at(position).hParent).hRight = position;
    39. }
    40. if (TREE_NODE::at(new_position).hLeft != -1)TREE_NODE::at(TREE_NODE::at(new_position).hLeft).hParent = new_position;
    41. if (TREE_NODE::at(position).hLeft != -1)TREE_NODE::at(TREE_NODE::at(position).hLeft).hParent = position;
    42. if (TREE_NODE::at(new_position).hRight != -1)TREE_NODE::at(TREE_NODE::at(new_position).hRight).hParent = new_position;
    43. if (TREE_NODE::at(position).hRight != -1)TREE_NODE::at(TREE_NODE::at(position).hRight).hParent = position;
    44. }
    45. else
    46. {
    47. debug_log << "直接对调 p " << TREE_NODE::at(p).toString(tmp) << endi;
    48. TREE_NODE t;
    49. t._CopyWithoutData(TREE_NODE::at(new_position));
    50. TREE_NODE::at(new_position)._CopyWithoutData(TREE_NODE::at(position));
    51. TREE_NODE::at(position)._CopyWithoutData(t);
    52. debug_log << "position " << TREE_NODE::at(position).toString(tmp) << endi;
    53. debug_log << "new_position " << TREE_NODE::at(new_position).toString(tmp) << endi;
    54. //注意,这里无法用isLeft来判断,因为父节点的hLeft和hRight是旧数据
    55. if (TREE_NODE::at(new_position).hParent != -1)
    56. {
    57. if (TREE_NODE::at(TREE_NODE::at(new_position).hParent).hLeft == position)TREE_NODE::at(TREE_NODE::at(new_position).hParent).hLeft = new_position;
    58. else TREE_NODE::at(TREE_NODE::at(new_position).hParent).hRight = new_position;
    59. }
    60. TREE_NODE::at(position).hParent = new_position;
    61. TREE_NODE::at(new_position).hLeft=position;
    62. if (TREE_NODE::at(position).hLeft != -1)TREE_NODE::at(TREE_NODE::at(position).hLeft).hParent = position;
    63. if (TREE_NODE::at(new_position).hRight != -1)TREE_NODE::at(TREE_NODE::at(new_position).hRight).hParent = new_position;
    64. if (TREE_NODE::at(position).hRight != -1)TREE_NODE::at(TREE_NODE::at(position).hRight).hParent = position;
    65. }
    66. //如果是根节点
    67. if (-1 == TREE_NODE::at(new_position).hParent)
    68. {
    69. TREE_NODE::at(new_position).bColorRed = false;
    70. tree_head->hHead = new_position;
    71. }
    72. debug_log << "position " << TREE_NODE::at(position).toString(tmp) << endi;
    73. debug_log << "new_position " << TREE_NODE::at(new_position).toString(tmp) << endi;
    74. debug();
    75. return true;
    76. }

            这个代码不复杂,就是累。

    四、简单情形

            现在要删除的节点要么没有子树,要么只有一个子树(说起来是子树,其实最多就是一个红节点,否则必定违反红黑树规则),只需要把子树接上去就可以了,然后把要删除的节点扔掉。

            这里我们还是用一般二叉树的概念来理解,以下说的节点都是数据节点,不包括空节点。

    1.         如果删除节点是红节点,不管下面有没有子树,不用平衡,因为不影响规则。
    2.         如果删除节点是黑节点而下面有一个红节点(前面已经说了,没有更多可能性),把这个红节点变成黑节点就行了。
    3.         如果删除节点是黑节点而下面没有可供变色的红节点(也就是没有子节点,红黑树规则不允许下面有黑色数据节点,因为只有一个子树,另外一边深度是0),这就导致红黑树规则被破坏,很多文章称这种情况为“双黑”,其实就是黑节点少了。

            第三种情形就被称为“复杂情形”,需要调用_RB_erase_Balance()来平衡。

    五、复杂情形

            前面已经说了,所谓复杂情形就是少了一个黑节点,现在涉及到这么几个节点:

            u 替换节点,也就是出问题的节点,这个节点的高度比兄弟少一。这个节点可能是是空节点。

            p 替换节点的父节点

            s 替换节点的兄弟节点

            由于红黑树的规则限制,最初进入这种情形只可能是被删除节点下没有子节点,所以一定是p的一边为空另一边黑高为1。递归之后黑高少一的节点就有了各种可能,可能是红色或黑色,可能有或没有子节点。

            重新平衡有三种策略:

    1. 如果能把p改成黑同时让s黑高减一,这样就恢复平衡了,这要求p为红,p为红则s必为黑,如果s的子节点均为黑(此时s的子节点必定都是空节点,否则违反红黑树规则),只需要将p和s颜色交换就可以了。
    2. 另一边如果有红色节点,能挪过来改成黑色,平衡就完成了。按照红色节点的位置不同,需要有不同的挪法,虽然繁琐但并不困难。
    3. 另一边如果有红色节点,挪过来也不能平衡,想办法把问题往上移,递归处理。
    psss处理
    1黑黑/不存在p、s交换颜色,问题上移,递归
    2红红挪一个红改成黑
    3黑黑-[红红红红]旋转p,p、s交换颜色,p还是少1,但是变色了,重新处理p,递归
    4红红挪一个红改成黑
    5黑黑/不存在p、s交换颜色

            上表是所有情形,“不存在”指的是空节点,也是黑色。ss指的是s的子节点。“红红”也可能是只有一个红,因为处理时只要有一个红就不用管另一个。第三种情形的ss是双黑,后面还可能有红色子节点,不过与算法无关。

            为什么只有五种组合而不是八种?因为不允许连续红,另外三种不可能存在。

            上面第二种和第四种处理方式相同,因为并不关心p的颜色,通过挪一个改成黑,下面就平衡了。

            代码的实际处理逻辑是从ss开始判断的,先分成ss至少有一个红和没有红,然后没有红的分三种,有红的分四种(所谓四种不过是四种结构的挪法而已)。

            代码如下:

    1. //删除时做平衡,u可能是空节点
    2. bool _RB_erase_Balance(T_SHM_SIZE p, T_SHM_SIZE u)
    3. {
    4. if (-1 == p)
    5. {
    6. debug_log << "已经到顶,结束" << endi;
    7. return true;
    8. }
    9. string tmp;
    10. debug_log << "p " << TREE_NODE::at(p).toString(tmp) << endi;
    11. if (u != -1)debug_log << "u " << TREE_NODE::at(u).toString(tmp) << endi;
    12. else debug_log << "u -1" << endi;
    13. bool isL = (u == TREE_NODE::at(p).hRight);//兄弟节点是否是左节点
    14. T_SHM_SIZE s = (isL ? TREE_NODE::at(p).hLeft : TREE_NODE::at(p).hRight);
    15. T_SHM_SIZE r = -1;//s的红色子节点
    16. bool is_rL;//s的红色子节点是否是左节点
    17. debug_log << "平衡:p " << p << " u " << u << " s " << s << " TREE_NODE::at(s).hRight " << TREE_NODE::at(s).hRight << endi;
    18. if (TREE_NODE::at(s).hRight != -1 && TREE_NODE::at(TREE_NODE::at(s).hRight).bColorRed)
    19. {
    20. r = TREE_NODE::at(s).hRight;
    21. is_rL = false;
    22. }
    23. else if (TREE_NODE::at(s).hLeft != -1 && TREE_NODE::at(TREE_NODE::at(s).hLeft).bColorRed)
    24. {
    25. r = TREE_NODE::at(s).hLeft;
    26. is_rL = true;
    27. }
    28. else
    29. {
    30. r = -1;
    31. is_rL = false;//无意义
    32. }
    33. debug_log << "p " << p << " u " << u << " s " << s << " r(-1表示双黑) " << r << endi;
    34. debug();
    35. if (-1 == r)
    36. {
    37. debug_log << "s子节点均为黑" << endi;
    38. if (TREE_NODE::at(p).bColorRed)
    39. {
    40. debug_log << "p为红,s必为黑 s改为红p改为黑 结束" << endi;//s子节点均为黑,p改为黑,所以s改为红是安全的,不会造成双红
    41. TREE_NODE::at(s).bColorRed = true;
    42. TREE_NODE::at(p).bColorRed = false;
    43. debug();
    44. }
    45. else
    46. {
    47. debug_log << "p为黑" << endi;
    48. if (!TREE_NODE::at(s).bColorRed)
    49. {
    50. debug_log << "s为黑 s改为红平衡下层并向上递归" << endi;//p的左右分支均少1,所以p成为新的双黑节点
    51. //置s为红,p为新的u,递归
    52. TREE_NODE::at(s).bColorRed = true;
    53. debug();
    54. return _RB_erase_Balance(TREE_NODE::at(p).hParent, p);
    55. }
    56. else
    57. {
    58. debug_log << "s为红 " << TREE_NODE::at(s).toString(tmp) << endi;
    59. debug_log << TREE_NODE::at(TREE_NODE::at(s).hParent).toString(tmp) << endi;
    60. if (TREE_NODE::at(s).isLeft())
    61. {
    62. debug_log << "s为左 " << s << endi;
    63. //右旋p
    64. _RRotate(p);
    65. }
    66. else
    67. {
    68. debug_log << "s为右" << endi;
    69. _LRotate(p);
    70. }
    71. //p和s变色
    72. TREE_NODE::at(s).bColorRed = false;
    73. TREE_NODE::at(p).bColorRed = true;
    74. debug();
    75. //此时深度情况不变,但p变成了红,重新对p和u做平衡处理
    76. return _RB_erase_Balance(p, u);
    77. }
    78. }
    79. }
    80. else
    81. {
    82. if (isL && is_rL)
    83. {
    84. debug_log << "LL" << ende;
    85. TREE_NODE::at(r).bColorRed = TREE_NODE::at(s).bColorRed;
    86. TREE_NODE::at(s).bColorRed = TREE_NODE::at(p).bColorRed;
    87. _RRotate(p);
    88. TREE_NODE::at(p).bColorRed = false;
    89. }
    90. else if (isL && !is_rL)
    91. {
    92. debug_log << "LR" << endi;
    93. debug();
    94. TREE_NODE::at(r).bColorRed = TREE_NODE::at(p).bColorRed;
    95. debug();
    96. _LRotate(s);
    97. debug();
    98. _RRotate(p);
    99. debug();
    100. TREE_NODE::at(p).bColorRed = false;
    101. debug();
    102. }
    103. else if (!isL && is_rL)
    104. {
    105. debug_log << "RL------------------------" << endi;
    106. debug();
    107. TREE_NODE::at(r).bColorRed = TREE_NODE::at(p).bColorRed;
    108. debug();
    109. _RRotate(s);
    110. debug();
    111. string tmp;
    112. debug_log << "p " << TREE_NODE::at(p).toString(tmp) << endi;
    113. debug_log << "s " << TREE_NODE::at(p).toString(tmp) << endi;
    114. _LRotate(p);
    115. debug();
    116. TREE_NODE::at(p).bColorRed = false;
    117. debug();
    118. }
    119. else if (!isL && !is_rL)
    120. {
    121. debug_log << "RR" << endi;
    122. TREE_NODE::at(r).bColorRed = TREE_NODE::at(s).bColorRed;
    123. TREE_NODE::at(s).bColorRed = TREE_NODE::at(p).bColorRed;
    124. _LRotate(p);
    125. TREE_NODE::at(p).bColorRed = false;
    126. }
    127. else
    128. {
    129. thelog << "非预期的状态" << ende;
    130. return false;
    131. }
    132. }
    133. return true;
    134. }

            我感觉吧,我应该刚好解决了一部分人的困惑。

    (这里是结束,而且是整个系列的结束)

  • 相关阅读:
    Dockerfile指令与Docker-compose容器编排-搭建docker私有仓库
    【博客550】k8s乐观锁机制:控制并发请求与数据一致性
    uniapp - 微信小程序新版本发布之后用户端如何手动更新
    QBC CriteriaQuery用法
    2023-10-03 LeetCode每日一题(买卖股票的最佳时机 III)
    jQuery中的Ajax
    【环境配置】基于Docker配置Chisel-Bootcamp环境
    (const char *format, ...) 可变参数在文本日志中的巧妙使用
    zookeeper无法连接,导致elasticjob控制台无法使用
    深度之眼(二)——矩阵及其基本运算
  • 原文地址:https://blog.csdn.net/2301_77171572/article/details/134435927