紫薇星上的数据结构(7)

今天我们来整理一下有关于树的知识点,这个地方非常的繁杂,大家要耐心学习。


树是数据结构中重要的一个知识点,我们会整理到的有:树和二叉树的定义及抽象数据类型、树的存储结构及常见的三种表示方法、二叉树的性质和存储结构、遍历二叉树和线索二叉树、数和二叉树的转换及应用、哈夫曼树及应用。

7.1初识树

紫薇星上的数据结构(1)中,也就是本系列的开头我们讲过,逻辑结构包括四种:集合结构、线性结构、树形结构、图形结构;其中集合结构我们整理过,就是数据元素之间除了“同属一个集合”外,无其他关系;线性结构则表示“一对一”的关系,线性表、栈、队列、串这些都是线性结构;而今天我们来认识一个新的逻辑结构:树形结构,也就是“一对多”的关系,之后我们还会整理图形结构,也就是“多对多”的关系,可以将树形结构看作简化版的图形结构。

树的定义

树形结构,也就是“一对多”的关系,我们生活中常常能见到树形结构,比如书中的目录、游戏中的科技树、家族的族谱、电脑中的文件夹等都是一对多的关系,是树的结构。

在《数据教程|C语言版 第二版》中定义:树(Tree)是 n (n ≥ 0)个结点的有限集合,它或为空树(n = 0);或为非空树,对于非空树T:

  • 有且仅有一个称之为根的结点;
  • 除根结点以外的其余结点可分为 m (m > 0)个互不相交的有限集T1,T2,...,Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

在这个树中,我们可以看到有根结点、子结点、叶子结点,其中叶子结点是指树的子主题,也就是终端结点。

每个结点挂接的子树的个数就称为度,一般度表示的是树中子树的度的最大值,在上面的树中的度为3,是根节点的度;而从根结点到最终端的叶子结点所经过的层数就成为树的层次,整个树的层数就成为树的深度,上图中树的深度为3。

在树中有以下几个概念需要理解,我们用家谱的方式来解释:

  • 双亲结点:上层的结点,也就是当前结点的直接前驱;
  • 孩子结点:下层结点的子树的根,也就是当前结点的直接后继;
  • 兄弟结点:同一双亲下的同层次结点;
  • 堂兄弟结点: 不同双亲下的同层次结点;
  • 祖先结点:从根结点到该结点所经过的分支中的所有结点;
  • 子孙结点:该结点下的子树中任一结点。

森林的定义

森林就是去掉根节点的树,我们将一个树的根结点删除掉,剩下的子树的集合就是森林,森林里的树是各不相交的,同时树还分为有序树与无序树两种,我们举个例子:

  • 我们将树的根结点A删掉,只留下子树B,C,D,那么B,C,D所组成的集合就是森林;
  • 有序树:结点各子树从左至右有序,不能互换(左为第一),子树B的顺序就是E→F→G;
  • 无序树:结点各子树之间可以互换位置,如果B为无序树,那么顺序可以为EFG,也可以为GFE。

7.2初识二叉树

刚才我们了解了树,那么现在我们来看一下树中比较特殊的一个——二叉树。

二叉树(Binary Tree)是n(n > 0)个结点所构成的集合,它或为空树(n = 0);或为非空树,对于非空树T:

  • 有且仅有一个称之为根的结点;
  • 除根结点以外的其余结点可分为2个互不相交的有限子集T1,T2,并且称为根的左子树和右子树,且T1和T2本身也是二叉树。

我们举一个例子来看一下二叉树,二叉树与树一样都有递归性质,但是他们也有不同:

二叉树
至多只有两棵子树(结点的度不能超过二)

没有限制子树个数

子树有左右之分,次序不能任意颠倒(有序树)可以是有序树,也可以是无序树

我们在生活中也能找到二叉树的影子,例如各种比赛的晋级流程图:32进16,16进8,8进4,4进2,2进1,这种的流程就是一个典型的二叉树。二叉树说白了就是只有两个“叉”的有序树,除了空树和只有根结点的情况,二叉树的情况大概有这么多:

