第二章 - 第3节 - 二叉树 - 课件

1. 二叉树的定义与基本性质

1.1 二叉树的定义

二叉树是一种特殊的树形结构,它的每个节点最多只能有两个子节点,通常称为左子节点和右子节点。
1.以下是一个二叉树的示例:
在这里插入图片描述

二叉树有以下几个重要的性质:

这是一个二叉树图:

        A
       / \
      B   C
  1. 左子树和右子树是有顺序的,不能随意交换。
        A
       / \
      B   C
  1. 即使某个节点只有一个子节点,也要区分它是左子节点还是右子节点。
        A
       /
      B
  1. 二叉树可以为空,即没有任何节点。

  2. 二叉树的子树也是二叉树。

        A
       / \
      B   C
     / \
    D   E

在这个例子中,以节点B为根的子树也是一棵二叉树。

二叉树的这些性质使得它成为一种非常重要和有用的数据结构。许多重要的数据结构,如二叉搜索树,平衡二叉树,堆,哈夫曼树等,都是二叉树的变种或者特例。

二叉树的递归定义如下:

  1. 空树是二叉树。
  2. 如果T是一棵二叉树,那么它的左子树和右子树也是二叉树。

这个递归定义反映了二叉树的层次结构和子树的性质。它是理解和操作二叉树的基础。

总之,二叉树是一种简单而又强大的数据结构。它的定义清晰,性质丰富,在计算机科学和软件工程中有广泛的应用。学习和掌握二叉树,是学习其他高级树形结构的基础。

1.2 二叉树的特点

二叉树有以下几个重要的特点:

  1. 层次结构明显。
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/4e08820805f04ef990b91fa2d4a6acc4.png
    从图中可以清晰地看出二叉树的层次结构,A是根节点(第1层),B和C是第2层,D、E、F、G是第3层。

  2. 每个节点最多有两个子节点。

        A
       / \
      B   C

节点A有两个子节点B和C,这是二叉树允许的最大子节点数量。

  1. 左右子树是有序的。
        A
       / \
      B   C

二叉树的左右子树是有区别的,不能随意交换。

  1. 即使某个节点只有一个子节点,也要区分左右。
        A
       /
      B

在这个例子中,节点B可以是A的左子节点,也可以是右子节点,这是不同的两棵二叉树。

  1. 二叉树可以为空。
    一棵空的二叉树没有任何节点。

  2. 二叉树的每个节点都可以看作一棵二叉树的根节点。

        A
       / \
      B   C
     / \
    D   E

在这个例子中,节点B及其子节点D、E可以看作一棵以B为根的二叉树。

  1. 二叉树的高度最小为0(空树),最大为节点数(退化为链表)。
        A
       /
      B
       \
        C
         \
          D

这个例子中的二叉树退化为一条链表,高度为4(节点数)。

  1. 对于任意一棵二叉树,如果其叶子节点数为 n 0 n_0 n0,度为2的节点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1
    这个特点反映了二叉树的基本结构,可以用数学归纳法证明。

这些特点反映了二叉树的基本性质,是理解和应用二叉树的基础。在实际问题中,我们经常利用这些特点来设计算法,分析复杂度,优化性能。例如,二叉树的层次结构和高度特性,经常用于设计高效的搜索和插入删除操作。

总之,二叉树的这些特点构成了二叉树的基本理论,是学习和应用二叉树的重要基础。深入理解这些特点,有助于我们更好地掌握二叉树这一数据结构,并将其应用到实际问题中。

1.3 二叉树的分类

二叉树可以根据其形态和特性分为几类,常见的有满二叉树、完全二叉树、平衡二叉树等。下面我们详细介绍这些分类。

1.3.1 满二叉树

满二叉树(Full Binary Tree)是一种特殊的二叉树,它的每一层都是满的,即每一层的节点数都达到了最大值。
满二叉树有以下特点:

  1. 除了叶子节点,每个节点都有两个子节点。
        A
       / \
      B   C
     / \ / \
    D  E F  G

在这个例子中,除了叶子节点D、E、F、G,其他节点A、B、C都有两个子节点。

  1. 所有叶子节点都在同一层。
        A
       / \
      B   C
     / \ / \
    D  E F  G

