一图彻底搞懂数据结构二叉树的创建与遍历

二叉树基本概念

关于二叉树的基本概念大家可以参考这位博主整理的博客:
数据结构 二叉树的建立与遍历

以下概念摘自上面这位博主的博客中整理的内容,对基本概念了解的朋友可以直接条跳到目录中“一图彻底搞懂递归创建”这一小节,
二叉树(Binary Tree)是n(n >= 0)个节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两颗互不相交的,分别称为根节点的左子树和右子树的二叉树组成。

二叉嘛,也就是每个节点最多有两个分支。

图示:
在这里插入图片描述

二叉树具有五种基本形态:

1.空二叉树

2.只有一个根节点

3.根节点只有左子树

4.根节点只有右子树

5.根节点既有左子树又有右子树

特殊的二叉树:

1.斜树(只有左子树的称为左斜树,只有右子树的称为右斜树,两者统称为斜树)

2.满二叉树(所有的分支节点都存在左子树和右子树,并且所得树叶都在同一层上)

3.完全二叉树(对一颗具有n个节点的二叉树按层序编号,如果编号为i(1 <= i <= n)的节点与同样深度的满二叉树中编号为i的节点在二叉树中位值完全相同)

如果理解不了看下面的图,体会它们的共同特征

完全二叉树的特征:

1.叶子结点只能出现在最下两层

2.最下层的叶子一定集中在左部连续位置

3.倒数二层,若有叶子结点,一定都在右部连续位置

4.如果节点度为1(这里的度与离散数学中的度不一样,这里的度是指节点所拥有的子树数),则该节点只有左孩子,即不存在只有右子树的情况

5.同样节点数的二叉树,完全二叉树的深度最小

二叉树的性质:

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

性质2:深度为k的二叉树至多有2^k - 1个节点(k >= 1)

性质3:对任何一颗二叉树T,如果其终端节点数为n0,度为2的节点数为n2,则n0 = n2 + 1

性质4:具有n个节点的完全二叉树的深度为[log2n] + 1([]表示不大于x的最大整数)

性质5:如果对一颗有n个节点的完全二叉树(其深度为[long2n] + 1)的节点按层序编号(从第1层到第[log2n] + 1层,每层从左到右),对任一节点i(1 <= i <= n)有

①如果i = 1,则节点i是二叉树的根,无双亲;如果i > 1,则双亲是节点[i / 2]

②如果2i > n,则节点i无左孩子(节点i为叶子结点);否则其左孩子是节点2i

③如果2i + 1 > n,则节点i无右孩子;否则其右孩子是节点2i + 1

二叉树的存储结构同样分为两种:

一种为二叉树顺序存储结构,另一种为二叉树的链式存储结构

一般顺序存储结构只用于完全二叉树

下面我们来介绍链式存储结构的二叉树

二叉链表:有一个数据域和两个指针域

二叉树的递归创建

关于二叉树的创建,比较推荐的方法就是递归创建,后面你会发现,递归思想极大的简化了我们的代码量,虽然递归调用过程中需要不停的进行压栈操作,理论上讲会由于栈的不断生长造成溢出,无法创建巨型二叉树,但是对于绝大多数的二叉树而言,这种情况基本不会发生。

二叉链表的节点结构定义

二叉链表的节点由一个数据域和两个指针域组成,这两个指针域分别指向该节点的左孩子和右孩子
在这里插入图片描述
在这里插入图片描述

定义如下:

typedef struct BiTNode
{
    
    
    TElemType data; //节点数据
    struct BiTNode *lchild, *rchild;    //左右孩子指针
}BiTNode, *BiTree;

递归创建

注意:以下关于递归过程和遍历过程的解释均参照下图这一二叉树
在这里插入图片描述
代码如下:

  • 输入顺序要求:先输入根,再输入左子树,再输入右子树(左边优先)
  • 空节点以‘#’代替
void CreatBiTree(BiTree &T)
{
    
    
    ElemType ch;
    cin >> ch;
    if(ch == '#')    //#代表为空
        T = BULL;
    else
    {
    
    
        T = new BiTNode;    //生成根节点
        if(!T)
            exit(1);
        CreatBiTree(T->lchild); //构造左子树
        CreatBiTree(T->rchild); //构造右子树
    }
}

???这就创建完了?
对,没有错,就是这么少!
不理解? 想不出上述代码的递归过程?那请看下图:

一图彻底搞懂递归创建

上面的代码看不懂没关系,因为递归过程本来就绕来绕去,挺难想的,还是得画一下,下图就是本文之精华,箭头上的数字指示了创建的顺序和历程。
举例说明如下:
图中的 “1”号箭头,表明这是第一次递归过程,“2”号箭头表明第一次递归过程被第二次递归打断,“END2” 表明第二次递归结束,“END1”表明第一次递归在被打断且经过一系列子递归后终于结束。
以此类推:
在这里插入图片描述

二叉树的遍历

先来个简单的对比:

前序遍历

//前序遍历
void PreOrderTraverse(BiTree T, int level)
{
    
    
    if(!T)
        return;
    visit(T->data, level);    //访问节点数据
    PreOrderTraverse(T->lchild, level+1);
    PreOrderTraverse(T->rchild, level+1);
}

中序遍历

