Skip to content

Latest commit

 

History

History

BinaryTreeNote

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
  • 树形结构

在前面章节中介绍到的数据结构,都为线性结构,比如链表,数组,队列等,都属于线性结构,类似于通过一根线串在一起

1569326231986

树形结构示意图

二叉树

多叉树

现实生活中的树,如果将现实生活中的树倒过来,就类似于树形结构

1569326332050

  • 生活中经常用到的树形结构

1.公司的组织架构

1569326550287

2.平时的文件夹组织

1569326603326

总结:1.使用树形结构可以大大提高效率

​ 2.树形结构是算法面试的重点

  • 树(Tree)的基本概念

  • 节点,根节点,父节点,子节点,兄弟节点
  • 一棵树可以没有任何节点,称为空树
  • 一棵树可以只有一个节点,也就是只有根节点
  • 子树,左子树,右子树
  • 节点的(degree):子树的个数
  • 树的:所有节点度中的最大值
  • 叶子节点(leaf):度为0的节点
  • 非叶子节点:度不为0的节点
  • 层数(level):根节点在第一层,根节点的子节点在第二层,以此类推(有些书籍也从第0层开始计算)
  • 节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数
  • 节点的高度(height):当从前节点到最远叶子节点的路径上的节点总数
  • 树的深度:所有节点深度中的最大值
  • 树的高度:所有节点高度中的最大值(一般来讲,树的深度与树的高度相等)

多叉树

  • 有序树
  • 树中任意节点的子节点之间有顺序关系(如上面树中,节点2,3,4,5,6是完全按照顺序排列的,如果调换位置,就变成了另外一棵树)
  • 无序树
  • 树种的任意节点的子节点之间没有顺序关系,也称为自由树
  • 森林
  • 由m(m ≥ 0)棵互不相交的树组成的集合
  • 二叉树(Binary Tree)

1569329918589

二叉树的特点

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

二叉树有以下几种形态

1.空树

1569330125361

2.只有一个节点

1569330155373

3.只有左子树

1569330177474

4.只有右子树

1569330199137

1569330254306

5.右左右子树

1569330220346

🤔二叉树是有序树还是无序树?

  • 二叉树的性质

1569330455760

1569330549453

  • 非空二叉树的第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
  • 真二叉树(Proper Binary Tree)

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

1569331161342

下图不是真二叉树

1569331210101

  • 满二叉树(Full Binary Tree)

满二叉树:所有节点的度都要么为0,要么为2,并且所有的叶子节点都在最后一层

1569331356078

在同样高度的二叉树中,满二叉树的叶子节点数量最多,总节点数量最多

满二叉树一定是真二叉树,真二叉树不一定是满二叉树

假设满二叉树的高度为h(h ≥ 1),那么

第i层的节点数量:2^(i - 1)

叶子节点的数量:2(h - 1)

总节点数量n: n = 2^h - 1 = 2^0 + 2^1 +2^2 + …… + 2^(h - 1)

  • 完全二叉树(Complete Binary Tree)

完全二叉树:叶子节点只会出现在最后2层,并且最后1层的叶子节点都靠左对齐

1569331870996

完全二叉树的记忆方法:所有节点从上往下,从左往右一次排布,就位完全二叉树,如下图

1569332166943

如果所有节点都排满了,就叫做满二叉树

1569332216547

完全二叉树从根节点倒数第二层是一个满二叉树

满二叉树一定是一棵完全二叉树,完全二叉树不一定是满二叉树

  • 完全二叉树的性质

度为1的节点只有左子树

度为1的节点,要么是1个,要么是0个

同样节点数量的二叉树,完全二叉树的高度最小

假设完全二叉树的高度为h(h ≥ 1),那么

至少有2^(h - 1)个节点(2^0 + 2^1 + …… + 2^(h - 2) +1)

最多有2^ - 1个节点(满二叉树)

如果此时,总节点数量为n,那么有结论 2^(h -1) ≤ n < 2^h

对 2^(h -1) ≤ n < 2^h取对数,则有h - 1 ≤ log2n < h,可以得出,h与n之间的关系为 h = log2n (向下取整) +1,即 h = floor(log2n) +1

一棵有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,它没有右子节点

一棵有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 + 1> n,它没有右子节点

1569331870996

  • 下面的二叉树不是完全二叉树

1569417448928

  • 面试题

如果一棵完全二叉树有768个节点,求叶子节点的个数

解题思路:

  1. 假设叶子节点个数为n0,度为1的节点个数为n1,度为2的节点个数为n2
  2. 总结点个数 n = n0 +n1 + n2 ,而且由前面的公式知道,n0 = n2 + 1