在这个例子中,所有叶子节点D、E、F、G都在第3层。

  1. 如果满二叉树的高度为h,那么它的节点总数为 2 h − 1 2^h-1 2h1,叶子节点数为 2 h − 1 2^{h-1} 2h1
        A
       / \
      B   C

这个满二叉树的高度为2,节点总数为 2 2 − 1 = 3 2^2-1=3 221=3,叶子节点数为 2 2 − 1 = 2 2^{2-1}=2 221=2

满二叉树的这些特点反映了它的结构特征,即每一层都是满的,没有任何"空位"。这种结构使得满二叉树具有一些特殊的性质,例如:

  • 满二叉树的节点编号可以很容易地计算出来。如果根节点的编号为1,那么对于编号为i的节点,它的左子节点编号为2i,右子节点编号为2i+1。
  • 满二叉树的节点数和高度有明确的数学关系,可以相互计算。

在实际应用中,满二叉树并不常见,因为它对树的形状有非常严格的要求。但是,满二叉树的概念和性质在理论分析中非常重要,许多算法和定理都是基于满二叉树来讨论的。

此外,满二叉树也是完全二叉树的一种特殊情况。一棵满二叉树一定是完全二叉树,但反过来不成立。

总之,满二叉树是二叉树的一种特殊形态,它的每一层都是满的。虽然在实际应用中不太常见,但它的概念和性质在二叉树的理论研究中非常重要。理解满二叉树,可以帮助我们更好地理解二叉树的结构特征和理论基础。

1.3.2 完全二叉树

完全二叉树(Complete Binary Tree)是另一种特殊的二叉树。它的定义是:在一棵二叉树中,除了最后一层外,所有层都是满的,并且最后一层的节点都集中在左边。

完全二叉树有以下特点:

  1. 任何一个节点,如果它有右子节点,它一定有左子节点。
         A
       /   \
      B     C
     / \    /
    D   E  F

在这个例子中,节点C有右子节点F,因此它一定有左子节点(虽然图中没有画出)。

  1. 任何一个节点,如果它是最后一层的节点,那么它右边的节点都是叶子节点。
        A
       / \
      B   C
     / \   \
    D   E   F

在这个例子中,节点C是最后一层的节点,它右边的节点E是叶子节点。

  1. 如果我们给完全二叉树的节点按照层序遍历的顺序编号(从1开始),那么对于任意一个节点i,有:
  • 如果i=1,那么节点i是根节点;
  • 如果i>1,则节点i的父节点编号为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor i/2;
  • 如果2i<=节点总数,则节点i的左子节点编号为2i;
  • 如果2i+1<=节点总数,则节点i的右子节点编号为2i+1。

完全二叉树的这些特点反映了它的结构特征,即除了最后一层,其他层都是满的,并且最后一层的节点都集中在左边。这种结构使得完全二叉树在某些应用中具有特殊的优势,例如:

  • 完全二叉树可以很容易地用一个数组来表示,节点的父子关系可以通过下标计算,不需要额外的空间。这在堆等数据结构中广泛使用。
  • 对于n个节点的完全二叉树,其高度为 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 log2n+1。这个性质在一些算法的复杂度分析中非常有用。

在实际应用中,完全二叉树比满二叉树更常见。许多重要的数据结构,如堆、van Emde Boas树等,都是基于完全二叉树的。

总之,完全二叉树是二叉树的一种重要形态。它在最后一层以外的层都是满的,最后一层的节点都集中在左边。这种结构赋予了完全二叉树一些特殊的性质和优势,使其在许多领域都有重要的应用。理解完全二叉树,是学习和运用许多高级数据结构的基础。

1.3.3 二叉搜索树

二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,它的定义是:对于树中的任意一个节点,其左子树中所有节点的值都小于这个节点的值,其右子树中所有节点的值都大于这个节点的值。

下面是一个二叉搜索树的例子:

        8
       / \
      3   10
     / \    \
    1   6    14
       / \
      4   7

