二分搜索树(带动画详解)

在理解二分搜索树之前,我们先来看看二叉树是什么。

1.1 二叉树

二叉树也是一种动态的数据结构。每个节点只有两个叉,也就是两个孩子节点,分别叫做左孩子,右孩子,而没有一个孩子的节点叫做叶子节点。每个节点最多有一个父亲节点,最多有两个孩子节点(也可以没有孩子节点或者只有一个孩子节点)。47左半边的所有节点组合起来形成了47的左子树,47右半边所有节点结合起来形成了47的右子树。如下图所示:

综合一下,涉及到的概念有:

根节点:二叉树的起始节点,唯一没有父亲节点的节点; 父亲节点:每个节点只有一个父亲节点。如上图47就是35的父亲节点; 左右孩子节点:每个节点至多拥有两个孩子节点,分别叫左孩子,右孩子; 左子树右子树:每个节点左边或者右边部分所有节点组合成的树结构。

1.2 二分搜索树

1.2.1 性质

第一,二分搜索树是一颗二叉树,满足二叉树的所有定义; 第二,二分搜索树每个节点的左子树的值都小于该节点的值,每个节点右子树的值都大于该节点的值。 第三,任意节点的每颗子树都满足二分搜索树的定义。

1.2.2 意义

当我们看到二分搜索树的定义时,是否会去联系这样定义的意义何在呢?其实,二分搜索树是在给数据做整理,因为左右子树的值和根节点存在大小关系,所以在查找元素时,我们于根节点进行对比后,就能每次近乎一半的去除掉查找范围,这就大大的加快了我们的查询速度,插入元素时也是一样。

在图1-2中,如果要查找元素55,那么和根节点47对比后,发现55比47大,于是就往47右孩子60中去查询,接着发现55比60小,就往60左孩子中查询,于是就找到了这个元素。想象一下,如果是一个链表,那么将一个一个查询下去,速度可想而知。

其实在生活中,这样的例子也比比皆是,我们去超市买东西,超市也把一二三楼卖的是啥写的很清楚,假如三楼卖的是生鲜果蔬,而我们要买今天的菜,那么我们就直接去三楼,一楼和二楼我们就可以不用去找了,大大加快了我们选购商品的速度。图书馆找书也是这样的例子。所以二分搜索树的意义也就在此,很多时候数据结构其实来源于生活,于生活中解决实际问题,这就是技术的力量和价值的体现。

但是,为了达到这样的高效性,树结构由此也需要每个节点之间具备可比较性。而链表数据结构就没有这类要求,所以还是那句话,有得必有失。

到此我们就可以轻松定义出来二分搜索树的基本代码如下:

public class BST<E extends Comparable<E>> {

    private class Node {
        public E e;
        public Node left, right;

        public Node(E e) {
            this.e = e;
            left = right = null;
        }

    }

    private int size;
    private Node root;

}
复制代码

1.3 添加元素

我们一起来看看在一个树中插入元素的动画演示过程,如下图元素65插入树结构所示,元素在插入时,不断跟当前根节点进行对比,以来选择插入到左子树还是右子树。

通过动画,我们对树结构插入操作有了一个大致了解。现在我们来一步一步实现添加过程,将复杂问题简单化:

1.3.1 第一步

当前状态:无节点。

当然是从一棵空树开始,这个时候来一个元素,这时当然是赋值给root根节点。很自然,我们就想到如下代码:

    public void add(E e) {
        if (root == null) {
            root = new Node(e);
            size++;
        } else {

        }
    }
复制代码

1.3.2 第二步

当前状态:有节点,但节点左孩子或右孩子为空或左右孩子都为空。

这一步,我们已经有了根节点了,那么将在else中写逻辑。由下面动画我们知道,这时需要判断两个条件:

第一,插入元素与当前节点大小的比较; 第二,程序当前所在节点下左右节点是否为空,如果为空,那么就把添加的节点插入在这个位置上。

接下来,我们将用递归的方式进行元素的添加,我们先解决在某一节点上添加元素这个子问题。那么下面代码块中 add(root, e);这句代码把根节点传入,就是在根节点上添加元素,那么代码如下:

    public void add(E e) {
        if (root == null) {
            root = new Node(e);
            size++;
        } else {
            add(root, e);
        }
    }

    // 在node为根节点的树中,添加元素e
    private void add(Node node, E e) {
        if (node.e.compareTo(e) == 0) {
            return;
        }

        // 添加到左孩子的位置,
        // 条件:添加的元素小于node节点的元素,node节点的元素的左孩子为空
        if (e.compareTo(node.e) < 0 && node.left == null) {
            node.left = new Node(e);
            size++;
            return;
        }

       // 添加到右孩子的位置
        if (e.compareTo(node.e) > 0 && node.right == null) {
            node.right = new Node(e);
            size++;
            return;
        }

    }
复制代码

1.3.3 第三步

当前状态:有节点,左右孩子都不为空。

