跳转至

二叉树(基础)

约 4029 个字 374 行代码 18 张图片 预计阅读时间 18 分钟

二叉树的概念和结构

一棵二叉树是结点的一个有限集合,该集合为空或者由一个根节点加上两棵别称为左子树和右子树的二叉树组成

二叉树的特点:

  1. 二叉树不存在度大于2的结点(但是可以存在空节点或者1个节点)
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

特殊的二叉树:

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是\(2^K-1\)个,则它就是满二叉树
  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树,并且完全二叉树节点个数范围为[\(2^{k-1}\)\(2^k-1\)](最少时即为K层只有一个节点,最多时为完全二叉树)

Note

完全二叉树最后一层可以不满,但是必须从左到右连续

二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有\(2^{(i - 1)\)个结点
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点总数是\(2^h-1\)
  3. 对任何一棵非空的二叉树, 如果度为0其叶结点个数为\(n_0\) , 度为2的分支结点个数为\(n_2\),则有\(n_0\)\(n_2\)+1

    Question

    一个树是由度为0的节点一次一次构建出来的,当出现度为1的节点时,肯定伴随着一个度为0的节点减少,但是同时也会增加一个度为0的节点,故此时度为0的节点还是1个,而度为1的节点从原来的0个变为现在的1个,同理可得度为2的节点,因为一个节点想从度为0变为度为2,则需要先变为度为1的节点,再成为度为2的节点,当一个节点度为2时,那么伴随着的就是两个度为0的节点,即度为0的节点从1变成了2,度为2的节点从原来的0变为了现在1,故有等式\(n_0\)\(n_2\)+1

  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=\(log_{2}{(n+1)}\)

  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
    1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
    2. 若2i+1=n否则无左孩子
    3. 若2i+2=n否则无右孩子

二叉树的性质基础练习

1.一棵完全二叉树的节点数为531个,那么这棵树的高度为

A 11

B 10

C 8

D 12

Tip

答案:B

解析:

因为完全二叉树最少的节点为\(2^h\)个,最多为\(2^{h-1}\)个,故当节点数为531个时,最少满足层数为9层(至少有512个节点),最多满足层数为十层(至多为1024个节点),故当前树的高度为10

2.一个具有767个节点的完全二叉树,其叶子节点个数为

A 383

B 384

C 385

D 386

Tip

答案:B

解析:

假设度为0的节点有n个,度为1的节点有k个,度为2的节点有i个,那么有等式n+k+i = 767,因为n = i +1,故原式化为n-1+k+n = 767,即2n+k=768,因为节点个数肯定为整数,故当节点总数为奇数时,肯定不存在度为1的节点,反之最多存在一个度为1的节点,故得度为0的节点的个数n = 384

3.在具有 2n 个结点的完全二叉树中,叶子结点个数为

A n

B n+1

C n-1

D n/2

Tip

答案:A

解析:

本题与上题思路类似,假设度为0的节点m个,度为1的节点有k个,度为2的节点有i个,故有等式m+k+i = 2n,化简得2m-1+k=2n,因为节点个数为偶数个,则至多存在一个度为1的节点,则2m-1+1=2n,即m = n,故叶子节点个数为n

二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

  1. 顺序存储

    顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

  2. 链式结构

    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef int BTNDataType;
// 二叉链
struct BinaryTreeNode
{
    struct BinTreeNode* Left;   // 指向当前节点左孩子
    struct BinTreeNode* Right; // 指向当前节点右孩子
    BTNDataType data; // 当前节点值域
}

// 三叉链
struct BinaryTreeNode
{
    struct BinTreeNode* Parent; // 指向当前节点的双亲
    struct BinTreeNode* Left;   // 指向当前节点左孩子
    struct BinTreeNode* Right; // 指向当前节点右孩子
    BTNDataType data; // 当前节点值域
}

二叉链式二叉树的基本功能实现

二叉链式二叉树的结构定义

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//定义二叉树的数据类型
typedef int BTNDataType;

//定义二叉树结构
typedef struct BinaryTreeNode
{
    BTNDataType data;
    struct BinaryTreeNode* leftTree;
    struct BinaryTreeNode* rightTree;
}BTN;

主要实现功能

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//创建树节点
BTN* createNode(int x);
//创建树
BTN* createTree();
//前序遍历
void frontOrder(BTN* tree);
//中序遍历
void midOrder(BTN* tree);
//后序遍历
void postOrder(BTN* tree);
//二叉树节点的个数
int binaryTreeSize(BTN* tree);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTN* root);
// 求二叉树的高度
int BinaryTreeHeight(BTN* tree)
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTN* root, BTNDataType k);
// 二叉树查找值为x的节点
BTN* BinaryTreeFind(BTN* root, BTNDataType x);
// 二叉树的销毁
void BinaryTreeDestory(BTN* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTN* root);

