图解红黑树及Java进行红黑二叉树遍历的方法

article/2025/10/1 4:02:46

红黑树
红黑树是一种数据结构与算法课堂上常常提到但又不会细讲的树,也是技术面试中经常被问到的树,然而无论是书上还是网上的资料,通常都比较刻板难以理解,能不能一种比较直观的方式来理解红黑树呢?本文将以图形的方式来解释红黑树的插入与删除操作。
对树结构的学习是一个递进的过程,我们通常所接触的树都是二叉树,二叉树简单来说就是每个非叶子节点都有且只有两个孩子,分别叫做左孩子和右孩子。二叉树中有一类特殊的树叫二叉查找树,二叉查找树是一种有序的树,对于每个非叶子节点,其左子树的值都小于它,其右子树的值都大于它。比二叉查找树更进一步的是二叉平衡树,二叉平衡树除了保证有序外,还能够保持每个节点左右子树的高度相差不超过1。常见的平衡树有AVL树,Treap,红黑树,伸展树,等等。
红黑树是一种二叉查找树,但在每个节点上增加一个存储位表示节点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
红黑树满足一下5个性质:

  • 每个节点是红色或者黑色;
  • 根节点是黑色;
  • 每个叶子节点NIL是黑色;
  • 如果一个节点是红色,则它的两个孩子都是黑色;(每条路径上不能有两个连续的红色节点)
  • 任一节点到其所有子孙叶子节点NIL的路径上包含相同数目的黑色节点。

注意,在红黑树中,把传统二叉树的叶子节点的孩子指向NIL,称NIL为红黑树中的叶子节点。NIL节点中含有指向父节点的指针,这可能是需要把null改为NIL的原因。

一、插入操作
首先以二叉查找树的插入方式(插入的新节点都在叶子节点处)插入新的节点,并将其绘为红色。然后再重绘其颜色或旋转以保持红黑树的性质,调整分为以下三种情况:
1 新节点N没有父节点(即位于根上)
将新节点N绘为黑色。

201652393817496.png (461×97)

2 新节点N的父节点P为黑色
不用调整。

201652393842590.png (630×162)

3 新节点N的父节点P为红色
因为红黑树不允许有两个连续的红色节点(性质4),所以需要调整,根据N的叔父节点颜色分为两种情况:(我们以 N的父节点P为左孩子为例,P为右孩子的情况类似,不再详述)
3.1 新节点N的叔父节点U为红色
将新节点N的父节点P和叔父节点U都绘为黑色,将其祖父节点G绘为红色,这样保证从G到每个null节点的路径上所包含的黑色节点个数与原来保持一致。但由于我们把G变成了红色,如果G的父亲也是红色,就可能导致连续两个红色节点(违反性质4),所以,需要重新检查G是否违反了红黑树性质。

201652393927178.png (922×248)

3.2 新节点N的叔父节点U为黑色
若新节点N是其父节点P的左孩子:将其父节点P绘为黑色,祖父节点G绘为红色,然后对G进行一次右旋转。

201652393947586.png (771×246)

若新节点N是其父节点P的右孩子:对其父节点进行一次左旋转,问题转化为左孩子的情况。

201652394009586.png (810×245)

 

二、删除操作
《算法导论》和维基百科上的做法都是,当删除一个黑色节点D时,把D的黑色“下推”至其子节点C,也就是说C除了本身的颜色外多了一重额外的黑色,然后不断把这重额外的黑色沿树上移,直到碰到一个红色节点,把其变为黑色以保证路径上黑色节点数目不变,或者移到树的根部,这样所有路径上的黑色节点数目都减一,保持相等。上移过程中可能需要旋转和修改一些节点的颜色,以保证路径上黑色节点数目不变。
这种做法可能有利于代码的实现(可用迭代的方式),但却不便于理解(个人认为)。本着理解优先的目的,我根据被删除节点D的孩子是否为NIL做如下分类:
1 被删除节点D的两个孩子都是NIL
1.1 被删除节点D是红色
用NIL替换D即可。

201652394028190.png (616×162)

1.2 被删除节点D是黑色(我们以D是左孩子为例)
1.2.1 被删除节点D的兄弟节点B的两个孩子都为NIL
将D的兄弟节点B绘为红色,父节点P绘为黑色。

