这一篇我们来聊聊红黑树,写这篇文章的起因是在阅读HashMap源码时,发现JDK1.8对于HashMap的实现引入了红黑树来处理哈希冲突以提高性能(戳这里,有详述),而红黑树的数据结构和操作都是较为复杂的,自己看得过程中有些地方也反复了多次。。。俗话说得好,好记性不如烂笔头,因此决定写下这篇笔记供自己和需要的人日后参考。在开始之前,首先要感谢张拭心同学的这两篇关于红黑树和二叉查找树的文章:
http://blog.csdn.net/u011240877/article/details/53242179
http://blog.csdn.net/u011240877/article/details/53329023
这两篇文章讲得十分详细,使我受益匪浅,在这里也强烈推荐大家阅读一下。由于拭心同学的文章在分析二叉查找树的查找,插入和删除时引用的是递归的实现,为了不重复,本文分析时将采用循环的实现,为大家提供另一种思路。
OK,正式开始,何为红黑树?红黑树(Red-Black Tree) 是一种自平衡二叉查找树,其每个节点都带有黑或红的颜色属性。由于它的本质也是一种二叉查找树,因此它的查找,插入和删除操作均以二叉查找树的对应操作作为基础;但由于红黑树自身要保证平衡(也即要始终满足其五条特性,这个下文会有详述),每次插入和删除之后它都要进行额外的调整,以恢复自身的平衡,这是它与普通二叉查找树不同的地方,也正因为如此,红黑树的查找,插入和删除操作在最坏情况下的时间复杂度也能保证为O(logN),其中N为树中元素个数。
既然红黑树本质是二叉查找树,那么就有必要先来看一下二叉查找树的相关知识。
二叉查找树
二叉查找树(Binary Search Tree),又名二叉排序树,二叉搜索树,B树。顾名思义,它的节点是可比较的并且具有以下性质:
a. 若左子树不为空,则根节点的值大于其所有左子树中节点的值;
b. 若右子树不为空,则根节点的值小于或等于其所有右子树中节点的值;
c. 左右子树也分别为二叉查找树;
d. 没有键值相等的节点。由于以上性质,中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉查找树变成一个有序序列,构造树的过程即为对无序序列进行查找的过程。每次插入的新的结点都是二叉查找树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索、插入、删除的复杂度等于树高,期望 O(logN),最坏 O(N)(数列有序,树退化成线性表)。
这里先给出一个二叉查找树节点的结构,下文代码中就用它作为树节点的类:
class BSTNode{ int value; //节点的值 BSTNode left; //节点的左子树 BSTNode right; //节点的右子树 BSTNode parent; //节点的父节点 BSTNode(int value, BSTNode parent) { this.value = value; this.parent = parent; } @Override public boolean equals(Object obj) { //两个节点的value相等,则认为两个节点相等 return (obj instanceof BSTNode) && (((BSTNode) obj).value == this.value); } }
下面就分别看一下二叉查找树的查找,插入和删除操作的实现,此处采用循环来实现。
查找
在二叉搜索树T中查找key的过程为:
a. 若T是空树,则搜索失败,否则:
b. 若key等于T的根节点的数据域之值,则查找成功;否则:
c. 若key小于T的根节点的数据域之值,则搜索左子树;否则:
d. 查找右子树。下面是这个过程的Java代码实现:
/** * @param key 目标节点的键值 * @return 与key匹配的节点,若未能成功匹配则返回null */ BSTNode searchBST(int key) { //若根节点为空,或根节点与key匹配成功,则直接返回 if (mRoot == null || mRoot.value == key) { return mRoot; } BSTNode t = mRoot; //从根节点开始循环查找 do { if (key < t.value) { t = t.left; //若key比节点小,则在左子树中继续查找 } else if (key > t.value) { t = t.right; //若key比节点大,则在右子树中继续查找 } else { return t; //匹配成功,返回匹配节点 } } while (t != null); return null; //匹配失败,返回null }
插入
插入可以理解为先查找,找到了就说明已经存在该节点不用再进行插入了(也有可能找到后做覆盖操作,比如HashMap的put方法),找不到就将指针最后停留的叶子节点当做待插入节点的父节点,根据两个节点值的大小关系确定该作为左子树还是右子树插入。下面是相关代码:
/** * @param key 待插入节点的键值 */ void insertBST(int key) { if (mRoot == null) { //若根节点为空,则使用key创建根节点,插入完成 mRoot = new BSTNode(key, null); return; } BSTNode t = mRoot; BSTNode parent; //指向当前遍历到的节点的指针 //从根节点开始循环查找 do { parent = t; if (key < t.value) { t = t.left; //若key比节点小,则在左子树中继续查找 } else if (key > t.value) { t = t.right; //若key比节点大,则在右子树中继续查找 } else { return; //若key与节点的值相等,则说明节点已存在,不需要插入,直接返回(若需要覆盖节点,在这里完成) } } while (t != null); //执行到这一步说明值为key的节点不存在,新创建一个节点,将parent指针指向的节点作为父节点 BSTNode nodeToInsert = new BSTNode(key, parent); if (key < parent.value) { parent.left = nodeToInsert; //若key比parent的值小,则作为parent的左子树插入 } else { parent.right = nodeToInsert; //若key比parent的值大,则作为parent的右子树插入 } }
删除
删除操作第一步也是查找,找到待删除节点后分下列几种情况:
a. 若节点为子节点,直接删除即可;
b. 若节点只有左子树或右子树,则删除该节点后,将其唯一的子树与父节点相连;
c. 若节点有两个子树,则需要选择一个子树,并从中选出合适的节点K与待删除节点的父节点相连。这时树的结构会发生变化,节点K将接替待删除节点作为这一棵子树的根,那么显然,K需要大于其左子树的所有节点且小于右子树的所有节点。这里对于K有两种选择,要么选择待删除节点的左子树中最大的节点,要么选择其右子树中最小的节点,二者皆可,我们选择前者来实现。下面是删除操作相关代码:
/** * @param key 待删除节点的键值 */ void deleteBST(int key) { if (mRoot == null) { return; //若树为空,则无法删除,返回 } BSTNode t = mRoot; BSTNode nodeToDelete = null; //需要删除的节点 //循环查找待删除节点 do { if (key < t.value) { t = t.left; //在左子树中继续 } else if (key > t.value) { t = t.right; //在右子树中继续 } else { nodeToDelete = t; //匹配成功,找到待删除节点,退出循环