如果当前节点左右孩子都不为空,那么就递归的把当前节点的左孩子或右孩子传递进add方法。如下:

    public void add(E e) {
        if (root == null) {
            root = new Node(e);
            size++;
        } else {
            add(root, e);
        }
    }

    private void add(Node node, E e) {
        if (node.e.compareTo(e) == 0) {
            return;
        }

        if (e.compareTo(node.e) < 0 && node.left == null) {
            node.left = new Node(e);
            size++;
            return;
        }

        if (e.compareTo(node.e) > 0 && node.right == null) {
            node.right = new Node(e);
            size++;
            return;
        }

         // 递归调用,如果当前节点左右节点不为空,则进入下一轮递归的调用。
        if (e.compareTo(node.e) < 0) {
            add(node.left, e);
        } else {
            add(node.right, e);
        }
    }
复制代码

换一种思考方式

我们如果仔细观察上面的逻辑就会发现,只要添加元素,那么这个位置肯定是空节点,用递归的话,我们的循环终止条件就可以用当前位置是否为空来判断,代码瞬间简单了许多,如下:


    public void add(E e) {
        root = addAnother(root, e);
    }

    // 返回插入新节点后,二分搜索树的根
    private Node addAnother(Node node, E e) {

        if (node == null) {
            size++;
            return new Node(e);
        }

        if (e.compareTo(node.e) < 0) {
            node.left = addAnother(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = addAnother(node.right, e);
        }

        return node;
    }
复制代码

1.4 树的遍历

树的遍历分为前序遍历,中序遍历,后序遍历,层序遍历。我们依次来讲一讲。

1.4.1 前序遍历

前序遍历,就是我们在递归调用前,先做我们的逻辑(这里的逻辑就是打印一下当前元素),前序遍历代码如下:

    private void preOrder(Node node) {
        if (node == null) {
            return;
        }

        System.out.println(node.e);
        preOrder(node.left);
        preOrder(node.right);

    }
复制代码

我们以一个节点为例子,如节点后还有节点,递归会循环处理。以下树结构最终打印的结果就是47,35,60。

1.4.2 中序遍历与后续遍历

那么接下来就是中序遍历,中序遍历就是把对元素的操作操作放在了中间,先不断递归左孩子,然后对元素进行操作,最后递归右孩子。所以很容易的推理出来,中序遍历是对元素从小到大的排序遍历。这个性质可以作为判断二分搜索树的一个条件。

同理可以推出还有后续遍历,这里就不再赘述了。

1.4.3 层序遍历

最后我们一起来聊聊层序遍历,顾名思义,层序遍历就是对树结构一层一层的遍历。要做到这一点,我们可以很方便的想到利用队列来实现这一过程。我们每次从队列中取出一个元素时,接着就把该元素的左右两孩子推入队列中,然后依次取出元素,以此来做到按层把元素遍历一遍。

一起来看看动画爸爸给我们的演示,最直观。

由动画爸爸很容易看出,依次打印的顺序分别为:47,35,60,32,45,55,65,完美完成层序遍历。

然后我来奉上代码妈妈:

    public void levelOrder() {
        Queue<Node> q = new LinkedList<>();
        // 把根节点放入队列
        q.add(root);

        while (!q.isEmpty()) {
            // 移除并返回队列中的第一个节点
            Node front = q.remove();
            System.out.print(front.e + "  ");

            // 把移除元素的左右孩子推出队列
            if (front.left != null) {
                q.add(front.left);
            }

            if (front.right != null) {
                q.add(front.right);
            }
        }
    }
复制代码

1.5 删除元素

1.5.1 后继节点

接下来,我们聊聊元素的删除。在此之前,我们先得引入一个概念,元素的前驱或者后继节点。

后继节点:一个节点右子树中,最小的节点为该节点的后继节点。后继节点是比该节点所有大的元素中最小的元素。

之所以要引入这个概念,原因是在删除元素时,我们需要找一个元素替代被删除位置的元素,但是由于二分搜索树的特性,不能随便找元素过来代替,必须得找一个和被删除元素最接近的元素来替代其位置。所以找前驱或后继替代都可以。

比如说,如下图,47的后继节点就是55,右子树中最小的元素。

1.5.2 删除元素的过程

我们分情况讨论,待删除元素可能有以下三种情形。

第一种,只有左孩子;

第二种,只有右孩子;

第三种,左右孩子都有;(当然还有一种情况就是为叶子节点,那么直接删除即可)

同理很容易就能推出右子树为空时的删除过程。

接下来我们来看看左右子树不为空的情况。这种情况动画演示比较简单,我们看看整体的过程。

第一步,找到待删除元素;

第二步,找到待删除元素的后继节点;

第三步,把待删除元素的左子树赋值给后继节点的左子树,把待删除元素的右子树最小元素删除后,赋值给后继节点的右子树。

整体代码如下:

   public void remove(E e) {
        root = remove(root, e);
   }

   private Node remove(Node node, E e) {
        if (node == null) {
            return null;
        }

        if (e.compareTo(node.e) < 0) {
            node.left = remove(node.left, e);
            return node;
        } else if (e.compareTo(node.e) > 0) {
            node.right = remove(node.right, e);
            return node;
        } else { // 找到了待删除元素
            if (node.left == null) {
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }

            if (node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }

            // 后继节点
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;
            return successor;

        }

    }
复制代码

转载于:https://juejin.im/post/5c2338f8f265da616c65a5ff

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值