因此有 n = 2n0 + n1 - 1

由于题目告知二叉树为完全二叉树,那么我们知道度为1的节点数量,为1或者为0,所以

  • 当n1为1时,n = 2n0,n必然是偶数,所以叶子节点是数量n0 = n / 2,非叶子节点个数 n1 +n2 = n / 2
  • 当n1位0时,n = 2n0 - 1,n必然位奇数,所以叶子节点的数量n0 = (n +1) / 2,非叶子节点的数量 n1 + n2 = (n - 1) / 2

所以最终,叶子节点的数量n0 = floor((n +1) / 2) = ceilling(n / 2)

非叶子节点的数量n1 + n2 = floor(n / 2) = ceilling((n - 1) / 2)

因此最终结果为384

  • 国外资料的说法

Full Binary Tree: 完满二叉树

  • 所有非叶子节点的度都为2
  • 也就是我们所说的"真二叉树“

1569419190351

Perfect Binary Tree:完美二叉树

  • 所有非叶子节点的度都为2,且所有的叶子节点都在最后一层
  • 也就是我们所说的"满二叉树“

1569419344147

Complete Binary Tree:完全二叉树

  • 和我们说的一样

1569419403055


以下内容,建议阅读二叉搜索树部分后再来继续阅读

  • 二叉树的遍历

遍历是数据结构中的常见操作

  • 把所有元素都遍历一遍

在前面我们介绍的线性数据结构,它的遍历方法比较简单,一般是

  • 正序遍历
  • 逆序遍历

二叉树遍历中,根据节点访问的顺序不同,常见的遍历方式有4种,分别为

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后序遍历(Postorder Traversal)
  • 层序遍历(Level Order Traversal)
  • 前序遍历(Preorder Traversal)

例如现有如下的二叉树

1569636777670

访问顺序

  1. 优先访问根节点,然后再前序遍历左子树,前序遍历右子树[访问顺序:下图]

1569637244824

1569637367512

  • 前序遍历的代码实现

结合前面二叉搜索树的代码,我们可以在里面新增一个preorderTraversal(Node<E> node)的方法

通过递归的方式实现代码

public void  preorderTraversal(Node<E> node) {
    if (node == null) {
        return;
    }
   	System.out.println(node.element);
    preorderTraversal(node.left);
    preorderTraversal(node.right);
}

另外,读者可以尝试使用其他的方式进行实现。如迭代

  • 中序遍历(Inorder Traversal)

访问顺序

  1. 先中序遍历左子树,然后访问根节点,最后中序遍历右子树

最终的访问逻辑

1569638362655

由于我们当前的二叉树是一棵二叉搜索树,因此我们可以看到,如果我们使用中序遍历的方法进行遍历的话,最红遍历的结果为1,2,3,4,5,6,7,8,9,10,11,12。它们的顺序是从小到大的顺序进行遍历的

如果我们的访问顺序是:

先中序遍历右子树,然后访问根节点,最后中序遍历左子树

我们不难发现,最终的访问结果为12,11,10,9,8,7,6,5,4,3,2,1。它们的顺序是中大到小的顺序进行遍历的

因此:二叉搜索树的中序遍历结果是升序或者降序的

  • 中序遍历的代码实现

我们新增一个inorderTraversal(Node<E> node)的方法

通过递归实现中序遍历的代码

public void inorderTraversal(Node<E> node) {
    if (node == null) return;
    inorderTraversal(node.left);
    System.out.println(node.element);
    inorderTraversal(node.right);
}

同样的,读者可以使用其他方式来实现

  • 后序遍历(Postorder Traversal)

访问顺序

  1. 先后续遍历左子树,然后后续遍历右子树,最后访问根节点

最终的访问逻辑

1569639417209

  • 后续遍历的代码实现

我们新增一个postorderTraversal(Node<E> node)的方法

public void postorderTraversal(Node<E> node) {
    if (node == null) return;
    inorderTraversal(node.left);
    inorderTraversal(node.right);
    System.out.println(node.element);
}
  • 层序遍历(Level Order Traversal)

一层一层的往下进行访问

访问顺序

  1. 从上到下,从左到右依次访问每个节点

也就意味着,我们最终的访问顺序为

1569639885407

实现思路:使用队列

  1. 将根节点入队
  2. 循环执行下面的操作,直到队列为空
  • 将队列头节点A出队,进行访问
  • 将A的左子节点入队
  • 将A的右子节点入队
  • 后续遍历的代码实现

我们新增一个levelorderTraversal(Node<E> node)的方法

层序遍历的实现代码

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);
        }
    }
}