201652394046686.png (519×177)

图中半红半黑表示该节点可能为红色,也可能为黑色。如果P原来是红色,这样修改后路径上的黑色节点数目和删除D之前一样;如果P原来是黑色,那么删除D后会导致路径上黑色节点的数目比删除前少了一个,所以还需继续检查经过P的路径上黑色节点数目的改变是否影响了红黑树的性质。
1.2.2 被删除节点D的兄弟节点B有一个孩子不为NIL
这个孩子一定是红色的,否则从D的父节点到各个叶子节点的路径上黑色节点的数目就会不等(违反性质5)。
若这个孩子为右孩子,将B的这个右孩子绘为黑色,B绘为其父节点P原来的颜色,P绘为黑色,然后对P进行一次左旋转。

201652394103606.png (921×259)

若这个孩子为左孩子,将B的这个左孩子绘为黑色,B绘为红色,然后对B进行一次右旋转,问题转化为右孩子的情况。

201652394118597.png (870×264)

1.2.3 被删除节点D的兄弟节点B的两个孩子都不为NIL
若B为红色,则B的两个孩子一定为黑色。将B绘为黑色,B的左孩子绘为红色,然后对P进行一次左旋转。

201652394133085.png (958×259)

若B为黑色,则B的两个孩子一定为红色。将B的父节点P绘为黑色,B的右孩子绘为黑色,B绘为其父节点P原来的颜色,然后对P进行一次左旋转。

201652394149885.png (960×259)

 

2 被删除节点D的两个孩子都不是NIL
按照二叉查找树删除节点的方法找到D的后继节点S,交换D和S的内容(颜色保持不变),被删除节点变为S,如果S有不为NIL的节点,那么继续用S的后继节点替换S,直到被删除节点的两个孩子都为NIL,问题转化为被删除节点D的两个孩子都为NIL的情况。
3 被删除节点D有一个孩子不是NIL
这个孩子C一定是红色节点,否则从D到各个NIL节点的路径上的黑色节点数目就会不同(违反性质5)。
交换D和C的内容(颜色保持不变),被删除节点变为C,问题转化为被删除节点D的两个孩子都为NIL的情况。

201652394222004.png (616×165)

 

二叉树的遍历
二叉树的遍历有三种:前序遍历、中序遍历和后序遍历。每种遍历的实现又有递归和迭代两种,这篇文章我们来讨论如何用比较优雅的代码来实现二叉树的遍历。
首先我来定义一个二叉树的节点:

?

1

2

3

4

5

6

7

8

9

public class TreeNode {

  int val;

  TreeNode left;

  TreeNode right;

   

  public TreeNode(int x) {

    val = x;

  }

}

 
一、前序遍历(Preorder Traversal)
简单来讲,前序遍历就是先访问父节点,再访问左孩子,最后访问右孩子,即以父、左、右的顺序来遍历。
递归实现非常简单,代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class Solution {

  List<Integer> result = new ArrayList<Integer>();

   

  public List<Integer> preorderTraversal(TreeNode root) {

    dfs(root);

    return result;

  }

   

  private void dfs(TreeNode root) {

    if (root == null) {

      return;

    }

    result.add(root.val);

    dfs(root.left);

    dfs(root.right);

  }

}

迭代实现需要借助一个栈,存储没被访问的右节点,代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public class Solution {

  

  public List<Integer> preorderTraversal(TreeNode root) {

    List<Integer> result = new ArrayList<Integer>();

     

    if (root == null) {

      return result;

    }

     

    Stack<TreeNode> stack = new Stack<TreeNode>();

    stack.push(root);

     

    while (!stack.isEmpty()) {

      TreeNode curr = stack.pop();

      result.add(curr.val);

       

      if (curr.right != null) {

        stack.push(curr.right);

      }

      if (curr.left != null) {

        stack.push(curr.left);

      }

    }

     

    return result;

  }

}

 
二、中序遍历(Inorder Traversal)
简单来讲,中序遍历就是先访问左孩子,再访问父节点,最后访问右孩子,即以左、父、右的顺序遍历。
递归代码也比较容易,如下所示:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class Solution {

  

  public List<Integer> inorderTraversal(TreeNode root) {

    List<Integer> result = new ArrayList<Integer>();

    recurse(root, result);

    return result;

  }

   

  private void recurse(TreeNode root, List<Integer> result) {

    if (root == null) return;

    recurse(root.left, result);

    result.add(root.val);

    recurse(root.right, result);

  }

}

