数据结构 - 红黑树(Red Black Tree)插入详解与实现(Java)
最终还是决定把红黑树的篇章一分为二,插入操作一篇,删除操作一篇,因为合在一起写篇幅实在太长了,写起来都觉得累,何况是阅读并理解的读者。
红黑树删除操作请参考 数据结构 - 红黑树(Red Black Tree)删除详解与实现(Java)
现在网络上最不缺的就是对某个知识点的讲解博文,各种花样标题百出,更有类似“一文讲懂xxx”,“史上最简单的xxx讲解”,“xxx看了还不懂你打我”之类云云。其中也不乏有些理论甚至是举例都雷同的两篇不同文章,至于作者是不是真的理解自己所写的内容暂且不说,技术博客这种东西,本来就是提供给大家分享自己学习体会的一个平台,我也不敢说自己写的就足够全面简洁易懂,只能说有些东西确实不是一两篇文章就能理解透彻的,只有多读,多思考,慢慢的就会明了,我也是读了好几个人的博文才读懂的,一些前辈的文章确实很不错,值得参考和学习。仅希望我所写这两篇关于红黑树的文章能在众多的同类博文中给偶然看到的读者一点点启示。
正文。
本文要求懂得二叉搜索树的原理,如果还不理解可以转阅(理解第一篇便可以):
一、数据结构 - 从二叉搜索树说到AVL树(一)之二叉搜索树的操作与详解(Java)
二、数据结构 - 从二叉搜索树说到AVL树(二)之AVL树的操作与详解(Java)
众所皆知,二叉平衡树(Binary Balanced Tree)的出现是为了让一棵二叉搜索树的查找效率尽可能的最大化,同时为了构造这么一棵树,在插入和删除的时候也要根据一定的规则进行操作,这些操作在一定情况下也会影响到整棵树的使用效率,所以,我们想有没有这么一种树,我们并不必严格要求这棵树要平衡度很高(比如所有路径的长度差都必须在一个很小的范围之内)以提高插入和删除的效率,同时又不能太影响到查找的效率,已达到一个比较好的使用效果。
在此之前,本文图例约定如下:
红黑树(Red Black Tree - RB Tree)就是这样一种数据结构,和很多数据结构一样,红黑树也有自己的一套事先规定好的规则,无论在什么状态下,一颗红黑树都必须满足以下五个规则(定义), 破坏任何一条规则都不再是一颗红黑树。
1. 红黑树的节点不是红色的就是黑色的
2. 红黑树的根节点永远是黑色的
3. 所有叶子节点都是黑色的(注意:红黑树的叶子节点是指Nil节点)
4. 同一路径上不能有相邻两个节点都是红色的
5. 从任一节点到所有叶子节点所经历的黑色节点个数相同
以上五个定义即使不能背下来,也要十分熟悉。用以上的定义去实现一颗红黑树,能使所有搜索路径长度相差最大不过一倍。
定义红黑树节点的数据结构:
public class TreeNode { private int elem; private TreeNode left, right; private TreeNode parent; private NodeColor color; public TreeNode (int elem) { this.elem = elem; color = NodeColor.RED; } }
比普通二叉搜索树多了一个属性表示节点颜色,初始化一个节点的时候,节点颜色设置为红色,因为插入一个红色节点,只要不违反红黑树的规则,插入之后不需要对树进行调整,但如果直接插入一个黑色节点,那肯定会违反上面所说的第5个规则,势必要进行调整,所以多一事不如少一事。
在此之前先讲一些基本操作,然后再讲具体
红黑树的基本操作包括染色和旋转,染色没有什么可说的,根据上面所说的第一条定义,染色无非是把一个节点从黑色染成红色或反之。
旋转包括右旋和左旋,具体的操作图例和代码从我之前写的一篇文章复制过来就好。
右旋:
做法是以A节点为轴,节点A的左子树指向其左孩子B的右子树2,然后节点B的左子树指向节点A,然后原本节点A的父节点R对应的子树指向节点B,其他节点不作变化,这边便完成了左旋操作。
相应的代码如下:以A点为轴进行右旋
private void rotateRight(TreeNode pivot) { TreeNode leftChild = pivot.getLeft(); TreeNode grandChildRight = leftChild.getRight(); TreeNode parent = pivot.getParent(); if (null == parent) { this.root = leftChild; } else if (pivot == parent.getLeft()) { parent.setLeft(leftChild); } else { parent.setRight(leftChild); } leftChild.setParent(parent); pivot.setLeft(grandChildRight); if (null != grandChildRight) { grandChildRight.setParent(pivot); } leftChild.setRight(pivot); pivot.setParent(leftChild); }
左旋:
左旋的操作跟右旋一样,但是结构是相反的,以节点A为轴,节点A的右子树指向其有孩子B的左子树2,然后节点B的左子树指向节点A,再使原节点A的父节点对应的子树指向节点B,其他节点不做改变。
相应的代码如下:以A点为轴进行左旋
private void rotateLeft(TreeNode pivot) { TreeNode rightChild = pivot.getRight(); TreeNode grandChildLeft = rightChild.getLeft(); TreeNode parent = pivot.getParent(); if (null == parent) { // pivot node is root this.root = rightChild; } else if(pivot == parent.getLeft()) { parent.setLeft(rightChild); } else { parent.setRight(rightChild); } rightChild.setParent(parent); pivot.setRight(grandChildLeft); if (null != grandChildLeft) { grandChildLeft.setParent(pivot); } rightChild.setLeft(pivot); pivot.setParent(rightChild); }
同样,请牢牢记住这个旋转规则,当需要的时候可以信手拈来,不要卡在这种基础操作上。
上面已经说到初始化一个新的节点N(New)的时候,节点的颜色设置为红色,然后根据插入的情况可以分为以下两种:
一、插入节点的父节点P(Parent)是黑色节点
这种情况很舒服,插入一个红色节点,而父节点又恰好是黑色的,不违反以上某一条定义,插入结束。
二、插入的节点父节点P是红色节点
这种情况插入时直接违反了上面第四条定义,从这个条件接下去细分,观察插入节点的叔叔节点U(Uncle)
① 如果节点U是红色的,做法是把祖父节点GP(Grandparent)染为红色,并把父节点P和叔叔节点U染为黑色。有人有疑问说那如果祖父节点GP本来就是红色的怎么办,GP节点不可能为红色,因为如果GP节点为红色,那插入之前就违反了第四条定义。无论在什么情况下,请确保插入前的树是一颗合格的红黑树!
如果N为右子树也同理(注意图中省略了Nil节点)
② 如果节点U是黑色的(其实就是Nil节点,因为如果如果U不为Nil节点,那N所在的位置本来就应该是一个不为Nil的黑色节点,否则从GP节点下来就会出现两条黑色节点数不同的路径,与第五条定义相悖),且节点N为节点U的远侄子节点,此时的调整做法是把节点P染为黑色,把节点GP染为红色,并以GP节点根据实际情况做相应的旋转(若节点U为GP的右子树,则以GP为轴做右旋操作,若节点U为GP的左子树,则以GP为轴做左旋操作)。
若此时节点N是节点U的近侄子节点,做法是以节点P为轴做相应的旋转操作(若N为P的左子树,则以P为轴做右旋操作,若N为P的右子树,则以P为轴做左旋操作),旋转之后转为上面的情况①,再按照情况①的操作进行调整。
这样操作之后确保从GP下来的黑色节点数目在调整前后保持不变,如果此时GP节点不是根节点,那如果GP节点的父节点也是红色的,那此时要把GP当做新插入的节点继续向上调整,调整规则与上面①②一致,直到遇到黑色节点或者根节点为止(主要针对①情况,因为②情况调整之后当前子树的根节点就已经是黑色的不会影响整棵树的结构),每次插入结束后如果根节点不是黑色的,根据第二条定义,把根节点设置为黑色。
所有情况处理好之后,开始写代码
写一个插入新元素的公共方法:
public boolean insert(int elem) { TreeNode node = new TreeNode(elem); boolean inserted = false; if (null == this.root) { this.root = node; inserted = true; } else { inserted = insertNode(this.root, node); } setRootBlack(); //the root must be always black return inserted; }
子方法 private boolean insertNode(TreeNode node, TreeNode newNode) 表示把newNode插入到node的子树当中,插入成功返回true,元素已经存在则返回false,方法体如下:
private boolean insertNode(TreeNode node, TreeNode newNode) { if (node.getElem() == newNode.getElem()) { return false; // the element already exist } else if (node.getElem() < newNode.getElem()) { if (null == node.getRight()) { node.setRight(newNode); newNode.setParent(node); insertFixUp(newNode); return true; } else { return insertNode(node.getRight(), newNode); } } else { if (null == node.getLeft()) { node.setLeft(newNode); newNode.setParent(node); insertFixUp(newNode); return true; } else { return insertNode(node.getLeft(), newNode); } } }
插入之后就是调整啦,根据上面的调整规则编写函数 private void insertFixUp(TreeNode node) 表示从node开始向上调整红黑树
private void insertFixUp(TreeNode node) { TreeNode parent = node.getParent(); while (null != parent && parent.getColor() == NodeColor.RED) { // parent should not be root for root node must be black boolean uncleInRight = parent.getParent().getLeft() == parent; TreeNode uncle = uncleInRight ? parent.getParent().getRight() : parent.getParent().getLeft(); if (null == uncle) { // uncle is Nil and could not be black node if (uncleInRight) { if (node == parent.getLeft()) { // case 1 parent.setColor(NodeColor.BLACK); parent.getParent().setColor(NodeColor.RED); rotateRight(parent.getParent()); break; } else { // case 2 rotateLeft(parent); node = node.getLeft(); // convert to case 1 } } else { if (node == parent.getRight()) { // case 3 parent.setColor(NodeColor.BLACK); parent.getParent().setColor(NodeColor.RED); rotateLeft(parent.getParent()); break; } else { // case 4 rotateRight(parent); node = node.getRight(); // convert to case 3 } } } else { // uncle node is red parent.setColor(NodeColor.BLACK); uncle.setColor(NodeColor.BLACK); parent.getParent().setColor(NodeColor.RED); node = parent.getParent(); } parent = node.getParent(); } }
至此红黑树插入操作结束,步骤也是相对简单,希望对大家的理解有所帮助。
尊重知识产权,引用转载请通知作者!