17. 二叉树

一、什么是二叉树

  二叉树 T 是一个有穷的结点集合。这个结合可以为空。若不为空,则它是由 根结点 和称为其 左子树 \(T_{L}\)右子树 $T_{R} $ 的两个不相交的二叉树组成。二叉树是一颗度为 2 的树,并且二叉树的子树有左右之分。

二叉树的五种基本形态

  一些比较常见的特殊二叉树如下:

特殊二叉树

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

  • 一个二叉树第 i 层的最大结点数为:\(2^{i-1}, i ≥ 1\)
  • 深度为 k 的二叉树有最大结点总数为:\(2^{k} - 1, k ≥ 1\)
  • 对任何非空二叉树 T,若 \(n_{0}\) 表示叶节点的个数、\(n_{2}\) 是度为 2 的非叶节点个数,那么两者满足关系 \(n_{0} = n_{2} + 1\)
ADT BinaryTree
{
Data:
    二叉树T∈BinaryTree, Item∈ElementType,
Operation:
    void PreOrderTraverse(BinaryTree T);        // 先序遍历
    void InOrderTraverse(BinaryTree T);         // 中序遍历
    void PostOrderTraverse(BinaryTree T);       // 后序遍历
    void LevelOrderTraverse(BinaryTree T);      // 层次遍历
} ADT BinaryTree;

二、二叉树的存储结构

2.1、顺序存储实现

  一般的二叉树,我们可以将其补齐为完全二叉树。完全二叉树按从上至下、从左到右顺序存储,n 个结点的完全二叉树的结点父子关系如下:

  • 非根结点(序号 i>1)的父结点的序列是 \(\lfloor i/2 \rfloor\)
  • 结点(序号为 i)的左孩子结点的序号是 2i(若 2i ≥ n,没有左孩子)。右孩子结点的序号是 2i+1(若 2i+1 ≥ n,没有右孩子)。

二叉树的顺序存储

用顺序存储的方式存储一般的二叉树时,会造成大量的空间浪费。

2.2、链式存储实现

二叉树的链式存储

#include <stdio.h>
#include <stdlib.h>

typedef int ElementType;

typedef struct TreeNode
{
    ElementType Data;
    struct TreeNode * Left;
    struct TreeNode * Right;
} TreeNode, *BinaryTree;

三、二叉树的遍历

  先序、中序和后序遍历过程中经过结点的路线一样,只是访问各结点的时机不同。

3.1、先序遍历

  先序遍历过程如下:

  1. 访问根结点。
  2. 先序遍历其左子树。
  3. 先序遍历其右子树。
/**
 * @brief 先序遍历二叉树
 * 
 * @param T 二叉树
 */
void PreOrderTraversal(BinaryTree T)
{
    if (T)
    {
        printf("%d ", T->Data);
        PreOrderTraversal(T->Left);
        PreOrderTraversal(T->Right);
    }
}

先序遍历

  我们也可以使用栈来模拟先序遍历的递归过程:

  1. 遇到一个结点就直接访问它,然后把它压栈,并去遍历它的左子树。
  2. 当左子树遍历结束后,从栈顶弹出结点。
  3. 然后按其右指针再去先序遍历该结点的右子树。
/**
 * @brief 中序遍历二叉树
 * 
 * @param T 二叉树
 */
void InOrderTraversal(BinaryTree T)
{
    BinaryTree Tree = T;
    Stack S = CreateStack();                                                    // 创建栈

    while (Tree || !IsEmpty(S))
    {
        while (Tree)                                                            // 一直向左并将沿途结点压入栈中
        {
            printf("%d ", Tree->Data);
            Push(S, Tree);
            Tree = Tree->Left;
        }
        if (!IsEmpty(S))
        {
            Tree = Pop(S);                                                      // 弹出栈顶结点,访问该结点
            Tree = Tree->Right;                                                 // 转向右子树
        }
    }
}

3.2、中序遍历

  中序遍历过程如下:

  1. 中序遍历其左子树。
  2. 访问根结点。
  3. 中序遍历其右子树。
/**
 * @brief 中序遍历二叉树
 * 
 * @param T 二叉树
 */
void InOrderTraversal(BinaryTree T)
{
    if (T)
    {
        InOrderTraversal(T->Left);
        printf("%d ", T->Data);
        InOrderTraversal(T->Right);
    }
}

中序遍历

  我们也可以使用栈来模拟中序遍历的递归过程:

  1. 遇到一个结点,就把它压栈,并去遍历它的左子树。
  2. 当左子树遍历结束后,从栈顶弹出结点并访问它。
  3. 然后按其右指针再去中序遍历该结点的右子树。
/**
 * @brief 中序遍历二叉树
 * 
 * @param T 二叉树
 */
void InOrderTraversal(BinaryTree T)
{
    BinaryTree Tree = T;
    Stack S = CreateStack();                                                    // 创建栈

    while (Tree || !IsEmpty(S))
    {
        while (Tree)                                                            // 一直向左并将沿途结点压入栈中
        {
            Push(S, Tree);
            Tree = Tree->Left;
        }
        if (!IsEmpty(S))
        {
            Tree = Pop(S);                                                      // 弹出栈顶结点,访问该结点
            printf("%d ", Tree->Data);
            Tree = Tree->Right;                                                 // 转向右子树
        }
    }
}

3.3、后序遍历

  后序遍历过程如下:

  1. 后序遍历其左子树。
  2. 后序遍历其右子树。
  3. 访问根结点。
