二叉树

什么是树?

  这就是树:

 

  我们现实生活中经常有这样的结构,比如企业层级关系:

 

  系统目录结构:

 

  还比如家族族谱、书籍目录章节、后台系统页面上的菜单都是树形结构。

 

  特别要注意的是,树是不包含回路的,如下图:

节点与树叶

  一棵树中的所有组成部分叫做节点,由根节点伸展扩散,上下节点连接的关系称作父子关系

  如下面这棵树:

  其中A节点就是根节点(root)。

  B和F的关系就是父子关系,B是父(parent),F是子(child)。

  B和C有共同的父节点,所以B和C的关系为兄弟节点(sibling)。

 

  一棵树由N个节点组成,由于每个节点都有一条线连上父节点,唯独根节点没有父节点,所以一棵树总共有N-1条边。

 

  每个节点可以有任意多个子节点,没有子节点的节点被称作这棵树的树叶(leaf)。

 

高度,深度与层数

  还是这个示例:

  每个节点的高度是自身节点到当前分支的树叶的长度,比如B节点的高度为1,C节点的高度为2。

  所有树叶的高度都是0,如F节点高度为0。

  一棵树的高度等于它的根节点的高度。

 

  每个节点的深度为根节点到自身的路径长度,比如C节点的深度为1,F的深度为2。

  根节点的深度固定为0。

  一棵树的深度等于它最深的那个树叶的深度。

 

  层数和深度的计算差不多,不同之处是层数的计数起点是1,也就是说根节点位于整棵树的第1层。

 

二叉树基础

什么是二叉树?

  顾名思义,二叉树的意思就是有两个叉的树,也就是每个节点最多只有两个子节点(最多的意思就是小于等于),分别叫做左子节点和右子节点。

  如下图:

 

  当然,也有四叉树、八叉树,之所以二叉树这么火,是因为二叉树的应用非常广泛。

 

  二叉树还有两种特殊的表现形式,叫做满二叉树完全二叉树

 

满二叉树

  如果一颗二叉树所有的叶子节点都有相同的深度,也就是整棵树除了叶子节点之外的所有节点都有两个子节点,那么这棵二叉树叫做满二叉树。

  如下图:

完全二叉树

  如果一棵二叉树所有的叶子节点都在最底下两层,最后一层的叶子节点靠左排列,这种二叉树叫做完全二叉树。

  也就是说,这棵二叉树快成满二叉树了,但是最后一层的叶子节点还没铺满,并且叶子节点是从左到右排列的叫做完全二叉树。

  如图:

二叉树的存储

  树这种结构并非物理存储结构,它需要使用数组或链表实现。

链式存储法

  存储结构如图:

  每个节点包含三部分:存储的数据、指向左子节点的指针,指向右子节点的指针。

  节点属性:

class Node{
    Object data;
    Node left;
    Node right;
}

 

顺序存储法

  用数组来存储,就应该将每个节点严格对应到数组下标上,鉴于二叉树最多只能两个叉,所以我们按照层级一层一层的排号。

  如图:

  正常情况下,为了方便计算子节点,把整棵树串起来,我们将根节点存储在数组下标为1的位置,每一个节点的位置映射成数组下标。

  假设每个节点的下标为i,那么我们可以得知每个节点的左子节点为 i*2,每个节点的右子节点为 i*2+1。

  反之,每个节点的父节点的下标为 i / 2。

 

  对于一棵满二叉树来说,用数组存储仅仅只空余了一个下标为0的数组空间。

 

  那对于一棵稀疏的二叉树来说,用数组存储是比较浪费空间的,比如下面这棵非常牛逼的二叉树:

  

二叉查找树