前面我们都可以容易看出结构,最后两种结构特别相似,但其实不是一种结构,他们的终端结点分别是左结点和右结点。

如果我们要问只有三个结点有几种形态,我们可以容易得出有五种形态:

那么我们为什么从研究树要跳转到研究二叉树呢?如果不把普通的树(多叉树)转换成二叉树,那么运算就很难实现,因为二叉树的结构最简单,规律性最强;可以证明得出,所有的树都可以转化成唯一对应的二叉树,并且不失二叉树的一般性,这里我们就不证明了。

7.3二叉树的性质

性质1:在二叉树的第 i 层上至多有2^(i - 1)个结点。

这是因为二叉树的每个结点最多只有两个子结点,所以在第一层最多有一个结点,在第二层最多有两个结点,第三层最多有四个结点,...,以此类推,第 i 层上至多有2^(i - 1)个结点。同时因为每个结点最多只有两个子结点,所以下一层的结点数最多是上一层的2倍。

性质2:深度为 k 的二叉树至多有2^k - 1个结点。

注意这里说的是深度与整个树的结点的关系,性质1是层数与当前层数结点的关系,大家不要搞混,因为性质1的存在,所以深度为1的树最多有一个结点,深度为2的树最多有 1 + 2 = 3个结点,深度为3的树最多有 1 + 2 + 4 = 7个结点,...,以此类推,深度为 k 的二叉树至多有2^k - 1个结点。

性质3:对于任何一个二叉树,若度为2的结点有 n_2个,那么叶子数 n_0 必定为 n_2 + 1,即​​​​n_0 = n_2 + 1

这个性质可以证明,我们设度为0的结点总数为n_0,度为1的结点总数为n_1,度为2的结点总数为n_2,总结点数称为 n ,分支数称为B,就可以得出:

分支数B = n_1 + 2n_2,结点数n = n_0 + n_1 + n_2 = B + 1,那么就可以得出n_0 + n_1 + n_2 = n_1 + 2 n_2 + 1,也就是n_0 = n_2 + 1。我们用一张图来解释一下:

在这张图中我们将所有的度为0的结点个数总和称为n_0,度为1的结点个数总和称为n_1,度为2的结点个数总和称为n_2​​​​​​​,总结点数称为 n ,分支数称为B,那么就有:n = 10, n_0 = 5, n_1 = 1, n_2 = 4, B = n - 1 = 9,代入上面的结论,就可以验证了。

完全二叉树与满二叉树

完全二叉树:深度为K且含有2^k - 1个结点的二叉树;每层结点数都是最大结点数。

满二叉树:深度为K,有 n 个结点的二叉树,当且仅当其中每一个结点都与深度为K的满二叉树中的结点编号顺序一一对应。

当我们遇到一个深度为K的二叉树,我们只需要按照深度为K进行满顺序编号,如果此二叉树的编号顺序与满顺序编号完全相同,则是满二叉树;如果在某个编号后免得全都没有,那么就是完全二叉树;我们举几个例子来看一下:

完全二叉树的特点,可以与上图中进行对比理解:

  • 叶子结点只能出现在最下面两层;
  • 最下面一层的节点一定是在左部的连续位置;
  • 倒数第二层若有叶子结点,那么一定在右侧连续位置;
  • 如果有度为1的结点,那么这个结点一定只有左孩子;
  • 不存在只有右子树、右孩子的情况出现;
  • 同样结点数的二叉树,完全二叉树的深度最小。

性质4:具有 n 个结点的完全二叉树的深度必定为\left \lfloor log{_2}^n \right \rfloor + 1,其中\left \lfloor x \right \rfloor表示对x进行向下取整,\left \lfloor 2.3 \right \rfloor = 2

由性质2可得,在 k - 1 层至多有2^{(k - 1)} - 1个结点,在 k 层至多有2^k - 1个结点,那么就可以得出结点数 n 在第 k 层是有2^{(k - 1)} - 1 < n \leq 2^k - 1,也就是k - 1 \leq log{_2}^n < k (k为整数),所以k = \left \lfloor log{_2}^n \right \rfloor + 1

