zoukankan      html  css  js  c++  java
  • 二叉树

    什么是树?

      这就是树:

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

      系统目录结构:

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

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

    节点与树叶

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

      如下面这棵树:

      其中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);//右子节点入队
                }
            }
        }

    最后:感谢阅读。

  • 相关阅读:
    [ASP.NET][实例]用户控件的设计与使用
    构造器[java、C#]
    [转]clob和blob两个字段什么分别?
    C#的反射机制调用方法
    C# WinForm 控件美化之改变ListView Head 的背景色
    C# 创建快捷方式
    Copy Html To Clipboard
    改善C#程序的建议在线程同步中使用信号量
    Paste html from Clipboard
    Winform部署mshtml程序集出错的一个解决方案
  • 原文地址:https://www.cnblogs.com/fengyumeng/p/10910933.html
Copyright © 2011-2022 走看看