文章超字数限制了,所以拆成上下两篇
5、平衡二叉树
二叉查找树查找算法的性能取决于二叉树的结构,而 二叉查找树的形状则取决于其数据集。
如果数据呈有序排列,则二叉排序树是线性的,查找的时间复杂度为O(n); 反之,如果二叉排序树的结构合理,则查找速度较快,查找的时间复杂度为 O(log2n)。
树的高度越小,查找速度越快——从树的形态来看,就是使树尽可能平衡。
有资料将平衡二叉树和AVL视作一体,本文采用了AVL树是平衡二叉树的一种的说法。
5.1、AVL树
AVL树是最先发明的自平衡二叉查找树。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。
AVL树是带平衡条件的二叉查找树:
- (1 ) 左子树和右子树的深度之差的绝对值不超过1;
- (2) 左子树和右子树也是平衡二叉树。
若将二叉树上结点的平衡因子(Balance Factor, BF)定义为该结点左子树和右子树的深度之 差,则平衡二叉树上所有结点的平衡因子只可能是 -1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1 , 则该二叉树就是不平衡的。
在AVL中任何节点的两个儿子子树的高度最大差别为1,所以它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。得益于这个特征,它的深度和 log2n 是同数量级的(其中n为结点数)。 由此,其查找的时间复杂度是O(log2n)。
5.1.2、AVL树的平衡调整方法
插入结点时, 首先按照二叉排序树处理, 若插入结点后破坏了平衡二叉树的特性, 需对平衡二叉树进行调整。 调整方法是:找到离插入结点最近且平衡因子绝对值超过1的祖先结点, 以该结点为根的子树称为最小不平衡子树, 可将重新平衡的范围局限千这棵子树。
在平衡调整的过程中,有一个关键点是旋转。
这里有一个具体例子:
- (1) 空树和1个结点⑬的树显然都是平衡的二叉树。在插入24之后仍是平衡的, 只是根结点的平衡因子BF由0变为-1, 如图18(a) -(c)所示。
- (2) 在继续插入37之后, 由千结点 ⑬ 的BF值由 -1 变成 -2, 由此出现了不平衡的现象。此时好比一根扁担出现一头重一头轻的现象, 若能将扁担的支撑点由 ⑬ 改至 ㉔ , 扁担的两头就平衡了。此,可以对树做一个向左逆时针 " 旋转 " 的操作,令结点 ㉔为根,而结点 ⑬ 为它的左子树,此时,结点⑬ 和 ㉔ 的平衡因子都为0, 而且仍保持二叉排序树的特性,如图18(d)~ (e)所示。
- (3) 在继续插入90和53之后,结点 ㊲ 的BF值由-1变成-2, 排序树中出现了新的不平衡现象,需进行调整。但此时由于是结点邸插在结点 (90) 的左子树上,因此不能如上做简单调整。离插入结点最近的最小不平衡子树是以结点 ㊲为根的子树。这时,必须以 (53) 作为根结点,而使 ㊲ 成为它的左子树的根,(90) 成为它的右子树的根。这好比对树做了两次 “旋转” 操作,先向右顺时针旋转,后向左逆时针旋转(见图18 (f)~(h)), 使二叉排序树由不平衡转化为平衡。
一般情况下,假设最小不平衡子树的根结点为 A, 则失去平衡后进行调整的规律可归纳为下列4种情况。
- (1) LL 型:由于在 A 左子树根结点的左子树上插入结点,A 的平衡因子由 1 增至 2, 致使以A为根的子树失去平衡,则需进行一次向右的顺时针旋转操作,如图21所示。
图22所示为两个LL型调整的实例。
- (2) RR 型:由千在 A 的右子树根结点的右子树上插入结点, A 的平衡因子由 -1 变为 -2,致使以 A 为根结点的子树失去平衡,则需进行一次向左的逆时针旋转操作,如图23所示。
图24所示为两个RR型调整的实例。
- (3) LR型:由千在A的左子树根结点的右子树上插入结点, A的平衡因子由1增至2,致使以A为根结点的子树失去平衡, 则需进行两次旋转操作。 第一次对B及其右子树进行逆时针旋转, C转上去成为B的根, 这时变成了LL型, 所以第二次进行LL型的顺时针旋转即可恢复平衡。 如果C原来有左子树, 则洞整C的左子树为B的右子树, 如图25所示。
LR型旋转前后A、 B、C三个结点平衡因子的变化分为3种情况, 图 26 所示为3种 LR型调整的实例。
- (4) RL 型:由千在 A 的右子树根结点的左子树上插入结点, A 的平衡因子由 -1 变为-2,致使以 A 为根结点的子树失去平衡, 则旋转方法和 LR 型相对称, 也需进行两次旋转, 先顺时针右旋, 再逆时针左旋, 如图 27 所示。
同 LR 型旋转类似, RL 型旋转前后 A 、 B 、 C 三个结点的平衡因子的变化也分为 3 种情况,图 28 所示为 3 种 RL 型调整的实例。
上述 4 种情况中,(1) 和 (2) 对称,进行的是单旋转的操作;(3) 和 (4) 对称,进行的是双旋转的操作。
旋转操作的正确性容易由 “保持二叉排序树的特性:中序遍历所得关键字序列自小至大有序” 证明之。 同时, 无论哪一种情况, 在经过平衡旋转处理之后,以 B 或 C 为根的新子树为平衡二叉树,而且它们的深度和插入之前以 A为根的子树相同。
因此, 当平衡的二叉排序树因插入结点而失去平衡时, 仅需对最小不平衡子树进行平衡旋转处理即可。 因为经过旋转处理之后的子树深度和插入之前相同,因而不影响插入路径上所有祖先结点的平衡度。
5.1.3、AVL树的插入
在平衡的二叉排序树BBST上插入一个新的数据元素e的递归算法可描述如下。
在上面我们看到插入节点,如果破坏了AVL树的平衡,则需要进行旋转,即上面的四种情况:
- LL 执行一次右旋转
- RR 执行一次左旋转
-
LR 先左旋,后右旋
-
RL 先右旋后左旋
5.1.3、AVL树删除
前面已经看过二叉树的删除操作,AVL树的删除操作同样分为三种情况:
- 删除节点为叶子节点
- 删除节点有左子树或右子树
- 删除节点有左子树和右子树
只不过 AVL 树在删除节点后需要重新检查平衡性并修正,同时,删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡节点进行修正,而删除操作需要修正栈中的所有非平衡节点。
具体代码实现:
public class AVLBinaryTree {
public int size;
//节点
class Node{
public int val;
public Node left,right;
public int height;
public Node(int val){
this.val=val;
left=null;
right=null;
height=1;
}
}
//添加一个节点
public Node add(Node node,int val){
if (node==null){
size++;
return new Node(val);
}
if (node.val<val) node.right=add(node.right,val);
if (node.val>val) node.left=add(node.left,val);
//更新高度
node.height=Math.max(getHeight(node.left),getHeight(node.right))+1;
//计算平衡因子
int balanceFactor=getBlalanceFactor(node);
//维护平衡
//LL
if (balanceFactor>1&&getBlalanceFactor(node.left)>=0){
return rightRotate(node);
}
//RR
if (balanceFactor<-1&&getBlalanceFactor(node.right)<=0){
return leftRotate(node);
}
//LR
if (balanceFactor>1&&getBlalanceFactor(node.left)<0){
node.left=leftRotate(node.left);
return rightRotate(node);
}
//RL
if (balanceFactor<-1&&getBlalanceFactor(node.right)>0){
node.right=rightRotate(node.right);
return leftRotate(node);
}
return node;
}
/**
* 对根节点x进行向左旋转操作,更新height后返回新的根节点y
* @param x
* @return
*/
public Node leftRotate(Node x){
Node y=x.right;
Node T3=y.left;
y.left=x;
x.right=T3;
//更新height
x.height=Math.max(getHeight(x.left),getHeight(x.right))+1;
y.height=Math.max(getHeight(y.left),getHeight(y.right))+1;
return y;
}
/**
* 对根节点进行右旋转操作,更新height后返回新的根节点y
* @param x
* @return
*/
public Node rightRotate(Node x){
Node y=x.left;
Node T3=y.right;
y.right=x;
x.left=T3;
//更新height
x.height=Math.max(getHeight(x.left),getHeight(x.right))+1;
y.height=Math.max(getHeight(y.left),getHeight(y.right))+1;
return y;
}
//获得节点Node的高度
public int getHeight(Node node){
if (node==null){
return 0;
}
return node.height;
}
//获取节点的平衡因子
private int getBlalanceFactor(Node node){
if (node==null){
return 0;
}
return getHeight(node.left)-getHeight(node.right);
}
/**
* 删除节点
* @param node
* @param val
* @return
*/
public Node remove(Node node,int val){
if (node==null) return null;
Node retNode;
//递归查找要删除的节点
if (node.val<val){
node.left=remove(node.left,val);
retNode=node;
}else if(node.val>val){
node.right=remove(node.right,val);
retNode=node;
}else{
//找到了要删除的节点
//情形1:被删除节点为叶子节点
if (node.right==null){
Node leftNode=node.left;
node.left=null;
size--;
retNode=leftNode;
}
//情形2.1:被删除节点只有右孩子
if (node.left==null){
Node leftNode=node.left;
node.left=null;
size--;
retNode=leftNode;
}
//情形2.2:被删除节点只有左孩子
if (node.right==null){
Node rightNode=node.right;
node.right=null;
size--;
retNode=rightNode;
}else{
//情形3:被删除节点有左、右孩子
Node minNode=minimum(node);
minNode.right=remove(node.right,minNode.val);
node.left=node.right=null;
retNode=minNode;
}
}
if (retNode==null) return retNode;
//删除完成,开始进行二叉树的平衡
//更新高度
retNode.height= Math.max(getHeight(retNode.left),getHeight(retNode.right)+1);
//计算平衡因子
int balanceFactor=getBlalanceFactor(retNode);
//维护平衡
//维护平衡
//LL
if (balanceFactor>1&&getBlalanceFactor(retNode.left)>=0){
return rightRotate(retNode);
}
//RR
if (balanceFactor<-1&&getBlalanceFactor(retNode.right)<=0){
return leftRotate(retNode);
}
//LR
if (balanceFactor>1&&getBlalanceFactor(retNode.left)<0){
retNode.left=leftRotate(retNode.left);
return rightRotate(retNode);
}
//RL
if (balanceFactor<-1&&getBlalanceFactor(retNode.right)>0){
retNode.right=rightRotate(retNode.right);
return leftRotate(retNode);
}
return retNode;
}
//获取该节点的整个子树的最小值
public Node minimum(Node node){
if (node.left==null){
return node;
}
return minimum(node.left);
}
}
5.2、红黑树
红黑树是一种常见的自平衡二叉查找树,常用于关联数组、字典,在各种语言的底层实现中被广泛应用,Java 的 TreeMap 和 TreeSet 就是基于红黑树实现的。
5.2.1、红黑树的定义和性质
红黑树::红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,称之为"对称二叉B树",它现在的名字来自Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中。
红黑树是具有如下性质的二叉查找树:
-
(1)每个节点是黑色或者红色
-
(2)根节点是黑色。
-
(3)每个叶子节点是黑色。
-
(4)从任意一个节点到叶子节点,所经过的黑色节点数目必须相等
-
(5) 空节点被认为是黑色的
5.2.2、红黑树的平衡调整方法
作为一种平衡二叉树,红黑树的自平衡调整方法和AVL类似。关键也是在旋转。旋转同样也是左旋和右旋。找到了两个左旋和右旋的动图。
和AVL树不同的是,红黑树还有颜色性质,所以还会进行变色来平衡红黑树。
5.2.2、红黑树的插入
红黑树的插入和AVL树类似,同样是插入节点后需要对二叉树的平衡性进行修复。
新插入的节点是红色的,插入修复操作如果遇到父节点的颜色为黑则修复操作结束。也就是说,只有在父节点为红色节点的时候是需要插入修复操作的。
插入修复操作分为以下的三种情况,而且新插入的节点的父节点都是红色的:
- 叔叔节点也为红色。
- 叔叔节点为空,且祖父节点、父节点和新节点处于一条斜线上。
- 叔叔节点为空,且祖父节点、父节点和新节点不处于一条斜线上。
- 情形1 情形1的操作是将父节点和叔叔节点与祖父节点的颜色互换,这样就符合了RBTRee的定义。即维持了高度的平衡,修复后颜色也符合RBTree定义的第三条和第四条。下图中,操作完成后A节点变成了新的节点。如果A节点的父节点不是黑色的话,则继续做修复操作。
- 情形2 情形2的操作是将B节点进行右旋操作,并且和父节点A互换颜色。通过该修复操作RBTRee的高度和颜色都符合红黑树的定义。如果B和C节点都是右节点的话,只要将操作变成左旋就可以了。
- 情形3: 情形3的操作是将C节点进行左旋,这样就从情形3转换成情形2了,然后针对情形2进行操作处理就行了。情形2操作做了一个右旋操作和颜色互换来达到目的。如果树的结构是下图的镜像结构,则只需要将对应的左旋变成右旋,右旋变成左旋即可。
- 总结
插入后的修复操作是一个向root节点回溯的操作,一旦牵涉的节点都符合了红黑树的定义,修复操作结束。之所以会向上回溯是由于情形操作会将父节点,叔叔节点和祖父节点进行换颜色,有可能会导致祖父节点不平衡(红黑树定义3)。这个时候需要对祖父节点为起点进行调节(向上回溯)。
祖父节点调节后如果还是遇到它的祖父颜色问题,操作就会继续向上回溯,直到root节点为止,根据定义root节点永远是黑色的。在向上的追溯的过程中,针对插入的3中情况进行调节。直到符合红黑树的定义为止。直到牵涉的节点都符合了红黑树的定义,修复操作结束。
如果上面的3中情况如果对应的操作是在右子树上,做对应的镜像操作就是了。
5.2.3、红黑树的删除
红黑树的删除大体上和二叉查找树的删除类似,如果是叶子节点就直接删除,如果是非叶子节点,会用对应的中序遍历的后继节点来顶替要删除节点的位置。
但是,红黑树删除之后需要做修复的操作,使树符合红黑树的定义。
删除修复操作在遇到被删除的节点是红色节点或者到达root节点时,修复操作完毕。
删除修复操作是针对删除黑色节点才有的,当黑色节点被删除后会让整个树不符合RBTree的定义的第四条。需要做的处理是从兄弟节点上借调黑色的节点过来,如果兄弟节点没有黑节点可以借调的话,就只能往上追溯,将每一级的黑节点数减去一个,使得整棵树符合红黑树的定义。
删除操作的总体思想是从兄弟节点借调黑色节点使树保持局部的平衡,如果局部的平衡达到了,就看整体的树是否是平衡的,如果不平衡就接着向上追溯调整。
(删除黑色节点后)删除修复操作分四种情况:
- 情形1:待删除的节点的兄弟节点是红色的节点
由于兄弟节点是红色节点的时候,无法借调黑节点,所以需要将兄弟节点提升到父节点,由于兄弟节点是红色的,根据红黑树的定义,兄弟节点的子节点是黑色的,就可以从它的子节点借调了。
情形1这样转换之后就会变成后面的情形2,情形 3,或者情形 4进行处理了。上升操作需要对C做一个左旋操作,如果是镜像结构的树只需要做对应的右旋操作即可。
之所以要做情形1操作是因为兄弟节点是红色的,无法借到一个黑节点来填补删除的黑节点。
- 情形2:待删除的节点的兄弟节点是黑色的节点,且兄弟节点的子节点都是黑色的
情形2的删除操作是由于兄弟节点可以消除一个黑色节点,因为兄弟节点和兄弟节点的子节点都是黑色的,所以可以将兄弟节点变红,这样就可以保证树的局部的颜色符合定义了。这个时候需要将父节点A变成新的节点,继续向上调整,直到整颗树的颜色符合红黑树的定义为止。
情形2这种情况下之所以要将兄弟节点变红,是因为如果把兄弟节点借调过来,会导致兄弟的结构不符合红黑树的定义,这样的情况下只能是将兄弟节点也变成红色来达到颜色的平衡。当将兄弟节点也变红之后,达到了局部的平衡了,但是对于祖父节点来说是不符合定义4的。这样就需要回溯到父节点,接着进行修复操作。
- 情形3:待调整的节点的兄弟节点是黑色的节点,且兄弟节点的左子节点是红色的,右节点是黑色的(兄弟节点在右边),如果兄弟节点在左边的话,就是兄弟节点的右子节点是红色的,左节点是黑色的
情形3的删除操作是一个中间步骤,它的目的是将左边的红色节点借调过来,这样就可以转换成情形4状态了,在情形4状态下可以将D,E节点都阶段过来,通过将两个节点变成黑色来保证红黑树的整体平衡。
之所以说情形3是一个中间状态,是因为根据红黑树的定义来说,下图并不是平衡的,他是通过case 2操作完后向上回溯出现的状态。之所以会出现情形3和后面的情形4的情况,是因为可以通过借用侄子节点的红色,变成黑色来符合红黑树定义4.
- 情形4:待调整的节点的兄弟节点是黑色的节点,且右子节点是是红色的(兄弟节点在右边),如果兄弟节点在左边,则就是对应的就是左节点是红色的
情形4的操作是真正的节点借调操作,通过将兄弟节点以及兄弟节点的右节点借调过来,并将兄弟节点的右子节点变成红色来达到借调两个黑节点的目的,这样的话,整棵树还是符合红黑树的定义的。
情形这种情况的发生只有在待删除的节点的兄弟节点为黑,且子节点不全部为黑,才有可能借调到两个节点来做黑节点使用,从而保持整棵树都符合红黑树的定义。
图36:红黑树删除情形4
代码实现:
- 节点类
public class RBTreeNode<T extends Comparable<T>> {
private T value;//node value
private RBTreeNode<T> left;//left child pointer
private RBTreeNode<T> right;//right child pointer
private RBTreeNode<T> parent;//parent pointer
private boolean red;//color is red or not red
//省略getter、setter,构造方法
}
- 红黑树
public class RBTree<T extends Comparable<T>> {
private final RBTreeNode<T> root;
//node number
private java.util.concurrent.atomic.AtomicLong size =
new java.util.concurrent.atomic.AtomicLong(0);
//in overwrite mode,all node's value can not has same value
//in non-overwrite mode,node can have same value, suggest don't use non-overwrite mode.
private volatile boolean overrideMode=true;
public RBTree(){
this.root = new RBTreeNode<T>();
}
public RBTree(boolean overrideMode){
this();
this.overrideMode=overrideMode;
}
public boolean isOverrideMode() {
return overrideMode;
}
public void setOverrideMode(boolean overrideMode) {
this.overrideMode = overrideMode;
}
/**
* number of tree number
* @return
*/
public long getSize() {
return size.get();
}
/**
* get the root node
* @return
*/
private RBTreeNode<T> getRoot(){
return root.getLeft();
}
/**
* add value to a new node,if this value exist in this tree,
* if value exist,it will return the exist value.otherwise return null
* if override mode is true,if value exist in the tree,
* it will override the old value in the tree
*
* @param value
* @return
*/
public T addNode(T value){
RBTreeNode<T> t = new RBTreeNode<T>(value);
return addNode(t);
}
/**
* find the value by give value(include key,key used for search,
* other field is not used,@see compare method).if this value not exist return null
* @param value
* @return
*/
public T find(T value){
RBTreeNode<T> dataRoot = getRoot();
while(dataRoot!=null){
int cmp = dataRoot.getValue().compareTo(value);
if(cmp<0){
dataRoot = dataRoot.getRight();
}else if(cmp>0){
dataRoot = dataRoot.getLeft();
}else{
return dataRoot.getValue();
}
}
return null;
}
/**
* remove the node by give value,if this value not exists in tree return null
* @param value include search key
* @return the value contain in the removed node
*/
public T remove(T value){
RBTreeNode<T> dataRoot = getRoot();
RBTreeNode<T> parent = root;
while(dataRoot!=null){
int cmp = dataRoot.getValue().compareTo(value);
if(cmp<0){
parent = dataRoot;
dataRoot = dataRoot.getRight();
}else if(cmp>0){
parent = dataRoot;
dataRoot = dataRoot.getLeft();
}else{
if(dataRoot.getRight()!=null){
RBTreeNode<T> min = removeMin(dataRoot.getRight());
//x used for fix color balance
RBTreeNode<T> x = min.getRight()==null ? min.getParent() : min.getRight();
boolean isParent = min.getRight()==null;
min.setLeft(dataRoot.getLeft());
setParent(dataRoot.getLeft(),min);
if(parent.getLeft()==dataRoot){
parent.setLeft(min);
}else{
parent.setRight(min);
}
setParent(min,parent);
boolean curMinIsBlack = min.isBlack();
//inherit dataRoot's color
min.setRed(dataRoot.isRed());
if(min!=dataRoot.getRight()){
min.setRight(dataRoot.getRight());
setParent(dataRoot.getRight(),min);
}
//remove a black node,need fix color
if(curMinIsBlack){
if(min!=dataRoot.getRight()){
fixRemove(x,isParent);
}else if(min.getRight()!=null){
fixRemove(min.getRight(),false);
}else{
fixRemove(min,true);
}
}
}else{
setParent(dataRoot.getLeft(),parent);
if(parent.getLeft()==dataRoot){
parent.setLeft(dataRoot.getLeft());
}else{
parent.setRight(dataRoot.getLeft());
}
//current node is black and tree is not empty
if(dataRoot.isBlack() && !(root.getLeft()==null)){
RBTreeNode<T> x = dataRoot.getLeft()==null
? parent :dataRoot.getLeft();
boolean isParent = dataRoot.getLeft()==null;
fixRemove(x,isParent);
}
}
setParent(dataRoot,null);
dataRoot.setLeft(null);
dataRoot.setRight(null);
if(getRoot()!=null){
getRoot().setRed(false);
getRoot().setParent(null);
}
size.decrementAndGet();
return dataRoot.getValue();
}
}
return null;
}
/**
* fix remove action
* @param node
* @param isParent
*/
private void fixRemove(RBTreeNode<T> node,boolean isParent){
RBTreeNode<T> cur = isParent ? null : node;
boolean isRed = isParent ? false : node.isRed();
RBTreeNode<T> parent = isParent ? node : node.getParent();
while(!isRed && !isRoot(cur)){
RBTreeNode<T> sibling = getSibling(cur,parent);
//sibling is not null,due to before remove tree color is balance
//if cur is a left node
boolean isLeft = parent.getRight()==sibling;
if(sibling.isRed() && !isLeft){//case 1
//cur in right
parent.makeRed();
sibling.makeBlack();
rotateRight(parent);
}else if(sibling.isRed() && isLeft){
//cur in left
parent.makeRed();
sibling.makeBlack();
rotateLeft(parent);
}else if(isBlack(sibling.getLeft()) && isBlack(sibling.getRight())){//case 2
sibling.makeRed();
cur = parent;
isRed = cur.isRed();
parent=parent.getParent();
}else if(isLeft && !isBlack(sibling.getLeft())
&& isBlack(sibling.getRight())){//case 3
sibling.makeRed();
sibling.getLeft().makeBlack();
rotateRight(sibling);
}else if(!isLeft && !isBlack(sibling.getRight())
&& isBlack(sibling.getLeft()) ){
sibling.makeRed();
sibling.getRight().makeBlack();
rotateLeft(sibling);
}else if(isLeft && !isBlack(sibling.getRight())){//case 4
sibling.setRed(parent.isRed());
parent.makeBlack();
sibling.getRight().makeBlack();
rotateLeft(parent);
cur=getRoot();
}else if(!isLeft && !isBlack(sibling.getLeft())){
sibling.setRed(parent.isRed());
parent.makeBlack();
sibling.getLeft().makeBlack();
rotateRight(parent);
cur=getRoot();
}
}
if(isRed){
cur.makeBlack();
}
if(getRoot()!=null){
getRoot().setRed(false);
getRoot().setParent(null);
}
}
//get sibling node
private RBTreeNode<T> getSibling(RBTreeNode<T> node,RBTreeNode<T> parent){
parent = node==null ? parent : node.getParent();
if(node==null){
return parent.getLeft()==null ? parent.getRight() : parent.getLeft();
}
if(node==parent.getLeft()){
return parent.getRight();
}else{
return parent.getLeft();
}
}
private boolean isBlack(RBTreeNode<T> node){
return node==null || node.isBlack();
}
private boolean isRoot(RBTreeNode<T> node){
return root.getLeft() == node && node.getParent()==null;
}
/**
* find the successor node
* @param node current node's right node
* @return
*/
private RBTreeNode<T> removeMin(RBTreeNode<T> node){
//find the min node
RBTreeNode<T> parent = node;
while(node!=null && node.getLeft()!=null){
parent = node;
node = node.getLeft();
}
//remove min node
if(parent==node){
return node;
}
parent.setLeft(node.getRight());
setParent(node.getRight(),parent);
//don't remove right pointer,it is used for fixed color balance
//node.setRight(null);
return node;
}
private T addNode(RBTreeNode<T> node){
node.setLeft(null);
node.setRight(null);
node.setRed(true);
setParent(node,null);
if(root.getLeft()==null){
root.setLeft(node);
//root node is black
node.setRed(false);
size.incrementAndGet();
}else{
RBTreeNode<T> x = findParentNode(node);
int cmp = x.getValue().compareTo(node.getValue());
if(this.overrideMode && cmp==0){
T v = x.getValue();
x.setValue(node.getValue());
return v;
}else if(cmp==0){
//value exists,ignore this node
return x.getValue();
}
setParent(node,x);
if(cmp>0){
x.setLeft(node);
}else{
x.setRight(node);
}
fixInsert(node);
size.incrementAndGet();
}
return null;
}
/**
* find the parent node to hold node x,if parent value equals x.value return parent.
* @param x
* @return
*/
private RBTreeNode<T> findParentNode(RBTreeNode<T> x){
RBTreeNode<T> dataRoot = getRoot();
RBTreeNode<T> child = dataRoot;
while(child!=null){
int cmp = child.getValue().compareTo(x.getValue());
if(cmp==0){
return child;
}
if(cmp>0){
dataRoot = child;
child = child.getLeft();
}else if(cmp<0){
dataRoot = child;
child = child.getRight();
}
}
return dataRoot;
}
/**
* red black tree insert fix.
* @param x
*/
private void fixInsert(RBTreeNode<T> x){
RBTreeNode<T> parent = x.getParent();
while(parent!=null && parent.isRed()){
RBTreeNode<T> uncle = getUncle(x);
if(uncle==null){//need to rotate
RBTreeNode<T> ancestor = parent.getParent();
//ancestor is not null due to before before add,tree color is balance
if(parent == ancestor.getLeft()){
boolean isRight = x == parent.getRight();
if(isRight){
rotateLeft(parent);
}
rotateRight(ancestor);
if(isRight){
x.setRed(false);
parent=null;//end loop
}else{
parent.setRed(false);
}
ancestor.setRed(true);
}else{
boolean isLeft = x == parent.getLeft();
if(isLeft){
rotateRight(parent);
}
rotateLeft(ancestor);
if(isLeft){
x.setRed(false);
parent=null;//end loop
}else{
parent.setRed(false);
}
ancestor.setRed(true);
}
}else{//uncle is red
parent.setRed(false);
uncle.setRed(false);
parent.getParent().setRed(true);
x=parent.getParent();
parent = x.getParent();
}
}
getRoot().makeBlack();
getRoot().setParent(null);
}
/**
* get uncle node
* @param node
* @return
*/
private RBTreeNode<T> getUncle(RBTreeNode<T> node){
RBTreeNode<T> parent = node.getParent();
RBTreeNode<T> ancestor = parent.getParent();
if(ancestor==null){
return null;
}
if(parent == ancestor.getLeft()){
return ancestor.getRight();
}else{
return ancestor.getLeft();
}
}
private void rotateLeft(RBTreeNode<T> node){
RBTreeNode<T> right = node.getRight();
if(right==null){
throw new java.lang.IllegalStateException("right node is null");
}
RBTreeNode<T> parent = node.getParent();
node.setRight(right.getLeft());
setParent(right.getLeft(),node);
right.setLeft(node);
setParent(node,right);
if(parent==null){//node pointer to root
//right raise to root node
root.setLeft(right);
setParent(right,null);
}else{
if(parent.getLeft()==node){
parent.setLeft(right);
}else{
parent.setRight(right);
}
//right.setParent(parent);
setParent(right,parent);
}
}
private void rotateRight(RBTreeNode<T> node){
RBTreeNode<T> left = node.getLeft();
if(left==null){
throw new java.lang.IllegalStateException("left node is null");
}
RBTreeNode<T> parent = node.getParent();
node.setLeft(left.getRight());
setParent(left.getRight(),node);
left.setRight(node);
setParent(node,left);
if(parent==null){
root.setLeft(left);
setParent(left,null);
}else{
if(parent.getLeft()==node){
parent.setLeft(left);
}else{
parent.setRight(left);
}
setParent(left,parent);
}
}
private void setParent(RBTreeNode<T> node,RBTreeNode<T> parent){
if(node!=null){
node.setParent(parent);
if(parent==root){
node.setParent(null);
}
}
}
/**
* debug method,it used print the given node and its children nodes,
* every layer output in one line
* @param root
*/
public void printTree(RBTreeNode<T> root){
java.util.LinkedList<RBTreeNode<T>> queue =new java.util.LinkedList<RBTreeNode<T>>();
java.util.LinkedList<RBTreeNode<T>> queue2 =new java.util.LinkedList<RBTreeNode<T>>();
if(root==null){
return ;
}
queue.add(root);
boolean firstQueue = true;
while(!queue.isEmpty() || !queue2.isEmpty()){
java.util.LinkedList<RBTreeNode<T>> q = firstQueue ? queue : queue2;
RBTreeNode<T> n = q.poll();
if(n!=null){
String pos = n.getParent()==null ? "" : ( n == n.getParent().getLeft()
? " LE" : " RI");
String pstr = n.getParent()==null ? "" : n.getParent().toString();
String cstr = n.isRed()?"R":"B";
cstr = n.getParent()==null ? cstr : cstr+" ";
System.out.print(n+"("+(cstr)+pstr+(pos)+")"+"\t");
if(n.getLeft()!=null){
(firstQueue ? queue2 : queue).add(n.getLeft());
}
if(n.getRight()!=null){
(firstQueue ? queue2 : queue).add(n.getRight());
}
}else{
System.out.println();
firstQueue = !firstQueue;
}
}
}
public static void main(String[] args) {
RBTree<String> bst = new RBTree<String>();
bst.addNode("d");
bst.addNode("d");
bst.addNode("c");
bst.addNode("c");
bst.addNode("b");
bst.addNode("f");
bst.addNode("a");
bst.addNode("e");
bst.addNode("g");
bst.addNode("h");
bst.remove("c");
bst.printTree(bst.getRoot());
}
}
6、树、森林、二叉树
上面已经学习了二叉树以及一些特殊的二叉树,接下来学习树的表示及相关操作。
6.1、树的存储结构
表现树的存储结构的形式有很多,有3种比较常见。
6.1.1、双亲表示法
这种表示方法中, 以一组连续的存储单元存储树的节点,每个节点除了数据域data外,还附设一个parent域用以指示其双亲节点的位置, 其结点形式如图37所示。
这种存储结构利用了每个结点 (除根以外)只有唯一的双亲的性质。 在这种存储结构下 , 求结点的双亲十分方便, 也很容易求树的根, 但求结点的孩子时需要遍历整个结构。
6.1.2、孩子表示法
由于树中每个节点可能有多棵子树, 则可用多重链表, 即每个结点有多个指针域, 其中每个 指针指向一棵子树的根节点,此时链表中的节点可以有如图 39 所示的两种结点节点。
6.1.3、孩子兄弟法
又称二叉树表示法,或二叉链表表示法,即以二叉链表做树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为 firstchild 域和 nextsibling域,其结点形式如图41所示。
图42所示为图40中的树的孩子兄弟链表。利用这种存储结构便于实现各种树的操作。
6.2、树转换为二叉树
在这里我们约定树是有序的,树中每一个节点的儿子结点按从左到右的次序顺序编号。
如图43所示的一棵树,根节点 A有三个儿子 B、 C、 D, 可以认为节点 B为 A的第一个儿子节点, 结点 C 为 A的第二个儿子节点, 节点 D 为 A 的第三个儿子节点。
将一棵树转换为二叉树的方法是:
- (1) 树中所有相邻兄弟之间加一条连线;
- (2) 对树中的每个结点, 只保留它与第一个儿子结点之间的连线, 删去它与其他儿子结点之间的连线。
- (3) 以树的根结点为轴心, 将整棵树顺时针转动一定的角度, 使之结构层次分明。
树转换为二叉树的转换过程示意图如下:
6.3、二叉树还原为树
树转换为二叉树这一转换过程是可逆的, 可以依据二叉树的根结点有无右儿子结点,将一棵二叉树还原为树, 具体方法如下:
- (1) 若某结点是其双亲的左儿子, 则把该结点的右儿子、 右儿子的右儿子、 … 都与该结点的双亲结点用线连起来;
- (2) 删掉原二叉树中所有的双亲结点与右儿子结点的连线;
- (3) 整理由(1)、(2)两步所得到的树, 使之结构层次分明。
二叉树还原为树的过程示意图如下所示:
6.4、森林转换为二叉树
森林是若干棵树的集合, 森林亦可用二叉树表示。 森林转换为二叉树的方法如下:
- (1) 将森林中的每棵树转换成相应的二叉树;
- (2) 第一棵二叉树不动, 从第二棵二叉树开始, 依次将后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子, 当所有的二叉树连在一起后, 这样所得到的二叉树就是由森林转换得到的二叉树。
森林及其转换为二叉树的过程如下图所示:
6.5、树与森林的遍历
6.5.1、树的遍历
由树结构的定义可引出两种次序遍历树的方法:一种是先根(次序)遍历树,即:先访问树的根结点,然后依次先根遍历根的每棵子树;另一种是后根(次序)遍历,即先依次后根遍历每棵子树,然后访问根结点。
例如,对图 38 所示的树进行先根遍历,可得树的先根序列为:
R A D E B C F G H K
对该树进行后根遍历,则得树的后根序列为:
D E A B G H K F C R
按照森林和树相互递归的定义,可以推出森林的两种遍历方法:先序遍历和中序遍历。
6.5.2、森林的遍历
森林的遍历有两种方式: 前序遍历和中序遍历。
前序遍历
前序遍历的过程:
- (1) 访问森林中第一棵树的根结点;
- (2) 前序遍历第一棵树的根结点的子树森林;
- (3) 前序遍历剩余的其他子森林。
对于图 46 所示的森林进行前序遍历, 得到的结果序列为 A B C D E F G H I J K。
中序遍历
中序遍历的过程:
- (1) 中序遍历第一棵树的根结点的子树森林;
- (2) 访问森林中第一棵树的根结点;
- (3) 中序遍历剩余的其他子森林。
对于图 46 所示的森林进行中序遍历, 得到的结果序列为 B A D E F C J H K I G 。
根据森林与二叉树的转换关系以及森林和二叉树的遍历定义可以推论: 森林前序遍历和中序遍历分别与所转换的二叉树的前序遍历和中序遍历的结果序列相同。
7、B树
在前面学习了平衡二叉树,B树也是一种平衡查找树,不过不是二叉树。
B树也称B-树,它是一种多路平衡查找树。
一棵m阶的B树定义如下:
- 每个节点最多有m-1个关键字(可以存有的键值对)。
- 根节点最少可以只有1个关键字。 * 非根节点至少有m/2个关键字。
- 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
- 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
- 每个节点都存有索引和数据,也就是对应的key和value。
看一个B树的实例(字母大小 C>B>A)
看看B树的一些基本操作。
7.1、查找
查找和平衡二叉树类似,不过B树是多路的而已。以图47中查找15为例:
- (1)获取根节点的关键字进行比较,当前根节点关键字为39,15<39,所以往找到指向左边的子节点(二分法规则,左小右大,左边放小于当前节点值的子节点、右边放大于当前节点值的子节点);
- (2) 获取到关键字12、22, 12<15<22,所以查找12和22中间的节点
- (3)获取到关键字13和15,因为15=15,所以返回关键字和指针信息;如果没有找到所包含的节点,返回null。
7.2、插入
插入的时候,需要记住一个规则:判断当前结点key的个数是否小于等于m-1,如果满足,直接插入即可,如果不满足,将节点的中间的key将这个节点分为左右两部分,中间的节点放到父节点中即可。
例子:在5阶B树中,结点最多有4个key,最少有2个key(注意:下面的节点统一用一个节点表示key和value)。
-
插入18,70,50,40
-
插入22
插入22时,发现这个节点的关键字已经大于4了,所以需要进行分裂,分裂的规则在上面已经讲了,分裂之后,如下。
- 接着插入23,25,39
分裂,得到下面的。
7.3、删除
B树的删除操作相对于插入操作是相对复杂一些。
- 树初始状态如下
- 删除15,这种情况是删除叶子节点的元素,如果删除之后,节点数还是大于m/2,这种情况只要直接删除即可。
- 接着,把22删除,这种情况的规则:22是非叶子节点,对于非叶子节点的删除,我们需要用后继key(元素)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。对于删除22,需要将后继元素24移到被删除的22所在的节点。
此时发现26所在的节点只有一个元素,小于2个(m/2),这个节点不符合要求,这时候的规则(向兄弟节点借元素):如果删除叶子节点,如果删除元素后元素个数少于(m/2),并且它的兄弟节点的元素大于(m/2),也就是说兄弟节点的元素比最少值m/2还多,将先将父节点的元素移到该节点,然后将兄弟节点的元素再移动到父节点。这样就满足要求。
看看操作过程:
- 接着删除28,删除叶子节点,删除后不满足要求,所以,我们需要考虑向兄弟节点借元素,但是,兄弟节点也没有多的节点(2个),借不了,怎么办呢?如果遇到这种情况,首先,还是将先将父节点的元素移到该节点,然后,将当前节点及它的兄弟节点中的key合并,形成一个新的节点。
移动之后,跟兄弟节点合并。
8、B+树
B+树是B树的变体,也是一种多路搜索树。
B+树·和B树有一些共同的特性:
- 根节点至少一个元素
- 非根节点元素范围:m/2 <= k <= m-1
B+树和B树也有一些不一样的地方:
- B+树有两种类型的节点:非叶子结点(也称索引结点)和叶子结点。非叶子节点不存储数据,只存储索引,数据都存储在叶子节点。
- 非叶子结点中的key都按照从小到大的顺序排列,对于非叶子结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
- 每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
- 父节点存有右孩子的第一个元素的索引。
看一个B+树的示例:
8.1、查找
B+树的查找右两种方式:
-
(1)从最小关键字起顺序查找;
-
(2)从根节点开始,进行随机查找
在查找时,若非叶子节点上的关键字等于给定值,并不终止,而是继续向下直到叶子节点。因此,在B+树中,不管查找成功与否,每次查找都是走了一条从根到叶子节点的路径。其余同B树的查找类似。
8.2、插入
插入操作有一个规则:当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的。
以一颗5阶B+树的插入过程为例,5阶B+树的节点最少2个元素,最多4个元素。
- 插入5,10,15,20
- 插入25,此时元素数量大于4个了,分裂
- 接着插入26,30,继续分裂
8.3、删除
删除操作比B树简单一些,因为叶子节点有指针的存在,向兄弟节点借元素时,不需要通过父节点了,而是可以直接通过兄弟节移动即可(前提是兄弟节点的元素大于m/2),然后更新父节点的索引;如果兄弟节点的元素不大于m/2(兄弟节点也没有多余的元素),则将当前节点和兄弟节点合并,并且删除父节点中的key,
下面来看一个具体的实例:
- B+树的初始状态
- 删除10,删除后,不满足要求,发现左边兄弟节点有多余的元素,所以去借元素,最后,修改父节点索引
- 删除元素5,发现不满足要求,并且发现左右兄弟节点都没有多余的元素,所以,可以选择和兄弟节点合并,最后修改父节点索引
- 发现父节点索引也不满足条件,所以,需要做跟上面一步一样的操作
B+树相比较B树有一些优点:
- B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快
- B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定
- B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高
- B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描
这里不再给出B树和B+树代码实现,代码实现可见参考【26】
上一篇:重学数据结构(五、串)
本博客为学习笔记,参考资料如下!
水平有限,难免错漏,欢迎指正!
参考:
【1】:邓俊辉 编著. 《数据结构与算法》 【2】:王世民 等编著 . 《数据结构与算法分析》 【3】: Michael T. Goodrich 等编著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》 【4】:严蔚敏、吴伟民 编著 . 《数据结构》 【5】:程杰 编著 . 《大话数据结构》 【6】:[Data Structure] 数据结构中各种树 【7】:Tree 【8】:Binary Tree 【9】:Java数据结构与算法——二叉树及操作(包括二叉树遍历) 【10】:Java数据结构和算法(十)——二叉树 【11】:阿粉带你玩转二叉查找树 【12】:JAVA递归实现线索化二叉树 【13】:二叉查找树(三)之 Java的实现 【14】:一步一步写平衡二叉树(AVL树) 【15】:什么是平衡二叉树(AVL) 【16】:什么是平衡二叉树(AVL) 【17】:动画 | 什么是AVL树? 【18】:详解什么是平衡二叉树(AVL)(修订补充版) 【19】:红黑树深入剖析及Java实现 【20】:平衡查找树之红黑树 【21】:漫画:什么是红黑树? 【22】:面试官问你B树和B+树,就把这篇文章丢给他 【23】:平衡二叉树、B树、B+树、B*树 理解其中一种你就都明白了 【24】:B树和B+树的插入、删除图文详解 【25】:B树Java代码实现以及测试 【26】:Introduction of B-Tree 【27】:B+树详解