数据结构思维笔记(十四)二叉搜索树

本章继续承接上章的内容,具体实现TreeMap中的方

1.简单的TreeMap

这里比较核心的一个方法是findNode,用来寻找与键值相当的节点,下面是它的实现:

  private Node findNode(Object target) {
        if (target == null) {
            throw new IllegalArgumentException();
        }

        @SuppressWarnings("unchecked")
        Comparable<?super K> k = (Comparable<?super K>)target;

        Node node = root;
        while (node != null) {
            int cmp = k.compareTo(node.key);
            if (cmp <0)
                node = node.left;
            else if (cmp>0)
                node = node.right;
            else 
            return node;
        }
        return null;
    }
  • 在这个实现中,null不是键的合法值。
  • 在我们可以在target上调用compareTo之前,我们必须把它强制转换为某种形式的Comparable。这里使用的“类型通配符”会尽可能允许;也就是说,它适用于任何实现Comparable类型,并且它的compareTo接受K或者任和K的超类(可以同任何类型做比较)。

之后,实际搜索比较简单。我们初始化一个循环变量node来引用根节点。每次循环中,我们将目标与node.key比较。如果目标小于当前键,我们移动到左子树。如果它更大,我们移动到右子树。如果相等,我们返回当前节点(这里用的是迭代,不断赋值)。


2.搜索值

findNode运行时间与树的高度成正比,而不是节点的数量,因为我们不必搜索整个树。但是对于containsValue,我们必须搜索值,而不是键;BST 的特性不适用于值,因此我们必须搜索整个树。

下面是containsValue方法,这里用递归实现:

public boolean containsValue(Object target) {
    return containsValueHelper(root, target);
}

private boolean containsValueHelper(Node node, Object target) {
    if (node == null) {
        return false;
    }
    if (equals(target, node.value)) {
        return true;
    }
    if (containsValueHelper(node.left, target)) {
        return true;
    }
    if (containsValueHelper(node.right, target)) {
        return true;
    }
    return false;
}

这是containsValueHelper的工作原理:

  • 第一个if语句检查递归的边界情况。如果nodenull,那意味着我们已经递归到树的底部,没有找到target,所以我们应该返回false。请注意,这只意味着目标没有出现在树的一条路径上;它仍然可能会在另一条路径上被发现。
  • 第二种情况检查我们是否找到了我们正在寻找的东西。如果是这样,我们返回true。否则,我们必须继续。
  • 第三种情况是执行递归调用,在左子树中搜索target。如果我们找到它,我们可以立即返回true,而不搜索右子树。否则我们继续。
  • 第四种情况是搜索右子树。同样,如果我们找到我们正在寻找的东西,我们返回true。否则,我们搜索完了整棵树,返回false

该方法“访问”了树中的每个节点,所以它的所需时间与节点数成正比


3.实现put

put方法比起get要复杂一些,因为要处理两种情况:

  1. 如果给定的键已经在树中,则替换并返回旧值
  2. 否则必须在树中添加一个新的节点,在正确的地方
public V put(K key, V value) {
    if (key == null) {
        throw new IllegalArgumentException();
    }
    if (root == null) {
        root = new Node(key, value);
        size++;
        return null;
    }
    return putHelper(root, key, value);
}

private V putHelper(Node node, K key, V value) {
    Comparable<? super K> k = (Comparable<? super K>) key;
    int cmp = k.compareTo(node.key);

    if (cmp < 0) {
        if (node.left == null) {
            node.left = new Node(key, value);
            size++;
            return null;
        } else {
            return putHelper(node.left, key, value);
        }
    }
    if (cmp > 0) {
        if (node.right == null) {
            node.right = new Node(key, value);
            size++;
            return null;
        } else {
            return putHelper(node.right, key, value);
        }
    }
    V oldValue = node.value;
    node.value = value;
    return oldValue;
}

第一个参数node最初是树的根,但是每次我们执行递归调用,它指向了不同的子树。就像get一样,我们用compareTo方法来弄清楚,跟随哪一条树的路径。如果cmp < 0,我们添加的键小于node.key,那么我们要走左子树。有两种情况:

  • 如果左子树为空,那就是,如果node.leftnull,我们已经到达树的底部而没有找到key。这个时候,我们知道key不在树上,我们知道它应该放在哪里。所以我们创建一个新节点,并将它添加为node的左子树。
  • 否则我们进行递归调用来搜索左子树。

如果cmp > 0,我们添加的键大于node.key,那么我们要走右子树。我们处理的两个案例与上一个分支相同。最后,如果cmp == 0,我们在树中找到了键,那么我们更改它并返回旧的值。


4.中序遍历

这里我们还剩最后一个方法KeySet,它返回一个Set,按升序包含树中的键。在其他Map实现中,keySet返回的键没有特定的顺序,但是树形实现的一个功能是,对键进行简单而有效的排序。下面是如何实现它的:

public Set<K> keySet() {
    Set<K> set = new LinkedHashSet<K>();
    addInOrder(root, set);
    return set;
}

private void addInOrder(Node node, Set<K> set) {
    if (node == null) return;
    addInOrder(node.left, set);
    set.add(node.key);
    addInOrder(node.right, set);        
}