在这个例子中,根节点是8,它的左子树的所有节点(3, 1, 6, 4, 7)的值都小于8,右子树的所有节点(10, 14)的值都大于8。这个性质对于每一个子树也成立。

二叉搜索树有以下特点:

  1. 二叉搜索树的中序遍历结果是一个升序序列。
    对于上面的例子,其中序遍历结果为:1, 3, 4, 6, 7, 8, 10, 14。

  2. 在二叉搜索树中,最小的元素一定在最左下角,最大的元素一定在最右下角。
    在上面的例子中,最小的元素是1,最大的元素是14。

  3. 对于二叉搜索树中的任意一个节点,其左子树中所有节点的值都小于它,其右子树中所有节点的值都大于它。
    在上面的例子中,以6为根的子树,其左子树(4)的值小于6,右子树(7)的值大于6。

  4. 二叉搜索树可以很快地查找、插入和删除节点。在平均情况下,这些操作的时间复杂度都是O(log n)。

二叉搜索树的这些特点使其成为一种非常重要和有用的数据结构。它提供了快速的查找、插入和删除操作,在许多场景下都有广泛的应用,例如:

  • 在编译器中,二叉搜索树可以用来存储标识符和它们的属性。
  • 在操作系统中,二叉搜索树可以用来管理内存分配。
  • 在计算几何中,二叉搜索树可以用来存储点的坐标,加速区域查询。

当然,二叉搜索树也有其局限性。如果插入的数据是有序的(例如,按升序或降序插入),那么二叉搜索树会退化为链表,其操作的时间复杂度会降为O(n)。为了避免这种情况,就出现了平衡二叉搜索树(如AVL树,红黑树等)。

总之,二叉搜索树是一种非常重要的二叉树。它的特点是,对于每个节点,其左子树中的所有节点的值都小于它,右子树中的所有节点的值都大于它。这个性质使得二叉搜索树能够提供快速的查找、插入和删除操作,在许多领域都有广泛的应用。理解和掌握二叉搜索树,是学习更高级的数据结构(如AVL树,红黑树,B树等)的基础。

1.3.4 平衡二叉树

平衡二叉树(Balanced Binary Tree)是一种特殊的二叉搜索树。它的定义是:对于树中的任意一个节点,其左子树和右子树的高度差不超过1。

下面是一个平衡二叉树的例子:

        8
       / \
      3   10
     / \   / \
    1   6 14  13
       / \
      4   7

在这个例子中,每个节点的左子树和右子树的高度差不超过1。例如,根节点8的左子树高度为3(节点3-节点6-节点4),右子树高度也为2(节点10-到节点14或节点13),高度差为1。
平衡二叉树有以下特点:

  1. 平衡二叉树的左子树和右子树也都是平衡二叉树。
    在上面的例子中,以3为根的子树和以10为根的子树都是平衡二叉树。

  2. 平衡二叉树的查找、插入和删除操作的时间复杂度都是O(log n)。这是因为平衡二叉树的高度是log n量级的。
    在上面的例子中,树的高度为3,查找任何一个节点都不需要超过3次比较。

  3. 一棵完全二叉树一定是平衡二叉树,但反之不成立。
    下面是一个平衡二叉树,但不是完全二叉树的例子:

        8
       / \
      3   10
     / \
    1   6
  1. 为了维持平衡,在插入或删除节点后,可能需要对树进行旋转操作。
    下面是一个插入节点后进行右旋操作的例子:
        8
       / \
      3   10
     / \
    1   6
   /
  0

在这个例子中,插入节点0后,以3为根的子树不再平衡。我们需要进行一次右旋操作:

        3
       / \
      1   8
     /   / \
    0   6   10

平衡二叉树的这些特点使其在实践中非常有用。它结合了二叉搜索树快速查找的优点,和通过平衡保证最坏情况下仍然高效的优点。常见的平衡二叉树有AVL树,红黑树等。这些树在实际应用中广泛使用,如在操作系统的进程调度,编译器的符号表管理,数据库的索引结构等。

