• 友链

  • 首页

  • 文章归档
h u a n b l o g
h u a n b l o g

欢

HI,Friend

04月
01
数据结构

二叉树运算

发表于 2022-04-01 • 字数统计 13847 • 被 1,602 人看爆

二叉树的存储结构

顺序存储结构

完全二叉树的顺序存储结构

完全二叉树的顺序存储.png

性质五修改为:

如果将一棵有n个结点的完全二叉树按层从0开始编号,则对任一编号为i(0 ≤ i < n)的结点X有:
若i=0,则结点X是根;若i>0,则x的双亲的编号为[(i-1)/2]
若2i+1 < n,则结点X的左孩子编号为2i+1,否则,结点X无左孩子(且无右孩子),必定为叶子结点
若2i+2 < n,则结点X的右孩子的编号为2i+2,否则x无右孩子。

一般二叉树的顺序存储

一般二叉树的顺序存储.png

顺序存储二叉树的优点和缺点

  • ①对完全二叉树而言,顺序存储结构既简单又节省存储空间。
  • ②一般的二叉树采用顺序存储结构时,虽然简单,但易造成存储空间的浪费。最坏的情况下,一个深度为k且只有k个结点的右单支树需要2k-1个结点的存储空间(相当于满二叉树)。
  • ③在对顺序存储的二叉树做插入和删除结点操作时,要大量移动结点。

链式存储结构

二叉链结点结构

lchild data rchild

说明:

  • ①数据域data,用以存放元素值。
  • ②指针域lchild和rchild,分别指向该结点的左孩子和右孩子。没有左孩子时lchild=NULL,没有右孩子时rchild=NULL,

二叉链顺序存储结构例.png

二叉链结点类型定义

typedef struct node
{
    DataType data;
    Struct node *lchild,*rchild;           //左右孩子指针
    
}BinTNode;                                  //结点类型

typedef BinTNode *BinTree;                  //BinTree为指向BinTNode类型结点的指针类型

三叉链结点结构

lchild data parent rchild

说明: 增加的parent域指向其双亲

三叉链结构例.png

二叉树的运算

二叉树的生成

按广义表

按广义表表示二叉树结构生成二叉链表的算法

按广义表生成二叉链表.png

算法中用了一个指针数组来模拟栈存储结点的双亲指针,根据读入广义表中的字符分四种不同的情况进行处理:

算法描述

BinTNode * CreateTree(char * str)
{ 
    //str为存储广义表的字符串的指针
    BinTNode *st[100];                      //用指针数组模拟栈
    BinTNode *p=NULL;                   
    int top,k,j=0;
    top= -1;                                //置空栈
    char ch=str[j];                         //存放广义表的字符串的数组
    BinTNode *b=NULL;                       
    while(ch!='\0')                         //循环读广义表字符串中字符
    {
        switch(ch)
        {
            case '(': 
                top+t; st[top]=p; k=1;      //左括号表示新的子树的开始,所以刚建立的结点指针入栈
                break;
            case ')' : 
                top--;                      //右括号表示一个子树的结束,栈顶元素没有子树,出栈
                break;
            case ',': 
                k=2;
                break;
            default: 
                p=(BinTNode*)malloc(sizeof(BinTNode));  //读到的是字符
                p->data=ch;                             //填写数据域
                p->lchild=p->rchild=NULL;               //填写指针域
                if(b==NULL)                             //建立第一个结点
                {
                    b=p;
                }
                else {
                    switch(k) 
                    {
                        case 1: 
                            st[top]->lchild=p;          //前一个字符是'(',该结点应该插入到左子树
                            break;
                        case 2: 
                            st[top]->rchild=p;          //前一个字符是')',该结点应该插入到右子树
                            break;
                    }
                } 
        }
        j++;ch=str[j];                                  //读取下一个字符
        
    }
    
    return b;                                           //返回根结点的指针

}

按完全二叉树

按完全二叉树的层次顺序依次输入结点信息建立二叉链表的算法

按完全二叉树生成二叉链表.png

算法思想
  首先对一般的二叉树添加若干个虚结点,使其成为完全二叉树,然后依次输入结点信息,若输入的结点不是虚结点"@",则建立一个新结点,若是第一个结点,则令其为根结点,否则将新结点作为左孩子或右孩子链接到它的双亲结点上。如此重复下去,直到输入结束符号"#"时为止(假设结点数据域为字符型)。
  为了使新结点能正确地链接到其双亲结点,可设置一个指针数组作为队列,保存已输入的结点地址。因为是按层自左向右输入结点,所以先输入的结点的孩子结点先入队列,可以利用队列的队头指针front指向当前结点的双亲结点,利用队尾指针rear指向当前结点。若rear为偶数,则说明当前结点应作为左孩子链接到front所指向的结点上;否则,当前结点应作为右孩子链接到front所指向的结点上,链接后,使队头指针front指向下一个双亲结点。若当前结点为虚结点,则无需链接