性质5:完全二叉树,若从上至下、从左至右的进行顺序编号,则编号为 i 的结点,其左孩子编号必定为 2i ,其右孩子编号必定为 2i + 1,其双亲编号必定为\left \lfloor i / 2 \right \rfloor

二叉树的抽象数据类型

ADT BinaryTree
Data    D是具有相同特性的数据元素的集合
Relative若D = Ф,则R = Ф;
        若D ≠ Ф,则R = {H}; //存在二元关系
        ①root唯一          //关于根的说明
        ②Dj ∩ Dk = Ф       //关于子树不相交的说明
        ③......            //关于数据元素的说明
        ④......            //关于左子树和右子树的说明
Operation
        createBiTree(&T, defination);    //构建二叉树
        preOrderTraverse(T);             //先序遍历
        inOrderTraverse(T);              //中序遍历
        postOrderTraverse(T);            //后序遍历
        levelOrderTraverse(T);           //层序遍历
        ......                           //其他20多个操作
endADT

7.4二叉树的顺序存储结构

因为二叉树是顺序树,结点编号是由从上至下、从左至右的规律编号的,所以我们可以使用数组进行顺序存储结构的二叉树建立,将根结点放在第一个位置,按照满二叉树顺序的下一个结点放在第二个位置,因为二叉树不一定都是满二叉树,所以只要遇到没有结点的位置,我们在数组中存储一个‘0’进去,这样就能实现顺序存储结构存储二叉树,我们举个例子:

不过这样存储的话有个最大的缺陷就是:这是一个深度为4,且结点为4的二叉树,也就是说这个二叉树中只有一个根结点和三个左子树,那么它只有四个结点但存放它需要一个长度为15的数组。

也就是说在最坏的情况下,存放一个深度为 k 且只有 k 个结点的单支树(即不存在度为2的结点)却需要2^k - 1个长度的一维数组。

我们如何正确存放呢?举个例子说明一下:

这是一个满二叉树,我们已B结点为例子:B结点在数组中的下标为1,B结点的左子树D在数组中的下标为3,右子树E在数组中的下标为4;那么我们假设B结点的下标为 i ,那么D的下标为 2 * i + 1,E的下标为 2 * (i + 1);我们使用这个规律来存放。

我们使用代码来实现一下顺序存储二叉树,首先建立一个SeqTree.h和SeqTree.c文件,在SeqTree.h中编写操作:

#ifndef SEQTREE_H_INCLUDED
#define SEQTREE_H_INCLUDED

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

//最大结点数
#define MAX_SIZE 1024

//定义顺序树类型
typedef char SeqTree[MAX_SIZE];

//初始化
void InitSeqTree(SeqTree tree);

//创建完全二叉树,i为数组中的下标
void CreateSeqTree(SeqTree tree, int i);

//获取树的根结点元素
char GetSeqTreeRoot(SeqTree tree);

//获取树的长度
int GetSeqTreeLength(SeqTree tree);

//获取树的深度
int GetSeqTreeDepth(SeqTree tree);

#endif // SEQTREE_H_INCLUDED

然后在SeqTree.c中添加操作:

#include "SeqTree.h"
#include <math.h>
//初始化
void InitSeqTree(SeqTree tree){
    //将字符数组中的每个元素都设置为空字符
    for(int i = 0; i < MAX_SIZE; i++){
        tree[i] = '\0';
    }
}

//创建完全二叉树,i为数组中的下标
void CreateSeqTree(SeqTree tree, int i){
    char ch;
    ch = getchar();
    fflush(stdin);
    if(ch == '^'){
        tree[i] = '\0';
        return;
    }
    tree[i] = ch;
    //某个结点输入完成后,再让用户输入左孩子和右孩子
    printf("左孩子结点:");
    CreateSeqTree(tree, 2 * i + 1); //递归调用
    printf("右孩子结点:");
    CreateSeqTree(tree, 2 * (i + 1)); //递归调用

}

