2-3查找树
为了保持平衡性,同时也为了避免AVL那样多次的判断和旋转,需要一些灵活性,故允许一个节点可以保存两个键,它有三条链接,称3节点,BST中的节点称2节点。
2-3查找树的定义:它是一棵查找树或空树,由2节点和3节点组成,2节点含有一个键和两条链接,2节点的左子树中的键均小于该节点的键,右子树中的键均大于该节点的键;3节点含有两个键和三条链接,左子树中的键小于该节点的键,中子树中的键在该节点的两个键之间,右子树中的键均大于该节点的键。
完美平衡的2-3查找树中,所有空链接到树头节点的距离都是相等的。为方便描述,后面将完美平衡的2-3查找树称2-3树。
2-3树的插入操作
向2节点中插入
由于每个节点最多能含有两个链接,这种情况很简单,递归查找要插入的位置,插入即可
向3节点插入
- 该树只有一个3节点
临时创建一个4节点,然后将其分解为一个2-3树,分解后树的高度增加1
向一个父节点为2节点的3节点插入
临时创建一个4节点,然后将中键移动到原节点的父节点中,指向原3节点的一条链接替换为新父节点中的两条链接,这两条链接分别指向原3节点中的较小者和较大者
向一个父节点为3节点的3节点插入
还是临时创建一个4节点,然后将中间移动到原节点的父节点中,然后父节点进行同样的变换,一直不断的向上分解临时4节点,直到遇到一个2节点并将它替换为一个3节点,或者是到达3节点的根。
分解根节点
如果插入节点到根节点的路径上全是3节点,最终根节点将变成一个临时的4节点,处理方法类似向只有一个3节点的树插入,将一个4节点分解为3个2节点,并将树的高度增加1。
说了这么多,到底2-3树的插入操作有哪些特别的地方呢?对于向2-3树中插入节点,不论是向2节点还是3节点中插入亦或是分解根节点,都只需要修改当前的节点和父节点及链接,并不会涉及树的其他部分(可对比AVL),即是局部的,且保持了树的有序性和平衡性,每次只有在根节点分解时,树的高度才增加1,对比AVL,每次插入都可能会使树高度增加1,同样数量的节点,2-3树的高度会低很多。
红黑树的插入操作
红黑树的基本思想就是用标准的二叉查找树(全部是2节点)和一些额外的信息(替代3节点)来表示2-3树。红链接将两个2节点连接起来构成一个3节点,黑链接即普通的链接。
可能这样不是很好理解,红黑树的等价定义为:
- 红链接均为左链接
- 没有节点同时和两条红链接相连
- 红黑树是黑色完美平衡的,即任意空链接到根节点的路径上的黑链接数量相同
将红黑树的红链接画平,就是2-3树
颜色表示
为方便实现,每个节点都只会有一条指向自己的链接,故将颜色保存在节点的boolean变量中,若指向它的节点为红链接,则为true,黑色则为false。约定空链接为黑色
旋转操作
与AVL的不同,红黑树的旋转条件是:某一节点出现红色右链接或者两条红链接。通过旋转操作修复,使得节点满足红黑树的定义,同时在旋转时要改变颜色,而AVL中是树的高度。
由于先入为主,我自己实现的代码命名左旋转和右旋转与《数据结构与算法分析:C语言描述》上一致,与《算法第四版》的命名相反。
1. 左旋转
注意这是一个中间操作,后面会通过右旋转和颜色转换修复。
2. 右旋转
旋转的情况
什么情况下要旋转,左还是右?
向2节点插入
向2节点插入又分两种情况,对应2-3树中向2节点插入,单个2节点以及树底部的2节点
单个2节点
向左插入,则结果将是一个3节点,不需要修复;向右插入,出现右红链接,通过一次右旋转修复,修复结果为一个3节点。
树底的2节点
插入会新增节点,由于每次都是红链接将新节点与父节点相连,故如果是在左边插入,是不需要修复,结果将是新建一个3节点;在右边插入,出现红色右链接,通过一次右旋转修复,修复结果仍然是新建一个3节点。
图中没有画出不需要修复的情况
向一个3节点中插入
对应2-3树中的分解4节点。
向3节点中插入分3种情况,新键最大,新键最小,新键介于两者之间
新键最大
由于每次都是红链接将新节点与父节点相连,此时父节点将出现左右链接均为红色,将两条红色链接变为黑,就能得到由三个2节点构成的平衡树,同时也是满二叉树,对应2-3树中将一个4节点分解为三个2节点
新键最小
由于每次都是红链接将新节点与父节点相连,此时出现了包含最大键的节点的左孩子和其左孙子都为红节点,不满足红黑树的定义,通过一次左旋转,左旋转后出现了和新建最大一样的情况——处于中间的节点的左右链接均为红,进行颜色转换即可。
新键介于两者之间
相对来说,这种情况最复杂,但搞懂了AVL,其实也不难理解,和AVL中的右-左双旋转很相似,先通过右-左双旋转,使树平衡,然后就出现了和新键最大一样的情况——处于中间的节点的左右链接均为红,进行颜色转换即可。
通过上面几种情况的分析,综合起来,旋转和颜色修复的顺序是:
1. 如果出现红色右链接且左链接为黑,进行右旋转
2. 如果当前节点的左孩子和左孙子均为红节点,进行左旋转
3. 如果当前节点的左右链接均为红色,进行颜色转换
步骤一对应向2节点右边插入和向3节点插入时新键介于两者之间的右旋转,步骤二对应向3节点插入时新键最小和新键介于两者直接的左旋转,步骤三对应向3节点插入时新键最大和新键介于两者之间的颜色转换。注意左旋转不仅要满足出现红色左链接,而且要右链接为黑,不然颜色转换可能会失败。
最后上一张图来反映上述文字表述,更直观,注意我对旋转的描述与图相反(懒得去自己画图,): )
完整代码实现:
package BinarySearchTree;
import java.util.*;
/** * 红黑树 * @author 小锅巴 */
public class RBT {
private static final boolean RED = true;
private static final boolean BLACK = false;
private TreeNode root;
private class TreeNode{
String key;
int value;
TreeNode left, right;//左右链接
boolean color;//节点的颜色
TreeNode(String key, int value, boolean color){
this.key = key;
this.value = value;
this.color = color;
}
}
private boolean isRed(TreeNode node){
if(node == null)
return false;
return node.color == RED;
}
//左旋转,由于我先学得AVL,先入为主,我以维斯版的来命名,与algs4的相反
private TreeNode rotateWithLeft(TreeNode node){
TreeNode temp = node.left;
//完成旋转
node.left = temp.right;
temp.right = node;
//和AVL相比,旋转后要变换颜色,
temp.color = node.color;
node.color = RED;//被旋转的节点到了子树,所以肯定是设置被旋转的节点为红色节点
return temp;
}
//右旋转
private TreeNode rotateWithRight(TreeNode node){
TreeNode temp = node.right;
node.right = temp.left;
temp.left = node;
temp.color = node.color;
node.color = RED;
return temp;
}
//颜色转换的条件是左右子节点均为红节点,由红节点的定义,那么当前节点的的孩子均不会为null,不会出现空指针异常
private void flipColors(TreeNode node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
public void insert(String key, int value){
root = insert(root, key, value);
root.color = BLACK;//根节点都是黑色
}
private TreeNode insert(TreeNode node, String key, int value){
if( node == null)
return new TreeNode(key, value, RED);
int cmp = key.compareTo(node.key);
if(cmp < 0)
node.left = insert(node.left, key, value);
else if(cmp > 0)
node.right = insert(node.right, key, value);
else
node.value = value;
//旋转
if(!isRed(node.left) && isRed(node.right))//左链接为黑,右链接为红,则右旋转
node = rotateWithRight(node);
if(isRed(node.left.left) && isRed(node.left))//左链接为红,左孩子的左链接也为红,则左旋转
node = rotateWithLeft(node);
if(isRed(node.left) && isRed(node.right))//左右链接均为红,调整颜色
flipColors(node);
return node;
}
private void layerTraversal (TreeNode node){
Queue<TreeNode> s = new LinkedList<>();
s.add(node);
TreeNode curNode;
TreeNode nlast = null;//局部变量,记得赋值
TreeNode last = node;
while(!s.isEmpty()){
curNode = s.poll();
System.out.print(curNode.key+" ");
if(curNode.left != null){
nlast = curNode.left;
s.add(curNode.left);
}
if(curNode.right != null){
nlast = curNode.right;
s.add(curNode.right);
}
if(curNode == last){
System.out.println();
last = nlast;
}
}
}
private void preOrderTraversal(TreeNode node){
Stack<TreeNode> s = new Stack<>();
TreeNode curNode = null;
s.push(node);
while(!s.isEmpty()){
curNode = s.pop();
System.out.print(curNode.key+" ");
if(curNode.right != null)
s.push(curNode.right);
if(curNode.left != null)
s.push(curNode.left);
}
}
public static void main(String[] args) {
RBT rbt = new RBT();
System.out.print("请输入节点个数:");
Scanner s = new Scanner(System.in);
int num = s.nextInt();
System.out.println("请依次输入"+num+"个字母");
for (int i = 1; i <= num; i++){
String value = s.next();
rbt.insert(value, i);
}
System.out.println("层序遍历");
rbt.layerTraversal(rbt.root);
System.out.println();
System.out.println("先序遍历");
rbt.preOrderTraversal(rbt.root);
}
}
/** 测试用例 请输入节点个数:10 请依次输入10个字母 S E A R C H X M P L 层序遍历 M E R C L P X A H S 先序遍历 M E C A L H R P X S */
参考资料:
1. 算法第四版
2. 数据结构与算法分析:c语言描述