二叉搜索树 BinarySearchTree

参考算法导论第三版第十二章: 二叉搜索树。实现代码:百度网盘, 提取密码:duri。


 对二叉树进行这样的限制条件:对任意结点x,其左子树中所有关键字均小于x.key,其右子树中所有关键字均大于(等于)x.key。这样的二叉树被称为二叉搜索树。如下图:

 可以看出,相同的关键字集合,能够构建出不同结构的二叉搜索树,而任意结构都满足二叉搜索树的性质。由于二叉搜索树关键字组织特性,树的中序遍历即可获得关键字的按序排列。二叉搜索树中,从结点x依次向左查找,最后一个不为空的结点就是以x树为根的子树中关键字最小的结点,而向右查找,最后一个不为空的结点就是该子树中关键字最大的结点。


private TreeNode<K, V> min(TreeNode<K, V> node) {
    if (node == null)
        return null;
    while (node.left != null)
        node = node.left;
    return node;
}

private TreeNode<K, V> max(TreeNode<K, V> node) {
    if (node == null)
        return null;
    while (node.right != null)
        node = node.right;
    return node;
}


中序遍历:


 在二叉树的中序遍历过程中,当访问某个结点x前,先遍历结点x的左子树,然后访问结点x,最后遍历结点x的右子树。按照递归思想很容易实现中序遍历,也可以利用栈的LIFO特性实现非递归遍历:

/**
 * 对以node为根的子树进行中序遍历,递归实现
 *
 * @param node 树中某个结点,如果是root,就是对整棵树的遍历
 */
private void inOrderTraversal(TreeNode<K, V> node) {
    if (node != null) {
        inOrderTraversal(node.left);
        System.out.print(node);
        inOrderTraversal(node.right);
    }
}

/**
 * 中序遍历,非递归实现,借助栈的LIFO特性
 * @param node 树中某个结点,如果是root,就是对整棵树的遍历
 */
private void inOrder(TreeNode<K, V> node) {
    if (node != null) {
        TreeNode<K, V> r = node;
        Deque<TreeNode<K, V>> stack = new LinkedList<>();
        // 所有结点访问前均入栈,当某个结点访问完毕,说明其左子树中所有结点也访问完毕,
        // 则对其右子树进行遍历
        while (r != null || stack.size() > 0) {
            if (r != null) {
                stack.push(r); // 访问某个结点前将其入栈
                r = r.left; // 然后遍历其左子树
            } else {
                r = stack.pop(); // 出栈操作,得到栈顶结点
                System.out.print(r); // 从栈中取出的结点访问完毕,表明此结点的左子树中所有结点也访问完毕
                r = r.right; // 然后对其右子树进行遍历
            }
        }
    }
}


前驱和后继:


 根据二叉搜索树的特点,某个结点x按中序遍历的前驱结点:如果x存在左子树,则前驱结点应该为结点x左子树中最大的结点。否则应该从结点x向上查找到第一个键值小于结点x的祖先结点,这个结点就是结点x的前驱结点。若不存在,则结点x不存在前驱结点。后继结点同理。

/**
 * 查找node的前驱结点(中序)
 *
 * @param node 树中某个结点
 * @return node的前驱结点,可能返回null
 */
private TreeNode<K, V> predecessor(TreeNode<K, V> node) {
    if (node.left != null) // 如果结点node存在左子树,则该结点前驱结点为左子树中最大结点
        return max(node.left);
    TreeNode<K, V> p = node.parent;
    while (p != null && node == p.left) { // 向上查找,找到第一个小于结点node的祖先结点
        node = p;
        p = node.parent;
    }
    return p;
}

/**
 * 查找node的后继结点(中序)
 *
 * @param node 树中某个结点
 * @return node的后继结点,可能返回null
 */
private TreeNode<K, V> successor(TreeNode<K, V> node) {
    if (node.right != null) // 如果结点node存在右子树,则该结点后继结点为右子树中最小结点
        return min(node.right);
    TreeNode<K, V> p = node.parent;
    while (p != null && node == p.right) { // 向上查找,找到第一个大于结点node的祖先结点
        node = p;
        p = node.parent;
    }
    return p;
}



搜索操作:


 根据二叉搜索树特点,很容易对关键字key进行查找。当某个结点x.key=key时,x就是要找的结点。当结点key<x.key时,则在结点x的左子树中进行查找,当key>x.key时,则在结点x的右子树中进行查找。

/**
 * 查找关键字为key的结点
 *
 * @param key 要查找的关键字
 * @return 关键字为key的结点,查找失败返回null
 */