//获取树的根结点元素
char GetSeqTreeRoot(SeqTree tree){
    return tree[0];
}

//获取树的长度
int GetSeqTreeLength(SeqTree tree){
    int len;
    //从最后一位开始检查,不为‘\0’时即为总长度
    for(len = MAX_SIZE; len >= 1; len--){
        if(tree[len - 1] != '\0'){
            break;
        }
    }
    return len;
}

//获取树的深度
int GetSeqTreeDepth(SeqTree tree){
    //由性质2可得
    int depth = 0;
    int len = GetSeqTreeLength(tree);
    while((int)pow(2, depth) - 1 < len){
        depth++;
    }
    return depth;
}

这些就是一些基本的操作,我们在main.c中实现来看一下:

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

void TestSeqTree();

int main(){
    //printf("Hello world!\n");
    TestSeqTree();
    return 0;
}

void TestSeqTree(){
    SeqTree tree;
    InitSeqTree(tree);
    printf("请输入数据,如果结点无数据,请输入‘ ^ ’\n请输入根节点:");
    CreateSeqTree(tree, 0);

    for(int i = 0; i < 15; i++){
        printf("%c, ", tree[i]);
    }
    printf("\n");

    printf("树的根节点为:%c\n", GetSeqTreeRoot(tree));
    printf("树的长度为:%d\n", GetSeqTreeLength(tree));
    printf("树的深度为:%d\n", GetSeqTreeDepth(tree));

}

编译通过,我们就随便输入一个只有A、B、C的满二叉树进去看一下,自己输的时候要注意我们只设置了数组长为15,也就是说只能输到第四层:

请输入数据,如果结点无数据,请输入‘ ^ ’
请输入根节点:A
左孩子结点:B
左孩子结点:^
右孩子结点:^
右孩子结点:C
左孩子结点:^
右孩子结点:^
A, B, C,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,
树的根节点为:A
树的长度为:3
树的深度为:2

Process returned 0 (0x0)   execution time : 8.562 s
Press any key to continue.

7.5二叉树的链式存储结构

相信大家也可以看出来,顺序存储的时候有一个很大的问题就是浪费空间,如果有一个之前那样的单支树,那么就要建立一个很长的数组来存放几个数据,这样是很划不来的,所以我们通常都使用链式存储结构来建立二叉树,我们来举一个例子:

像这样的二叉树,我们使用顺序存储结构至少要建立一个长度为15的数组才能完全存放,如果以后想要对其进行增加,那就非常麻烦;不过我们使用链式存储结构时,就很容易了:每个结点都有两个指针域,分别指向它的左孩子和右孩子,这样存储不仅方便而且容易对其进行增删改查。

但同时我们要知道,如果指向的结点没有元素数据,那么就存储为空,虽然存储为空占的空间很小,但是仍然占据了一定空间,这时我们就要了解,在含有 n 个结点的二叉链表中有 n + 1 个空链域,利用这些链域可以存储其他有效信息,最大化利用空间,从而得到另一种链式存储结构——线索链表,这种链表我们之后会整理到。

使用二叉链表可以使用指针域可以快速地得到某个结点的孩子信息,甚至在孩子信息的空链域中可以得到一些譬如兄弟结点、堂兄弟结点的信息、也可以存放双亲结点的信息,但这只是在空链域中,真正的要从孩子结点中获得双亲结点的信息还是有些困难的,所以我们还有一种链表——三叉链表:

在三叉链表中每个结点有三个指针域,除了指向左右孩子之外,还有一个指针指向双亲结点,这样就能很快地寻找双亲结点,就相当于我们学习链表时的双向链表。

使用代码来实现一下二叉链,首先需要建立ElementType.h、BinaryTree.h和BinaryTree.c文件,在ElementType.h中编写:

#ifndef ELEMENTTYPE_H_INCLUDED
#define ELEMENTTYPE_H_INCLUDED

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

//最大结点数
#define MAX_SIZE 1024
#define NAME_SIZE 255