上面这种写法有别于前序遍历的递归代码,前序遍历的递归我们使用了成员变量来存储遍历的结果,这里我们使用方法参数来保存遍历的结果。两种写法都可以,喜欢哪种就使用哪种。
中序遍历的迭代实现没有前序遍历那么简单,虽然也需要借助一个栈,但迭代终止的条件却有所不同。想象一下,对于一棵二叉树,我们最先访问的节点是其最左边的节点,我们当然可以通过一个 while 循环到达其最左边,可是当我们回退时,我们如何知道某个节点的左孩子是否已经访问过了?我们使用一个 curr 变量记录当前访问的节点,当我们把一棵子树的右节点都访问完毕时,我们就该回退该子树的父节点了,而此时 curr 为 null,所以我们可以以此来区分一个节点的左子树是否已被访问过。代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public class Solution {

   

  public List<Integer> inorderTraversal(TreeNode root) {

    List<Integer> result = new ArrayList<Integer>();

     

    Stack<TreeNode> stack = new Stack<TreeNode>();

    TreeNode curr = root;

     

    while (curr != null || !stack.isEmpty()) {

      while (curr != null) {

        stack.push(curr);

        curr = curr.left;

      }

      curr = stack.pop();

      result.add(curr.val);

       

      curr = curr.right;

    }

     

    return result;

  }

}

 
三、后序遍历(Postorder Traversal)
简单来讲,后序遍历就是先访问左孩子,在访问右孩子,最后访问父节点, 即以左、右、父的顺序遍历。
仿照中序遍历,可以很容易地写出后序遍历的递归实现:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class Solution {

  

  public List<Integer> postorderTraversal(TreeNode root) {

    List<Integer> result = new ArrayList<Integer>();

    recurse(root, result);

    return result;

  }

   

  private void recurse(TreeNode root, List<Integer> result) {

    if (root == null) return;

    recurse(root.left, result);

    recurse(root.right, result);

    result.add(root.val);

  }

}

后序遍历的迭代,也需要一个标识要区分一个节点的左右孩子是否已经访问过了,如果没有,则依次访问其左右孩子,如果访问过了,则访问该节点。为此,我们用一个 pre 变量来表示上一个访问的节点,如果上一个访问的节点是当前节点的左孩子或右孩子,那么说明当前节点的左右孩子已经访问过了,那么就可以访问该节点了,否则,则需要进入左右孩子依次访问。代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class Solution {

  

  public List<Integer> postorderTraversal(TreeNode root) {

    List<Integer> result = new LinkedList<Integer>();

     

    Stack<TreeNode> stack = new Stack<TreeNode>();

    if (root != null) stack.push(root);

     

    TreeNode pre = root;

     

    while (!stack.isEmpty()) {

      TreeNode curr = stack.peek();

      if (curr.left == pre || curr.right == pre || (curr.left == null && curr.right == null)) {

        result.add(curr.val);

        stack.pop();

        pre = curr;

      } else {

        if (curr.right != null) stack.push(curr.right);

        if (curr.left != null) stack.push(curr.left);

      }

    }

     

    return result;

  }

}

后序遍历的迭代还有另外一种比较简单的实现,我们知道先序遍历的顺序是父、左、右,而后序遍历的顺序是左、右、父,那么如果我们把先序遍历稍作修改,改成父、右、左的顺序,那么就刚好与后序遍历的顺序相反了,以如此顺序访问完,最后我们对访问结果做个反转就可以了。而先序遍历的迭代实现相对来说比较容易,仿照上面写法我们可以如下实现:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public class Solution {

  

  public List<Integer> postorderTraversal(TreeNode root) {

    List<Integer> result = new LinkedList<Integer>();

     

    Stack<TreeNode> stack = new Stack<TreeNode>();

    if (root != null) stack.push(root);

     

    while (!stack.isEmpty()) {

      TreeNode curr = stack.pop();

      result.add(curr.val);

      if (curr.left != null) stack.push(curr.left);

      if (curr.right != null) stack.push(curr.right);

    }

  

    Collections.reverse(result);

     

    return result;

  }

}

 
四、总结
三种遍历的递归实现都很容易。前序遍历的迭代实现最好写,只需要一个栈就好;中序遍历最难,循环条件除了判断栈是否为空,还要判断当前节点是否为空,以表示是否左子树已经遍历完毕;后续遍历的迭代如果转化为前序遍历