keySet中,我们创建一个LinkedHashSet,这是一个Set实现,使元素保持有序,第一个参数node最初是树的根,但正如你的期望,我们用它来递归地遍历树。addInOrder对树执行经典的“中序遍历”。

  1. 按顺序遍历左子树。
  2. 添加node.key
  3. 按顺序遍历右子树。

5.二叉搜索树的问题

我们获取最有查询效率时,一般是O(log(n)),这种情况会在所搜索的树为平衡二叉树时出现,若不是平衡二叉树,搜索效率则会很低。

如果你思考put如何工作,你可以弄清楚发生了什么。每次添加一个新的键时,它都大于树中的所有键,所以我们总是选择右子树,并且总是将新节点添加为,最右边的节点的右子节点。结果是一个“不平衡”的树,只包含右子节点。

这种树的高度正比于n,不是logn,所以getput的性能是线性的,不是对数的


6.自平衡树

这个问题有两种可能的解决方案:

  • 你可以避免向Map按顺序添加键。但这并不总是可能的。 你可以制作一棵树,如果碰巧按顺序处理键,那么它会更好地处理键。(按顺序添加会导致这是一个极不平衡的树)
  • 第二个解决方案是更好的,有几种方法可以做到。最常见的是修改put,以便它检测树何时开始变得不平衡,如果是,则重新排列节点。具有这种能力的树被称为“自平衡树”。普通的自平衡树包括 AVL 树(“AVL”是发明者的缩写),以及红黑树,这是 JavaTreeMap所使用的。

总而言之,二叉搜索树可以以对数时间实现getput,但是只能按照使得树足够平衡的顺序添加键。自平衡树通过每次添加新键时,进行一些额外的工作来避免这个问题

7.二叉搜索树的删除

实现思路:

  1. 删除叶子节点
  2. 删除只有左孩子的节点
  3. 删除只有右孩子的节点
  4. 删除左右孩子都有的节点,这里我们需要找出待删除节点的右子树中的最小节点,并放到待删除位置上

下面是实现的具体方案:

public V remove(Object k) {
        // 一会儿我来填这个坑
        Comparable<?super K> key = (Comparable<?super K>)k;
        Node currentNode = this.root; //用来保存待删除节点
        Node parentNode = this.root;  //保存待删除节点的父节点
        boolean isLeftNode = true;   //左右节点判断
        V oldValue = null;

        // 寻找需要的节点(同时记录它的位置)
        while ( (currentNode != null) && (currentNode.key != key) ) {
            parentNode = currentNode;
            int cmp = key.compareTo(currentNode.key);
            if (cmp < 0) {
                currentNode = currentNode.left;
                isLeftNode = true;
            } else {
                currentNode = currentNode.right;
                isLeftNode = false;
            }
        }

        // 空树
        if (currentNode == null) {
            return null;
        }

        // 删除的节点是叶子节点
        if ( (currentNode.left == null) && (currentNode.right == null) ) {
            if (currentNode == this.root) {
                oldValue = root.value;
                root = null;
            } else if (isLeftNode) {
                oldValue = parentNode.left.value;
                parentNode.left = null;
            } else {
                oldValue = parentNode.right.value;
                parentNode.right = null;
            }
        } else if ( (currentNode.right == null) && (currentNode.left != null) ) {   //删除节点只有左孩子(分情况看它挂到哪)
            if (currentNode == this.root) {
                oldValue = root.value;
                root = currentNode.left;
            } else if (isLeftNode) {
                oldValue = currentNode.left.value;
                parentNode.left = currentNode.left;
            } else {
                oldValue = currentNode.right.value;
                parentNode.right = currentNode.left;
            }
        } else if ( (currentNode.right != null) && (currentNode.left == null) ) {   //删除节点只有右孩子
            if (currentNode == root) {
                oldValue = root.value;
                root = currentNode.right;
            } else if (isLeftNode) {
                oldValue = parentNode.left.value;
                parentNode.left = currentNode.right;
            } else {
                oldValue = parentNode.right.value;
                parentNode.right = currentNode.right;
            }
        } else {    //待删除节点既有左子树,又有右子树(思路:将待删除节点右子树的最小节点赋值给待删除节点)
            Node directPostNode = getDirectPostNode(currentNode);
            oldValue = directPostNode.value;
            currentNode.key = directPostNode.key;
            currentNode.value = directPostNode.value;
        }
        size--;
        return oldValue;
    }

得到待删除节点的直接后继节点:

   private Node getDirectPostNode(Node delNode) {

        Node parentNode = delNode;  //用来保存待删除节点的(直接后继节点的父亲节点)
        Node directNode = delNode;  //用来保存待删除节点的(直接后继节点)
        Node currentNode = delNode.right;   // 待删除节点右子树

        while (currentNode != null) {
            parentNode = directNode;
            directNode = currentNode;
            currentNode = currentNode.left;
        }

        // 直接删除此后继节点(因为不是直接相连的,最小的肯定是叶子节点或者没有左子树)
        if (directNode != delNode.right) {
            parentNode.left = directNode.right;
            directNode.right = null;
        }

        return directNode;
    }

具体的可以看图:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值