🤔如果允许外界遍历二叉树的元素,你会如何设计接口?

因此我们可以设计一个Visitor接口,定义一个方法,让调用者自己定义元素被访问后,需要做的操作

因此4中遍历方法,改造完成后的代码是这样的

public void preOrder(Visitor<E> visitor) {}
private void preOrder(Node<E> node , Visitor<E> visitor) {
    if (node == null || visitor == null) return;
    visitor.visit(node.element);
    preOrder(node.left,visitor);
    preOrder(node.right,visitor);
}

public void inOrder(Visitor<E> visitor) {}
private void inOrder(Node<E> node , Visitor<E> visitor) {
    if (node == null || visitor == null) return;
    preOrder(node.left,visitor);
    visitor.visit(node.element);
    preOrder(node.right,visitor);
}

public void postOrder(Visitor<E> visitor) {}
private void postOrder(Node<E> node ,Visitor<E> visitor) {
    if (node == null || visitor == null) return;
    preOrder(node.left,visitor);
    visitor.visit(node.element);
    preOrder(node.right,visitor);
}

public void levelOrder(Visitor<E> visitor) {
    if (root == null || visitor == null) return;
    Queue<Node<E>> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        Node<E> node = queue.poll();
        visitor.visit(node.element);
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
    }
}
  • 遍历的应用

前序遍历

树状结构展示(注意左右子树的顺序)

练习-利用前序遍历树状打印二叉树

中序遍历

二叉搜索树的中序遍历按照升序或者降序处理节点

后序遍历

适用于一些先子后父的操作

层序遍历

计算二叉树的高度

判断一棵树是否为完全二叉树

练习1:计算二叉树的高度

建议使用递归和迭代的方式实现。

递归实现方式

public int height(Node<E> node) {
    if (node == null) return 0;
    return 1 + Math.max(height(node.left),height(node.right));
}

使用迭代的方式实现

public int height() {
        if (root == null) return 0;
        int height = 0;//树的高度
        int levelSize = 1;//存储着每一层元素的数量
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        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(root);
    }

练习2:判断一棵树是否为完全二叉树

解题思路

  1. 如果树为空,返回false
  2. 如果树不为空,开始层序遍历二叉树(使用队列)
    • 如果node.left != null && node.right != null ,将node.left, node.right按顺序入队
    • 如果node.left == null && node.right != null,返回false
    • 如果node.left != null && node.right == null 或者node.left == null && node.right == null
      • 那么后序遍历的节点应该都为叶子节点,才是完全二叉树
      • 否则返回false

解题源码

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.hasTwoChildren()) {
            queue.offer(node.left);
            queue.offer(node.right);
        } else if (node.left == null && node.right != null) {
            return false;
        } else {
            //意味着后遍历的节点,都必须是叶子节点
            leaf = true;
        }
    }
    return true;
}

练习3:翻转二叉树 题目来自[Leetcode]

将所有节点的左右子树都交换

由于使用了多种解题方法,因此解题源码在这里

  • 根据遍历结果重构二叉树

即:根据遍历的结果,推导出该结果是哪种二叉树

通过以下结果可以保证重构出唯一的一棵二叉树

  • 前序遍历结果 + 中序遍历结果
  • 后序遍历结果 + 中序遍历结果

为什么根据前序遍历和中序遍历的结果,就能推导出一棵二叉树的结果

假设下图为前序遍历的结果示意图,其中红色为根节点

1569673096661

其中left(左子树)和right(右子树)结果,同样也是采用前序遍历

下图为中序遍历的结果示意图,其中红色为根节点

1569673161571

其中left(左子树)和right(右子树)结果,同样也是采用中序遍历

通过以上两个结果,我们可以知道

  1. 根据前序遍历的结果,我们可以知道结果中的第一个节点为根节点
  2. 找到根节点后,根据中序遍历的结果,可以找到左子树和右子树的边界,根节点左边的为左子树,根节点右边的为右子树,也就知道了哪些元素属于左子树,哪些元素属于右子树
  3. 通过中序遍历的结果,已经知道了哪些元素属于左子树,哪些元素属于右子树,因此,我们在前序遍历中,就知道了哪些元素为左子树,哪些元素属于右子树
  4. 已经知道了哪些节点属于哪个子树,因此前序遍历的左子树中的第一个元素,为左子树的根节点
  5. 通过前序遍历中找到的左子树根节点,可以在中序遍历的左子树中,找到左子树的左子树和右子树,因为中序遍历的特点是通过根节点,将左右区分开来
  6. 通过这种拆分,又可以拆分为一个规模更小的遍历结果,以此类推,谁是谁的左节点,谁是谁的右节点,谁是谁的父节点,都能拆分出来