/**
 * @brief 后序遍历二叉树
 * 
 * @param T 二叉树
 */
void PostOrderTraversal(BinaryTree T)
{
    if (T)
    {
        PostOrderTraversal(T->Left);
        PostOrderTraversal(T->Right);
        printf("%d ", T->Data);
    }
}

后序遍历

  我们也可以使用栈来模拟后序遍历的递归过程:

  1. 遇到一个结点,就把它压栈,并去遍历它的左子树。
  2. 当左子树遍历结束后,从获取栈顶元素,并判断它的右子树是否处理完成。
  3. 如果右子树没有处理完成,按其右指针再去后序遍历该结点的右子树。
  4. 如果右子树处理完成,则访问该结点。
/**
 * @brief 后序遍历二叉树
 * 
 * @param T 二叉树
 */
void PostOrderTraversal(BinaryTree T)
{
    BinaryTree Tree = T;
    BinaryTree pop = NULL, peek = NULL;
    Stack S = CreateStack();                                                    // 创建栈

    while (Tree || !IsEmpty(S))
    {
        while (Tree)                                                            // 一直向左并将沿途结点压入栈中
        {
            Push(S, Tree);
            Tree = Tree->Left;
        }
        if (!IsEmpty(S))
        {
            peek = Peek(S);                                                     // 访问栈顶结点
            if (peek->Right == NULL || peek->Right == pop)                      // 没有右子树或者右子树处理完毕
            {
                pop = Pop(S);
                printf("%d ", pop->Data);
            }
            else
            {
                Tree = peek->Right;                                             // 转向右子树
            }
        }
    }
}

3.4、层序遍历

  层序遍历可以用队列实现,遍历从根结点开始,首先将根结点入队,然后开始执行循环,结点出队、访问该结点、其左右儿子入队。

/**
 * @brief 层序遍历二叉树
 * 
 * @param T 二叉树
 */
void LevelOrderTraversal(BinaryTree T)
{
    BinaryTree Tree = T;
    Queue Q = NULL;

    if (Tree)
    {
        return;
    }
  
    Q = CreateQueue();
    Enqueue(Q, Tree);
    while (!IsEmpty(Q))
    {
        Tree = Dequeue(Q);
        printf("%d ", Tree->Data);
        if (Tree->Left)
        {
            Enqueue(Q, Tree->Left);
        }
        if (Tree->Right)
        {
            Enqueue(Q, Tree->Right);
        }
    }
}

四、遍历的应用

4.1、先序和中序确定二叉树

  1. 根据先序遍历序列的第一个结点确定根结点。
  2. 根据根结点在中序遍历序列中分割处左右两个子序列。
  3. 对左子树和右子树分别递归使用相同的方法继续分解。

先序和中序确定二叉树

/**
 * @brief 根据先序和中序序列创建二叉树
 * 
 * @param PreOrder 先序序列
 * @param InOrder 中序序列
 * @param Length 序列的长度
 * @return BinaryTree 指向二叉树的指针
 */
BinaryTree CreakeBinaryTreeByPreOrderAndInOrder(ElementType * PreOrder, ElementType * InOrder, int Length)
{
    if (Length == 0)
    {
        return NULL;
    }

    // 先序遍历的第一个结点就是根结点
    ElementType RootData = PreOrder[0];
    BinaryTree Root = (BinaryTree)malloc(sizeof(TreeNode));
    Root->Data = RootData;
  
    for (int i = 0; i < Length; i++)                                            // 区分左右子树
    {
        if (InOrder[i] == RootData)
        {
            // Inorder序列中从0到i-1是左子树,从i+1的InOrderLength-1是右子树
            Root->Left =  CreakeBinaryTreeByPreOrderAndInOrder(PreOrder + 1, InOrder, i);
            Root->Right = CreakeBinaryTreeByPreOrderAndInOrder(PreOrder + i + 1, InOrder + i + 1, Length - i - 1);

            break;
        }
    }
  
    return Root;
}

4.2、中序和后序确定二叉树

  1. 根据后序遍历序列的最后一个结点确定根结点。
  2. 根据根结点在中序遍历序列中分割处左右两个子序列。
  3. 对左子树和右子树分别递归使用相同的方法继续分解。

中序和后序确定二叉树

/**
 * @brief 根据中序和后序序列创建二叉树
 * 
 * @param InOrder 中序序列
 * @param PostOrder 后序序列
 * @param Length 序列的长度
 * @return BinaryTree 指向二叉树的指针
 */
BinaryTree CreakeBinaryTreeByInOrderAndPostOrder(ElementType * InOrder, ElementType * PostOrder, int Length)
{
    if (Length == 0)
    {
        return NULL;
    }

    // 后序遍历的最后一个结点就是根结点
    ElementType RootData = PostOrder[Length - 1];
    BinaryTree Root = (BinaryTree)malloc(sizeof(TreeNode));
    Root->Data = RootData;
  
    for (int i = 0; i < Length; i++)                                            // 区分左右子树
    {
        if (InOrder[i] == RootData)
        {
            // Inorder序列中从0到i-1是左子树,从i+1的InOrderLength-1是右子树
            Root->Left =  CreakeBinaryTreeByInOrderAndPostOrder(InOrder, PostOrder, i);
            Root->Right = CreakeBinaryTreeByInOrderAndPostOrder(InOrder + i + 1, PostOrder + i, Length - i - 1);

            break;
        }
    }
  
    return Root;
}
posted @ 2023-07-17 19:15  星光樱梦  阅读(63)  评论(0编辑  收藏  举报