总之,平衡二叉树是一种重要的二叉搜索树。它通过保证左右子树的高度差不超过1,来保证树的平衡,从而使得查找、插入和删除操作的最坏时间复杂度仍然是O(log n)。理解平衡二叉树的概念和操作,是学习AVL树,红黑树等高级数据结构的基础。在实际应用中,平衡二叉树也有广泛的用途,是每个程序员必须掌握的重要数据结构之一。


1.3.5 平衡二叉树示例

平衡二叉树(Balanced Binary Tree)是一种特殊的二叉搜索树。它的定义是:对于树中的任意一个节点,其左子树和右子树的高度差不超过1。

下面是一个正确的平衡二叉树:

         8
       /   \
      3     10
     / \    / \
    1   6  9  14
   / \ / \    / \
  0  2 4  7  13  15

让我们检查一下这个树是否满足平衡二叉树的条件:

  1. 根节点8:

    • 左子树高度: 3 (节点0到6)
    • 右子树高度: 3 (节点9到15)
    • 高度差: 0 ≤ 1
  2. 节点3:

    • 左子树高度: 2 (节点1到2)
    • 右子树高度: 2 (节点6到7)
    • 高度差: 0 ≤ 1
  3. 节点10:

    • 左子树高度: 1 (节点9)
    • 右子树高度: 2 (节点13到15)
    • 高度差: 1 ≤ 1
  4. 其他节点(1, 6, 14)都满足平衡条件。

因此,这是一棵正确的平衡二叉树。它的每个节点的左右子树高度差都不超过1,并且它的左子树和右子树也都是平衡二叉树。

这个例子也展示了平衡二叉树的另一个特点:它不需要是一棵完全二叉树。虽然这棵树的某些节点的子树不完全(如节点1缺少右子节点,节点14缺少中间子节点),但它仍然是平衡的。

平衡二叉树的这些特点使其在实践中非常有用。它结合了二叉搜索树快速查找的优点,和通过平衡保证最坏情况下仍然高效的优点。常见的平衡二叉树有AVL树,红黑树等。这些树在实际应用中广泛使用,如在操作系统的进程调度,编译器的符号表管理,数据库的索引结构等。

总之,平衡二叉树是一种重要的二叉搜索树。它通过保证左右子树的高度差不超过1,来保证树的平衡,从而使得查找、插入和删除操作的最坏时间复杂度仍然是O(log n)。理解平衡二叉树的概念和操作,是学习AVL树,红黑树等高级数据结构的基础。在实际应用中,平衡二叉树也有广泛的用途,是每个程序员必须掌握的重要数据结构之一。


1.4 二叉树的性质

二叉树有许多有趣而重要的性质。这些性质揭示了二叉树的内在规律,是我们理解和应用二叉树的基础。下面我们来详细探讨其中一些主要的性质。


1.4.1 性质1:二叉树第i层上的节点数最多为2(i-1)

这个性质描述了二叉树每一层节点数的上限。我们可以通过数学归纳法来证明这个性质:

  1. 基础情况:当i=1时,也就是根节点所在的第一层,最多有2(1-1)=1个节点。这显然是成立的。

  2. 归纳假设:假设第i层最多有2(i-1)个节点,我们要证明第i+1层最多有2i个节点。

  3. 归纳步骤:在二叉树中,第i+1层的节点数取决于第i层的节点数。由于每个节点最多有两个子节点,所以第i+1层的节点数最多是第i层节点数的2倍。根据归纳假设,第i层最多有2(i-1)个节点,所以第i+1层最多有2×2(i-1)=2i个节点。

  4. 结论:由数学归纳法可知,对于任意的i≥1,二叉树第i层上的节点数最多为2(i-1)

我们可以用一个具体的例子来验证这个性质:

        A
       / \
      B   C
     / \ / \
    D  E F  G

在这个二叉树中:

  • 第1层(i=1)有1个节点(A),不超过2(1-1)=1。
  • 第2层(i=2)有2个节点(B,C),不超过2(2-1)=2。
  • 第3层(i=3)有4个节点(D,E,F,G),不超过2(3-1)=4。

这个性质在分析二叉树的时间和空间复杂度时非常有用。例如,如果我们知道一棵二叉树的高度(深度)为h,那么我们可以立即得出这棵树最多有2^h-1个节点(可以通过等比数列求和公式得到)。这个上限可以帮助我们估计算法的最坏情况下的复杂度。