Note

在上面的功能中,「创建树节点」和「创建树」并非必须,在下面的实现中只需要简单看一下即可

创建树节点

创建树节点的代码逻辑很简单,只需要根据指定数值开辟对应的树节点空间即可,参考代码如下:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//创建树节点
BTN* createNode(int x)
{
    BTN* node = (BTN*)malloc(sizeof(BTN));
    assert(node);

    node->data = x;
    node->leftTree = NULL;
    node->rightTree = NULL;

    return node;
}

创建树

创建树就是根据前面创建节点的函数创建出节点,再对节点进行链接即可,例如下面的代码:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//创建树
BTN* createTree()
{
    BTN* node1 = createNode(1);
    BTN* node2 = createNode(2);
    BTN* node3 = createNode(3);
    BTN* node4 = createNode(4);
    BTN* node5 = createNode(5);
    BTN* node6 = createNode(6);

    node1->leftTree = node2;
    node1->rightTree = node3;
    node2->leftTree = node4;
    node2->rightTree = node5;
    node5->leftTree = node6;

    return node1;
}

前序遍历、中序遍历、后序遍历(递归版本)以及层序遍历(迭代版本)

前序遍历、中序遍历和后序遍历

所谓二叉树中的前序遍历,根的遍历顺序在左子树和右子树之前,即先遍历根,再遍历左子树,最后右子树

同理可得二叉树的中序遍历,根的遍历顺序在左子树和右子树中间,即先遍历左子树,再遍历根,最后右子树

二叉树的后序遍历,根的遍历顺序在左子树和右子树后,即先遍历左子树,再遍历右子树,最后遍历根

二叉树的层序遍历,即一层一层遍历,例如先遍历第一层,再遍历第二层……

根据上面的思路可以思考如何编写递归版本的代码:

首先将递归的要素进行罗列:

  1. 递归终止条件
  2. 递归函数的参数和返回值
  3. 单层递归函数的处理逻辑

以前序遍历为例,因为是遍历二叉树,所以递归的主要目的就是遍历,所以单层递归的逻辑就可以是打印当前节点的值,接着为了执行递归,递归的本质就是「自己调用自己」,就需要再次调用当前函数,根据前序遍历的过程:中左右,先遇到的节点是中,已经在单词递归的逻辑中处理完毕,接下来就是找左子树,当前函数的作用就是遍历,所以将当前节点的左子树作为实参传递给当前函数,此时就是从左子树开始执行完整的函数,同样先进来的节点是根节点,所以先进行单层递归逻辑处理,接着继续走其左子树直到左子树为空时停止递归,这里就可以将「当前节点为空」作为递归结束条件

最后确定函数的返回值和参数,因为遍历过程中需要知道当前节点,并且需要判断当前节点是否为空,所以需要一个参数表示二叉树节点,并且这个节点是某一棵子树的根节点,接着就是返回值,可以看到在上面的过程中并不需要在递归结束后向主调函数返回任何内容,所以返回值为void即可