因此通过后序遍历的结果 + 中序遍历的结果,也是同理

通过这个结论,可以尝试以下练习

通过前序遍历结果 + 中序遍历结果重构二叉树

  • 前序遍历结果:4 2 1 3 6 5
  • 中序遍历结果:1 2 3 4 5 6

利用前面的结论,我们知道

  1. 当前二叉树的根节点为 4 1569674494819

  2. 通过中序遍历的结果知道,该二叉树的左子树为 1 2 3 ,右子树为 5 6

  3. 在通过前序遍历,就可以知道,左子树的根节点为 2,右子树的根节点为 61569674598968

  4. 由于已经知道了2为左子树的根节点,那么通过中序遍历的结果,2位1 3 的父节点,1为2的左子树,3为2的右子树1569674742368

  5. 前面知道6位右子树的根节点,那么通过中序遍历的结果,6为 5的父节点,5在6的左边,因此5为6的左子树1569674866123

🤔根据前序遍历的结果 + 后序遍历的结果,能重构出唯一的一棵二叉树吗?

  • 如果是一个真二叉树,它的结果是唯一的
  • 否则结果不唯一

因为有以下的情况

1569675214321

如果两个的遍历结果如上图所示,并且其中一个子树为空的话,就没法知道该结果为左子树的结果还是右子树的结果

如果二叉树的左右节点都不为空,遍历结果如下图

1569675498262

我们可以通过该结果得到以下信息

  1. 首先我们可以马上确认根节点,前序遍历的结果,最前面的节点为根节点,根节点的下一个节点为左子树的根节点
  2. 通过前序遍历中找到的左子树根节点,可以在后序遍历的左子树结果中找到,并且在后序遍历结果中,左子树根节点在左子树结果中的最后一位
  3. 在后序遍历中,通过左子树根节点的位置,就可以知道左子树和右子树的范围,然后再通过上面的结论,就可以重构出二叉树

1569675928686

  • 前驱节点(predecessor)

前驱节点:中序遍历时的前一个节点

  • 如果是二叉搜索树,前驱节点就是前一个比它小的节点

例如有下列的二叉树

1569676259946

它的中序遍历结果为

1569676281756

节点8的前驱节点为7

因此有以下情况:

情况1

如果左子树不为空 node.left != null

例如上图二叉树中的6 13 8均符合这种情况

前驱节点predecessor = node.left.right.right.right...

终止条件为:right 为null

情况2:

node.left == null && node.parent != null

例如上图二叉树中的7 11 9 1 均符合这种情况

前驱节点predecessor = node.parent.parent.parent....

终止条件为:node的parent在右子树当中

情况3:

node.left == null && node.parent == null

这种情况的假设上图二叉树的左子树不存在,即满足以上条件

在这种情况下,是没有前驱节点的

因此查找前驱节点的实现代码是这样的

private Node<E> predecessor(Node<E> node) {
    if (node == null) return null;
    Node<E> p = node.left;
    if (node.left != null) {
        //从左子树中找前驱
        while (p.right != null) {
            p = p.right;
        }
        return p;
    }
    //从父节点,祖父节点中寻找前驱节点
    while (node.parent != null && node == node.parent.left) {
        node = node.parent;
    }
    //node.parent == null
    //node == node.parent.right
    return node.parent;
}
  • 后继节点(successor)

后继节点:中序遍历是的后一个节点

如果是二叉搜索树,后继节点就是后一个比它大的节点

因此,如果有以下的一棵二叉树

1569678387546

通过中序遍历后的结果为

1569678406286

同样的,也有以下几种情况

情况1:

node.right != null

例如上图二叉树中的1 8 4均符合这种情况

successor = node.right.left.left.left....

终止条件:left == null

情况2:

node.right == null && node.parent != null

例如上图二叉树中的7 6 3 11均符合这种情况

successor = node.parent.parent.parent....

终止条件:node在parent的左子树中

情况3:

node.right = null && node.parent == null

那就没有前驱节点

例如:没有右子树的根节点

因此查找后继节点的代码是这样的

private Node<E> successor(Node<E> node) {
    if (node == null) return null;
    Node<E> p = node.right;
    if (p != null) {
        //从右子树中找前驱
        while (p.left != null) {
            p = p.left;
        }
        return p;
    }
    //从父节点,祖父节点中寻找前驱节点
    while (node.parent != null && node == node.parent.right) {
        node = node.parent;
    }
    //node.parent == null
    //node == node.parent.right
    return node.parent;
}