前言
这是这个系列上的第二篇文章,如果你还没有了解二叉树的话,可以先看我的文章中阅读量最高的二叉树(从建树、遍历到存储)Java.
一、定义
满是学究气息的文字定义我们先不看,还是沿用看图说话的惯例。
“数无形时少直觉,形少数时难入微”,所以文字上的定义也是不能少的,其实我们通过上图可以自己总结出来。
对于二叉树上的任意节点,若它的左子树不为空,那么左子树上所有节点的值均小于它的值,若它的右子树不为空,那么右子树上所有节点的值均大于它的值。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找、增加和删除数据的速度。因为在一个有序的集合中查找数据的速度总是快于在无序集合中,而且二叉树排序树这种非线性的结构也有利于增加和删除节点(类似链表)。
二、查找
在一本很畅销的数据结构书上这一部分的算法实现用的递归,我不是很赞同,虽然递归写起来代码很简洁,但是对于初学者而言比较难理解,随着一层一层的递归调用下去,跟着跟着就满脑浆糊了。
所以这里我们还是用循环来写,方便理解。
代码
public boolean contains(int e) {
//用current记录根节点
Node current = root;
while (current != null) {
//相等,说明找到了,返回true
if (e == current.data) {
return true;
//要查找的数比根节点大,指向根节点的右儿子
} else if (e > current.data) {
current = current.right;
//要查找的数比根节点下,指向根节点的左儿子
} else {
current = current.left;
}
}
//找不到,返回false
return false;
}
这只是一个代码片段,最后提供完整代码,更具有逻辑性。
图解
图一:初始状态,若查找80。
图二:
(1)node为根节点,不为空,进入while循环;
(2)要查找的是80,node.data的值是60,80大于60,node等于node.right。
图三:
(1)此时node为下图中的红色节点,不为空,进入while循环;
(2)要查找的是80,node.data是75,80大于75,node等于node.right。
图四:
(1)此时node为下图中的红色节点,不为空,进入while循环;
(2)要查找的80,node.data是80,80等于80,返回true。
三、增加节点
当要插入的数据在二叉排序树中不存在时,插入成功并返回true,否则返回false。
代码
public boolean add(int e) {
if (root == null) {
root = new Node(e);
return true;
}
//记录current的父节点
Node father = null;
Node current = root;
while (current != null) {
if (e > current.data) {
father = current;
current = current.right;
} else if (e < current.data) {
father = current;
current = current.left;
} else {
return false;
}
}
if (e > father.data) {
father.right = new Node(e);
} else {
father.left = new Node(e);
}
return true;
}
请对比查找代码:
public boolean contains(int e) {
Node current = root;
while (current != null) {
if (e == current.data) {
return true;
} else if (e > current.data) {
current = current.right;
} else {
current = current.left;
}
}
return false;
}
所以如果已经理解查找的代码,对于增加的代码也不难理解。
四、删除节点
“请佛容易送佛难”,从上面的代码我们知道增加节点的操作其实并不麻烦,一半代码还和“查找”操作的代码相同,但是要删除节点的话就没那么轻松了,因为要考虑到删除该节点后整个二叉树还满住二叉排序树的要求。
1.单身狗
即如下图中的深色节点,这个要删除的节点是一个可怜的单身狗。。。
那么就把它直接删除!我们可以看到直接删除后对整个二叉树排序树并没有什么影响。
2.独生子
这个节点的后代虽然只有一棵独苗,但也是有的。
当他驾崩后,这皇位要传给谁呢?当然是他的独生子69。而且我们把69放到62的位置发现二叉排序树的性质并没有被破坏,如下图。
当然,若这颗独苗也有后代,情况是一样的。
3.多子多孙
“哈哈哈哈!我可是多福多寿,多子多孙之人!”又来了一个节点。这个节点可是儿孙满堂,那么他驾崩后,“夺嫡”自然是不可避免的。
30他驾崩了,留下了大好河山,宗室诸王们蠢蠢欲动,谁都想过一把皇帝瘾,经过一番比拼,发现25或者35都可以继承皇位,那就二者选其一吧。
“等等!怎么就25或者35了?”一个外国人不明所以地问道。原来在二叉排序树内部皇位的继承不是看谁最强而是看谁最像的。在30的众多子孙中只有25和35是最像他的(和他的差距最小)。
但是在实际操作中我们不一定要真的把30删掉,可以稍稍变通一下,先找到继承人将他的值赋给要删除的节点,再将继承人删除。
看到这里有些人不禁会想这个方法听上去简单,实际中要怎么用算法找到继承人呢?之后会与大家分享两句口诀,保证让大家按图索骥,顺利找到这两个节点。
(1)25继承
口诀一:左转,向右到尽头。
我们再来看一个例子,如下图:
注意:对于继承人来说他只有单身狗和左独生子这两种情况,不存在右独生子和多子多孙的情况,不明的话,再看看口诀就明白了,如果他存在右子,那么皇位还能轮到他继承吗?口诀可是向右到尽头!
(2)35继承
口诀二:右转,向左到尽头。
同样我们再来看一个例子,如下图:
注意:同理,对于第二种情况的继承人来说他只有单身狗和右独生子这两种情况,不存在左独生子和多子多孙的情况,如果他存在左子,就不是他继承皇位了。
五、完整代码
这是我在学习过程中模仿Java集合类写的一个二叉排序树类,欢迎大家指导。
BinarySortTree类:
import java.util.LinkedList;
public class BinarySortTree {
//记录根节点
Node root;
public BinarySortTree() {
}
//增
public boolean add(int e) {
if (root == null) {
root = new Node(e);
return true;
}
//记录current的父节点
Node father = null;
Node current = root;
while (current != null) {
if (e > current.data) {
father = current;
current = current.right;
} else if (e < current.data) {
father = current;
current = current.left;
} else {
return false;
}
}
if (e > father.data) {
father.right = new Node(e);
} else {
father.left = new Node(e);
}
return true;
}
/* 删
*
* */
public boolean remove(int e) {
Node father = null;
Node current = root;
while (current != null) {
if (current.data == e)
return removeNode(current, father, e);
else if (e > current.data) {
father = current;
current = current.right;
} else {
father = current;
current = current.left;
}
}
return false;
}
final boolean removeNode(Node current, Node father, int e) {
//若左儿子为空,右儿子继承
if (current.left == null) {
if (current.data > father.data) {
father.right = current.right;
} else {
father.left = current.right;
}
//方便垃圾回收器回收
current = null;
}
//若右儿子为空,左儿子继承
else if (current.right == null) {
if (current.data > father.data) {
father.right = current.left;
} else {
father.left = current.left;
}
current = null;
}
//多子多孙
else {
//用于记录heir的父节点
Node heirFather = null;
/* heir(继承人)
* heir = current.left即口诀一中的左转
* */
Node heir = current.left;
//即口诀一中的向右到尽头
while (heir.right != null) {
heirFather = heir;
heir = heir.right;
}
//继承人的值赋给要被删除的节点
current.data = heir.data;
if (heir.data > heirFather.data) {
heirFather.right = heir.left;
} else {
heirFather.left = heir.left;
}
heir = null;
}
return true;
}
//查找
public boolean contains(int e) {
Node current = root;
while (current != null) {
if (e == current.data) {
return true;
} else if (e > current.data) {
current = current.right;
} else {
current = current.left;
}
}
return false;
}
//层次遍历
public void levelOrder() {
if (root == null) {
return;
}
LinkedList<Node> q = new LinkedList<Node>();
q.addFirst(root);
Node current;
while (!q.isEmpty()) {
current = q.removeLast();
System.out.print(current.data + " -> ");
if (current.left != null)
q.addFirst(current.left);
if (current.right != null)
q.addFirst(current.right);
}
}
private static class Node {
int data;
Node right;
Node left;
Node(int data) {
this.data = data;
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
BinarySortTree tree = new BinarySortTree();
tree.add(60);
tree.add(30);
tree.add(75);
tree.add(15);
tree.add(50);
tree.add(80);
tree.add(10);
tree.add(25);
tree.add(40);
tree.add(55);
tree.add(19);
tree.add(35);
System.out.print("层次遍历(删除前):");
tree.levelOrder();
System.out.println();
//删除
tree.remove(30);
System.out.print("层次遍历(删除后):");
tree.levelOrder();
System.out.println();
System.out.print("是否已存在:");
System.out.print(tree.contains(80));
}
}
测试类中构造出的二叉树排序树长这样:
结果:
层次遍历(删除前):60 -> 30 -> 75 -> 15 -> 50 -> 80 -> 10 -> 25 -> 40 -> 55 -> 19 -> 35 ->
层次遍历(删除后):60 -> 25 -> 75 -> 15 -> 50 -> 80 -> 10 -> 19 -> 40 -> 55 -> 35 ->
是否已存在:true
这里我只测试了一种情况,有兴趣的可以将代码粘贴复制到编译器上多测试几种情况。
写在最后
分享这篇文章的目的不仅仅是与大家一起讨论二叉排序树的使用,更大的目的是为了对付JDK1.8中的HashMap,其中用到了红黑树这种数据结构。
所以接下来还会分享其他类型二叉树的使用,总的路线为:二叉树——>二叉排序树(二叉搜索树)——>红黑树,再加一个哈希表(hash table)。
这是这条路线上的第二篇文章,之前还有一篇二叉树的二叉树(从建树、遍历到存储)Java.有兴趣的话可以看看,如果喜欢这个系列的话可以持续关注哦。