二叉搜索树——Java实现(非递归版本)

317 阅读3分钟

二叉搜索树(Binary Search Tree,BST),是一棵空树或者一棵具备以下特征的二叉树:

  • 若左子树非空,则左子树所有节点的值都小于根节点的值。
  • 若右子树非空,则右子树所有节点的值都大于根节点的值。
  • 左、右子树又分别为一棵二叉排序树。

二叉搜索树是一种有序的半线性数据结构,如果使用链式的物理存储方式,那么二叉搜索树在查找和插入操作(维持数据有序的前提下)有着不错的表现,这得益于二叉树具备有良好的二分特性,所以在设计算法时可以很好的利用二分的思想。

BST的Java语言实现

BST的主要操作有插入删除搜索序列化

  • 插入:将一个值插入到BST中。
  • 删除:若BST存在有待删除的值,则将其从BST中移除。
  • 搜索:查看BST中是否包含有某个值。
  • 序列化:将BST序列化为一维有序序列。

BST 定义

/**
 * Binary Search Tree 非递归实现版本
 * @author callback
 * @param <T> 数据域模板参数
 * @version 1.0.0
 * @copyright (c) 2020
 */
public class BST<T extends Comparable<T>>
{

    /* Binary Search Tree Node */
    private static class TreeNode<T extends Comparable<T>>
    {
        private TreeNode<T> left; /* 左节点 */
        private TreeNode<T> right; /* 右节点 */
        private TreeNode<T> parent; /* 双亲节点 */

        private T data; /* 数据域 */

        TreeNode() {}

        TreeNode(T data) { this.data = data; }

        TreeNode(TreeNode<T> parent, T data) {
            this.parent = parent;
            this.data = data;
        }
    }


    private TreeNode<T> root = null; /* 根节点 */
    private int size = 0; /* BST的规模 */
}

插入操作

二叉树是一种动态树表,其特点是树的结构通常不是一次性生成的,而是在查找过程中,当树中不存在关键字值等于给定值的节点时在进行插入。

/* 插入操作 */
public void insert(T data)
{
    TreeNode<T> p = root;
    TreeNode<T> parent = null; /* 记录双亲节点 */
    while (p != null) {
        parent = p;
        if (data.compareTo(p.data) > 0) p = p.right;
        else if (data.compareTo(p.data) < 0) p = p.left;
    }

    if (parent == null) {
        root = new TreeNode<>(null, data);
        size++; return;
    }

    if (data.compareTo(parent.data) < 0) parent.left = new TreeNode<>(parent, data);
    else if (data.compareTo(parent.data) > 0) parent.right = new TreeNode<>(parent, data);
    size++;
}

查找操作

/* 查看BST中是否包含 @param data */
public boolean contains(T data)
{
    TreeNode<T> p = root;
    for (;;) {
        if (p == null) return false;

        if (data.compareTo(p.data) > 0) p = p.right;
        else if (data.compareTo(p.data) < 0) p = p.left;
        else return true;
    }
}

删除操作

在BST中删除一个节点时,不能把以该节点为根的子树上的节点都删除。正确的操作是,将待删除的节点从BST中摘下,然后将以该节点为根的节点重新链起来。为确保BST的性质不会丢失,删除操作要分三种情况处理:

  • 删除的节点是一个叶子节点,这种情况直接删除即可,不会破坏BST的性质。
  • 删除的节点只有一棵左子树或右子树,删除时让删除节点的子树成为其父节点的子树即可。
  • 删除的节点有左右两棵子树,这种情况较为复杂,一般让该节点的直接后继节点(中序遍历中的下一个节点)代替当前节点,然后,将直接后继节点的父节点的左子树设置为直接后继节点的右子树即可。
/* 删除操作 */
public boolean delete(T data)
{
    TreeNode<T> p = root;

    for (;;) {
        if (p == null) return false;

        if (data.compareTo(p.data) == 0) break;
        else if (data.compareTo(p.data) < 0) p = p.left;
        else p = p.right;
    }

    if (p.left == null && p.right == null) {
        if (p == root) {
            root = null;
        } else {
            if (data.compareTo(p.parent.data) < 0) {
                p.parent.left = null;
            } else if (data.compareTo(p.parent.data) > 0) {
                p.parent.right = null;
            }
        }
    } else if (p.left == null) {
        if (p == root) {
            root = root.right;
        } else {
            if (data.compareTo(p.parent.data) < 0) {
                p.parent.left = p.right;
            } else if (data.compareTo(p.parent.data) > 0) {
                p.parent.right = p.right;
            }
        }
    } else if (p.right == null) {
        if (p == root) {
            root = root.left;
        } else {
            if (data.compareTo(p.parent.data) < 0) {
                p.parent.left = p.left;
            } else if (data.compareTo(p.parent.data) > 0) {
                p.parent.right = p.left;
            }
        }
    } else {
        /* 待删除节点既有左节点又有右节点 */
        TreeNode<T> next = getNextTreeNode(p); /* 获取该节点的直接后继节点 */
        p.data = next.data; /* 用直接后继节点的数据更新当前节点 */
        next.parent.left = next.right; /* 删除直接后继节点,直接后继节点的一定为一个极左节点。 */
    }
    size--; return true;
}
/* 获取一个节点的直接后继节点 */
private TreeNode<T> getNextTreeNode(TreeNode<T> node)
{
    if (node == null) return node;

    if (node.right != null) {
        node = node.right;
        while (node.left != null) node = node.left;
        return node;
    } else {
        TreeNode<T> parent = node.parent;

        while (parent != null && parent.left != node) {
            node = parent;
            parent = node.parent;
        }
        return parent;

    }
}

查找最值

BST的最值很好查找,只要一直沿着右子树去遍历,那么得到的就是最大值。一直沿着左子树去遍历,那么得到的就是最小值。

/* 查找BST中的最大值 */
public T max()
{
    TreeNode<T> p = root;
    while (p != null && p.right != null) {
        p = p.right;
    }
    return p != null ? p.data : null;
}

/* 查找BST中的最小值 */
public T min()
{
    TreeNode<T> p = root;
    while (p != null && p.left != null) {
        p = p.left;
    }
    return p != null ? p.data : null;
}

总结

BST这种数据结构能够维持数据的有序性,并且相对于普通的一维线性结构在性能上有着一定的优势,但是这种优势很不稳定。一般情况下,我们期望BST是横向增长速度高于纵向生长速度,即我们期望BST的是朝着枝繁叶茂的趋势去增长,但是一旦BST的纵向生长速度超过了横向生长速度,那么BST就会退化为一维的链表结构。这样在插入和查找上的性能就会有所损耗。所以为了让树的结构符合我们的预期,我们需要对树的”生长“以及”修剪“进行一定的约束。这也就有了后来的AVL树和红黑树。