算法描述

BinTree CreateBinTree (BinTree bt)
{
    //Q[1. . n]是一个BinNode类型的指针数组,front和rear分别是队头和队尾指针
    BinTNode *Q[100];                           //定义队列
    BinTNode *s;
    int front,rear; 
    char ch;
    ch=getchar(); 
    bt=NULL;                                    //置空二叉树      
    front=1; rear=O;                            //初始化队列
    while(ch!='#')                              //假设结点值为单字符,#为终止符。
    {
        s=NULL;                                 //先假设读入的为虚结点"@"
        if(ch!='@')
        {
            s=(BinTNode* )malloc(sizeof(BinTNode));         //申请新结点
            s->data=ch; 
            s->lchlid=s->rchiId=NULL;                       //新结点赋值
        }   
        rear++;                                             //队尾指针自增
        Q[rear]=s;                                          //将新结点地址或虚结点地址(NULL)入队
        if(rear==1)                                         //若rear为1,则说明是根结点,用bt指向
            bt=s;
        else
            {
                if(s!=NULL&&Q[front] !=NULL)                   //当前结点及其双亲结点都不是虚结点
                {
                    if(rear%2==0)                              //rear为偶数,新结点应作为左孩子
                        Q[front]->lchild=s;
                    e1se
                        Q[front]->rchild=s;                   //rear为奇数,新结点应作为右孩子
                    
                    if(rear%2!=0)
                        frontt+;                              //rear为奇数,说明front所指结点的左右儿子都处理完了,因此front加1指向下一个双亲
                        
               }
            }
            ch=getchar();                                   //读下一个结点值
    }
    
    return bt;
    
}

二叉树的遍历

遍历: 是指沿着某条搜索路径(线)周游二叉树,依次对树中每个结点访问且仅访问一次。

遍历方案

从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。因此,在任一给定结点上,可以按某种次序执行三个操作:

  • ①访问根结点本身(D),
  • ②遍历根结点的左子树(L),
  • ③遍历根结点的右子树(R)。

以上三种操作有六种执行次序:
DLR(根左右)、LDR(左根右)、LRD(左右根);
DRL(根右左)、RDL(右根左)、RLD(右左根)。

注意: 前三种次序(按先左后右)与后三种次序(按先右后左)对称,故只讨论先左后右的前三种次序。

递归遍历算法

三种遍历的递归定义:

(1)先序遍历DLR(根左右): 也叫先根遍历,若二叉树非空,则依次执行如下操作:

  • ①访问根结点;
  • ②遍历左子树;
  • ③遍历右子树。

(2)中序遍历LDR(左根右): 也叫中根遍历,若二叉树非空,则依次执行如下操作:

  • ①遍历左子树;
  • ②访问根结点;
  • ③遍历右子树。

(3)后序遍历LRD(左右根): 也叫后根遍历,若二叉树非空,则依次执行如下操作:

  • ①遍历左子树;
  • ②遍历右子树;
  • ③访间根结点。

例题1

分别写出图所示的二叉树的前序、中序、后序遍历序列。

递归遍历例题.png

例题2

假设二叉树的RNL遍历算法定义如下:
若二叉树非空,则依次执行如下操作:
(1)遍历右子树;
(2)访问根节点;
(3)遍历左子树。
已知一棵二叉树如图所示,请给出其RNL遍历的结果序列。

递归遍历例题2.png

解析:

根据二叉树的RNL(右根左)遍历算法定义和我们已经研究过的LNR(左根右)遍历算法定义,可以写出该二叉树的RNL(右根左)遍历的结果序列:GCFABD;二叉树的LNR(左根右)遍历的结果序列:DBAFCG;二者是对称的。

三种遍历算法

(1)前序遍历递归算法:

void Preorder(BinTree bt)
{
    //采用二叉链表存储结构,并设结点值为字符型
    if(bt!=NULL)
    {
        printf("%c",bt->data);         //访问根结点
        Preorder(bt->lchild);           //前序遍历左子树
        preorder(bt->rchild);           //前序遍历右子树
    }
}

(2)中序遍历递归算法:

void Inorder(BinTree bt)
{ 
    if(bt!=NULL)
    {
        Inorder(bt->lchild);            //中序遍历左子树
        printf("%c",bt->data);         //访问根结点
        Inorder(bt->rchild);            //中序遍历右子树
    }


(3)后序遍历递归算法:

void Postorder(BinTree bt)
{ 
    if(bt!=NULL)
    {
        Postorder(bt->lchild);          //中序遍历左子树
        Postorder(bt->rchild);          //中序遍历右子树
        printf("%c",bt->data);         //访问根结点
    }

递归算法描述

为便于理解递归算法,以下图(二叉树和二叉链表)为例,说明上面算法Preorder遍历二叉树的执行过程

二叉树和二叉链表表示示意图.png

前序遍历二叉树算法的执行过程示意图:
前序遍历二叉树算法的执行过程示意图.png

过程
二叉链表左边序号假设为该结点的存储地址

  当一个函数调用前序遍历算法时,首先需要以指向二叉树根结点的地址1作为实参,将它传递到算法中的形参bt,然后执行算法。在算法执行过程中,总是先前序遍历左子树,此时的右子树并没有得到及时的遍历,所以要记住此时右子树的访问结点,等左子树遍历结束后再回来遍历右子树,因此需要一个工作栈来存储右子树bt->child的访问结点
  假定该工作栈为S,当一开始调用函数时,由于bt不为NULL,访问根结点,输出A,递归调用前序遍历左子树bt=2,此时右子树的地址3存入S中,如图(a)所示,bt=2不为NULL,访问bt指向的结点,输出B,再递归调用,bt=4,右子树的地址5存入S中,如图(b)所示。此时的左子树bt不为NULL,访问bt指向的结点,输出D,此时左子树为空,右子树也为空,回溯到上一层,遍历2的右子树5,输出E,因为5的左右子树均为空,退栈,遍历1的右子树3,输出C,左子树空,再遍历右子树,输出F,这时左右子树均为空,遍历结束
  访问结点次序为:ABDECF

二叉树遍历的非递归算法

  由中序遍历递归算法的执行过程可知,递归工作栈包括两项:一项是递归调用的语句编号;另一项是指向根结点的指针
  当栈顶记录中的指针值为非空时,应该遍历左子树,即指向左子树根结点的指针进栈。否则当栈顶记录中的指针值为空时,则应该退回到上一层,此时若是从左子树返回,则应该访问当前栈顶记录中指针所指向的根结点;若是从右子树返回,则说明当前层已遍历结束,继续退栈

(1)利用栈的非递归中序遍历算法:

void Inorder1(BinTree bt)
{ 
    // 采用二叉链表存储结构
    SeqStack S; BinTNode *P;
    InitStack(&S);
    Push(&S,bt);                       //根结点入栈
    while(!StackEmpty(&S))
    {
        while(GetTop(&S))               //读栈顶元素,当栈顶不为空
        {
            Push(&S,GetTop (&S)->lchild);      //左孩子依次入栈,直到左子树空为止
        }
        p=Pop(&S);                      //最后一个入栈的空指针退栈
        if(!StackEmpty(&S))
        {
            printf("%c",GetTop(&S)->data);     //访问根结点
            p=Pop(&S);
            Push(&s,p->rchild);                //右子树进栈
        }
    }

(2)利用指针数组模拟栈实现的非递归中序遍历算法:

void Inorder2(BinTree bt)
{
    //二叉树非递归中序遍历算法
    BinTNode *ST[100];              //用指针数组模拟栈
    int top=0;                      //初始化数组
    ST[top]=bt; 
    do {
        while(ST[top] !=NULL)       //根结点及其所有的左结点地址装入数
        {
            top=top+1;
            ST[top]=ST[top-1]->lchild;
        }
        top=top-1;                  //最后一个入数组的空指针退"栈”
        if(top>=0)                  //判数组中地址是否访问完
        {
            printf("%c",ST[top]->data);        //访问结点
            ST[top]=ST[top]->rchild;            //扫描右子树
        }
    }while(top!=-1);
}


(3)利用栈的非递归前序遍历算法:
算法思想: 利用栈先将二叉树根结点指针入栈,然后执行出栈,获取栈顶元素值(即结点指针),若不为空值,则访问该结点,再将右、左子树的根结点指针分别入栈,依次重复出栈、入栈,直至栈空为止。

void Preorder1(BinTree bt)
{ 
    SeqStack S;
    InitStack(&S);              //初始化栈
    Push(&s, bt);              //根结点指针进栈
    while(!StackEmpty(&S))
    {
        bt=Pop (&S);            //出栈
        if(bt !=NULL)
        {
            printf("%c",bt->data);     //访问结点,假设数据域为字符型
            Push(&S,bt->rchild);       //右子树入栈先访问左子树,栈先进后出
            Push(&S,bt->lchiid);       //左子树入栈
        }
    }


(4)非递归的按层遍历二叉链表树:
按层遍历二叉树:从上到下,从左到右遍历二叉树。

例:
对下图二叉树按层进行遍历
按层遍历二叉链表.png

算法思想
采用一队列Q,若树不空,先将二叉树根结点输出,并将根结点指针入队,然后出队。若根结点有左子树,则将左子树的根结点输出并将其指针入队﹔若其有右子树,则将其右子树的根结点输出并将其指针入队,再出队,如此下去,直至队列空为止。

算法描述:

void TransLevel(BinTree bt)
{ 
    cirQueue Q;                 //按层遍历二叉树,从上到下,从左到右
    InitQueue(&Q);              //初始化队列为空队列
    if(bt==NULL)
        return;
    else{   
        printf("%c",bt->data);     //输出根结点,假设其数据域为字符型
        EnQueue(&Q,bt);            //根结点指针入队
        while(!QueueEmpty(&Q))
        {
            bt=DeQueue(&Q);         //出队列
            if(bt->lchild!=NULL)
            {
                printf("%c",bt->lchild->data);         //输出左子树根结点
                EnQueue(&Q,bt->lchild);                //左子树入队
            }
            
            if(bt->rchild!=NULL)
            {
                printf("%c",bt->rchild->data);         //输出右子树根结点
                EnQueue(8Q,bt->rchild);                //右子树入队列
            }
        }
    }

}   

由遍历序列恢复二叉树

已知二叉树的前序遍历序列和中序遍历序列,可以唯一地恢复该二叉树。
原则: 在前序序列中确定根结点〈最前面那个结点一定是根结点〕,然后根据根结点在中序序列中的位置分出根结点的左、右子树(根结点前面的那些结点为根结点的左子树上的结点,根结点后面的那些结点为根结点的右子树上的结点)。恢复该二叉树的任何一棵子树的过程仍然遵循这个原则。

例1

如下图二叉树,写出前、中、后序遍历序列

二叉树运算例题1.png

解析

前序序列:ABDHEICFG
中序序列:DHBEIAFCG
后序序列:HDIEBFGCA

例2

已知二叉树的前序和中序遍历序列或中序和后序遍历序列,求其二叉树
前序序列:ABDEGHCFI
中序序列:DBGEHACIF

分析:

根据二叉树的三种遍历算法可以得出这样一个结论:已知一个二叉树的前序和中序遍历或中序和后序遍历序列,可以唯一地确定一颗二叉树。具体方法如下:
(1)根据前序或后序遍历序列确定二叉树的各子树的根;
(2)根据中序遍历确定各子树根的左、右子树

求解过程

(1)由前序遍历序列确定二叉树的根为A,再由中序遍历确定A的左、右子树
A BDEGH CFI   //前序遍历序列的根、左子树和右子树
BDEGH A CIF   //中序遍历序列的左子树、根和右子树
(2)确定A的左子树
B D EGH  //前序遍历左子树的根、左子树和右子树
D B GEH  //中序遍历左子树、根和右子树
(3)再确定B的右子树
由前序序列EGH和中序序列GEH唯一确定E为根,G、H分别为左子树和右子树
E G H  //前序遍历左子树的根、左子树和右子树
G E H  //前序遍历左子树、根和右子树
(4)确定A的右子树
C FI  //前序遍历A右子树的根和右子树
C IF  //中序遍历A右子树的根和右子树
(5)再确定C的右子树。
由前序FI和中序IF确定F为根,I为左子树
F I  //前序遍历C左子树的根和左子树
I  F //中序遍历C左子树的根和左子树
由此可得该二叉树的后序遍历序列为:DGHEBIFCA
二叉树运算例题2.png

例3

已知一棵二叉树的前序遍历序列与中序遍历序列分别为:
前序遍历序列:A B C D E F G H I
中序遍历序列:B C A E D G H F I 
试恢复该二叉树。

解析

【解答】按照上述分解原则求得整棵二叉树的过程如所示。
遍历序列恢复二叉树例.png
同理,已知二叉树的中序遍历序列和后序遍历序列,也可以唯一地恢复该二叉树,只是在后序序列中去确定根结点〈最后面那个结点一定是根结点),而在中序序列中分出左右子树的过程与上述过程没有区别。
已知二叉树的前序遍历序列和后序遍历序列,无法唯一地恢复该二叉树,因为无法确定左右子树。

分享到:
线索二叉树
树与二叉树
  • 文章目录
  • 站点概览
欢

网红 欢

你能抓到我么?

Email RSS
看爆 Top5
  • mac系统版本与Xcode版本有冲突 4,383次看爆
  • JAVA_HOME环境配置问题 4,038次看爆
  • AssetBundle使用 3,711次看爆
  • VSCode配置C++开发环境 3,481次看爆
  • OpenGL笔记5-变换 3,408次看爆

Copyright © 2025 欢 粤ICP备2020105803号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图