红黑树完全解析(基于TreeMap)

红黑树的例子

在线例子网址

https://www.cs.usfca.edu/~galles/visualization/RedBlack.html

内部节点及类定义

Get/Set方法省略

static class RBNode<K extends Comparable<K>, V> {
  private RBNode parent;
  private RBNode left;
  private RBNode right;
  private boolean color;
  private K key;
  private V value;
}

记录根节点的信息以及定义红黑

public class RBTree<K extends Comparable<K>, V> {
    private static final boolean RED = false;
    private static final boolean BLACK = true;

    private RBNode root;
}

左旋右旋

和avl树没有区别

所谓的左旋,指的是向左方向旋,那么应该是右边比较多的情况,也就是RightRight的情况

private void leftRotate(RBNode p) {
  if (p != null) {
    RBNode r = p.right;
    p.right = r.left;
    if (r.left != null) {
      r.left.parent = p;
    }
    r.parent = p.parent;
    if (p.parent == null) {
      root = r;
    } else if (p.parent.left == p) {
      p.parent.left = r;
    } else {
      p.parent.right = r;
    }
    r.left = p;
    p.parent = r;
  }
}

相应的右旋

private void rightRotate(RBNode p) {
  if (p != null) {
    RBNode l = p.left;
    p.left = l.right;
    if (l.right != null) {
      l.right.parent = p;
    }
    l.parent = p.parent;
    if (p.parent == null) {
      root = l;
    } else if (p.parent.right == p) {
      p.parent.right = l;
    } else {
      p.parent.left = l;
    }
    l.right = p;
    p.parent = l;
  }
}

其他操作

返回节点的颜色

注意,当节点为null时返回的是黑色

private boolean colorOf(RBNode node) {
    return node == null ? BLACK : node.color;
}
返回父节点
private RBNode parentOf(RBNode node) {
    return node != null ? node.parent : null;
}
返回左右节点
private RBNode leftOf(RBNode node) {
    return node != null ? node.left : null;
}
private RBNode rightOf(RBNode node) {
  return node != null ? node.right : null;
}
给节点设置颜色
private void setColor(RBNode node, boolean color) {
    if (node != null) {
        node.color = color;
    }
}

查找前驱以及后继

前驱

如果有左孩子,那么前驱一定在左孩子的最右边。

如果没有左孩子,在红黑树对应的234树中一定处于叶子节点,所对应的红黑树中一定位于底节点或者倒数第二层,直接看代码:

private RBNode predecessor(RBNode node) {
  if (node == null) {
    return null;
  } else if (node.left != null) {//如果有左儿子
    RBNode p = node.left;
    while (p.right != null) {
      p = p.right;
    }
    return p;
  } else if (node.parent != null) { //如果没有左儿子,但是有祖先
    RBNode p = node.parent;
    RBNode c = node;
    while (p != null && p.left == c) {
      c = p;
      p = p.parent;
    }
    return p;
  }
  //如果没有左儿子,没有祖先,那么相当于没有前驱
  return null;
}
后继

TreeMap里删除使用的是后继。

private RBNode successor(RBNode node) {
    if (node == null) {
        return null;
    } else if (node.right != null) {
        RBNode p = node.right;
        while (p.left != null) {
            p = p.left;
        }
        return p;
    } else if (node.parent != null) {
        RBNode p = node.parent;
        RBNode c = node;
        while (p != null && p.right == c) {
            c = p;
            p = p.parent;
        }
        return p;
    }
    return null;
}

插入操作

先插入再调整,插入的节点统一为红色,之后再进行调整,根节点必黑。

插入根节点必黑,允许黑连黑,不允许红连红,新增红色,爸叔通红(爸叔)就变黑,爷爷节点再变红之后递归。爸红叔黑就旋转,哪黑往哪旋。如果插入父节点为黑,那么就不需要调整。

注意,如果插入的是root节点,也就是说一开始没有节点,那么直接变黑就行。

因为规定不能红色连红色,而调整本质上是为了让树的黑色节点平衡,也就是说如果没有插入节点的时候,爸和爷本质上是平衡的,现在尽量是在爸叔爷和已经新插入的节点的层面上完成平衡,所以需要进行旋转。

注意,不管是左旋还是右旋,统一旋转节点中辈分最大的节点。

