首先来了解一下二叉排序树的由来,也就是在什么情况下迫使老一辈头脑风暴的科学家发明使用这种方法。
先说普通顺序存储(注意并不是有序),先来先坐,有点类似于“栈”。插入数据直接放到最末尾,删除中间数据的话可以把待删除数据和最后一位数据进行互换,也可以把待删除数据后的全部数据统一往前挪一位。这里实在没有顺序的前提下,删除和插入的效率都没有毛病,但是查找就有点差强人意了,或者说效果很悲观。
那么对于有序线性表呢?查找我们自然有各种科学方法来达到良好的效率,但是在删除与插入数据的时候由于为了要保证整体有序性会使代码量暴增,且在程序执行会浪费掉更多的CPU执行时间。
这时候二叉排序树就以兼具二者优点的科学良方出现了。
首先我们随便定义一个线性表:int[] nodes = {12, 9, 15, 18, 2, 7, 10, 35, 27, -6, 32, 17, 0, 5, 44, 16},其存储结构如下:
如果表示成排好序的二叉树(我们称之为二叉排序树),其存储结构为:
如此以来,我们就得到一个二叉排序树,用有序序列表示为:{-6, 0, 2, 5, 7, 9, 10, 12, 15, 16, 17, 18, 27, 32, 35, 44}。
定义:二叉排序树(Binary Sort Tree)又称为二叉查找树,它可以是一颗空树,非空就具有如下性质:
- 若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值
- 若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值
- 它的左右子树可分别为二叉排序树
其实,基于有序数据集上的查找速度总是高于无需有续集中的查找,而且对于二叉排序树这种非线性结构也有利于数据的插入和删除。
现在,我们就用java来实现对二叉排序树的基本操作,包括对如何构建(其实就是依次插入,且不会有重复数据出现)一个二叉排序树,以及对构建好的二叉排序树的插入数据、遍历数据、查找数据、删除数据。其他的操作基本上可以以这几种方式为基础进行实现。
如下代码中的内部类、方法介绍如下:
- 内部类Node.java:代表一个个的节点
- void insertBNode(int key):插入一个数据,生成一个节点
- void nrInOrderTraverse():中序遍历二叉排序树,生成一个有序序列
- boolean searchNode(int key):查看节点是否存在,用布尔类型接收
- void deleteBST(int key):删除指定节点
代码如下(Bst.java):
package bst;
import java.util.Stack;
/**
* 二排序叉树(BinarySortTree简称BST)
*/
public class Bst {
private static Node root = null;
private static Node n = null; // 用于存放待删除节点的双亲节点
/**
* 中序非递归遍历二叉树,获得有序序列(由小到大)
*/
public static void orderTraversal() {
Stack<Node> stack = new Stack<Node>(); // jdk栈实现
Node node = root;
int count = 0;
while (node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node); // 将节点(元素)放置栈顶,与addElement方法作用相同
node = node.getlBst();
}
node = stack.pop(); // 从栈顶弹出一个节点
System.out.print(node.getValue() + " ");
count++;
node = node.getrBst();
}
System.err.println("\n" + "有" + count + "个数");
}
/**
* 删除二叉排序树中的结点
* 分为三种情况:(删除结点为p,其父结点为f)
* (1)要删除的p结点是叶子结点,只需要修改它的双亲结点的指针为空
* (2)若p只有左子树或者只有右子树,直接让左子树/右子树代替p
* (3)若p既有左子树,又有右子树,用p左子树中最大的那个值(即最右端s)代替p,删除s,重接其左子树
*/
public static void deleteNode(int key) {
deleteNode(root, key, null);
}
private static boolean deleteNode(Node node, int key, String lOrR) {
if (node == null || !searchNode(key)) {
return false; // 节点不存在的情况
} else {
if (key == node.getValue()) {
return delete(node, n, lOrR);
} else if (key < node.getValue()) {
n = node;
return deleteNode(node.getlBst(), key, "left");
} else {
n = node;
return deleteNode(node.getrBst(), key, "right");
}
}
}
private static boolean delete(Node node, Node n, String lOrR) {
Node q = null;
Node s = null;
if (node.getrBst() == null) { // 右子树空,只需要重接它的左子树,如果是叶子结点,在这里也把叶子结点删除了
if (null != lOrR && lOrR.equals("left")) {
n.setlBst(node.getlBst());
} else {
n.setrBst(node.getlBst());
}
}
else if (node.getlBst() == null) { // 左子树空, 重接它的右子树
if (null != lOrR && lOrR.equals("left")) {
n.setlBst(node.getrBst());
} else {
n.setrBst(node.getrBst());
}
} else { // 左右子树均不为空
q = node; // 将待删除节点和其直接左子节点赋给临时变量q和s
s = node.getlBst(); // 转向左子树
while (s.getrBst() != null) { // 然后向右走到尽头,找待删节点的前驱
q = s;
s = s.getrBst();
}
node.setValue(s.getValue()); // 将待删除节点的前驱赋给它本身
if (q != node)
q.setrBst(s.getlBst());
else // 待删除节点的前驱就是它的直接左子孩子情况时
q.setlBst(s.getlBst());
}
return true;
}
/**
* 查找二叉排序树中是否有key值
*/
public static boolean searchNode(int key) {
Node current = root;
while (current != null) {
if (key == current.getValue())
return true;
else if (key < current.getValue())
current = current.getlBst();
else
current = current.getrBst();
}
return false;
}
/**
* 向二叉排序树中插入结点
*/
public static void insertBNode(int key) {
Node n = root;
Node pre = null;
while (n != null) {
pre = n;
if (key < n.getValue())
n = n.getlBst();
else if (key > n.getValue())
n = n.getrBst();
else
return;
}
if (root == null)
root = new Node(key);
else if (key < pre.getValue())
pre.setlBst(new Node(key));
else
pre.setrBst(new Node(key));
}
/**
* 定义二叉树结点
*/
public static class Node {
private int value;
private Node lBst;
private Node rBst;
public Node() {
}
public Node(int value) {
this(null, null, value);
}
public Node(Node lBst, Node rBst, int value) {
this.lBst = lBst;
this.rBst = rBst;
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Node getlBst() {
return lBst;
}
public void setlBst(Node lBst) {
this.lBst = lBst;
}
public Node getrBst() {
return rBst;
}
public void setrBst(Node rBst) {
this.rBst = rBst;
}
}
public static void main(String[] args) {
int[] nodes = {12, 9, 15, 18, 2, 7, 10, 35, 27, -6, 32, 17, 0, 5, 44, 16};
for (int node : nodes) {
insertBNode(node);
}
System.err.println(searchNode(17));
orderTraversal();
deleteNode(12);
orderTraversal();
System.err.println(searchNode(32));
}
}
其中,参考某些资料时发现对删除操作的方法是如下定义的:
public static void deleteNode(int key) {
deleteNode(root, key);
}
private static boolean deleteNode(Node node, int key) {
if (node == null) {
return false; // 节点不存在的情况
} else {
if (key == node.getValue()) {
return delete(node);
} else if (key < node.getValue()) {
n = node;
return deleteNode(node.getlBst(), key);
} else {
n = node;
return deleteNode(node.getrBst(), key);
}
}
}
private static boolean delete(Node node) {
Node q = null;
Node s = null;
if (node.getrBst() == null) { // 右子树空,只需要重接它的左子树,如果是叶子结点,在这里也把叶子结点删除了
q = node;
node = node.getlBst();
}
else if (node.getlBst() == null) { // 左子树空, 重接它的右子树
q = node;
node = node.getrBst();
} else { // 左右子树均不为空
q = node; // 将待删除节点和其直接左子节点赋给临时变量q和s
s = node.getlBst(); // 转向左子树
while (s.getrBst() != null) { // 然后向右走到尽头,找待删节点的前驱
q = s;
s = s.getrBst();
}
node.setValue(s.getValue()); // 将待删除节点的前驱赋给它本身
if (q != node)
q.setrBst(s.getlBst());
else // 待删除节点的前驱就是它的直接左子孩子情况时
q.setlBst(s.getlBst());
}
return true;
}
经测试发现,可以成功删除既有左子节点又有右子节点的节点,但是对于只有左子节点或者右子节点的节点和左右子节点都为空的节点是无法完成删除操作的,于是就有了对上述代码的改动,但感觉有点繁琐,还有进行优化。
一下代码可以用来计算树的深度:
/**
* 计算树的深度
*/
private static int height() {
return height(root);
}
/**
* 计算以当前n节点为根节点的树的深度
* @param n
* 需要计算深度的节点
* @return
* 以当前n节点为根节点的树的深度
*/
private static int height(Node n){
if (n == null){
return 0;
}else {
int l = height(n.getlBst());
int r = height(n.getrBst());
return (l > r) ? (l + 1) : (r + 1);//返回并加上当前层
}
}
以上内容主要参考《大话数据结构》一书。