JAVA二叉树
1. 二叉树简介
在数据量较小的情况下,采用链表可以获得较高的性能(查询时为O(n)),在数据量较大的情况下,链表的检索性能会下降,这时使用二叉树(Binary Tree)进行存储,查询时时间复杂度为O(logn)。
下面为二叉树的示意图:
在二叉树中,数据保存在节点(Node)中,每一个节点的数据结构如下:
private class Node{
private Object data; //存放Comparable,可以比较大小
private Node parent;//保存父亲节点
private Node left;//保存左子树
private Node right;//保存右子树
}
2.二叉树遍历
二叉树的遍历按照访问根(root)的顺序分为三种,先序遍历,中序遍历,后序遍历。
- 先序遍历
- 访问根节点
- 访问左子树
- 访问右子树
- 中序遍历
- 访问左子树
- 访问根节点
- 访问右子树
- 后序遍历
- 访问左子树
- 访问右子树
- 访问根节点
但是要注意的是,这里的“根”并不是整个二叉树的根,二叉树的每一个非叶子节点都可以看作是这里的“根”。
二叉树遍历的应用
二叉树遍历的一个应用是“表达式树”,在这棵树中,每一个叶子都是有个操作数,其他的节点则是操作符。
上图是表达式树的一个例子。
按照先序遍历,获得的前缀表达式为:- + 4 * 1 - 5 2 / 6 3
按照中序遍历,获得的中缀表达式为:4 + 1 *5 - 2 - 6 / 3
按照后序遍历,获得的后缀表达式为:4 1 5 2 - * + 6 3 / -
构建表达式树可以通过栈来实现,获得中缀表达式 a b + c d e + * * 的表达式树的过程如下:
1.前两个字符都是操作数,创建两颗单节点树,将其压入栈中
2.第三个字符为"+",则弹出栈内两棵树,并以"+“为根节点,栈内原有两棵树分别作为左右子树,形成一颗新树,记为A
3.c d e被读入,同第一步,压入栈中
4.读入”+",弹出栈顶两棵树,并与"+",一起像第二步那样形成一颗新树,并压入栈中,记为B
5.读入第一个"*",弹出栈顶树B和单节点树c,以"*“为树构建新树C,压入栈中
6.读入第二个”*",弹出栈顶两棵树A、C,以"*"为根节点构建新树,得到最终结果
图片待补充
表达式的计算和转换待补充
3.二叉树的实现
// An highlighted block
package BinaryTreeDemo;
/**
* @author Shinelon
*
* @param <K>进行数据存储的key,通过它进行查询,这个key是Comparable的子类
* @param <V>保存具体的数据信息
*/
class MapBinaryTree <K extends Comparable<K>,V> {
public static class Entry<K extends Comparable<K>,V> implements Comparable<K>{
private K key; //保存key的信息
private V value;//保存对象信息
@Override
public int compareTo(K o) {
// TODO Auto-generated method stub
return this.key.compareTo(o);
}
public Entry(K key, V value) {
super();
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
private class Node{
private MapBinaryTree.Entry<K,V> data; //存放Comparable,可以比较大小
private Node parent;//保存父亲节点
private Node left;//保存左子树
private Node right;//保存右子树
public Node(MapBinaryTree.Entry<K,V> data) {
this.data = data;
}
/**
* 实现数据的添加
* @param newNode 创建的新节点
*/
private void addNode(Node newNode) {
if(newNode.data.key.compareTo(this.data.key)<=0) {//比当前节点数据小
if(this.left == null) {
this.left =newNode;
newNode.parent = this;//保存父节点
}else {
this.left.addNode(newNode);
}
}else {
if(this.right == null) {
this.right = newNode;
newNode.parent = this;
}else {
this.right.addNode(newNode);
}
}
}
/**
* 中序遍历
*/
public void toArrayNode() {
if(this.left!=null) {
this.left.toArrayNode();
}
MapBinaryTree.this.returnData[MapBinaryTree.this.foot ++] = this.data;
if(this.right!=null) {
this.right.toArrayNode();
}
}
public boolean containsNode(MapBinaryTree.Entry<K,V> data) {
if(data.key.compareTo(this.data.key) == 0){
return true;
}else if(data.key.compareTo(this.data.key)<0){
if(this.left != null) {
return this.left.containsNode(data);
}else {
return false;
}
}else {
if(this.right != null) {
return this.right.containsNode(data);
}else {
return false;
}
}
}
public V getNode(K key) {
if(key.compareTo(this.data.key) == 0) {
return this.data.value;
}else if(key.compareTo(this.data.key) < 0) {
if(this.left != null) {
return this.left.getNode(key);
}else {
return null;
}
}else {
if(this.right != null) {
return this.right.getNode(key);
}else {
return null;
}
}
}
/**
* 获取要删除的节点对象
* @param key
* @return
*/
public Node getRemoveNode(MapBinaryTree.Entry<K,V> data) {
if(data.key.compareTo(this.data.key) == 0){
return this;
}else if(data.key.compareTo(this.data.key)<0){
if(this.left != null) {
return this.left.getRemoveNode(data);
}else {
return null;
}
}else {
if(this.right != null) {
return this.right.getRemoveNode(data);
}else {
return null;
}
}
}
}
//-----------------二叉树功能实现----------------
private Node root;//保存根节点
private int count;//保存数据个数
private Object[] returnData; //返回的数据
private int foot = 0;//脚标控制
/**
* 进行数据的保存
* @param data 要保存的数据
* 保存数据为空抛出异常
*/
public void add(K key,V value) {
if(key == null||value == null) {
throw new NullPointerException("保存的数据不允许为空");
}
Node newNode = new Node(new MapBinaryTree.Entry(key, value));
if(this.root == null) {
this.root = newNode;
}else {
this.root.addNode(newNode);//交给Node类处理
}
this.count ++;
}
/**
* 以对象数组的形式返回所有数据
* @return
*/
public Object[] toArray() {
if(this.count == 0) {
return null;
}else {
this.returnData = new Object[this.count];
this.foot = 0;
this.root.toArrayNode();
return this.returnData;
}
}
/**
* 依靠Comparable实现比较
* @param data
* @return
*/
public boolean contains(K key) {
if(this.count==0) {
return false;
}
return this.root.containsNode(new MapBinaryTree.Entry(key,null));
}
/**
* 根据指定key获取对应value信息
* @param key
* @return
*/
public V get(K key) {
if(this.count == 0||key == null) {
return null;
}
return this.root.getNode(key);
}
/**
* 执行数据的删除处理
* @param key
*/
public void remove(K key) {
Node removeNode = this.root.getRemoveNode(new MapBinaryTree.Entry(key,null));
if(removeNode != null) {
//case1:没有任何子节点
if(removeNode.left == null&&removeNode.right == null) {
if(removeNode.data.key.compareTo(removeNode.parent.data.key) < 0) {
removeNode.parent.left = null;
}else {
removeNode.parent.right = null;
}
removeNode.parent = null;
}else if(removeNode.left !=null && removeNode.right == null) {
if(removeNode.data.key.compareTo(removeNode.parent.data.key) < 0) {
//要删除的节点在其父亲的左子树上
removeNode.parent.left = removeNode.left;
removeNode.left.parent = removeNode.parent;
}else {
//要删除的节点在其父亲的右子树上
removeNode.parent.right = removeNode.left;
removeNode.left.parent = removeNode.parent;
}
}else if(removeNode.left ==null && removeNode.right != null) {
if(removeNode.data.key.compareTo(removeNode.parent.data.key) < 0) {
//要删除的节点在其父亲的左子树上
removeNode.parent.left = removeNode.right;
removeNode.right.parent = removeNode.parent;
}else {
//要删除的节点在其父亲的右子树上
removeNode.parent.right = removeNode.right;
removeNode.right.parent = removeNode.parent;
}
}else {
if(removeNode.data.key.compareTo(removeNode.parent.data.key) < 0) {
//修复BUG,加入判断,单独讨论替代结点是删除节点儿子节点的情况
//要删除的节点在其父亲的左子树上
Node moveNode = removeNode.right;
while(moveNode.left!=null) {
moveNode = moveNode.left;
}
if(moveNode != removeNode.right) {
removeNode.parent.left = moveNode;
moveNode.left = removeNode.left;
moveNode.parent.left = null;
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right;
}else {
//替代节点是删除节点的子结点
moveNode.left = removeNode.left;
moveNode.parent = removeNode.parent;
removeNode.parent.left = moveNode;
}
}else {
//要删除的节点在其父亲的右子树上
Node moveNode = removeNode;
while(moveNode.left != null) {
moveNode = moveNode.left;
}
if(moveNode != removeNode.left) {
removeNode.parent.right = moveNode;
moveNode.left = removeNode.left;
moveNode.parent.left = null;
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right;
}else {
//替代节点是删除节点的子节点
moveNode.right = removeNode.right;
moveNode.parent = removeNode.parent;
removeNode.parent.right = moveNode;
}
}
}
}
this.count--;
}
}
public class MapBinaryTreeDemo {
}
二叉树的删除最为复杂,有三种情况:
- 目标结点没有子节点,可以直接删除。
- 目标结点只有一个子结点,可以用子结点替换删除结点。
- 目标结点有两个子结点,用后继结点替换删除结点。
这里提到了两个概念:替换和后继结点。
后继结点:
后继结点指的是大于删除结点的最小结点,简单来讲就是目标节点右子树中最左侧的结点。当它不存在的时候,可以使用前继结点,也就是目标节点左子树最右节点来替代。关于后继节点和前继节点有一个很直观的理解方式:
将所有的结点投影到横轴上,如果目标结点为P,那么与他相邻的两个节点R和M就是他对应的后继结点和前继结点。
替换
二叉树删除中的场景二使用唯一的子结点来替换目标结点。我们可以认为子节点中的数据已经完全转移到了目标节点中,现在要删除子节点。当子结点又有两个子结点时,场景二转换为场景三;当子结点已经是叶子节点时,场景二转换为场景一,当子结点有一个子结点时,向下递归,一定可以转换为场景一和场景三。
二叉树删除中第三种场景可以理解成是使用后继结点来取代目标结点的位置,同时删除后继节点位置上的数据。由于我们寻找到的后继结点一定是叶子结点,这时候第三种场景转化为场景一。
最后总结一下,二叉树的删除可以理解为删除替代节点(前继节点或后继节点),而替代节点一定会是一个叶子结点。这点性质可以简化红黑树删除节点的情况,对于理解红黑树删除时的自平衡策略是十分重要的。
测试Demo代码如下
package BinaryTreeDemo;
public class TestDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MapBinaryTree<Integer,Person> tree = new MapBinaryTree<Integer,Person>();
tree.add(80, new Person("1,1",80));
tree.add(50, new Person("2,1",50));
tree.add(90, new Person("2,2",90));
tree.add(60, new Person("3,1",60));
tree.add(30, new Person("3,2",30));
tree.add(85, new Person("3,3",85));
tree.add(95, new Person("3,4",95));
tree.add(10, new Person("4,1",10));
tree.add(55, new Person("4,2",55));
tree.add(70, new Person("4,3",70));
// System.out.println(tree.contains(150));
tree.remove(50);
tree.remove(90);
// Object res[] = tree.toArray();
// for(Object re : res) {
// MapBinaryTree.Entry<Integer,Person> entry = (MapBinaryTree.Entry<Integer,Person>) re;
// System.out.println(entry.getKey() + "---------" + entry.getValue().toString());
// }
System.out.println(tree.get(90));
System.out.println("-------------------------");
}
}
在上面的例子中,实现了二叉树的增add()、删remove()、查contains(),由MapBinaryTree类完场,具体操作由MapBinaryTree内部类Node实现。
插入insert()待补充
4.红黑树
4.1 红黑树简介
二叉树的结构对二叉树的性能有着较大的影响,以查找为例,一颗平衡的二叉树查找时时间复杂度为O(logn),考虑极端情况,当所有的儿子都是他的父亲的左子树,这时候二叉树退化为单链表,复杂度提升到O(n)。这种退化是二叉树的不平衡导致的。
红黑树是一种特殊的二叉查找树,他在二叉查找树的基础上二维添加了颜色标记,同时具有一定的规则。可以很好的保持二叉树的平衡性,使得二叉查找树在插入,删除,查找时时间复杂度O(logn),红黑树具有以下特点。
- 每个节点一定是红色或黑色。
- 红黑树的根节点黑色。
- 红黑树的叶子节点一定是黑色。
注:在Java实现中叶子节点我们认为是null,红黑树使用null表示空节点,因此在遍历时看到的叶子节点不一定是黑色的。 - 红色节点的儿子节点一定是黑色节点,黑色节点的节点可以是黑色节点。
- 从一个节点到该节点的子孙节点的所有路径上包括数量相同数目的黑色节点。
注:考虑极端情况,根节点的左子树上全部都是黑色节点,而右子树则是红黑交替。如果左子树上有N-1个连续的黑色节点,右子树上有2(N-1)个节点。
红黑树之所以可以自平衡,靠的是在添加和删除数据之后进行的左旋、右旋和变色操作。
- 左旋:以某一个节点作为支点(旋转节点),对其进行如下操作:
左旋前 | 左旋后 |
---|---|
旋转节点的左子节点 | 不做改变 |
旋转节点的右子节点 | 旋转节点的父节点 |
旋转节点 的 右子节点 的 左子节点(树) | 旋转节点的右子节点 |
旋转节点 的 右子节点 的 右子节点(树) | 不做改变 |
- 右旋
右旋前 | 右旋后 |
---|---|
旋转节点的左子节点 | 旋转节点的父节点 |
旋转节点的右子节点 | 不做改变 |
旋转节点 的 左子节点 的 右子节点(树) | 旋转节点的左子节点 |
旋转节点 的 左子节点 的 左子节点(树) | 不做改变 |
- 变色:红色变为黑色或者黑色变为红色。
4.2 红黑树插入时的平衡策略
注:红黑树在插入时结点默认的颜色为红色。
红黑树在插入时有很多种情况,下面分情况讨论。
4.2.1. 红黑树为空树
这是违反了红黑树的第二条规则:红黑树根节点一定是黑色,此时只需要对插入结点变色,并将其设置为根节点。
4.2.2. 插入结点的key在红黑树中已经存在
由于插入之前二叉树已经平衡,所以只需要将插入节点变色为要替换的点的颜色,直接插入。
4.2.3. 插入节点的父亲节点为黑色节点
由于红黑树允许红色结点作为黑色结点的儿子,所以无需变色。在这种情况下,红黑树的插入有两种方式:
a. 直接插入到适当位置
b.考虑插入节点与父亲节点的数据,若果Son<Parent,则直接插入到父亲节点的左子节点,否则插入到父亲结点的右子节点,并以父亲节点为支点进行一次左旋
4.2.4. 插入节点的父亲结点是红色结点
这种情况又分为几个小类,下面以父亲节点P是祖父结点G的左子结点为例一一说明。
4.2.4.1 叔叔节点存在且为红色节点
根据红黑树树第四条特性,可以知道祖父结点G一定为黑色节点,如果插入节点X小于P,将其插入父亲节点左子结点,这时违反了红黑树第五条性质,需要将父亲节点P和叔叔节点U变色为黑色,将祖父结点G变色为红色。这时由于祖父颜色发生改变,需要判断祖父结点G的父亲结点的颜色,吐过是红色则继续向上递归,使其平衡。
如果插入结点X的值大于P,则将其插入父亲结点P的右子结点,以父亲结点P为支点进行左旋,并将插入节点X和叔叔节点U变色为黑色。
4.2.4.2 叔叔节点为黑色结点或U不存在
如果插入结点X小于父亲结点P,将其插入父亲结点P的左子结点。由于X和P均为红色,违反红黑树规则,以祖父结点G为支点右旋,依然不满足红黑树规则,将X、P、G进行变色,得到平衡的红黑树。
如果插入节点X大于父亲结点P,将其插入父亲结点P的右子结点。此时需要以P为支点进行左旋,得到的树类似上图1,进行与上述相同的右旋和变色即可得到平衡的红黑树。
- 以上就是红黑树插入时自平衡的策略,很多情况均可以通过左旋、右旋或是变色来转化为上述几种情况,另外可以看出红黑树的生长一定是从叶子向根生长的,而普通的二叉查找树则是从根向叶子生长的。
4.3 红黑树删除时的自平衡策略
在二叉树的删除部分中,已经说明所有二叉树的删除最后都可以归结为替换节点(叶子节点)的删除,红黑树的删除也不例外,下面讨论不同情况中红黑树自平衡的策略。
我们约定X/R为待删除的结点,P为X的父亲结点,S为X的儿子结点,B为X的兄弟结点,BL为B的左子结点,BR为B的右子结点。
4.3.1 替代节点是红色结点
要注意的是,虽然我们说的替代结点是叶子结点,但是在红黑树中叶子结点也会有两个黑色的null结点,所以叶子结点也有可能是红色的。
在这种情况下,由于替换节点的颜色是红色的,删除树末的替换节点并不会对红黑树的平衡产生影响,所以只需要对替代节点变色为删除节点的颜色,之后做替代操作就可以。
4.3.2 替代节点是黑色结点
替代节点是黑色的情况下,删除替代节点后必然会违反红黑树规则五,造成二叉树的不平衡,这时我们就必须通过左旋右旋和变色处理,使得红黑树重新平衡。
4.3.2.1 替代结点的兄弟结点是红色结点
这种情况下,根据红黑树规则四,替代结点的父亲节点一定是黑色结点。我们进行以下操作:
- 将S设置为黑色
- 将P设置为红色
- 以P为支点进行左旋
- 见4.3.2.2C
4.3.2.2 替代结点的兄弟结点是黑色结点
这种情况下是无法确认替代结点的父亲结点和儿子结点的颜色,又需要分情况讨论
A 替代结点的右子节点是红色结点,左子结点颜色任意
如下图所示,我们现在想要删除P左子树上的R结点,左子树黑色结点数量-1,需要进行以下几步处理:
- 将S的颜色变色为P的颜色
- 将P变色为黑色
- 将SR变色为黑色
- 对P进行左旋
这里我在第一次看的时候不能理解为什么最后得到的树是平衡的,这里解释一下:
这里的R是替代结点,它即将被替换到删除结点的位置了,在删除进行之前,R还在原来的位置参与整棵树的自平衡,等树完全平衡之后才会替换到删除节点的位置。
在没有进行任何操作之前,整棵树是平衡的,根据红黑树规则五,SL只有红色和null两种情况,我们对树进行上述操作后,R将会被删除,这样以S为根节点的子树并不会违反红黑树规则,整棵树就达到了自平衡。
B 替代节点的兄弟节点的右子结点为黑色节点,左子节点为红色结点
这种情况我们可以通过变换,转换为情况4.3.2.1.1,具体操作如下:
- 将兄弟节点S与其左子结点SL颜色互换
从A、B两种情况可以看出,当替代结点的兄弟节点为黑色且兄弟节点的子树中有红色结点时,我们总可以向他们"借"来一个红色结点,补充替代结点所在子树中减少的那个黑色结点。
C 替代结点的兄弟结点的儿子结点都为黑色结点
这种情况下,无法从父亲结点右子树中寻找红色结点去补充,只能寄希望与它的父亲结点。这时候我们需要向上递归,从父亲节点的兄弟节点的子树中寻找红色节点来补充。这时候又分为两种情况
- 如果父亲结点已经是红色结点,这时候只需要将P和B颜色互换即可
- 父亲节点是黑色结点这时候需要将这颗子树看作一个整体,从底向上递归。
总结一下,红黑树的难点在与插入和删除中带来的自平衡问题,解决自平衡的核心是自底向上进行递归,所有的子树平衡了,得到的红黑树也一定是平衡的。在红黑树删除过程的自平衡我自己的印象也不是很深刻,有时间会对文章中不清楚的地方进行修改和补充。
红黑树在Linux进程调度、Java Hashmap和TreeMap中有较多的应用,写下本文的原因也是为了方便理解HashMap和TreeMap的源码。
参考资料:
[1].https://www.jianshu.com/p/e136ec79235c
[2].https://segmentfault.com/a/1190000014037447