实验要求
编码实现红黑树的插入算法,使得插入后依旧保持红黑性质。(即:实现教材p178页的RB-INSERT, RB_INSERT_FIXUP算法)
节点属性:(也可自行定义, 不做强制要求)
TNode = { Color: red/black; Key: int; Left: TNode*; Right: TNode*; P: TNode*; }
目录
1.实验内容
掌握红黑树的插入过程。
2.实验目的
编码实现红黑树的插入算法,使得插入后依旧保持红黑性质。
3.算法设计思路
红黑树的插入过程主要分为两个阶段:插入结点和维护红黑树性质。首先按照二叉搜索树的插入规则将新节点插入到树中,并将该节点的颜色设为红色。接下来要进行红黑树性质的维护操作。
分析可知,在插入的过程中,只有可能破坏性质2和性质4。如果违反了性质2,则红色根节点一定是新增的结点z ,他是树中唯一的内部节点。因为z的父结点和子节点都是黑色的nil 结点,没有违反性质4。如果违反了性质4,则由于z 的子节点是黑色nil ,且该树在z 加入之前没有其他性质的违反,所以违反必然是因为z 和z.p 都是红色的,因此z.p.p 必然存在且为黑色。
基于以上分析,可以将需要调整的情况分为6种,其中case1~case3 为z.p 为z.p.p 的左孩子,case4~case6 为z.p 为z.p.p 的右孩子。
case1:z 的叔叔结点y 为红色。这种情况发在z.p 和y 都为红色时发生。因为z.p.p 是黑色的,所以将z.p 和y 都设为黑色,一次解决z 和z.p 都是红色的问题,将z.p.p 设为红色以保持性质5,然后把z.p.p 作为新的z 来重复该过程,上溯到根节点时,将根节点设为黑色。
case2:z 的叔叔结点y 是黑色且z 是z.p 的右孩子。这种情况发生时我们可以对z.p 做一个左旋来使该情况转化为case3。
case3:z 的叔叔结点y 是黑色且z 是z.p 的左孩子。此情况下将z.p 和z.p.p 变色,然后对z.p.p 右旋。
case4~case6 与case1~case3 对称,因此不做分析。
(此节所述性质1-5及case1-6参考《算法导论》书上定义)
4.源码 + 注释
4.1 红黑树的结点结构
如下所示为红黑树的结点结构,实现过程中提供了对每个属性的getter 和setter ,其中红色为false ,黑色为true 。
public class RBTNode<T>
{
private T key; //结点值
private boolean color; //结点的颜色
private RBTNode<T> left; //左孩子
private RBTNode<T> right; //右孩子
private RBTNode<T> parent; //父结点
}
//颜色设置
private static final boolean RED = false;
private static final boolean BLACK = true;
4.2插入算法
整个插入过程分为三部分1.insert(T key): 将关键字key 插入树中;2.insert(RBTNode node): 将node 插入树中;3.insertFixup(node): 维护红黑树。
1.insert(T key)
//外部接口
public void insert(T key)
{
RBTNode<T> node = new RBTNode<>(key, BLACK, null, null, null);
insert(node);
}
上述代码所示为调用插入方法时的接口,该方法接收结点的值,以该值建立一个结点node ,将node 作为参数传入内部接口。
2.insert(RBTNode node)
//内部接口
private void insert(RBTNode<T> node)
{
RBTNode<T> parent = null; //当前结点的父结点
RBTNode<T> x = root;
while (x != null)
{
parent = x;
if (node.getKey().compareTo(x.getKey()) < 0) x = x.getLeft(); //node.key < x.key, 向左走
else x = x.getRight();
}
node.setParent(parent);
if (parent == null) root = node; //空树
else
{
if (node.getKey().compareTo(parent.getKey()) < 0) parent.setLeft(node); //node是parent的左孩子
else parent.setRight(node);
}
node.setLeft(null);
node.setRight(null);
node.setColor(RED);
insertFixup(node);
}
该方法首先按照二叉搜索树的规则将结点插入树中,parent 始终指向插入结点的父结点。当while 循环结束后,若parent 为空,说明当前树为空树,于是当前节点作为根节点插入树中。插入后将其左孩子和右孩子设为nil ,并将其颜色设为RED ,然后调用insertFixup 方法。
3.insertFixup(node)
根据3节分析可知,若违反了性质2,则直接将根结点涂黑即可。若违反了性质4,则可分为6种情况。当z 的父结点不为nil 且为RED 时,即违反了性质4,此时需要调整。设置parent ,grandparent 和uncle 结点来方便表示。
//case 1,2,3
if (parent == grandparent.getLeft()) //case 1,2,3:父结点是祖父结点的left
{
RBTNode<T> uncle = grandparent.getRight(); //叔叔结点
if (uncle != null && uncle.getColor() == RED) //case 1:叔叔为RED
{
uncle.setColor(BLACK);
parent.setColor(BLACK);
grandparent.setColor(RED);
node = grandparent;
System.out.print("1 ");
}
else //case 2,3:叔叔为BLACK
{
if (node == parent.getRight()) //case 2:node是父结点的right
{
RBTNode<T> tmp;
leftRotate(parent);
tmp = parent;
parent = node;
node = tmp;
System.out.print("2 ");
}
//case 3:node是父结点的left
parent.setColor(BLACK);
grandparent.setColor(RED);
rightRotate(grandparent);
System.out.print("3 ");
}
}
上述代码所示为case1,2,3 的处理方法。若uncle 为RED ,则将uncle 与parent 设为BLACK ,并将grandparent 设为RED ,然后将node 上溯到grandparent ,继续迭代。若uncle 为BLACK 并且当前节点为parent 的右孩子,则对parent 执行左旋操作,此时node 为parent 的左孩子,将parent 和grandparent 变色,并对grandparent 右旋,即可完成修复过程。case4,5,6 的处理过程与上述过程对称。
4.3左旋与右旋方法
//左旋
private void leftRotate(RBTNode<T> x)
{
//Step1:y指向x的右孩子
RBTNode<T> y = x.getRight();
//Step2:y.left连接到x.right
x.setRight(y.getLeft());
if (y.getLeft() != null) y.getLeft().setParent(x);
//Step3:x父结点的left或right指向y
y.setParent(x.getParent());
if (x.getParent() == null) root = y; // 如果 “x的父亲” 是空节点,则将y设为根节点
else if (x == x.getParent().getLeft()) x.getParent().setLeft(y); // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
else x.getParent().setRight(y); // 如果 x是它父节点的右孩子,则将y设为“x的父节点的右孩子”
//Step4:x连接到y.left
y.setLeft(x);
x.setParent(y);
}
上述代码所示为左旋算法。首先将y 指向x 的右孩子,然后将y 的左子树连接到x 的右孩子位置,接下来将y 连接到x 的parent 孩子位置,最后将x 连接到y 的左孩子,这样就完成了对x 的左旋。右旋与其对称不再赘述。
5.算法正确性测试
将insert.txt的数据依次插入红黑树中并打印,得到LOT.txt文件如下所示。
使用可视化模型建立的红黑树如下,对比可知二者相同,可验证算法的正确性。
6.实验过程中遇到的困难及收获
通过本次实验,细致的了解了红黑树结点插入的过程,对红黑树的性质有了更深的理解。同时也看了JDK源码中的红黑树的实现,还需加强学习。
7.实验源代码
RBTNode.java
/*
* @Title:红黑树结点类
* @Package
* @Description:
* @author yangf257
* @date 2021/11/6 15:09
*/
public class RBTNode<T>
{
private T key; //结点值
private boolean color; //结点的颜色
private RBTNode<T> left; //左孩子
private RBTNode<T> right; //右孩子
private RBTNode<T> parent; //父结点
public T getKey()
{
return key;
}
public boolean getColor()
{
return color;
}
public void setColor(boolean color)
{
this.color = color;
}
public RBTNode<T> getLeft()
{
return left;
}
public void setLeft(RBTNode<T> left)
{
this.left = left;
}
public RBTNode<T> getRight()
{
return right;
}
public void setRight(RBTNode<T> right)
{
this.right = right;
}
public RBTNode<T> getParent()
{
return parent;
}
public void setParent(RBTNode<T> parent)
{
this.parent = parent;
}
public RBTNode(T key, boolean color, RBTNode<T> left, RBTNode<T> right, RBTNode<T> parent)
{
this.color = color;
this.key = key;
this.left = left;
this.right = right;
this.parent = parent;
}
}
RBTree.java
/*
* @Title:红黑树类
* @Package
* @Description:红黑树的插入
* @author yangf257
* @date 2021/11/6 15:04
*/
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class RBTree<T extends Comparable<T>>
{
private static final boolean RED = false;
private static final boolean BLACK = true;
private RBTNode<T> root; //根结点
public RBTree()
{
}
public RBTNode<T> getRoot()
{
return root;
}
/*
* 插入算法包括三部分
* 1.insert(T key):将关键字key插入树中;
* 2.insert(RBTNode node):将node插入树中;
* 3.insertFixup(node):维护红黑树;
*/
//外部接口
public void insert(T key)
{
RBTNode<T> node = new RBTNode<>(key, BLACK, null, null, null);
insert(node);
}
//内部接口
private void insert(RBTNode<T> node)
{
RBTNode<T> parent = null; //当前结点的父结点
RBTNode<T> x = root;
while (x != null)
{
parent = x;
if (node.getKey().compareTo(x.getKey()) < 0) x = x.getLeft(); //node.key < x.key, 向左走
else x = x.getRight();
}
node.setParent(parent);
if (parent == null) root = node; //空树
else
{
if (node.getKey().compareTo(parent.getKey()) < 0) parent.setLeft(node); //node是parent的左孩子
else parent.setRight(node);
}
node.setLeft(null);
node.setRight(null);
node.setColor(RED);
insertFixup(node);
}
//维护
private void insertFixup(RBTNode<T> node)
{
RBTNode<T> parent, grandparent;
while ((parent = node.getParent()) != null && parent.getColor() == RED)
{
grandparent = parent.getParent();
//case 1,2,3
if (parent == grandparent.getLeft()) //case 1,2,3:父结点是祖父结点的left
{
RBTNode<T> uncle = grandparent.getRight(); //叔叔结点
if (uncle != null && uncle.getColor() == RED) //case 1:叔叔为RED
{
uncle.setColor(BLACK);
parent.setColor(BLACK);
grandparent.setColor(RED);
node = grandparent;
System.out.print("1 ");
}
else //case 2,3:叔叔为BLACK
{
if (node == parent.getRight()) //case 2:node是父结点的right
{
RBTNode<T> tmp;
leftRotate(parent);
tmp = parent;
parent = node;
node = tmp;
System.out.print("2 ");
}
//case 3:node是父结点的left
parent.setColor(BLACK);
grandparent.setColor(RED);
rightRotate(grandparent);
System.out.print("3 ");
}
}
//case 4,5,6:父结点是祖父结点的right
else
{
RBTNode<T> uncle = grandparent.getLeft();
if (uncle != null && uncle.getColor() == RED) //case 4:叔叔为RED
{
uncle.setColor(BLACK);
parent.setColor(BLACK);
grandparent.setColor(RED);
node = grandparent;
System.out.print("4 ");
}
else //case 5,6:叔叔为BLACK
{
if (node == parent.getLeft()) //case 5:node是父结点的left
{
RBTNode<T> temp;
rightRotate(parent);
temp = node;
node = parent;
parent = temp;
System.out.print("5 ");
}
//case 6:node是父结点的right
parent.setColor(BLACK);
grandparent.setColor(RED);
leftRotate(grandparent);
System.out.print("6 ");
}
}
}
root.setColor(BLACK);
System.out.println();
}
/*
* 辅助方法
* 1.leftRotate(RBTNode x):左旋
* 2.rightRotate(RBTNode y):右旋
* 3.print():打印
* 4.levelOrder()
*/
//左旋
private void leftRotate(RBTNode<T> x)
{
//Step1:y指向x的右孩子
RBTNode<T> y = x.getRight();
//Step2:y.left连接到x.right
x.setRight(y.getLeft());
if (y.getLeft() != null) y.getLeft().setParent(x);
//Step3:x父结点的left或right指向y
y.setParent(x.getParent());
if (x.getParent() == null) root = y; // 如果 “x的父亲” 是空节点,则将y设为根节点
else if (x == x.getParent().getLeft()) x.getParent().setLeft(y); // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
else x.getParent().setRight(y); // 如果 x是它父节点的右孩子,则将y设为“x的父节点的右孩子”
//Step4:x连接到y.left
y.setLeft(x);
x.setParent(y);
}
//右旋
private void rightRotate(RBTNode<T> y)
{
//Step1:x指向y的左孩子
RBTNode<T> x = y.getLeft();
//Step2:x.right连接到y.left
y.setLeft(x.getRight());
if (x.getRight() != null) x.getRight().setParent(y);
//Step3:y父结点的left或right指向x
x.setParent(y.getParent());
if (y.getParent() == null) root = x; // 如果 “y的父亲” 是空节点,则将x设为根节点
else if (y == y.getParent().getRight()) y.getParent().setRight(x); // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子”
else y.getParent().setLeft(x); // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子”
//Step4:y.left连接到x
x.setRight(y);
y.setParent(x);
}
//打印
public void print(RBTNode<T> tree)
{
List<RBTNode<T>> level = levelOrder(tree);
try
{
File writeName = new File("LOT.txt"); // 相对路径,如果没有则要建立一个新的LOT.txt文件
writeName.createNewFile(); // 创建新文件,有同名的文件的话直接覆盖
try (FileWriter writer = new FileWriter(writeName); BufferedWriter out = new BufferedWriter(writer))
{
for (int i = 0; i < level.size(); i++)
{
String str = level.get(i).getKey() + ", " + (level.get(i).getColor() ? "BLACK" : "RED");
out.write(str + "\r\n");
out.flush();
}
}
} catch (IOException e)
{
e.printStackTrace();
}
}
//层序遍历
private List<RBTNode<T>> levelOrder(RBTNode<T> root)
{
List<RBTNode<T>> level = new ArrayList<>();
if (root == null) return null;
Queue<RBTNode<T>> queue = new LinkedList<>();
queue.add(root);
RBTNode<T> curr;
while (!queue.isEmpty())
{
curr = queue.poll();
level.add(curr);
if (curr.getLeft() != null) queue.add(curr.getLeft());
if (curr.getRight() != null) queue.add(curr.getRight());
}
return level;
}
}
TestMethod.java
/*
* @Title:
* @Package
* @Description:
* @author yangf257
* @date 2021/11/6 17:16
*/
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class TestMethod
{
public static void main(String[] args)
{
List<Integer> array = input();
RBTree<Integer> rbTree = new RBTree<>();
for (int i = 0; i < array.size(); i++)
{
rbTree.insert(array.get(i));
System.out.print("插入第" + (i + 1) + "个结点:");
}
rbTree.print(rbTree.getRoot());
}
private static List<Integer> input()
{
String pathname = "insert.txt";
String line = null;
try (FileReader reader = new FileReader(pathname); BufferedReader br = new BufferedReader(reader))
{
int count = 0;
while ((line = br.readLine()) != null)
{
count++;
if (count == 2) break;
}
} catch (IOException e)
{
e.printStackTrace();
}
List<Integer> array = new ArrayList<>();
int num = 0;
for (int i = 0; i < line.length(); i++)
{
if (line.charAt(i) != ' ')
{
if (num != 0) num *= 10;
num += line.charAt(i) - '0';
}
else
{
array.add(num);
System.out.println(num);
num = 0;
}
}
array.add(num);
return array;
}
}
insert.txt
20
12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17