typedef struct{
    int id;
    char name[NAME_SIZE];
}ElementType;

#endif // ELEMENTTYPE_H_INCLUDED

然后在BinaryTree.h中编写操作:

#ifndef BINARYTREE_H_INCLUDED
#define BINARYTREE_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>
#include "ElementType.h"
#include "TreeNode.h"

typedef struct{
    TreeNode *root; //根结点
    int length; //二叉链结点总数
    int depth; //二叉链深度
    int diameter; //从叶子节点到叶子结点的最长路径
}BinaryTree;

//初始化
void InitBinaryTree(BinaryTree *tree);

//构造二叉树,外部需要实现为结点分配内存
//返回值为0时表示创建失败(不创建)
int CreateBinaryTree(TreeNode *root);

#endif // BINARYTREE_H_INCLUDED

然后在BinaryTree.c中实现操作:

#include "BinaryTree.h"
#include <string.h>

//用来实现结点ID的自增长
static int id = 0;

//初始化
void InitBinaryTree(BinaryTree *tree){
    tree->root = NULL;
    tree->length = 0;
    tree->depth = 0;
    tree->diameter = 0;
}

//构造二叉树,外部需要实现为结点分配内存
//返回值为0时表示创建失败(不创建)
int CreateBinaryTree(TreeNode *root){
    //根结点为空,退出创建
    if(!root){
        return 0;
    }
    char inputName[NAME_SIZE]; //用户输入的结点名
    gets(inputName);
    //输入回车表示结束当前子树的创建
    if(strcmp(inputName, "\0") == 0){
        return 0;
    }
    //创建当前结点
    root->data.id = ++id;
    strcpy(root->data.name, inputName);
    //为左右结点做准备-为左右结点指针分配内存
    root->left = (TreeNode*)malloc(sizeof(TreeNode));
    root->right = (TreeNode*)malloc(sizeof(TreeNode));
    //分别递归创建左右子树
    printf("左结点:");
    if(CreateBinaryTree(root->left) == 0){
        //不再创建这个结点则销毁刚分配的内存
        free(root->left);
        root->left = NULL;
    }
     printf("右结点:");
    if(CreateBinaryTree(root->right) == 0){
        //不再创建这个结点则销毁刚分配的内存
        free(root->right);
        root->right = NULL;
    }
}

这些结构原理在注释中都讲的很清楚,大家一定要多看几遍注释,然后main.c中实现:

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

void TestBinaryTree();

int main(){
    TestBinaryTree();
    return 0;
}
void TestBinaryTree(){
    BinaryTree tree;
    InitBinaryTree(&tree);
    //容易遗漏的点:根结点需要事先分配内存
    tree.root = (TreeNode*)malloc(sizeof(TreeNode));
    printf("请输入根结点内容:");
    CreateBinaryTree(tree.root);

    free(tree.root);
}

编译通过,运行结果如下:

请输入根结点内容:A
左结点:B
左结点:
右结点:
右结点:C
左结点:
右结点:

Process returned 0 (0x0)   execution time : 10.633 s
Press any key to continue.

这样就实现了二叉树的链式存储结构,这里只实现了初始化和创建结点构造二叉树,至于遍历二叉树我们单独整理。

7.6四种遍历方式

遍历是二叉树操作中最常见的,它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。同时我们知道二叉树是树形结构,我们在观看的时候很容易可以找到规律,但是计算机不会,计算机只能将它处理为线性结构再来进行运算,所以我们就会使用遍历这种方式将二叉树变为线性结构。

遍历二叉树时从根节点出发,按照某种次序依次的访问二叉树中的所有结点,使得每个结点均被访问一次且仅被访问一次。

前序遍历

前序遍历的操作过程为先访问根结点、然后访问左子树、然后访问右子树。

在进行前序遍历时,如果二叉树为空,则返回空;否则:

  • 访问根结点;
  • 前序遍历访问左子树;
  • 前序遍历访问右子树。

