二叉树解析

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;
}
posted @ 2021-03-20 22:12  codeFiler  阅读(167)  评论(0编辑  收藏  举报