public void put(K key, V value) {
  RBNode t = this.root;
  //如果是根节点
  if (t == null) {
    root = new RBNode<>(key, value == null ? key : value, null);
    return;
  }
  int cmp;
  //寻找插入位置
  //定义一个双亲指针
  RBNode parent;
  if (key == null) {
    throw new NullPointerException();
  }
  //沿着跟节点寻找插入位置
  do {
    parent = t;
    cmp = key.compareTo((K) t.key);
    if (cmp < 0) {
      t = t.left;
    } else if (cmp > 0) {
      t = t.right;
    } else {
      t.setValue(value == null ? key : value);
      return;
    }
  } while (t != null);

  RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
  //如果比较最终落在左子树,则直接将父节点左指针指向e
  if (cmp < 0) {
    parent.left = e;
  }
  //如果比较最终落在右子树,则直接将父节点右指针指向e
  else {
    parent.right = e;
  }
  //调整
  fixAfterPut(e);
}

调整操作

如果插入节点的父节点是黑色,那么不用调整。

如果插入节点的父节点是红色,且父节点的兄弟(叔)也为红色,为了平衡黑色,那么在爷爸和新插入节点这个层面下进行调整:将爸叔变为黑色,同时爷变成红色,因为爷变成了红色,对于上层的节点有影响,所以把x赋值成爷循环调整。

如果插入节点的父节点是红色,同时父节点的兄弟为黑色(不存在也是黑色),那么为了保持平衡,需要进行旋转,此时分成两种情况:

1)如果父节点是爷节点的右,插入节点是父节点的左,那么先要把父节点右旋,也就是把父节点和插入节点位置换一下,此时爷,父,子都在一条靠右的线上(如果插入节点是父节点的右则不用操作)。将爷染成红,父染成黑之后,将爷左旋,ok。

2)如果父节点是爷节点的左,插入节点是父节点的右,那么先要把父节点左旋,也就是把父节点和插入节点位置换一下,此时爷,父,子都在一条靠右的线上(如果插入节点是父节点的左则不用操作)。将爷染成红,父染成黑之后,将爷右旋,ok。

private void fixAfterPut(RBNode x) {
  x.color = RED;
  //本质上就是父节点是黑色就不需要调整
  while (x != null && x != root && x.parent.color == RED) {
    //1、x的父节点是爷爷的左孩子(左3)
    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
      //叔叔节点
      RBNode y = rightOf(parentOf(parentOf(x)));
      //第3种情况
      if (colorOf(y) == RED) {
        setColor(parentOf(x), BLACK);
        setColor(y, BLACK);
        setColor(parentOf(parentOf(x)), RED);
        //爷爷节点递归
        x = parentOf(parentOf(x));
      }
      //第2种情况
      else {
        //如果插入节点是父节点的右,统一弄到左边
        //旋转过后父亲节点下去了成了子节点
        //如果不旋的话,那么子节点调整过后会连接在现在爷节点的左边,那么还是不平衡
        if (x == rightOf(parentOf(x))) {
          x = parentOf(x);
          leftRotate(x);
        }
        //父亲变黑
        setColor(parentOf(x), BLACK);
        //爷爷变红
        setColor(parentOf(parentOf(x)), RED);
        //根据爷爷节点右旋转
        rightRotate(parentOf(parentOf(x)));
      }
    }
    //2、跟第一种情况相反操作
    else {
      //右3
      //叔叔节点
      RBNode y = leftOf(parentOf(parentOf(x)));
      //第3种情况
      if (colorOf(y) == RED) {
        setColor(parentOf(x), BLACK);
        setColor(y, BLACK);
        setColor(parentOf(parentOf(x)), RED);
        //爷爷节点递归
        x = parentOf(parentOf(x));
      }
      //第2种情况
      else {
        if (x == leftOf(parentOf(x))) {
          x = parentOf(x);
          rightRotate(x);
        }
        //父亲变黑
        setColor(parentOf(x), BLACK);
        //爷爷变红
        setColor(parentOf(parentOf(x)), RED);
        //根据爷爷节点右旋转
        leftRotate(parentOf(parentOf(x)));
      }
    }
  }
  root.color = BLACK;
}

删除操作

先要通过key指找到节点,这一部分简单

public V remove(K key) {
  RBNode node = getNode(key);
  if (node == null) {
    return null;
  }
  V oldValue = (V) node.value;
  deleteNode(node);
  return oldValue;
}
private RBNode getNode(K key) {
  RBNode node = this.root;
  while (node != null) {
    int cmp = key.compareTo((K) node.key);
    if (cmp < 0) {
      node = node.left;
    } else if (cmp > 0) {
      node = node.right;
    } else
      return node;
  }
  return null;
}

找到节点后执行删除,还是先删除再调整

如果要删除的是红色节点,直接删除即可

删除的时候要明确一点就是:真正删除的节点永远对应234树的叶子节点。