这样写大家可能不能直观理解,解释一下:

  • 在每次访问左右子树时,被访问的结点就是它所在的子树的根结点;
  • 所以依然使用前序遍历先访问左结点再访问右结点;
  • 再次访问到下一个左子树时这个左结点又变为了它所在的子树的根结点;
  • 如此递归循环直到没有左子树可以访问,然后退出循环
  • 此时左子树已被访问,根据前序遍历应该访问右子树
  • 在右子树中重复上面的操作,直到所有的右子树也被访问完;
  • 这时候我们的位置一定在最下层的最有结点处,退出遍历。

或者如图所示:

要记得我们第一位访问的一定是根结点,然后是左子树,然后是右子树,所以前序遍历又可以记为:根左右遍历,代码非常简单,我们要了解的是思想。

使用代码实现一下,每次都是要自己输入非常麻烦,所以我们可以写一个测试用的满二叉树,在BinaryTree.h中编写操作:

//前序遍历:根左右
void PreOrderTraverse(TreeNode *root);

//测试版的创建函数
int CreateBinaryTree_Test(TreeNode *root);

在BinaryTree.c中实现操作:

//模仿用户输入顺序
char *nodeNames[] = {"A",
        "B", "D", "#", "#", "E", "#", "#",
        "C", "F", "#", "#", "G", "#", "#"};
//访问nodeNames数组中的下标
static int nodeNamesIndex = 0;

//测试版的创建函数
int CreateBinaryTree_Test(TreeNode *root){
    //根结点为空,退出创建
    if(!root){
        return 0;
    }
    char inputName[NAME_SIZE]; //用户输入的结点名
    //gets(inputName);
    strcpy(inputName, nodeNames[nodeNamesIndex++]);
    //输入回车表示结束当前子树的创建
    if(strcmp(inputName, "#") == 0){
        return 0;
    }
    //创建当前结点
    root->data.id = ++id;
    strcpy(root->data.name, inputName);
    //为左右结点做准备-为左右结点指针分配内存
    root->left = (TreeNode*)malloc(sizeof(TreeNode));
    root->right = (TreeNode*)malloc(sizeof(TreeNode));
    //分别递归创建左右子树
    //printf("左结点:");
    if(CreateBinaryTree_Test(root->left) == 0){
        //不再创建这个结点则销毁刚分配的内存
        free(root->left);
        root->left = NULL;
    }
    //printf("右结点:");
    if(CreateBinaryTree_Test(root->right) == 0){
        //不再创建这个结点则销毁刚分配的内存
        free(root->right);
        root->right = NULL;
    }
}

//前序遍历:根左右
void PreOrderTraverse(TreeNode *root){
    if(root){
        printf("[%d, %s]-", root->data.id, root->data.name);
        PreOrderTraverse(root->left);
        PreOrderTraverse(root->right);
    }
}

然后main.c中实现:

void TestBinaryTree(){
    BinaryTree tree;
    InitBinaryTree(&tree);
    //容易遗漏的点:根结点需要事先分配内存
    tree.root = (TreeNode*)malloc(sizeof(TreeNode));
    printf("请输入根结点内容:");
    //CreateBinaryTree(tree.root);
    CreateBinaryTree_Test(tree.root);
    printf("\n前序遍历:\n");
    PreOrderTraverse(tree.root);
    free(tree.root);
}

一个简单的满二叉树,编译通过,运行结果如下:

请输入根结点内容:
前序遍历:
[1, A]-[2, B]-[3, D]-[4, E]-[5, C]-[6, F]-[7, G]-
Process returned 0 (0x0)   execution time : 0.025 s
Press any key to continue.

中序遍历

有了前序遍历,那么就会有中序遍历、后序遍历。中序遍历与前序遍历大同小异,不过中序遍历的流程为左根右遍历

在进行中序遍历时,如果二叉树为空,则返回空;否则:

  • 中序遍历访问左子树;
  • 访问根节点;
  • 中序遍历访问右子树。

这里要注意我们遍历一定是从根结点开始,然后在中序遍历过程中访问的都是子树的根结点