//中序遍历
void InOrderTraverse(BiTree T, int level)
{
    
    
    if(!T)
        return;
    InOrderTraverse(T->lchild, level+1);
    visit(T->data, level);    //访问节点数据
    InOrderTraverse(T->rchild, level+1);
}

后序遍历

//后序遍历
void PostOrderTraverse(BiTree T, int level)
{
    
    
    if(!T)
        return;
    PostOrderTraverse(T->lchild, level+1);
    PostOrderTraverse(T->rchild, level+1);
    visit(T->data, level);    //访问节点数据
}

讨论:遍历顺序的前中后到底有何不同?

我们会发现,所谓前序/中序/后序,不过就是访问数据域的那一行的代码的位置不同,那这会带来什么影响呢?
在这里插入图片描述
上图为递归创建链式二叉树的D节点的创建过程,下面我们来观察一下如果用三种不同的方法遍历到这一节点会发生什么

  • 按照前序遍历的流程:
    visit(T->data, level);    //访问节点数据
    PreOrderTraverse(T->lchild, level+1);
    PreOrderTraverse(T->rchild, level+1);

由于if(!T)会检查节点是否为空,所以
该节点的数据域将会在第4次递归开始前就被打印到屏幕上,即先访问数据域再开始递归。

  • 按照中序遍历的流程:
//中序遍历
    InOrderTraverse(T->lchild, level+1);
    visit(T->data, level);    //访问节点数据
    InOrderTraverse(T->rchild, level+1);

由于if(!T)会检查节点是否为空,所以
该节点的数据域将会在第4次递归后第5次递归开始前被打印到屏幕上,即先访问左孩子,然后打印该节点的数据域,再开始访问右孩子。

  • 按照后序遍历的流程:
//后序遍历
    PostOrderTraverse(T->lchild, level+1);
    PostOrderTraverse(T->rchild, level+1);
    visit(T->data, level);    //访问节点数据

由于if(!T)会检查节点是否为空,所以
该节点的数据域将会在第4次、第5次递归都结束后才去访问当前节点的数据域并将其打印到屏幕上,即先访问完左右孩子的递归结束后再打印。

值得注意的是:这里举得例子较为简单,因为该数据域为D的节点的左右孩子节点都是空的,所以if(!T)使得子递归迅速结束(空操作),如果以数据域为B的那个节点举例,要注意当前递归结束的条件必须是其子递归都结束。读者可结合递归创建图解将我们的示例二叉树带入,会有更深的体会!

代码整合:

注意:遍历函数中的传入的实参为:level+1,切忌写为level++,由于递归关系,level+1中的level作为实参传入递归函数后,level本身压栈,下次出栈时还是level;
而一旦写为level++,虽然传入的实参从数值上并未改变,但是,递归到来前压栈的level值却是level自身+1后的值,而不再是原来的level本身,自然下次出栈时就不再是未+1前的level了,那么当这个层值传给遍历右孩子的函数时,层级关系自然就不对了!!!

#include <iostream>
#include <cstdlib>
using namespace std;
//节点层数变量定义
int pre_level=1, in_level=1, post_level=1;
 
//二叉树链表的存储结构
typedef struct BiTNode
{
    
    
    char data;   //节点数据
    struct BiTNode *lchild, *rchild;    //左右孩子指针
}BiTNode, *BiTree;
//二叉树的建立
void CreatBiTree(BiTree &T)
{
    
    
    char ch;
    cin >> ch;
    if(ch == '#')
        T = NULL;
    else
    {
    
    
        T = new BiTNode;    //生成根节点
        if(!T)
            exit(1);
        T->data = ch;
        CreatBiTree(T->lchild); //构造左子树
        CreatBiTree(T->rchild); //构造右子树
    }
}

//打印数据域和层级
void visit(char data, int level)
{
    
    
	cout << data;    //打印节点数据
	cout <<"位于第"<< level << "层" << endl; // 显示节点层数
}
//前序遍历
void PreOrderTraverse(BiTree T, int level)
{
    
    
    if(!T)
        return;
    visit(T->data, level);    //访问节点数据
    PreOrderTraverse(T->lchild, level+1);
    PreOrderTraverse(T->rchild, level+1);
}
//中序遍历
void InOrderTraverse(BiTree T, int level)
{
    
    
    if(!T)
        return;
    InOrderTraverse(T->lchild, level+1);
    visit(T->data, level);    //访问节点数据
    InOrderTraverse(T->rchild, level+1);
}
//后序遍历
void PostOrderTraverse(BiTree T, int level)
{
    
    
    if(!T)
        return;
    PostOrderTraverse(T->lchild, level+1);
    PostOrderTraverse(T->rchild, level+1);
    visit(T->data, level);    //访问节点数据
}
int main()
{
    
    
    BiTree T;
	
    CreatBiTree(T);
    cout << "前序排序:";
    PreOrderTraverse(T, pre_level);    //前序遍历
    cout << endl;
    cout << "中序排序:";
    InOrderTraverse(T, in_level);     //中序遍历
    cout << endl;
    cout << "后序排序:";
    PostOrderTraverse(T, post_level);   //后序遍历
    cout << endl;
    return 0;
}

输出结果如下:
在这里插入图片描述
对照这一图片,可以验证创建和遍历输出均正确:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_28816873/article/details/108018931