http://chatgpt.dhexx.cn/article/aOYx2pPS.shtml

相关文章

算法 红黑树

红黑树 红黑树概述红黑树性质 红黑树的插入代码实现 红黑树概述 红黑树&#xff08;Red Black Tree&#xff09;是一种自平衡二叉查找树&#xff0c;是在计算机科学的中用到的一种数据结构&#xff0c;典型的用途是实现关联数组&#xff0c;红黑树和AVL树类似&#xff0c;都是…

【数据结构】--二叉树,红黑树

【数据结构】--二叉树&#xff0c;红黑树 &#x1f3c6;概念&#x1f34d;定义&#x1f34b; 术语&#x1f4dd;可视化网站 ☀️二叉树✨ 二叉搜索树&#x1f34d;定义&#x1f351;查找节点&#x1f34b;插入节点&#x1f345;遍历节点&#x1f355; 前序排序&#x1f9c0; 后…

红黑树的详细实现(C++)

红黑树概念(concept) 树型结构主要用于搜索&#xff0c;一直是科学领域的重要演算法&#xff0c;当中探讨了树可能遇到的问题&#xff1a;树的成长可能偏向于一边&#xff0c;也就是不平衡现象。 二叉树是常见且广泛使用的一种树&#xff0c;面临其可能退化成链表的潜藏缺点&…

二叉树——二叉查找树和红黑树

二叉树 二叉树&#xff0c;是一个非常重要的数据结构&#xff0c;在日常的开发中起着很重要的作用&#xff0c;它也衍生出来的各种高效的复杂的数据结构&#xff0c;为我们解决问题提供了高效的解决方案。 二叉树&#xff0c;它是由各个数据节点和左右链接构成的一种类似树的数…

Java——红黑树

概念 红黑树也是一种二叉搜索树&#xff0c;但是和avl树不同&#xff0c;它并不是依靠平衡因子来保证树的平衡的&#xff0c;而是通过颜色 红黑树每个节点中会存储颜色&#xff0c;分为红色和黑色&#xff0c;通过红黑树的限制条件&#xff0c;可以保证从根节点出发到叶子节点…

二叉树--红黑树

红黑树 定义 红黑树&#xff0c;顾名思义&#xff0c;就是树的节点只有红色和黑色两种状态&#xff0c;通过这两种状态的标识和规定颜色的使用&#xff0c;来使树达到相对平衡。为什么说相对平衡&#xff1f;因为在红黑树中&#xff0c;所有的条件限制只能保证&#xff0c;所…

二叉树之红黑树

红黑树 概述 为什么要有红黑树&#xff1f;&#xff1f;&#xff1f; 特点 红黑规则 如何在红黑树上添加节点&#xff1f; &#xff08;1&#xff09;我们不妨假设加入的节点都是黑色 &#xff08;2&#xff09;如果我们加入的节点都是红色 红黑树添加节点后如何保持红…

红黑二叉树

红黑树 红黑树是每个节点都带有颜色属性的二叉查找树&#xff0c;颜色或红色或黑色。在二叉查找树强制一般要求以外&#xff0c;对于任何有效的红黑树我们增加了如下的额外要求: 节点是红色或黑色。 根节点是黑色。 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有…

红黑树-Java实现

目录 一、定义 二、插入 三、删除 四、全部代码 五、颜色效果 一、定义 红黑树是特殊的平衡二叉树&#xff0c;具有以下特性&#xff1a; 1、根节点的颜色是黑色 2、节点颜色要么是黑色、要么是红色 3、如果一个节点的颜色是红色&#xff0c;则它的子节点必须是黑色&…

红黑二叉树原理分析