我们使用代码来实现一下,有了上次的经验这次就好很多了,先在BinaryTree.h中编写操作:

//中序遍历:左根右
void InOrderTraverse(TreeNode *root)

在BinaryTree.c中实现操作:

//中序遍历:左根右
void InOrderTraverse(TreeNode *root){
    if(root){
        PreOrderTraverse(root->left);
        printf("[%d, %s]-", root->data.id, root->data.name);
        PreOrderTraverse(root->right);
    }
}

然后main.c中实现:

void TestBinaryTree(){
    BinaryTree tree;
    InitBinaryTree(&tree);
    //容易遗漏的点:根结点需要事先分配内存
    tree.root = (TreeNode*)malloc(sizeof(TreeNode));
    printf("请输入根结点内容:");
    //CreateBinaryTree(tree.root);
    CreateBinaryTree_Test(tree.root);
    printf("\n前序遍历:\n");
    PreOrderTraverse(tree.root);
    printf("\n中序遍历:\n");
    InOrderTraverse(tree.root);
    free(tree.root);
}

编译通过,运行结果如下:

请输入根结点内容:
前序遍历:
[1, A]-[2, B]-[3, D]-[4, E]-[5, C]-[6, F]-[7, G]-
中序遍历:
[2, B]-[3, D]-[4, E]-[1, A]-[5, C]-[6, F]-[7, G]-
Process returned 0 (0x0)   execution time : 0.026 s
Press any key to continue.

后序遍历

看到这里大家应该对后序遍历有点把握了吧,后序遍历是左右根遍历:

在进行后序遍历时,如果二叉树为空,则返回空;否则:

  • 后序遍历访问左子树;
  • 后序遍历访问右子树。
  • 访问根节点;

我们使用代码来实现一下,先在BinaryTree.h中编写操作:

//后序遍历
void PostOrderTraverse(TreeNode *root);

在BinaryTree.c中实现操作:

//后序遍历
void PostOrderTraverse(TreeNode *root){
    if(root){
        PreOrderTraverse(root->left);
        PreOrderTraverse(root->right);
        printf("[%d, %s]-", root->data.id, root->data.name);
    }
}

然后main.c中实现:

void TestBinaryTree(){
    BinaryTree tree;
    InitBinaryTree(&tree);
    //容易遗漏的点:根结点需要事先分配内存
    tree.root = (TreeNode*)malloc(sizeof(TreeNode));
    printf("请输入根结点内容:");
    //CreateBinaryTree(tree.root);
    CreateBinaryTree_Test(tree.root);
    printf("\n前序遍历:\n");
    PreOrderTraverse(tree.root);
    printf("\n中序遍历:\n");
    InOrderTraverse(tree.root);
    printf("\n后序遍历:\n");
    PostOrderTraverse(tree.root);
    free(tree.root);
}

编译通过,运行结果如下:

请输入根结点内容:
前序遍历:
[1, A]-[2, B]-[3, D]-[4, E]-[5, C]-[6, F]-[7, G]-
中序遍历:
[2, B]-[3, D]-[4, E]-[1, A]-[5, C]-[6, F]-[7, G]-
后序遍历:
[2, B]-[3, D]-[4, E]-[5, C]-[6, F]-[7, G]-[1, A]-
Process returned 0 (0x0)   execution time : 0.035 s
Press any key to continue.

层次遍历

最后我们来说一下层次遍历,我们还可以使用从上至下、从左至右的方式一层一层的遍历结点,这样看起来和我们储存时差不多,不过不能使用前面那样的递归实现了,因为他不是通过递归来存放的,而是遍历顺序与存储顺序相同,再深入一点就是打印顺序与存储顺序相同,先入先出式,所以我们就使用队列来实现。

首先我们要创建新的文件LinkedQueue.h与LinkedQueue.c,在LinkedQueue.h中编写操作:

#ifndef LINKEDQUEUE_H_INCLUDED
#define LINKEDQUEUE_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>
#include "ElementType.h"
#include "TreeNode.h"