当然,这个性质给出的是每层节点数的上限。在实际中,并不是所有二叉树都达到了这个上限(满二叉树除外)。


1.4.2 性质2:深度为k的二叉树最多有2(k)-1个节点

这个性质建立在性质1的基础上,给出了一棵二叉树总节点数的上限。我们可以用数学归纳法来证明这个性质:

  1. 基础情况:当k=1时,也就是只有根节点的二叉树,最多有2(1)-1=1个节点。这显然是成立的。

  2. 归纳假设:假设深度为k的二叉树最多有2(k)-1个节点,我们要证明深度为k+1的二叉树最多有2(k+1)-1个节点。

  3. 归纳步骤:根据性质1,一棵深度为k+1的二叉树,第1层至第k层最多有2(k)-1个节点(归纳假设),第k+1层最多有2k个节点。所以总节点数最多有(2(k)-1)+(2k)=2(k+1)-1个。

  4. 结论:由数学归纳法可知,对于任意的k≥1,深度为k的二叉树最多有2(k)-1个节点。

我们可以用一个具体的例子来验证这个性质:

        A
       / \
      B   C
     / \ / \
    D  E F  G

这个二叉树的深度为3(k=3),总共有7个节点,不超过2(3)-1=7。

这个性质在分析二叉树的时间和空间复杂度时也非常有用。如果我们知道一棵二叉树的节点数为n,那么我们可以立即得出这棵树的深度至少为 log ⁡ 2 ( n + 1 ) \log_2(n+1) log2(n+1)(向上取整)。这个下限可以帮助我们估计某些算法的最好情况下的复杂度。

例如,在二叉搜索树中,查找一个节点的时间复杂度取决于树的深度。如果一棵二叉搜索树有n个节点,那么其深度至少为 log ⁡ 2 ( n + 1 ) \log_2(n+1) log2(n+1),所以查找操作的时间复杂度至少为 O ( log ⁡ n ) O(\log n) O(logn)

同样,这个性质给出的是总节点数的上限。除了满二叉树,大多数二叉树的节点数都不会达到这个上限。

1.4.3 性质3:任意一棵二叉树,如果其叶节点数为N0,度为2的节点数为N2,则N0=N2+1

这个性质揭示了二叉树中叶节点数和度为2的节点数之间的关系。我们可以用数学归纳法来证明这个性质:

  1. 基础情况:

    • 当二叉树只有一个节点(根节点)时,N0=1,N2=0,N0=N2+1成立。
    • 当二叉树为空时,N0=0,N2=0,N0=N2+1也成立。
  2. 归纳假设:假设对于节点数小于等于n的二叉树,都有N0=N2+1。

  3. 归纳步骤:对于一棵节点数为n+1的二叉树,我们分三种情况讨论:

    • 如果新增的节点是一个叶节点,那么N0增加1,N2不变,仍然满足N0=N2+1。
    • 如果新增的节点是一个度为1的节点,那么这个新节点的父节点原来一定是一个叶节点(因为二叉树不存在度为1的节点)。现在这个父节点的度变为1,所以N0和N2都不变,仍然满足N0=N2+1。
    • 如果新增的节点是一个度为2的节点,那么这个新节点的父节点原来一定是一个度为1的节点(因为二叉树不存在度为1的节点)。现在这个父节点的度变为2,所以N0不变,N2增加1,仍然满足N0=N2+1。
  4. 结论:由数学归纳法可知,对于任意的二叉树,都有N0=N2+1。

我们可以用两个具体的例子来验证这个性质:

        A
       / \
      B   C
     / \ / \
    D  E F  G
   / \
  H   I

在这个二叉树中,叶节点(度为0的节点)有5个(E,F,G,H,I),度为2的节点有4个(A,B,C,D),满足N0=N2+1。

         8
       /   \
      3     10
     / \    / \
    1   6  14  13
       / \
      4   7

在这个二叉树中,叶节点(度为0的节点)有5个(1,4,7,14,13),度为2的节点有4个(8,3,6,10),满足N0=N2+1。