1.引言 HashMap的基本结构是数组&#xff0c;链表和红黑树。以数组为基本形态&#xff0c;数组中的元素先以链表形式储存&#xff0c;当链表的长 度超过8时&#xff08;包含数组上的那个链表头&#xff09;就会将链表转换为红黑树&#xff0c;以加快修改和查询效率。当然除了H…

理解红黑树及代码实现

1.红黑树定义 红黑树是一颗 红-黑的平衡二叉树,它具有二叉树的所有特性,是一颗自平衡的排序二叉树.(树中任何节点值都大于左子节点的值&#xff0c;而且都小于右子节点的值),其检索效率高&#xff0c;它是一颗空树或它的左右两个子树高度差的绝对值不超过1&#xff0c;并且左右…

红黑二叉树的漫画讲解(轻松理解红黑二叉树原理)

———————————— 二叉查找树&#xff08;BST&#xff09;具备什么特性呢&#xff1f; 1.左子树上所有结点的值均小于或等于它的根结点的值。 2.右子树上所有结点的值均大于或等于它的根结点的值。 3.左、右子树也分别为二叉排序树。 下图中这棵树&#xff0c;就是…

Java的二叉树、红黑树、B+树

数组和链表是常用的数据结构&#xff0c;数组虽然查找快&#xff08;有序数组可以通过二分法查找&#xff09;&#xff0c;但是插入和删除是比较慢的&#xff1b;而链表&#xff0c;插入和删除很快&#xff08;只需要改变一些引用值&#xff09;&#xff0c;但是查找就很慢&…

二叉树与红黑树

二叉树的形成 二叉树是n(n>0)个结点的有限集合&#xff0c;该集合或者为空集&#xff08;称为空二叉树&#xff09;&#xff0c;或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成 二叉树特点 由二叉树定义以及图示分析得出二叉树有以下特点&#xff1…

红黑树(C++实现)

文章目录 红黑树的概念红黑树的性质红黑树结点的定义红黑树的插入红黑树的验证红黑树的查找红黑树的删除红黑树与AVL树的比较 红黑树的概念 红黑树是一种二叉搜索树&#xff0c;但在每个结点上增加了一个存储位用于表示结点的颜色&#xff0c;这个颜色可以是红色的&#xff0c;…

红黑二叉树详解及理论分析

发表于我的博客网站(prajna.top)&#xff1a; http://prajna.top/doc/2/175 什么是红-黑二叉树&#xff1f; 红-黑二叉树首先是一颗二叉树&#xff0c;它具有二叉树的所有性质&#xff0c;是一种平衡二叉树。普通二叉树在生成过程中&#xff0c;容易出现不平衡的现象&#xff…

二叉树到红黑树

二叉树查找树 又叫二叉排序树。二叉查找树或者是一棵空树&#xff0c;或者是一棵具有如下性质的二叉树&#xff1a; 对于任何一个结点X若它的左子树非空&#xff0c;则左子树上所有结点的值均小于等于X的值&#xff1b;若它的右子树非空&#xff0c;则右子树上所有结点的值均大…

详解c++---红黑二叉树的原理和实现

目录标题 什么是红黑二叉树树红黑树的性质红黑树的效率分析红黑树的准备工作红黑树的insert函数节点的调整情况一情况二情况三 转换的实现打印函数find函数检查函数 什么是红黑二叉树树 avl树是通过控制平衡因子来控制二叉搜索树的平衡&#xff0c;当某个节点的平衡因子等于2或…

红黑树和二叉树有什么区别?

红黑树和二叉树有什么区别&#xff1f; 什么是二叉树&#xff1f;什么是红黑树&#xff1f; 二叉树&#xff08;Binary Tree&#xff09;是指每个节点最多只有两个分支的树结构&#xff0c;即不存在分支大于 2 的节点&#xff0c;二叉树的数据结构如下图所示 这是一棵拥有 6 …

二叉树系列:红黑树

介绍 红黑树(Red-Black Tree&#xff0c;简称R-B Tree)&#xff0c;它一种特殊的二叉查找树。红黑树是特殊的二叉查找树&#xff0c;意味着它满足二叉查找树的特征&#xff1a;任意一个节点所包含的键值&#xff0c;大于等于左孩子的键值&#xff0c;小于等于右孩子的键值。除了…