什么是二叉查找树?

  二叉查找树,也叫做二叉排序树,又叫二叉搜索树,这三个概念指的都是同一个东西,由名字可以得知,二叉查找树的特性是快速的增删改查。

 

  二叉查找树在普通的树上添加了两个条件:  

  在树中的任意一个节点,其左子树中的每个节点的值,都要小于本身节点的值。
  在树中的任意一个节点,其右子树上的每个节点的值,都要大于本身节点的值。

 

  满足了这两个条件,那么这棵树就叫二叉查找树,整棵树中的每个节点的子树也就都是二叉查找树。

   下图就是一棵二叉查找树:

 

查找节点

  假设我们要查找16这个数据,首先第一步找到根节点10:

  发现16大于10。

 

  找到10的右子节点15:

  发现16还是大于15。

 

  继续找到15的右子节点17:

  发现16小于17。

 

  找到17的左子节点:

  找到了16。

 

代码实现:

        public Node find(int data) {
            while (null != node) {
                if(data == node.data){
                    return node;
                }else if(data < node.data){
                    node = node.left;
                }else if(data > node.data){
                    node = node.right;
                }
            }
            return null;
        }

 

最小节点和最大节点

  由于小的节点总是在左子节点方向,因此我们可以用递归来实现找到整棵树中的最小节点:

    public Node minNode(){
        return minNode(tree);
    }
    private Node minNode(Node node)
    {
        if(null == node.left){
            return node;
        }
        return minNode(node.left);
    }

 

  最大节点也是一样,递归往右找,到底为止。

    public Node maxNode(){
        return maxNode(tree);
    }
    private Node maxNode(Node node)
    {
        if(null == node.right){
            return node;
        }
        return maxNode(node.right);
    }

 

插入

  新插入的节点,也要遵循二叉查找树的规则,逐一比较大小,比节点大就往右边,比节点小就往左边。

  比如我们插入13这个元素,遵循规则的判断后最终位置会确定成14的左子节点。

代码实现:

        public void insert(int data) {
            Node newNode = new Node(data);
            if (null == tree) {
                tree = newNode;//如果整棵树不存在 那么新数据就是根节点
                return;
            }
            while (null != tree) {
                if(data == tree.data){
                    return;//重复值
                }
                //新元素大于当前节点
                if (data > tree.data) {
                    //右子节点有空位,则插入
                    if (null == tree.right) {
                        tree.right = newNode;
                        return;
                    }
                    tree = tree.right;
                } else {
                //新元素小于
                    if (null == node.left) {
                        tree.left = newNode;
                        return;
                    }
                    tree = tree.left;
                }
            }
        }

 

  当我们依次插入10、8、15、7、14、17这几个元素后,二叉树的结构是这个样子的:

 