这个性质在某些二叉树的操作中非常有用。例如,在二叉树的先序遍历中,我们先访问根节点,然后递归地先序遍历左子树和右子树。如果我们知道叶节点的数量,就可以预测递归的次数。

另一个应用是在Huffman编码中。在Huffman树中,所有的数据都存储在叶节点上,所以叶节点的数量决定了编码的效率。通过这个性质,我们可以估计Huffman树中度为2的节点的数量,从而分析编码的时间和空间复杂度。

总之,这个性质展示了二叉树的不同组成部分(叶节点和度为2的节点)之间的数量关系,提供了一个新的角度来理解二叉树的结构。在一些具体的算法和操作中,这个性质可以提供关键的信息,帮助我们进行复杂度分析和性能优化。

2. 二叉树的表示与存储

2.1 二叉树的顺序存储

在计算机中,二叉树的存储方式主要有顺序存储和链式存储两种。顺序存储通常使用数组来实现,通过节点在数组中的索引来表示节点之间的父子关系。

顺序存储的基本思想:

  • 使用一维数组来存储二叉树的节点,数组的下标表示节点的位置。
  • 对于数组中的第 i 个节点,其左孩子节点的下标为 2*i + 1,右孩子节点的下标为 2*i + 2
  • 如果一个节点在下标 i 处,则其父节点的下标为 (i - 1) / 2

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

按照顺序存储的方式,这棵二叉树可以存储在一个数组中,如下:

[A, B, C, D, E, F, G]
  • A 在下标 0 处,没有父节点。
  • B 在下标 1 处,父节点为 A,左孩子为 D,右孩子为 E
  • C 在下标 2 处,父节点为 A,左孩子为 F,右孩子为 G
  • D 在下标 3 处,父节点为 B,没有孩子节点。
  • E 在下标 4 处,父节点为 B,没有孩子节点。
  • F 在下标 5 处,父节点为 C,没有孩子节点。
  • G 在下标 6 处,父节点为 C,没有孩子节点。

这种存储方式的优点是可以通过数组下标直接访问节点,效率较高;但缺点是如果二叉树比较稀疏,会造成存储空间的浪费。

我们可以用具体的例子来验证这种顺序存储方式:

1
2
3
4
5
6

按照顺序存储的方式,这棵二叉树可以存储在一个数组中,如下:

[1, 2, 3, 4, 5, 6]
  • 1 在下标 0 处,没有父节点。
  • 2 在下标 1 处,父节点为 1,左孩子为 4,右孩子为 5
  • 3 在下标 2 处,父节点为 1,左孩子为 6
  • 4 在下标 3 处,父节点为 2,没有孩子节点。
  • 5 在下标 4 处,父节点为 2,没有孩子节点。
  • 6 在下标 5 处,父节点为 3,没有孩子节点。

这种顺序存储方式适合完全二叉树,对于一般的二叉树,通常采用链式存储。


2.2 二叉树的链式存储

2.2.1 二叉链表

二叉链表是一种常见的二叉树链式存储结构,每个节点包含三个指针:一个指向左孩子,一个指向右孩子,一个指向数据。通过这种结构,可以灵活地表示二叉树。

二叉链表节点的结构如下:

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针
};

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其二叉链表的表示方式如下:

TreeNode* root = new TreeNode('A');
root->left = new TreeNode('B');
root->right = new TreeNode('C');
root->left->left = new TreeNode('D');
root->left->right = new TreeNode('E');
root->right->left = new TreeNode('F');
root->right->right = new TreeNode('G');
2.2.2 三叉链表

三叉链表是一种扩展的二叉树链式存储结构,每个节点包含四个指针:一个指向左孩子,一个指向右孩子,一个指向父节点,一个指向数据。通过增加父节点指针,可以方便地在树中进行向上遍历。

三叉链表节点的结构如下:

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针
    TreeNode* parent;   // 指向父节点的指针
};

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其三叉链表的表示方式如下:

TreeNode* root = new TreeNode('A');
root->left = new TreeNode('B');
root->left->parent = root;
root->right = new TreeNode('C');
root->right->parent = root;
root->left->left = new TreeNode('D');
root->left->left->parent = root->left;
root->left->right = new TreeNode('E');
root->left->right->parent = root->left;
root->right->left = new TreeNode('F');
root->right->left->parent = root->right;
root->right->right = new TreeNode('G');
root->right->right->parent = root->right;

这种链式存储方式适用于需要频繁进行向上遍历的场景。通过增加父节点指针,可以方便地在树中进行回溯操作。

3. 二叉树的遍历

3.1 前序遍历


3.1.1 递归实现

前序遍历是一种遍历二叉树的方式,即先访问根节点,然后递归地遍历左子树,最后递归地遍历右子树。前序遍历的顺序为:根节点 -> 左子树 -> 右子树。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其前序遍历的结果为:A B D E C F G。

前序遍历的递归实现如下:

#include <iostream>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void preOrder(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    // 访问根节点
    cout << root->data << " ";
    // 递归遍历左子树
    preOrder(root->left);
    // 递归遍历右子树
    preOrder(root->right);
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "前序遍历结果: ";
    preOrder(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,前序遍历按照根节点 -> 左子树 -> 右子树的顺序访问各节点,输出结果为:A B D E C F G。


3.1.2 非递归实现

前序遍历的非递归实现通常使用栈来模拟递归过程。具体步骤如下:

  1. 初始化一个栈,并将根节点压入栈中。
  2. 当栈不为空时,执行以下步骤:
    • 从栈顶弹出一个节点,并访问该节点。
    • 如果该节点有右孩子,将右孩子压入栈。
    • 如果该节点有左孩子,将左孩子压入栈。
  3. 重复步骤2,直到栈为空。

这个过程确保了先访问根节点,然后访问左子树,最后访问右子树的顺序。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其前序遍历的结果为:A B D E C F G。

前序遍历的非递归实现如下:

#include <iostream>
#include <stack>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void preOrderNonRecursive(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    
    stack<TreeNode*> s;
    s.push(root);
    
    while (!s.empty()) {
        TreeNode* node = s.top();
        s.pop();
        
        // 访问节点
        cout << node->data << " ";
        
        // 先将右孩子压入栈
        if (node->right) {
            s.push(node->right);
        }
        
        // 再将左孩子压入栈
        if (node->left) {
            s.push(node->left);
        }
    }
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "前序遍历结果(非递归): ";
    preOrderNonRecursive(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,前序遍历按照根节点 -> 左子树 -> 右子树的顺序访问各节点,输出结果为:A B D E C F G。


3.2 中序遍历

3.2.1 递归实现

中序遍历是一种遍历二叉树的方式,即先递归遍历左子树,然后访问根节点,最后递归遍历右子树。中序遍历的顺序为:左子树 -> 根节点 -> 右子树。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其中序遍历的结果为:D B E A F C G。

中序遍历的递归实现如下:

#include <iostream>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void inOrder(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    // 递归遍历左子树
    inOrder(root->left);
    // 访问根节点
    cout << root->data << " ";
    // 递归遍历右子树
    inOrder(root->right);
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "中序遍历结果: ";
    inOrder(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,中序遍历按照左子树 -> 根节点 -> 右子树的顺序访问各节点,输出结果为:D B E A F C G。


3.2.2 非递归实现

中序遍历的非递归实现通常使用栈来模拟递归过程。具体步骤如下:

  1. 初始化一个栈,并设置当前节点为根节点。
  2. 当当前节点不为空或栈不为空时,执行以下步骤:
    • 如果当前节点不为空,将当前节点压入栈,并将当前节点设置为其左孩子。
    • 如果当前节点为空,从栈顶弹出一个节点,访问该节点,并将当前节点设置为其右孩子。
  3. 重复步骤2,直到当前节点为空且栈为空。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其中序遍历的结果为:D B E A F C G。

中序遍历的非递归实现如下:

#include <iostream>
#include <stack>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void inOrderNonRecursive(TreeNode* root) {
    stack<TreeNode*> s;
    TreeNode* current = root;
    
    while (current != nullptr || !s.empty()) {
        // 走到最左边的节点
        while (current != nullptr) {
            s.push(current);
            current = current->left;
        }
        
        // 访问节点
        current = s.top();
        s.pop();
        cout << current->data << " ";
        
        // 准备访问右子树
        current = current->right;
    }
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "中序遍历结果(非递归): ";
    inOrderNonRecursive(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,中序遍历按照左子树 -> 根节点 -> 右子树的顺序访问各节点,输出结果为:D B E A F C G。


3.3 后序遍历

3.3.1 递归实现

后序遍历是一种遍历二叉树的方式,即先递归遍历左子树,然后递归遍历右子树,最后访问根节点。后序遍历的顺序为:左子树 -> 右子树 -> 根节点。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其后序遍历的结果为:D E B F G C A。

后序遍历的递归实现如下:

#include <iostream>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void postOrder(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    // 递归遍历左子树
    postOrder(root->left);
    // 递归遍历右子树
    postOrder(root->right);
    // 访问根节点
    cout << root->data << " ";
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "后序遍历结果: ";
    postOrder(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,后序遍历按照左子树 -> 右子树 -> 根节点的顺序访问各节点,输出结果为:D E B F G C A。


3.3.2 非递归实现

后序遍历的非递归实现通常使用两个栈来模拟递归过程。具体步骤如下:

  1. 初始化两个栈,s1s2,并将根节点压入 s1
  2. s1 不为空时,执行以下步骤:
    • s1 弹出一个节点,将其压入 s2
    • 如果该节点有左孩子,将左孩子压入 s1
    • 如果该节点有右孩子,将右孩子压入 s1
  3. s1 为空时,从 s2 弹出所有节点并访问它们。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其后序遍历的结果为:D E B F G C A。

后序遍历的非递归实现如下:

#include <iostream>
#include <stack>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void postOrderNonRecursive(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    
    stack<TreeNode*> s1, s2;
    s1.push(root);
    
    while (!s1.empty()) {
        TreeNode* node = s1.top();
        s1.pop();
        s2.push(node);
        
        if (node->left) {
            s1.push(node->left);
        }
        
        if (node->right) {
            s1.push(node->right);
        }
    }
    
    while (!s2.empty()) {
        TreeNode* node = s2.top();
        s2.pop();
        cout << node->data << " ";
    }
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "后序遍历结果(非递归): ";
    postOrderNonRecursive(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,后序遍历按照左子树 -> 右子树 -> 根节点的顺序访问各节点,输出结果为:D E B F G C A。


3.4 层序遍历

层序遍历是一种遍历二叉树的方式,即按照树的层次从上到下、从左到右依次访问每一层的节点。层序遍历通常使用队列来实现。

具体步骤如下:

  1. 初始化一个队列,并将根节点入队。
  2. 当队列不为空时,执行以下步骤:
    • 从队列中取出一个节点并访问该节点。
    • 如果该节点有左孩子,将左孩子入队。
    • 如果该节点有右孩子,将右孩子入队。
  3. 重复步骤2,直到队列为空。

例如,给定一个二叉树如下:

A
B
C
D
E
F
G

其层序遍历的结果为:A B C D E F G。

层序遍历的实现如下:

#include <iostream>
#include <queue>
using namespace std;

struct TreeNode {
    int data;           // 节点数据
    TreeNode* left;     // 指向左孩子的指针
    TreeNode* right;    // 指向右孩子的指针

    TreeNode(int x) : data(x), left(nullptr), right(nullptr) {}
};

void levelOrder(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        TreeNode* node = q.front();
        q.pop();
        
        // 访问节点
        cout << node->data << " ";
        
        // 将左孩子入队
        if (node->left) {
            q.push(node->left);
        }
        
        // 将右孩子入队
        if (node->right) {
            q.push(node->right);
        }
    }
}

int main() {
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "层序遍历结果: ";
    levelOrder(root);
    cout << endl;

    // 释放内存(省略清理代码)
    return 0;
}

在这个例子中,二叉树的结构通过二叉链表实现,层序遍历按照从上到下、从左到右的顺序访问各节点,输出结果为:A B C D E F G。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天秀信奥编程培训

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值