• 二叉树解析


    04_二叉树

    1、树形结构

    Snipaste_2021-03-19_10-10-14

    2、生活中的树形结构

    Snipaste_2021-03-19_10-10-37

    • 使用树形结构可以大大提高效率
    • 树形结构是算法面试的重点

    3、树的基本概念

    • 节点、根节点、父节点、子节点、兄弟节点

      • 节点:每个元素称为树的节点

        如图中的所有元素都可以称为树的节点

      • 根节点:有一个特定的结点被称为根结点或树根

        如图中树的根节点即为1

      • 父节点:若一个结点含有子结点,则这个结点称为其子结点的[父结点]

        如图,2,3,4,5,6的父节点是1

      • 子节点:一个结点含有的子树的根结点称为该结点的子结点

        如图,1的字节点为2,3,4,5,6

      • 兄弟节点:具有相同父结点的结点互称为兄弟结点

        如图:2,3,4,5,6都有一个相同的父节点1,所以2,3,4,5,6互称为兄弟节点

    • 一棵树可以没有任何节点,称为空树

    • 一棵树可以只有一个节点,也就是只有根节点

      Snipaste_2021-03-19_10-13-17

    • 子树、左子树、右子树

      我们把下面的这个树的一部分结构拿出来讲解:

      image-20210319115400405

      如图:51为5的左子树,52则为右子树

    image-20210319114619995

    • 节点的:子树的个数

      拿上图的1节点来说,它有2,3,4,5,6一共5个子节点,所以1这个节点的度为5

    • 树的:所有节点度中的最大值

      树的度就是所有节点度中的最大值,上图所示中,节点最多的为根节点,所以,这颗树的度即为5

    • 叶子节点:度为0的节点

      如上图中的:21,31,51,52,61,221,222,223

    • 非叶子节点:度不为0的节点

    • 层数(level):根节点在第1层,根节点的子节点在第2层,以此类推

    • 节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数

      如上图所示:根节点1的深度即为1->2->22->221|222|223,

      所以节点1的深度就为4

    • 节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数

      如上图所示:根节点1的高度即为最远路径到它的路径上的节点总数,所以节点1的高度即为4

    • 树的深度:所有节点深度中的最大值

    • 树的高度:所有节点高度中的最大值

    • 树的深度等于树的高度

    4、有序树、无序树、森林

    • 有序数
      • 树种任意节点的子节点之间有顺序
    • 无序树
      • 树种任意节点的子节点之间没有顺序关系
      • 也称为“自由树”
    • 森林
      • 由m(m>=n)颗互不相交的树组成的集合

    5、二叉树(Binary Tree)

    • 二叉树的特点
      • 每个结点的度最大为2(最多拥有2颗子树)
      • 左子树和右子树是有顺序的
      • 即使某节点只有一颗子树,也要区分左右子树
      • 二叉树是有序树

    如下图所示都为二叉树:

    Snipaste_2021-03-19_10-22-54

    Snipaste_2021-03-19_10-22-54

    5.1、二叉树的性质

    • 非空二叉树的第i层,最多有2^(i-1)个节点(i>=1)

    • 在高度为h的二叉树上最多有2^h-1个节点(h>=1)

    • 对于任何一颗非空二叉树,如果叶子节点个数为n0,度为2的节点个数为n2,则有:n0=n2+1

      • 假设度为1的节点个数为n1,那么二叉树的节点总数n=n0+n1+n2
      • 二叉树的边数T=n1+2*n2=n-1=n0+n1+n2-1
      • 因此n0=n2+1

      Snipaste_2021-03-19_10-22-54

    5.2、真二叉树(Proper Binary Tree)

    真二叉树:所有节点的度都要么为0,要么为2

    如图所示即为真二叉树:

    Snipaste_2021-03-19_10-30-01

    如下图所示不是真二叉树:

    Snipaste_2021-03-19_10-30-22

    5.3、满二叉树(Full Binary Tree)

    • 满二叉树:最后一层节点的度都为0,其他节点的度都为2
    • 在同样高度的二叉树中,满二叉树的叶子节点数量最多,总节点数量最多
    • 满二叉树一定是真二叉树,真二叉树不一定是满二叉树

    image-20210319122617370

    5.4、完全二叉树(Complete Binary Tree)

    • 完全二叉树:对节点从上至下,从左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
    • 叶子节点只会出现最后2层,最后1层的叶子节点都靠左对齐
    • 完全二叉树从根节点至倒数第二层是一棵满二叉树
    • 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树

    Snipaste_2021-03-19_10-36-48

    5.4.1、完全二叉树的性质

    • 度为1的节点只有左子树
    • 度为1的节点要么是1个,要么是0个
    • 同样节点数量的二叉树,完全二叉树的高度最小

    image-20210319123105593

    解析:节点最少的情况,就是最底下的一层只有一个节点,最多节点的对应情况其实就是满二叉树

    • 一棵有n个节点的完全二叉树(n>0),从上到下,从左到右对节点从1开始进行编号,对任意第i个节点

      • 如果i = 1,它是根节点

      • 如果i > 1,它的父节点编号为floor(i / 2)

      • 如果2i <= n,它的左子节点编号为2i

      • 如果2i > n,它无左子节点

      • 如果2i + 1 <= n,它的右子节点编号为2i + 1

      • 如果2i + 1 > n,它无右子节点

        image-20210319125334499

    • 一棵有n个节点的完全二叉树(n>0),从上到下,从左到右对节点从0开始进行编号,对任意第i个节点

      • 如果i = 0,它是根节点
      • 如果i > 0,它的父节点编号为floor( (i - 1) / 2 )
      • 如果2i + 1 <= n - 1,它的左子节点编号为2i + 1
      • 如果2i + 1 > n - 1,它无左子节点
      • 如果2i + 2 <= n - 1,它的右子节点编号为2i + 2
      • 如果2i + 2 > n - 1,它无右子节点

    Snipaste_2021-03-19_10-51-14

    image-20210319125843597

    5.5、二叉树的遍历

    • 遍历是数据结构中的常见操作
      • 把所有元素都访问一遍
    • 线性数据结构的遍历比较简单
      • 正序遍历
      • 逆序遍历
    • 根据节点访问顺序的不同,二叉树的常见遍历有四种
      • 前序遍历(Preorder Traversal)
      • 中序遍历(Inorder Traversal)
      • 后序遍历(Postorder Traversal)
      • 层序遍历(Level Order Traversal)

    5.5.1、前序遍历(Preorder Traversal)

    • 访问顺序

      • 根节点、前序遍历子树、前序遍历子树

      • 如下图:前序遍历的结果是7,4,2,1,3,5,9,2,11,10,12

        先遍历左子树上的节点,然后再遍历根节点,然后再遍历右子树上的节点

    image-20210320210152480

    我们用递归的方法可以解决前序遍历的问题,具体代码如下:

    /**
    * 前序遍历
    *//
    public void preorderTraversal(){
        preorderTraversal(root);
    }
    
    //**
    * 前序遍历
    *//
    private void preorderTraversal(Node<E> node){
        if(node == null) {
            return;
        }
    
        System.out.println(node.element);
        preorderTraversal(node.left);
        preorderTraversal(node.right);
    }
    

    5.5.2、中序遍历(Inorder Traversal)

    • 访问顺序
      • 中序遍历子树、根节点、中序遍历子树
      • 1,2,3,4,5,7,8,9,10,11,12
    • 如果访问顺序是下面这样呢?
      • 中序遍历子树、根节点、中序遍历子树
      • 12,11,10,9,8,7,5,4,3,2,1

    image-20210320211201617

    值得注意的是,二叉搜索树的中序遍历结果是升序或者是降序的

    实现代码如下:

    /**
    * 中序遍历
    *//
    public void inorderTraversal(){
        inorderTraversal(root);
    }
    
    //**
    * 中序遍历
    *//
    private void inorderTraversal(Node<E> node){
        if(node == null) {
            return;
        }
    
        inorderTraversal(node.left);
        System.out.println(node.element);
        inorderTraversal(node.right);
    }
    

    5.5.3、后续遍历

    • 访问顺序
      • 后序遍历子树、后序遍历子树、根节点
      • 1,3,2,5,4,8,10,12,11,9,7

    image-20210320211644803

    代码实现如下:

    /**
    * 后序遍历
    *//
    public void postorderTraversal(){
        postorderTraversal(root);
    }
    
    //**
    * 后序遍历
    *//
    private void postorderTraversal(Node<E> node){
        if(node == null) {
            return;
        }
    
        postorderTraversal(node.left);
        postorderTraversal(node.right);
        System.out.println(node.element);
    }
    

    5.5.4、层序遍历(Level Order Traversal)

    • 访问顺序
      • 从上到下、从左到右依次访问每一个节点
      • 7,4,9,2,5,8,11,1,3,10,12
    • 实现思路:使用队列
    • 1.将根节点入队
    • 2.循环执行以下操作,直到队列为空
      • 将A的左子节点入队
      • 将A的右子节点入队

    Snipaste_2021-03-19_21-45-38

    1. 如上图所示,我们要用层序遍历实现对这个二叉树的访问,我们实现要使用队列Queue来将这棵树的根节点放入队列中,当我们把这个root节点放入队列中时,此时我们的队列就有了一个元素,也就是我们上图中的根节点7
    2. 遍历完根节点之后,我们就将根节点poll出去,也即将根节点弹出,然后我们就将根节点的左右子树分别放入队列中,现在我们的队列中就有了俩个元素,分别是根节点7的左子节点4和右子节点9
    3. 然后我们就对队列进行遍历,当我们遍历完节点4之后,我们就将节点4的左右子节点放入队列中
    4. 然后我们继续遍历根节点7的右子节点,当根节点7的右子节点遍历完之后,我们就将根节点7的右子节点弹出队列,然后将9这个节点的左右子节点分别放入队列中
    5. 以此类推,知道所有的节点都被遍历即可
    //**
    * 层序遍历
    *//
    public void levelOrderTraversal(){
        if(root == null){
            return;
        }
    
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
    
        while (!queue.isEmpty()){
            Node<E> node = queue.poll();
            System.out.println(node.element);
    
            if(node.left != null){
                queue.offer(node.left);
            }
    
            if(node.right != null){
                queue.offer(node.right);
            }
        }
    }
    

    5.6、二叉树的高度

    遍历二叉树的高度,我们有俩种方法进行实现

    第一种方法:递归

    /**
    * 二叉树的高度,递归方式
    * @return
    */
    public int height(){
        return height(root);
    }
    
    /**
    * 二叉树的高度,递归方式
    * @return
    */
    private int height(Node<E> node){
        if(node == null){
            return 0;
        }
    
        return Math.max(height(node.left),height(node.right)) + 1;
    }
    

    第二种方法:迭代

    下面给的代码就是求二叉树高度的迭代方法:

    其实这个方法的中心思想就是我们要求这个二叉树的高度的话

    其实说到底就是对这个二叉树进行层序遍历的一个过程

    定义一个height来记录二叉树的高度

    同时用一个levelSize来记录每一层二叉树节点的个数,每遍历一个节点就让这个levelSize--,直到levelSize减为0为止,也就说明这个二叉树当前层已经被遍历完,hight++,同时重新维护这个levelSize,让它等于下一层的节点的数量,也就是queue.size()

    /**
     * 二叉树的高度,迭代方法
     * @return 二叉树的高度
    */
    public int high(){
        if(root == null){
            return 0;
        }
    
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
    
        // 记录二叉树的高度
        int height = 0;
        // 记录二叉树每一层的节点个数
        int levelSize = 1;
    
        while (!queue.isEmpty()){
            Node<E> node = queue.poll();
            levelSize--;
            if(node.left != null){
                queue.offer(node.left);
            }
    
            if(node.right != null){
                queue.offer(node.right);
            }
    
            if(levelSize == 0){
                levelSize = queue.size();
                height++;
            }
        }
    
        return height;
    }
    

    5.7、判断一棵树是不是完全二叉树

    如下图所示:

    ​ 我们要判断一颗二叉树,首先我们得知道二叉树的定义,如下图,这棵树就是一个完全二叉树。

    ​ 我们需要保证的是,看下图所示,假如E这个节点的左子树为空,而它的右子树不为空,那么这个树就不是一个完全二叉树

    如果我们要保证它是一个完全二叉树,那么我们就得保证E这个节点要么左右子树为空,要么左子树有,没有右子节点,同时我们必须得保证F,G节点都是叶子节点即可

    Snipaste_2021-03-20_10-34-31

    代码如下:

    /**
     * 判断是否为完全二叉树
     * @return
    */
    public boolean isComplete(){
        if(root == null){
            return false;
        }
    
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
    
        boolean leaf = false;
    
        while (!queue.isEmpty()){
            Node<E> node = queue.poll();
    
            if(leaf && !node.isLeaf()){
                return false;
            }
    
            if(node.left != null){
                queue.offer(node.left);
            }else if(node.right != null){
                return false;
            }
    
            if(node.right != null){
                queue.offer(node.right);
            }else {
                leaf = true;
            }
        }
    
        return true;
    }
    

    5.8、二叉树的前驱节点(Predecessor)

    • 前驱节点:中序遍历时的前一个节点
      • 如果是二叉搜索树,前驱结点就是前一个比它小的节点
    • node.left != null
      • 举例:6,13,8
      • Predecessor = node.left.right.right....
      • 终止条件:right为null
    • node.left == null && node.parent != null
      • 举例:7,11,9,1
      • Predecessor = node.parent.parentparent....
      • 终止条件:node在parent的右子树中
    • node.left == null && node.parent == null
      • 那就没有前驱节点
      • 举例:没有左子树的根节点

    Snipaste_2021-03-20_12-45-52

    如上图所示,我们进行分析:

    ​ 我们要找一个节点的前驱节点,就需要先对左子树的查找有一定的了解,比如我们所知道的根节点是8的前驱节点是7,为什么是7,其实就是因为,7是最靠近8的前一个节点。

    ​ 首先我们需要看第一种情况,假如左子树不为空,也就是node.left != null这种情况,我们以根节点8为例根节点8的前驱结点是7我们要想找到根节点8的前驱结点,其实就是要从左子树上去找,因为前驱结点就是要小于根节点,所以我们的前驱节点就是根节点8的左子树上找,然后我们在找左子树上的最大值,这个最大值就是根节点8的前驱结点,也就是我们要首先求Node node = root.left,然后我们在从这个node节点的右节点开始遍历,每次遍历后都执行node = node.right,知道跳出循环即可

    ​ 然后我们再看第二种情况,如果左子树是空,但是左子树的父节点不是空的话,也就是node.left == null && node.parent != null,如上图,假如我们要求节点7的前驱结点,我们很明显的就能看到节点7的前驱结点就是节点6,也就是它的父节点,其实这种情况就是我们不断的找这个节点的父节点,知道node在parent的右子树中即可

    ​ 最后一种情况是,假如左子树为空,而且也没有父亲节点,那么就说明这个树没有前驱节点

    代码如下:

    /**
    * 寻找前驱结点
    * @param node
    * @return 前驱结点
    */
    private Node<E> predecessor(Node<E> node){
        if(node == null){
            return null;
        }
    
        Node<E> pre = node.left;
        if(pre != null){
            while (pre.right != null){
                pre = pre.right;
            }
    
            return pre;
        }
    
        while (node.parent != null && node == node.parent.left){
            node = node.parent;
        }
    
        return node.parent;
    }
    

    5.9、二叉树的后继节点(successor)

    后继节点的考虑情况同样很简单,我就不一一赘述了

    • 后继节点:中序遍历时的后一个节点
      • 如果是二叉搜索树,后继节点就是后一个比它大的节点
    • node.right != null
      • 举例:1,8,4
      • successor = node.right.left.left....
      • 终止条件:left 为null
    • node.right == null && node.parent != null
      • 举例:7,6,3,11
      • successor = node.parent .parent .parent ....
      • 终止条件:node在parent的左子树中
    • node.right == null && node.parent == null
      • 那就没有后继节点
      • 举例:没有右子树的根节点

    Snipaste_2021-03-20_13-35-16

    代码如下:

    /**
    * 寻找前驱结点
    * @param node
    * @return 前驱结点
    */
    private Node<E> successor(Node<E> node){
        if(node == null){
            return null;
        }
    
        Node<E> suc = node.right;
        if(suc != null){
            while (suc.left != null){
                suc = suc.right;
            }
    
            return suc;
        }
    
        while (node.parent != null && node == node.parent.right){
            node = node.parent;
        }
    
        return node.parent;
    }
    
  • 相关阅读:
    内联函数和宏
    C++内联函数与宏定义
    C++函数声明和定义深度解析
    C++中的头文件和源文件
    国外程序员整理的 C++ 资源大全
    c语言中的字符数组与字符串
    iOS应用架构谈(二):View层的组织和调用方案(中)
    iOS应用架构谈(一):架构设计的方法论
    解决xib约束冲突
    tableView设置首尾
  • 原文地址:https://www.cnblogs.com/coderD/p/14561843.html
Copyright © 2020-2023  润新知