@SuppressWarnings("unchecked")
private TreeNode<K, V> treeNode(K key) {
    if (key == null)
        throw new NullPointerException("Key can not be null");
    TreeNode<K, V> r = root; // 从根结点开始查找
    while (r != null && key.compareTo(r.key) != 0) { // 直到r为null,或者r就是要找的结点,结束循环
        if (key.compareTo(r.key) < 0) // 当key<r.key,则在r的左子树中继续查找
            r = r.left;
        else // 当key>r.key,则在r的右子树中继续擦找
            r = r.right;
    }
    return r;
}

插入操作:


 插入操作和查找操作类似,都是根据关键字key的比较找到正确的位置。插入操作通过比较直到找到某个结点的空孩子处,在此处创建新的结点。比较过程中若发现相等的关键字,可以进行替换value操作。

/**
 * 向树种插入结点
 *
 * @param key         关键字
 * @param value       对应的值
 * @param putIfAbsent true:不存在相同的key时进行插入,false:存在相同的key时,替换value
 */
@SuppressWarnings("unchecked")
private void insert(K key, V value, boolean putIfAbsent) {
    if (root == null) // 当前树为空,创建根结点
        root = newTreeNode(key, value, null);
    else {
        TreeNode<K, V> r = root, p;
        do {
            p = r;
            if (key.compareTo(r.key) == 0) { // 如果查找到相同的key,则根据putIfAbsent和当前value值决定是否替换
                if (!putIfAbsent || r.value == null)
                    r.value = value;
                return;
            } else if (key.compareTo(r.key) < 0) // key小于当前结点关键字,则到左子树中进行定位
                r = r.left;
            else // key大于当前结点关键字,则到右子树中进行定位
                r = r.right;
        } while (r != null);
        // 循环结束时,p至多有1个非空孩子结点(r为其一个孩子,r=null),通过key和p.key的比较确定新结点位置

        r = newTreeNode(key, value, p); // 以p结点为父结点构建新插入的结点
        if (r.key.compareTo(p.key) < 0) // 如果key小于p.key,则r作为p的左孩子,否则作为右孩子
            p.left = r;
        else
            p.right = r;
    }
    size++;
}

删除操作:


 删除操作较插入操作要复杂一点。当删除叶子结点时,直接删除即可。而当删除某个非叶子结点z时,存在三种情况:
 一、结点z只有左子树,则将其左子树代替其位置。
 二、结点z只有右子树,则将其右子树代替其位置。
 三、结点z既有左子树又有右子树,则有两种策略:第一种是在结点z的左子树中选最大结点代替它,第二种是在结点z右子树中选取最小结点代替它,无论哪种策略都能够保证删除后,树依然保持二叉搜索树特性。

 而在第三种情况中,有存在两种情况:

 1、右子树中最小结点y是结点z的右孩子,则直接用右子树代替结点z。
 2、右子树中最小结点y不是结点z的右孩子(结点y一定没有左孩子,否则结点y就不是右子树中最小结点),则先用结点y的右子树代替结点y,然后用结点y取代结点z。



/**
 * 从树中删除关键字为key的结点
 * @param key 目标关键字
 * @return 被删除结点的value,若不存在关键字为key的结点,则返回null
 */
public V remove(K key) { // node就是结点z,t就是结点y
    TreeNode<K, V> node = treeNode(key); // 先找到要删除的目标结点
    if (node == null)
        return null;
    if (node.left == null) // 若目标结点不存在左子树,则用其右子树代替目标结点(情况二),右子树空相当于直接删除
        transplant(node, node.right);
    else if (node.right == null) // 若目标结点不存在右子树,则用其左子树代替目标结点(情况一)
        transplant(node, node.left);
    else { // 左右子树均存在(情况三)
        TreeNode<K, V> t = min(node.right); // 找到右子树中最小结点t
        if (t.parent != node) { // 如果结点结点t不是目标结点的右孩子(情况三.2),先用结点t的右子树代替结点t
            transplant(t, t.right);
            t.right = node.right;
            t.right.parent = t;
        }
        transplant(node, t); // 情况三.1,也是情况三.2中的一部分,用结点t代替目标结点
        t.left = node.left;
        t.left.parent = t;
    }
    size--;
    return node.value;
}

/**
 * 子树替换操作,在二叉搜索树中,用以结点v为根结点的子树代替以结点u为根结点的子树
 * @param u 某个结点u
 * @param v 某个结点v
 */
private void transplant(TreeNode<K, V> u, TreeNode<K, V> v) {
    if (u.parent == null) // u是根结点
        root = v;
    else if (u == u.parent.left) // 修改结点u父结点的子结点
        u.parent.left = v;
    else
        u.parent.right = v;
    if (v != null) // 修改结点v的父结点
        v.parent = u.parent;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值