//链队结点
typedef struct qNode{
    TreeNode *data;
    struct qNode *next;
}QueueNode;

//链队列
typedef struct{
    QueueNode *qFront;
    QueueNode *qRear;
}LinkedQueue;

void InitLinkedQueue(LinkedQueue *linkQueue);

void enQueue(LinkedQueue *linkedQueue, TreeNode *data);

TreeNode *deQueue(LinkedQueue *linkedQueue);

int IsLinkedQueueEmpty(LinkedQueue *linkedQqueue);

#endif // LINKEDQUEUE_H_INCLUDED

在LinkedQueue.c编写操作:

#include "LinkedQueue.h"

void InitLinkedQueue(LinkedQueue *linkQueue){
    linkQueue->qFront = (QueueNode*)malloc(sizeof(QueueNode));
    linkQueue->qFront->next = NULL;
    linkQueue->qRear = linkQueue->qFront;

}

void enQueue(LinkedQueue *linkedQueue, TreeNode *data){
    QueueNode *node = (QueueNode*)malloc(sizeof(QueueNode));
    node->data = data;
    node->next = NULL;
    linkedQueue->qRear->next = node;
    linkedQueue->qRear = node;
}

TreeNode *deQueue(LinkedQueue *linkedQueue){
    TreeNode *data = NULL;
    if(linkedQueue->qFront == linkedQueue->qRear){
        return data;
    }
    QueueNode *node = linkedQueue->qFront->next;
    data = node->data;
    linkedQueue->qFront->next = node->next;
    if(linkedQueue->qRear == node){
        linkedQueue->qRear = linkedQueue->qFront;
    }
    free(node);
    return data;
}

int IsLinkedQueueEmpty(LinkedQueue *linkedQqueue){
    if(linkedQqueue->qFront == linkedQqueue->qRear){
        return 1;
    }
    return 0;
}

在BinaryTree.h中编写操作:

//层序遍历
void ZOrderTraverse(TreeNode *root)

在BinaryTree.c中实现操作:

//层序遍历
void ZOrderTraverse(TreeNode *root){
    LinkedQueue queue;
    InitLinkedQueue(&queue);
    //根结点入队
    enQueue(&queue, root);
    while(!IsLinkedQueueEmpty(&queue)){
        TreeNode *root= deQueue(&queue);
        printf("[%d, %s]-", root->data.id, root->data.name);
        if(root->left != NULL){
            enQueue(&queue, root->left);
        }
        if(root->right != NULL){
            enQueue(&queue, root->right);
        }
    }
}

然后main.c中实现:

void TestBinaryTree(){
    BinaryTree tree;
    InitBinaryTree(&tree);
    //容易遗漏的点:根结点需要事先分配内存
    tree.root = (TreeNode*)malloc(sizeof(TreeNode));
    printf("请输入根结点内容:");
    //CreateBinaryTree(tree.root);
    CreateBinaryTree_Test(tree.root);
    printf("\n前序遍历:\n");
    PreOrderTraverse(tree.root);
    printf("\n中序遍历:\n");
    InOrderTraverse(tree.root);
    printf("\n后序遍历:\n");
    PostOrderTraverse(tree.root);
    printf("\n层序遍历:\n");
    ZOrderTraverse(tree.root);
    free(tree.root);
}

编译通过,运行结果如下:

请输入根结点内容:
前序遍历:
[1, A]-[2, B]-[3, D]-[4, E]-[5, C]-[6, F]-[7, G]-
中序遍历:
[2, B]-[3, D]-[4, E]-[1, A]-[5, C]-[6, F]-[7, G]-
后序遍历:
[2, B]-[3, D]-[4, E]-[5, C]-[6, F]-[7, G]-[1, A]-
层序遍历:
[1, A]-[2, B]-[5, C]-[3, D]-[4, E]-[6, F]-[7, G]-
Process returned 0 (0x0)   execution time : 0.042 s
Press any key to continue.

今天我们整理了树和二叉树的一些知识点,明天我们将把剩下的关于树的知识点整理完,我们下次见👋

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值