同理可以得出中序遍历,中序遍历的遍历顺序是:左中右,所以就需要先找到最左节点,也就是调用当前函数一直到当前节点(左节点)为空为止,接着就是处理单层递归的逻辑,依旧是打印当前节点,此时的第一个节点就是当前二叉树的最左节点,接着就是处理右子树,逻辑与前面相同,此处不再赘述

同理可得后序遍历,后序遍历的顺序是:左右中,按照上面的过程推导即可

上面三种遍历方式的代码如下:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//前序遍历
void frontOrder(BTN* tree)
{
    if (tree == NULL)
    {
        printf("N ");
        return;
    }

    printf("%d ", tree->data);
    frontOrder(tree->leftTree);
    frontOrder(tree->rightTree);
}
C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//中序遍历
void midOrder(BTN* tree)
{
    if (tree == NULL)
    {
        printf("N ");
        return;
    }

    midOrder(tree->leftTree);
    printf("%d ", tree->data);
    midOrder(tree->rightTree);
}
C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//后序遍历
void postOrder(BTN* tree)
{
    if (tree == NULL)
    {
        printf("N ");
        return;
    }

    postOrder(tree->leftTree);
    postOrder(tree->rightTree);
    printf("%d ", tree->data);
}

以前序遍历进行递归分析图如下:

前序遍历、中序遍历和后序遍历的迭代版本见算法:二叉树基础练习篇

层序遍历

层序遍历思路分析:

层序遍历思路就是利用队列先进先出的性质,首先让根节点进入队列,接着循环判断队列是否为空,如果队列为空,证明当前二叉树已经遍历完毕,否则就取出当前节点并从队列弹出该节点,接着如果取出的这个节点是有效节点,那么就可以插入其左子树和右子树为下一次层序遍历做准备,示意图如下:

参考代码如下:

Note

注意,下面的代码使用到了前面使用C语言模拟实现的队列

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//层序遍历
void levelOrder(BTN* tree)
{
    //初始化队列
    Queue q;
    QueueInit(&q);

    //插入根节点
    QueuePush(&q, tree);

    //使用队列结构实现层序遍历
    while (!QueueEmpty(&q))
    {
        //记录当前的队列中的节点,该指针指向二叉树的节点,而不是队列的指针,即某一层的数据
        BTN* front = QueueFront(&q);
        //删除当前数据
        QueuePop(&q);
        if (front)
        {
            printf("%d ", front->data);
            //注意此处需要改变向队列中添加的数据,此时不能在使用根节点的指针
            //找到左子树节点并插入
            QueuePush(&q, front->leftTree);
            //找到右子树节点并插入
            QueuePush(&q, front->rightTree);
        }
    }

    //销毁队列
    QueueDestroy(&q);
}

需要注意,在上面的代码中并没有将节点进行分层,也就是说,在输出中并不知道哪些节点属于哪一层,对于需要知道的情况可以考虑使用一个计数器统计当前队列中节点的个数,再根据个数依次弹出元素即可实现分层,具体见算法:二叉树基础练习篇

基础选择题练习

  1. 二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为

    A E

    B F

    C G

    D H

    Tip

    答案: A

    解析:

    先序遍历的顺序为:根 左子树 右子树,故可以通过前序遍历确定二叉树的根节点为E

  2. 设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为____。

    A adbce

    B decab

    C debac

    D abcde

    Tip

    答案:D

    解析:

    本题与上题类似,因为后序遍历的顺序为:左子树 右子树 根,由此可以确定二叉树的根节点为a,而因为中序遍历为badce,故可以a为界限,分出根节点的左子树b和右子树dce,同样根据后序遍历中dec的位置,可以判断出c为根节点,则右子树dce中d为左子树,c为根节点,e为右子树,故前序遍历为abcde

二叉树的节点个数

计算二叉树的节点个数时,可以采用分治的思想来进行计算,即由每一个子树计算自己所含有的节点个数,最后所有子树的个数及根节点相加得出节点个数

而所谓分治的思想,可以一个现实中的管理实例来分析:

对于统计节点个数,基本思路如下:

采用后序遍历二叉树,分别统计当前节点的左子树的个数+右子树的节点个数+1再返回给当前节点,注意此处不能遗忘+1,因为当前节点并没有在左子树或者右子树中统计

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//二叉树节点的个数
int binaryTreeSize(BTN* tree)
{
    //确定结束条件
    if (tree == NULL)
    {
        return 0;//节点为空时代表没有孩子节点
    }

    return binaryTreeSize(tree->leftTree) + binaryTreeSize(tree->rightTree) + 1;// +1代表左右孩子节点为空,但是存在自己即当前节点为叶子节点
}

二叉树叶子节点的个数

计算二叉树叶子节点的个数同样是采用分治的思想进行计算,与上面不同的是,不需要统计有孩子的节点,也就是一棵子树的根节点

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 二叉树叶子节点个数
int binaryTreeLeafSize(BTN* tree)
{
    //递归结束条件
    if (tree == NULL)
    {
        return 0;
    }

    //计算叶子节点个数
    if (tree->leftTree == NULL && tree->rightTree == NULL)
    {
        return 1;
    }

    return binaryTreeLeafSize(tree->leftTree) + binaryTreeLeafSize(tree->rightTree);
}

求二叉树叶子节点的个数递归分析图如下

求二叉树的高度

计算二叉树的高度同样是采用分治的思想进行计算

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 求二叉树的高度
int binaryTreeHeight(BTN* tree)
{
    //递归结束条件
    if (tree == NULL)
    {
        return 0;//注意此处不是返回1,因为下面的返回语句尽管为0时依旧会进行一次+1,若返回1则会多算1层
    }

    //获取左子树和右子树的高度并记录
    int leftTreeHeight = binaryTreeHeight(tree->leftTree);
    int rightTreeHeight = binaryTreeHeight(tree->rightTree);

    //找出较大的子树加1返回
    return leftTreeHeight > rightTreeHeight ? leftTreeHeight + 1 : rightTreeHeight + 1;
}

求二叉树高度的递归分析图如下

二叉树第k层节点个数

求二叉树第k层节点个数,首先就是找到第k层,而在递归中,定义一个变量和k比较的方式并不合适,但是根据递归的特点,只要每一次递归的函数中的k为上一次的k-1,即可实现k减小,当k为1时即可找到所谓的第k

Note

注意,在二叉树层数定义中,建议第一层为1,第二层为2,以此类推

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 二叉树第k层节点个数
int binaryTreeLevelKSize(BTN* tree, BTNDataType k)
{
    //确保k不为0
    assert(k > 0);

    //递归结束条件
    if (tree == NULL)
    {
        return 0;
    }

    //找到第k层开始计算
    if (k == 1)
    {
        return 1;
    }

    return binaryTreeLevelKSize(tree->leftTree, k - 1) + binaryTreeLevelKSize(tree->rightTree, k - 1);
}

二叉树第k层节点个数的递归分析图

二叉树查找值为x的节点

二叉树中查找某个值时,遍历二叉树即可,注意此处需要处理好返回值的问题,具体过程分析如下

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 二叉树查找值为x的节点
BTN* binaryTreeFind(BTN* tree, BTNDataType x)
{
    //递归结束条件
    if (tree == NULL)
    {
        return NULL;
    }

    //如果找到返回tree节点
    if (tree->data == x)
    {
        return tree;
    }

    //没找到时需要继续遍历左子树和右子树
    //如果在左子树找到,返回左子树的节点
    BTN* ret1 = binaryTreeFind(tree->leftTree, x);
    if (ret1)
    {
        return ret1;
    }

    BTN* ret2 = binaryTreeFind(tree->rightTree, x);
    if (ret2)
    {
        return ret2;
    }

    //注意还需要处理没有找到时返回的空结果
    return NULL;
}

代码细致分析

二叉树的销毁