TreeMap里删除是找后继节点,如果没有后继节点,那么这个节点一定在234树的叶子节点上。(因为234树的特性,转换成红黑树后,如果找不到后继节点,那么必是叶子节点或者是三节点裂变的上黑下红,而234树非叶子节点的上黑下红裂变必然左右两边都挂着子节点,所以只有可能在叶子节点),如果有后继节点,那么这个后继节点必然在叶子节点上,所以234树永远删除的是叶子节点。

所以,步骤是:

1.先判断是不是同时有左右孩子(判断是不是同时都有,因为先判断这个可以将其转换成下面的情况),如果满足就找后继(后继有可能是红黑树的叶子或者有一个孩子),找到后继之后,将后继的值赋给待删除节点,现在的目标就转换成了删除这个后继节点了。

2.判断待删除节点是不是只有一个孩子(和上面一脉相承),如果是,直接拿孩子替换,此时必然是待删除节点是黑,孩子是红(因为只有一个孩子,必然是3节点分裂的),所以将孩子染黑代替父节点就完了。如果不是,如果待删除节点是红色的,替换的孩子就不用变色(因为不破坏黑色节点相等的条件),如果待删除节点是黑色的,替换的孩子节点必须染成黑色

3.现在只剩下是待删除节点是叶子节点的情况了,如果叶子节点是红色的,直接删。如果是黑色的,那么有点复杂(注意这里是先调整再删除):

因为要尽量保证删除节点之后树的平衡,所以尽量在待删除节点以他的父节点为根的那一颗子树上保持平衡,所以优先找兄弟节点借。

如果待删除叶子黑节点是父节点的左:

此时找父节点的右,也就是兄弟节点,如果兄弟节点为红的话,那么可以肯定,兄弟节点是由234树的父节点裂变的,且兄弟节点下面必有其他节点(因为如果是3节点的话,肯定是由底下的节点因为满了而往上跑的),因为这个兄弟节点是红色的,而平衡的节点必须要是黑色,兄弟节点必须是黑色才能接下来的操作(产生兄弟节点是红色的原因是因为3节点裂变的的时候有两个方向都可以,导致的不一致),所以只需要旋转回去就行了(这种情况可以百分之一百肯定父节点是黑色),将父节点左旋就行,此时兄弟节点必黑。再判断兄弟节点的右是不是存在也就是是否为黑~~(为什么不判断左是不是有节点,因为要左旋需要保证兄弟节点的右节点为黑,这样旋转之后才能平衡)~~(好吧我也不清楚为什么不判断左节点是否存在,可能的原因是左右节点有可能都存在,记住一个结论:在这种情况下优先判断是否为空),如果为黑色,那么先需要右旋,保证右节点上必须有值(那么问题来了,为什么兄弟节点必有孩子呢?因为前面讲兄弟节点没有孩子情况排除了,这一点稍后再写),最后左旋。

在上面操作还要插入一行,要判断兄弟节点是否有孩子,这样才能借(因为兄弟节点这个时候必然是黑色,如果是红色234树会不成立),如果兄弟节点也是叶子节点的话,那么就将兄弟节点染成红色(不能借),同时将待调整的节点赋给父节点,因为以父节点为根的这颗子树来说,无论怎么调整,已经不能保证平衡了,所以相当于直接减少了支路的黑色节点,由父节点再往上进行调整看看能不能补回。

调整完之后再删除

/**
     * 删除操作:
     * 1、删除叶子节点,直接删除
     * 2、删除的节点有一个子节点,那么用子节点来替代
     * 3、如果删除的节点有2个子节点,此时需要找到前驱节点或者后继节点来替代
     *
     * @param node
     */