删除的三种情况

  二叉查找树最难实现的就是删除,它要分三种情况

 

  情况一:如果要删除的节点是叶子节点,比如我们要删除6这个节点

  由于叶子节点没有子节点,所以只需要将它自身删除即可,也就是将该节点的父节点中指向它的指针置为null。

 

  情况二:需要删除的节点含有一个子节点,比如我们要删除14这个节点

  我们只需要把14拿掉,把14的子节点13连上14的父节点15就可以了

 

  情况三:如果要删除的节点含有两个子节点,比如我们要删除15这个节点

  这种情况也有两种做法。

 

  其一是删除15这个元素,然后在15的右子树中查找最小的那个节点,替换到15的位置,结果如图:

 

  其二是删除15这个元素,然后在15的左子树中查找最大的那个节点,替换到15的位置,结果如图:

  两种做法都可以满足二叉树的规则。

 

  代码实现:

    public void delete(int data) {
        Node deleteNode = tree; //目标删除节点 首先默认根节点
        boolean is_find = false;//是否找到节点标记
        Node deleteNodeParent = null; //目标删除节点的父节点
        while (null != deleteNode) {
            if(deleteNode.data == data){
                //找到了要删除的节点
                is_find = true;
                break;
            }
            deleteNodeParent = deleteNode;
            if (data > deleteNode.data) {
                deleteNode = deleteNode.right;//如果要删除的数据比当前节点大,则往后边走。
            } else {
                deleteNode = deleteNode.left;//如果要删除的数据比当前节点大,则往后边走。
            }
        }

        //没有该节点
        if (!is_find) {
            return;
        }

        // 要删除的节点有两个子节点
        if (null != deleteNode.left && null != deleteNode.right) {
            // 查找右子树中最小节点
            Node nodeMin = deleteNode.right;//右子树中最小节点
            Node nodeMinParent = deleteNode; // 右子树中最小节点的父节点
            while (null != nodeMin.left) {
                //只往左边找,最小的数据永远都在最左边
                nodeMinParent = nodeMin;
                nodeMin = nodeMin.left;
            }
            deleteNode.data = nodeMin.data; // 将目标删除节点中的数据替换成右子树最小节点的数据,由于是引用传递,所以树(tree)中要删除的节点内容已经替换成了右子树中最小节点内容
            deleteNode = nodeMin; //更新要删除的节点 转移删除目标 现在需要删除右子树中的最小节点 (程序走到这里 此时deleteNode是一定没有子节点的 因为它本身就是一个叶子节点)
            deleteNodeParent = nodeMinParent;//更新删除目标的父节点
        }

        // 删除节点是叶子节点或者仅有一个子节点
        Node childNode = null; // 接管被删除节点下面的子节点

        //如果你有子节点 那么将子树拿出来
        if(null != deleteNode.left){
            childNode = deleteNode.left;
        }else if(null != deleteNode.right){
            childNode = deleteNode.right;
        }

        //如果你要删的是根节点
        if(null == deleteNodeParent){
            tree = childNode;
        }else{
            //处理被删除节点的父节点 将父节点的的指针指向下面的子节点
            if(deleteNodeParent.left == deleteNode){
                deleteNodeParent.left = childNode;//如果是在父节点的左子节点 则把子节点对接给父节点的left
            }else{
                deleteNodeParent.right = childNode;//如果是在父节点的右子节点 则把子节点对接给父节点的right
            }
        }
    }

 

  虽然我们没有真实的去做删除节点这个操作,但是更改了引用之后,它很快就会被垃圾收集器清理掉。

 

懒惰删除

  由于删除工作要沿着整棵树进行两趟搜索,所以它的效率是有提升空间的,因此在二叉查找树的删除方面还有一个方案,叫做懒惰删除。

  懒惰删除的做法是删除一个元素时候,只是将它标记为已删除,并不是真正的把这个节点删掉,它仍然存留在树中,只是属性里多了一个标记。

 

  懒惰删除使删除操作变的简单,但缺点是比较浪费空间,只有确定删除的次数很少的时候几乎才会考虑,并且遍历时却需要对整棵树重新排序,因此个人认为这种删除方式更加麻烦,绝大部分实现二叉树的开发者都不会采用懒惰删除这种方法。

 

二叉树的遍历

深度优先遍历

  深度优先遍历的思想很简单,就是一条线先走到尽头,再回头走另一条线。

  以如下这棵树为例,首先遍历出 10->8->7->5->1

  然后再遍历出 6

 

  然后是 15->14->13

 

  然后是17->16  

 

  最后是19

 

代码:

    public void preTraversal(Node node){
        if(node == null){
            return;
        }
        System.out.println(node.data);
        preTraversal(node.left);
        preTraversal(node.right);
    }

 

广度优先遍历

  广度优先遍历还有另一种称呼叫层序遍历,像洋葱一样一层一层的剥,如下图:

  这种遍历需要借助队列来完成:

    public static void levelTraversal(Node root){
        if(null == root){
            return;
        }
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);//初始化
        while(!queue.isEmpty()){
            Node node = queue.poll();//弹出节点
            System.out.println(node.data);//打印
            if(node.left != null){
                queue.offer(node.left);//左子节点入队
            }
            if(node.right != null){
                queue.offer(node.right);//右子节点入队
            }
        }
    }

 

最后:感谢阅读。

 

posted @ 2019-05-23 11:34  不该相遇在秋天  阅读(...)  评论(...编辑  收藏