前言
我这个人懒得不行,其实早就想学习学习红黑树了,以前只是知道这东西,也知道他是构成HashMap的底层数据结构(jdk1.8以后),但是每次一看红黑树的5条性质就头疼,不想去自习考虑这个数据结构。这个寒假终于下定决心用java手撕一遍红黑树,为了以后的面试做准备。由于这个数据结构有点庞大,我不想从基础开始讲解了,B站好多教学视频,我看了好多,但是现场手撕红黑树的,翻来翻去就那么几个。他们写代码的风格我不是太喜欢,且多是用的idea这个IDE,学生哪有钱买正版,我就直接用eclipse了,反正原理都一样,只是工程结构有差异而已。
本文旨在稍微了解红黑树原理的情况下,如何手写红黑树的教学。当然,这篇文章我也是边学边写的,这样能立刻记录下我再学习的时候不会的地方,与大家一起讨论。同时,写代码的思想还是很重要的。相信这篇文章看完,能够了解如何写好这么一个庞大的数据结构。从而在面试的时候手撕类似的数据结构不会慌张。
简介
这里的简介就是红黑树的5条性质:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点是黑色。
- 如果一个节点是红色的,则它的子节点必须是黑色的。叶子节点是空的,不显示,是逻辑虚拟出来的节点,实际是null
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
其中就是5不好理解。意思是从一个节点,到达以该节点为根节点的所有叶子节点的路径中,每个路径的黑色节点数保持相同。
我们写代码,最主要的目的就是维护这5条性质
当然,维护这5条性质最核心的操作就是左右旋,我们要知道什么时候左旋,什么时候右旋。大可不必为左右旋而烦恼,左右旋的代码极其简单,只是听起来好难。左右旋本质上就是节点重新插入的过程。
举个例子:
但是重新插入又不好插,毕竟4,7,9几个节点下面可能还有其他节点。全部重新插入时间复杂度太大了,所以我们才有了左旋右旋的说法。具体的代码在用到了再说。
代码实现
节点定义
/**
* @author Administrator
* We should implement Comparable methods to compare the values
*/
public class RBNode implements Comparable<RBNode>{
/**There is only 2 colors, so we can use boolean type to represent them**/
public static final boolean RED = true;
public static final boolean BLACK = false;
/**Now that we define them as public, we don't have to add setters and getters**/
public String value;
public RBNode left;
public RBNode right;
public RBNode parent;
public boolean color;
@Override
public int compareTo(RBNode o) {
if(this.value == o.value) {
return 0;
}
return this.value.hashCode()>o.value.hashCode()?1:-1;
}
public RBNode(String value) {
this.value = value;
this.left = null;
this.right = null;
this.parent = null;
this.color = RED;
}
@Override
public String toString() {
return "value = " + value + ", " + (this.color==true?"RED":"BLACK");
}
}
这里我说一下为什么这么写。
- 定义颜色就不用说了,设置成常量替代,有助于逻辑的书写。
- 其他的属性我都定义成了public,也就不用再写setters和getters方法了。当然,正常开发这么写肯定是不行的,咱们主要是逻辑的疏通,把他们换成private还是很简单的,这里就直接用public了。
- 初始化方法中,只用赋值value就好,color初始化为红色
- 重载toString()方法。
- 实现compare的比较,这个不用说。
有什么没有考虑到的东西我们可以边写边添加。
红黑树定义
框架搭建
/**
* @author Administrator
* All the operations about Red-Black Tree.
* 1. insert()
* 2. delete()
* 3. All the must operations
* 3.1 left rotate
* 3.2 right rotate
* 3.3 renovation
* 4. show(), inOrder go through
*/
public class RBTree {
/**root of the tree**/
private RBNode root;
/**insert operations**/
public void insert(RBNode node) {
}
/**delete operations**/
public void delete(RBNode node) {
}
/**show operations**/
public void inOrder(RBNode rootNode) {
}
/**other operations**/
private void leftRotate(RBNode curNode) {
}
private void rightRotate(RBNode curNode) {
}
private void renovate(RBNode rootNode) {
}
}
先确定框架,这是至少需要的操作,如果后续有需要的其他操作,我们再添加就好。这个遍历操作只实现了中序遍历,其他遍历方法很简单,不再书写。
插入操作
public void insert(String value) {
RBNode node = new RBNode(value);
RBNode temp = this.root;
if(temp == null){
root = node;
}
while(temp!=null) {
if(node.compareTo(temp) > 0) {
// node.value > temp.value
if(temp.right != null) {
temp = temp.right;
}else {
temp.right = node;
node.parent = temp;
}
}else if(node.compareTo(temp) < 0) {
// node.value < temp.value
if(temp.left != null) {
temp = temp.left;
}else {
temp.left = node;
node.parent = temp;
}
}else {
// node.value = temp.value, cover the original node;
/**It's strange, but the idea is as this**/
node.left = temp.left;
node.right = temp.right;
node.color = temp.color;
if(temp.parent != null) {
RBNode parent = temp.parent;
node.parent = parent;
if(temp.equals(parent.left)) {
parent.left = node;
}else if (temp.equals(parent.right)) {
parent.right = node;
}
}
}
}
renovate(node);
}
我们信奉特殊值特殊处理的方法,那就是如果插入值与树内部的值重复,替换就好了。最后的renovate再做其他处理,以后再实现。
其实整体的插入操作分为两部分,第一部分就是二叉查找树的插入,第二部分就是对红黑树性质的维护。当然,好多人将插入和维护写在一起了,其实我们推荐这种写法的,只是对新手不太友好,不好理解。
删除操作
同插入一样,我们需要遍历所有的情况,那么删除一个二叉搜索树的节点有几种情况呢?
- 无孩子节点的删除
- 无左孩子节点的删除:右孩子接上删除节点的父节点
- 无右孩子节点的删除:左孩子接上删除节点的父节点
- 无孩子节点删除:直接删除