private void deleteNode(RBNode node) {
  //3、node节点有2个孩子
  if (node.left != null && node.right != null) {
    /**
             *  这里要注意,如果使用下面这个网站演示的话,此网站用的是前驱节点替代
             *  下面代码里我使用的是后继节点替代,删除节点后显示可能会和该网站不一致,
             *  但是这两种方法红黑树删除都是合法的
             *  (可以自行把前驱节点替代方案屏蔽放开,后继节点替代方案注释掉测试下)
             *
             *  https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
             */

    //后继节点替代
    RBNode rep = successor(node);
    //前驱节点替代
    //            RBNode rep= predecessor(node);
    node.key = rep.key;
    node.value = rep.value;
    node = rep;
  }
  //到这里时,node也就是要删除的节点只可能是只有一个孩子或者没有孩子的了。
  //也就是说,只有可能是对应234树的非叶子节点。
  RBNode replacement = node.left != null ? node.left : node.right;
  //2、替代节点不为空
  if (replacement != null) {
    //替代者的父指针指向的原来node的父亲
    replacement.parent = node.parent;
    //node是根节点
    if (node.parent == null) {
      root = replacement;
    }
    //node是左孩子,所以替代者依然是左孩子
    else if (node == node.parent.left) {
      node.parent.left = replacement;
    }
    //node是右孩子,所以替代者依然是右孩子
    else {
      node.parent.right = replacement;
    }
    //将node的左右孩子指针和父指针都指向null(此时node处于游离状态,等待垃圾回收)
    node.left = node.right = node.parent = null;

    //替换完之后需要调整平衡
    if (node.color == BLACK) {
      //需要调整,这种情况一定是红色(替代节点一定是红色,此时只要变色)
      fixAfterRemove(replacement);
    }
  }
  //删除节点就是根节点,且没有替代节点
  else if (node.parent == null) {
    root = null;
  }
  //1、node节点是叶子节点,replacement为null
  else {
    //先调整
    if (node.color == BLACK) {
      fixAfterRemove(node);
    }
    //再删除
    if (node.parent != null) {
      if (node == node.parent.left) {
        node.parent.left = null;
      } else if (node == node.parent.right) {
        node.parent.right = null;
      }
      node.parent = null;
    }
  }
}
/**
     * 删除后调整
     *
     * @param x
     */
private void fixAfterRemove(RBNode x) {
  while (x != root && colorOf(x) == BLACK) {
    //x是左孩子的情况
    if (x == leftOf(parentOf(x))) {
      //兄弟节点
      RBNode rnode = rightOf(parentOf(x));

      //判断此时兄弟节点是否是真正的兄弟节点
      if (colorOf(rnode) == RED) {
        setColor(rnode, BLACK);
        setColor(parentOf(x), RED);
        leftRotate(parentOf(x));
        //找到真正的兄弟节点
        rnode = rightOf(parentOf(x));
      }
      //情况三,找兄弟借,兄弟没得借,这里不是指的为黑色,而是指的空,因为colorOf在为空的时候返回的是黑
      if (colorOf(leftOf(rnode)) == BLACK && colorOf(rightOf(rnode)) == BLACK) {
        //这种情况的时候,在父节点以下已经无法调整,所以把其兄弟变为红色。
        //此时将矛盾转到父节点,也就是看看从父节点开始还能不能调整。
        setColor(rnode, RED);
        x = parentOf(x);
      }
      //情况二,找兄弟借,兄弟有的借
      else {
        //分2种小情况:兄弟节点本来是3节点或者是4节点的情况
        if (colorOf(rightOf(rnode)) == BLACK) {
          setColor(leftOf(rnode), BLACK);
          setColor(rnode, RED);
          rightRotate(rnode);
          rnode = rightOf(parentOf(x));
        }
        setColor(rnode, colorOf(parentOf(x)));
        setColor(parentOf(x), BLACK);
        setColor(rightOf(rnode), BLACK);
        leftRotate(parentOf(x));
        //表示直接跳出循环,把x赋给root,顺便在最后一步的时候把root变黑
        x = root;
      }
    }
    //x是右孩子的情况
    else {
      //兄弟节点
      RBNode rnode = leftOf(parentOf(x));
      //判断此时兄弟节点是否是真正的兄弟节点
      if (colorOf(rnode) == RED) {
        setColor(rnode, BLACK);
        setColor(parentOf(x), RED);
        rightRotate(parentOf(x));
        //找到真正的兄弟节点
        rnode = leftOf(parentOf(x));
      }
      //情况三,找兄弟借,兄弟没得借
      if (colorOf(rightOf(rnode)) == BLACK && colorOf(leftOf(rnode)) == BLACK) {
        //情况复杂,暂时不写
        setColor(rnode, RED);
        x = parentOf(x);
      }
      //情况二,找兄弟借,兄弟有的借
      else {
        //分2种小情况:兄弟节点本来是3节点或者是4节点的情况
        if (colorOf(leftOf(rnode)) == BLACK) {
          setColor(rightOf(rnode), BLACK);
          setColor(rnode, RED);
          leftRotate(rnode);
          rnode = leftOf(parentOf(x));
        }
        setColor(rnode, colorOf(parentOf(x)));
        setColor(parentOf(x), BLACK);
        setColor(leftOf(rnode), BLACK);
        rightRotate(parentOf(x));
        x = root;
      }
    }
  }
  //情况一、替代节点是红色,则直接染黑,补偿删除的黑色节点,这样红黑树依然保持平衡
  setColor(x, BLACK);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值