二叉树的销毁最好使用后序遍历,先销毁最后一个节点,再依次往上

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 二叉树的销毁
void BinaryTreeDestory(BTN* root)
{
    //使用后序遍历的思路进行销毁
    if (root == NULL)
    {
        return;
    }

    BinaryTreeDestroy(root->leftTree);
    BinaryTreeDestroy(root->rightTree);
    free(root);
}

判断是否是完全二叉树

判断一个树是否是完全二叉树一定不能直接按照数量来判断,因为完全二叉树除了数量上有规定,还必须满足按顺序排列每一层的节点,考虑下面的思路:

使用层序遍历查看二叉树,如果遇到队列中的某一元素为空时,终止遍历二叉树,接着遍历队列剩余的内容,如果在NULLNULL之间有非NULL的内容,则不是完全二叉树,否则是完全二叉树

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTN* root)
{
    Queue q = {0};
    QueueInit(&q);
    if (root == NULL)
    {
        return true;
    }

    //插入数据
    QueuePush(&q, root);
    while (!QueueEmpty(&q))
    {
        //数据出队
        BTN* front = QueueFront(&q);
        QueuePop(&q);

        if (front == NULL)
        {
            break;
        }

        QueuePush(&q, front->leftTree);
        QueuePush(&q, front->rightTree);
    }

    //如果有空值时,则开始判断是否有非空值在空值之间
    while (!QueueEmpty(&q))
    {
        BTN* front = QueueFront(&q);
        QueuePop(&q);

        if (front)
        {
            QueueDestroy(&q);
            return false;
        }
    }

    QueueDestroy(&q);
    //走到此位置代表为完全二叉树
    return true;
}

二叉树基础练习

单值二叉树

具体见算法:二叉树基础练习篇

检查两棵树是否相同

具体见算法:二叉树基础练习篇

对称二叉树

具体见算法:二叉树基础练习篇

二叉树的前序遍历

具体见算法:二叉树基础练习篇

二叉树的中序遍历

具体见算法:二叉树基础练习篇

二叉树的后序遍历

具体见算法:二叉树基础练习篇

另一棵树的子树

具体见算法:二叉树基础练习篇

二叉树前序构建

题目链接:二叉树遍历_牛客题霸_牛客网 (nowcoder.com)

问题描述:

Quote

描述

编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。例如如下的先序遍历字符串:ABC##DE#G##F###其中#表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

输入描述: 输入包括1行字符串,长度不超过100。

输出描述: 可能有多组测试数据,对于每组数据, 输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。每个输出结果占一行。

示例1:

C++
1
2
输入abc##de#g##f###
输出c b e g d f a 

思路解析:

参考代码:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#define _CRT_SECURE_NO_WARNINGS 1

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

typedef char BTNDataType;
typedef struct BinaryTreeNode
{
    BTNDataType data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTN;

BTN* CreateSpace(BTNDataType x)
{
    BTN* root = (BTN*)malloc(sizeof(BTN));
    assert(root);
    root->data = x;
    root->left = root->right = NULL;

    return root;
}

//生成树
BTN* BinaryTreeCreate(BTNDataType* a, int* pi)
{
    if (a[*pi] == '#')
    {
        //注意更改pi的值,否则会导致pi始终指向第一次遇到空的位置
        (*pi)++;
        return NULL;
    }

    //开辟空间
    BTN* root = CreateSpace(a[(*pi)]);
    (*pi)++;

    root->left = BinaryTreeCreate(a, pi);
    root->right = BinaryTreeCreate(a, pi);

    return root;
}

//中序遍历
void InOrder(BTN* root)
{
    if (root == NULL)
    {
        return;
    }

    InOrder(root->left);
    printf("%c ", root->data);
    InOrder(root->right);
}

int main() {
    //读取二叉树的数据
    char datac[100] = { 0 };
    scanf("%s", datac);
    //生成下标遍历数组
    int i = 0;
    BTN* root = BinaryTreeCreate(datac, &i);
    InOrder(root);
    return 0;
}