树和二叉树的详解

1 树

1.1 树的定义

(Tree)是n(n>=0)个结点的有限集。在任何一棵树非空中:

(1)有且仅有一个特定的称为根(Root)的结点;

(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm ,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

注意:树是一种非线性的数据结构,也是一种逻辑结果,同时也是一种分层结果。

1.2 相关基本术语

以上图为例讲解有关树的术语。

结点∶树的结点包含一个数据元素及若干指向其子树的分支。A、B、C、D、E等都是结点,但是结点不仅包含数据元素,而且包含指向子树的分支。例如,B结点不仅包含数据元素 B,而且包含2个指向E、F对应子树的指针。 度:结点拥有的子树数或者分支的个数称为结点的度 (Degree) 。例如,B结点有2棵子树,所以 B结点的度为2。 叶子结点:度为的结点称为叶子 (Leaf) 或终端结点。

分支结点:又叫作非终端结点,指度不为0的结点,如 A、B、C、D、E、H结点都是非终端结点。除了根结点之外的非终端结点,也叫作内部结点,如 B、C、D、E、H结点都是内部结点。

孩子结点:结点的子树的根,例如,B结点有2棵子树,所以 B结点的孩子为E、F。

双亲结点:结点的上一个结点,与孩子结点的定义相对应。例如, B结点的双亲为A。

兄弟结点:拥有同一个双亲的结点之间称为兄弟结点。例如, B结点的兄弟为C、D。

祖先结点:从根到某结点的路径上的所有结点,都是这个结点的祖先。例如,L的祖先有A、B、F。

子孙结点:以某结点为根的子树中的所有结点,都是该结点的子孙。如 B的子孙为E、F、K、L。

层次:从根开始,根为第一层,根的孩子为第二层,根的孩子的孩子为第三层,以此类推。该图的总层次为四层。

树的高度(或者深度)∶树中结点的最大层次。如例子中的树共有4层,所以高度为4。

森林 (Forest):m(m~O) 棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。

1.3 树的性值

(1)树中的结点数等于所有结点的度数之和加1。

(2)度为m的树中第i层上至多有个结点(i≥1)。

(3)高度为h的m叉树至多有个结点3。

(4)具有n个结点的m叉树的最小高度为向上取整。

2 二叉树

2.1 二叉树的定义

二叉树 (Binary Tree) 是另一种树型结构,它的特点是每个结点至多只有两棵子树(即二叉树中不存在度大于的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。

(1)每个结点最多只有两棵子树,即二叉树中结点的度只能为0、1、2。

(2)子树有左右顺序之分,不能颠倒。

由此,二叉树的五种形态,如图所示。

(a)空二叉树

(b)仅有一个结点的二叉树

(c)只有左子树的二叉树

(d)只有右子树的二叉树

(e)左右子树都有的二叉树

注意:二叉树与度为2的有序树的一些区别,首先度为2的树,其至少存在3个结点,但是而二叉树可以为空,不存在结点。度为2的有序树的孩子结点的左右次序是相对于另一孩子结点而言的,没有明确的左右之分。若某个结点只有一个孩子结点,则这个孩子结点就无须区分其左右次序,而二叉树无论其孩子数是否为2,都需要确定其左右位置。

2.2 特殊的二叉树

(1)满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。. 也就是说,如果一个二叉树的层数为K,且结点总数是 (2^k) -1 ,则它就是满二叉树。满二叉树的结点要么是叶子结点,要么它有两个子结点。

(2)完全二叉树:一棵深度为k的有n个结点的 二叉树 ,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与 满二叉树 中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

(3)二叉排序树:又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。

一棵空树,或者是具有下列性质的二叉树:

1.若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2.若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

3.左、右子树也分别为二叉排序树;

(4)平衡二叉树:树上任意一结点的左子树和右子树的深度之差不超过1。

2.3 二叉树的性质

性质1    在二叉树的第i层上至多有个结点(i>=1)

性质2    深度为k的二叉树至多有个结点,(k>=1)

性质3    对任何一棵二叉树T,如果其终端结点数为,度为2的结点数为,则=+1

设n,为二叉树T中度为1的结点数。因为二叉树中所有结点的度均小于或等于2.所以其结点总数为

再看二叉树中的分支数。除了根结点外,其余结点都有一个分支进入,设B为分支总数﹐则n=B+1。由于这些分支是由度为1或2的结点射出的,所以又有于是得

由上两个公式得

性质4    具有n个结点的完全二叉树的深度为

证明:假设深度为k,则根据性质2和完全二叉树的定义有

     或     

于是  ,因为k是整数,所以

性质5    如果对一棵有n个结点的完全二叉树(其深度为)的结点按层序编号(从第1层到第)层,每层从左到右),则对任一结点i(1≤i≤n),有

(1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲PARENT(i)是结点

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

(3)如果2i+1>n,则结点主无右孩子;否则其右孩子RCHILD(i)是结点2i+1。

我们只要先证明(2)和(3),便可以从(2)和(3)导出(1)。

对于i=1,由完全二叉树的定义﹐其左孩子是结点2。若2>n,即不存在结点2,此时结点主无左孩子。结点主的右孩子也只能是结点3,若结点3不存在,即3>n,此时结点i无右孩子。

对于i>1可分两种情况讨论:(1)设第j(1≤j≤)层的第一个结点的编号为i(由二叉树的定义和性质2可知i=),则其左孩子必为第j+1层的第一个结点,其编号为,若2i>n,则无左孩子;其右孩子必为第j+1层的第二个结点,其编号为2i+1,若2i+1>n,则无右孩子;(2)假设第层上某个结点的编号为,且2i+1≤n,则其左孩子为2i,右孩子为2i+1,又编号为i+1的结点是编号为i的结点的右兄弟或者堂兄弟,若它有左孩子,则编号必为2i+2=2(i+1),若它有右孩子,则其编号必为2i+3=2(i+1)+1。

2.4 二叉树的存储结构

可用顺序存储和链式存储两种结构来表示二叉树,但是要注意二叉树本来就是一种非线性的存储结构。

2.4.1 二叉树的顺序存储

#define MAX TREE_SIZE   100
typedef int SqBiTree[MAX TREE_SIZE];
SqBitree bt;

顺序存储是采用一段连续的存储单元依次自上而下、自左往右存储完全二叉树的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下标为i-1的分量中。

该存储方式适合运用于完全二叉树,如果采用其他一般的二叉树会造成存储空间的浪费。因为完全二叉树是按照编号顺序依次存储到一个数组中,在中间不存在没有存储的空间位置。

可以看到,在上述一般的二叉树中,存储在许多空位没有存储数据造成存储空间的浪费。

2.4.2 二叉树的链式存储

二叉树的链式结构与之前学习的栈和队列的链式结构有所不同。根据二叉树的定义可知,二叉树右两个指针结点和一个数据元素。两个指针结点分别指向该结点的左、右两个子树,所以表示二叉树的链表至少包含三个域。

因此设计了含有一个数据域和两个指针域的链式结点结构,具体如下∶

data表示数据域,用于存储对应的数据元素;Ichild 和 rchild分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的位置。这种存储结构又称为二叉链表存储结构。

typedef struct BTNode{
    int data;
    struct BTNode *lchild;    //左孩子
    struct BYNode *rchild;    //右孩子
}BTNode;

可以看到,在上述一般的二叉树中,存储数据时不存在存储空间的浪费。

2.5 二叉树的创建

实验平台随机生成二叉树如图所示(用二叉树的层次表示法):

2.5.1 先序创建二叉树

先序递归建立(CreatePerBiTree)的操作过程如下:

1)建立根结点

2)建立左子树

3)建立右子树

递归实现如下:

void CreatePreBiTree(BTNode *BT)        /*先序递归建立二叉树*/
{
    char ch;
    scanf_s("%c",&ch);
    if(ch=='#')
        *BT=NULL;
    else{
        *BT=(*BTNode)malloc(sizeof(BTNode));
        (*BT)->data=ch;
        CreateBiTree(&(*BT)->lchild);    
        CreateBiTree(&(*BT)->rchild);
    }
}

2.5.2 中序创建二叉树

先序递归建立(CreatePerBiTree)的操作过程如下:

1)建立左子树

2)建立根结点

3)建立右子树

递归实现如下:

void CreateInBiTree(BTNode *BT)        /*中序递归建立二叉树*/
{
    char ch;
    scanf_s("%c",&ch);
    if(ch=='#')
        *BT=NULL;
    else{
        *BT=(*BTNode)malloc(sizeof(BTNode));
        CreateInBiTree(&(*BT)->lchild);  
        (*BT)->data=ch;
        CreateInBiTree(&(*BT)->rchild);
    }
}

2.5.3 后序创建二叉树

先序递归建立(CreatePostBiTree)的操作过程如下:

1)建立左子树

2)建立右子树

3)建立根结点

递归实现如下:

void CreatePostBiTree(BTNode *BT)        /*中序递归建立二叉树*/
{
    char ch;
    scanf_s("%c",&ch);
    if(ch=='#')
        *BT=NULL;
    else{
        *BT=(*BTNode)malloc(sizeof(BTNode));
        CreatePostBiTree(&(*BT)->lchild);  
        CreatePostBiTree(&(*BT)->rchild);
        (*BT)->data=ch;
    }
}

实验平台创建二叉树三种表示方式:

1)空号表示法,结构为A(C(D(G,J),E(I,H)),B(#,F))

2)层次表示法,结构如图所示。

3)凹表示法(使用栈将二叉树中的数据按照指定格式(兄弟间等长、(r)代表根节点、(0)代表左节点、(1)代表右节点)输出。),结构如图所示。

2.6 二叉树的遍历

按某条搜索路径巡访树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。假如以分别表示遍历左子树、访问根结点和遍历右子树。则可有 DLR LOR LRD DRL RDL RLD 遍历二叉树的方案。若限定先左后右,则只有前种情况,分别称之为先(根)序遍历、中(根)序遍历和后(根)序遍历。

实验平台创建上述二叉树如下图所示。

visit(BTNode *p){
    printf("%d",p->data);
}

2.6.1 先序遍历

先序遍历(PerOrder)的操作过程如下:

若二叉树为空,则上面都不用操作;否则

1)访问根结点

2)先序遍历左子树

3)先序遍历右子树

递归实现如下:

void perorder(BTNode *p){
    if(p!=NULL){
        visit(p);    //打印数据
        perorder(p->lchild);    //先序遍历左子树
        perorder(p->rchild);    //先序遍历右子树
    }
}

快速记法口诀:中左右的顺序进行递归。

以上图为例,先序遍历结果为:1  2  4  3  5  6

2.6.2 中序遍历

中序遍历(InOrder)的操作过程如下:

若二叉树为空,则上面都不用操作;否则

1)中序遍历左子树

2)访问根结点

3)中序遍历右子树

递归实现如下:

void inorder(BTNode *p){
    if(p!=NULL){
        inorder(p->lchild);    //中序遍历左子树
        visit(p);    //打印数据
        inorder(p->rchild);    //中序遍历右子树
    }
}

快速记法口诀:左中右的顺序进行递归。

以上图为例,中序遍历结果为:4  2  1  5  6  3

2.6.3 后序遍历

后序遍历(PostOrder)的操作过程如下:

若二叉树为空,则上面都不用操作;否则

1)后序遍历左子树

2)后序遍历右子树

3)访问根结点

递归实现如下:

void postorder(BTNode *p){
    if(p!=NULL){
        postorder(p->lchild);    //后序遍历左子树
        postorder(p->rchild);    //后序遍历右子树
        visit(p);    //打印数据
    }
}

快速记法口诀:左右中的顺序进行递归。

以上图为例,后序遍历结果为:4  2  6  5  3  1

实验平台显示先序,中序,后序如下图所示。

2.6.4  先序非递归遍历

思路:中左右的顺序进行遍历

先序非递归遍历(NRPreOrder)的操作过程如下:

若二叉树为空,则上面都不用操作;否则

1)将结点数据输出,将其压入栈中,如果存在左孩子,就让结点指向左孩子,并且继续输出结点和压入栈中,直到不存在左孩子,才停止。

2)然后把结点出栈,并且每次指向右孩子,如果右孩子为空,则继续出栈;如果不为空,则输出右孩子结点数据,并且压入栈中执行1.

3)一直重复上述过程,直到结点为空且栈为空时候结束。

Status NRPreOrder(BiTree BT)            /*先序非递归递归遍历*/
{
    BiTree stack[Max_Tree_SIZE],p;
    int top;
    if(BT==NULL)
        return FALSE;
    top=0;
    p=BT;
    while(!(p==NULL&&top==0))
    {
        while (p!=NULL)
        {
            printf("%c",p->data);
            if(top<Max_Tree_SIZE-1)
            {
                stack[top]=p;    //压栈
                top++;
            }
            else
            {
                printf("溢出");
                return 0;
            }
            p=p->lchild;    //指向左孩子
        }
        if(top<=0)
            return 0;
        else    //当p为空出栈
        {
            top--;
            p=stack[top];
        }
        p=p->rchild;    //指向右孩子
    }
    return OK;
}

或者

void PreOreder(BiTree T){
    InitStack(S);BiTree p=T;    //初始化栈
    while(p||IsEmpty(S)){
        if(p){        //不为空,输出并进栈
            visit(p);
            Push(S,p);
            p=p->lchild;    //再查看左子树
        }
        else{
            Pop(S,p);    //出栈并查看右子树
            p=p->rchild;
        }
    }
}

下面是先序遍历非递归的出栈入栈的整个过程图,以左边子树为例:

2.6.5 中序遍历非递归

思路:左中右的顺序进行遍历

中序非递归遍历(NRInOrder)的操作过程如下:

若二叉树为空,则上面都不用操作;否则

1)将结点压入栈中,如果存在左孩子,就让结点指向左孩子,并且继续输出结点和压入栈中,直到不存在左孩子,才停止。

2)出栈,并且输出结点数据

3)如果存在右孩子,压栈

4)一直重复上述过程,直到结点为空且栈为空时候结束。

void InOrder(BiTree T){
    InitStack(S);BiTree p=T;   //初始化栈
    while(p||!IsEmpty(S)){    //栈不为空,p不为空时候
        if(p){                //找存在的左子树压栈
            Push(S,p);
            p=p->lchild;
        }
        else{                //没有左子树出栈
            Pop(S,p);
            visit(p);
            p=p->rchild;    //输出之后再看有没有右子树
        }
    }
}

2.6.6 后序遍历非递归

思路:左右中的顺序进行遍历

后序非递归遍历(NRPostOrder)的操作过程如下:

若二叉树为空,则上面都不用操作;否则

1)沿着左孩子一直入栈,直到左孩子为空

2)读取栈顶结点,如果该结点右孩子不为空且没有被访问过,将其右孩子入栈继续进行1操作;否则,出栈并访问该节点输出信息

void PostOrder(BiTree T){
    InitStack(S);
    p=T;
    r=NULL;
    while(p||IsEmpty(S)){        
        if(p){            //该的左子树全压入栈中
            Push(p)
            p=p->lchild;
        }
        else{            //向右
            GetTop(S,p);
            if(p->rchild&&p->rchild!=r)        //右子树存在且没有被访问过
                p=p->rchild;                    //向右
            else{                    //否则出栈
                Pop(S,p);
                visit(p);
                r=p;                //标记最近访问的结点
                p=NULL;
            }
        }
}

2.6.7 层次非递归遍历二叉树

typedef struct BiTNode
{
    TElemType data,n;    //多一个标记n来查看顺序
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;


void LevelOrder(BiTree BT)            /*按层次非递归遍历*/
{
    for(int i=1;i<BT->n;i++)
        if(BT->data!='#')
            visit(BT);
 }

2.7 二叉树的其他应用

二叉树如图所示。

实验平台求该二叉树的深度、叶子结点数目、度为一的结点数目、度为二的结点数目的结果如下图所示。

从实验平台结果可以知道:

该二叉树的深度为:4;

二叉树的结点数目为:10;

叶子结点数目为:4;

非叶子结点数目为:6;

度为一的结点数目:3;

度为二的结点数目:4;

该二叉树不是对称的二叉树;

2.7.1 求二叉树高度

思路:左右递归查找深度最大的子树。

int max(int a,int b)
{
    if(a>b)
        return a;
    else
        return b;
}
 
int TreeDepth(BiTree BT)                /*求高度*/
{
    if(BT==NULL)
        return 0;
    else
        return max(TreeDepth(BT->lchild),TreeDepth(BT->rchild))+1;
}

2.7.2 求二叉树叶子结点数目

思路: 左右递归,找出不存在左子树和右子树的结点,然后加一。

int LeafNumber(BiTree BT,int &count)           /*求叶结点数*/ 
{
    
        if(BT){ 
            LeafNumber(BT->lchild,count);
            if( (!BT->lchild && !BT->rchild))
                count++;
            LeafNumber(BT->rchild,count);
            return 0;
        }
    return 0;
}

2.7.3 求二叉树度为一的结点数目

思路:左右递归,找出结点只有左孩子结点或右孩子结点的,然后加一

int DsonNodes(BiTree BT,int &count)           /*求度为一的结点数*/ 
{
    
        if(BT){ 
            DsonNodes(BT->lchild,count);
            if( (!BT->lchild && BT->rchild)||(BT->lchild && !BT->rchild))
                count++;
            DsonNodes(BT->rchild,count);
            return 0;
        }
    return 0;
}

2.7.4 求二叉树度为二的结点数目

思路:左右递归,找出结点有左孩子结点和右孩子结点的,然后加一

int DualNodes(BiTree BT,int &count)            /*求度为二的结点数*/
{
    
        if(BT){ 
            DualNodes(BT->lchild,count);
            if( BT->lchild && BT->rchild)
                count++;
            DualNodes(BT->rchild,count);
            return 0;
        }
    return 0;
}

2.7.5 判断二叉树是否对称

思路:先判断其左右子树结构是否一致,在判断其对称位置的结点内数据是否一致。

结构有以下两种情况:

1)如果树为空,则直接判断是对称二叉树

2)如果在左右递归时候,左子树中有结点为空,而右子树对应的结点不为空,则不对称;反之,左子树中有结点为不空,而右子树对应的结点为空,也不对称;

最后再判断对应数据是否一直;

bool isSymmetrical(BiTree root1,BiTree root2){
    if(root1 == null && root2 ==null)
        return true;
    if(root1 == null || root2 ==null)
        return false;
    if(root1->data!=root2->data)
        return false;
    return isSymmetrical(root1->lchild,root2->rchild)&&isSymmetrical(root1->rchild,root2->lchild);

2.7.6 交换二叉树左右结点位置

思路:左右递归,如果存在结点不为叶子结点,就将其左右子树进行交互,这里要先建立一个中间变量才能进行交换。

void change_left_right(BiTree BT)
{
    BiTree p;
    p=(BiTree)malloc(sizeof(BiTNode));
    if(BT)
    {
        change_left_right(BT->lchild);
        change_left_right(BT->rchild);
        p=BT->lchild;
        BT->lchild=BT->rchild;
        BT->rchild=p;
    }
}

2.7.7 二叉树寻找最近公共祖先

思路: 首先是递归的算法对于递归算法核心就是查找,即在递归的过程中不断查找要查找的结点找到该结点时,就将该结点向上返回,当一个结点为需要查找结点的最小公共祖先时就将该节点向上返回。

BiTree *LowestCommonAncestorInBinaryTree(BiTree *BT,int p,int q){    //p,q为两个结点的值
    if(!BT)
        return NULL;
    if(BT->data==p||BT->data==q)    //如果结点是p或者q,则返回该结点
        return BT;
    BiTree *left=LowestCommonAncestorInBinaryTree(BT->lchild,p,q); //左递归
    BiTree *right=LowestCommonAncestorInBinaryTree(BT->rchild,p,q);//右递归

    if(left!=NULL && right!=NULL)    //祖先结点一定左右都不为空
        return BT;
     if(left==NULL && right==NULL)
        return NULL;
     return  left==NULL?right:left;

实验平台找寻最近公共祖先图如下。

可以看出HJ两个结点的最近公共祖先为A。

2.8 线索二叉树

2.8.1 线索二叉树的概念

在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。

对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。

这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

注意:线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题,解决了二叉链表找左、右孩子困难的问题。

2.8.2 线索二叉树的结构

试作如下规定:若结点有左子树,则其Ichild域指示其左孩子,否则令Ichild域指示其前驱;若结点有右子树,则其rchild域指示其右孩子,否则令rchild域指示其后继。为了避免混淆,尚需改变结点结构,增加两个标志域

lchild

LTag

data

RTag

rchild

其中,标志域的含义如下:

以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的指针,叫做线索。加上线索的二叉树称之为线索二叉树(Threaded Bina-ry Tree)

线索化:例如下图所示为中序线索二叉树,与其对应的中序线索链表。其中实线为指针(指向左、右子树),虚线为线索(指向前驱和后继)。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化

typedef struct ThreadNode{
    int data;
    struct ThreadNode *lchild,*rchild;        //左右孩子
    int Ltag,Rtag;                            //左右标志
}ThreadNode,*ThreadTree;

2.8.3 中序线索二叉树

构建线索二叉树唯一的思想就是当左孩子不存在时候,其指向前驱;当右孩子不存在时候,其指向后继。前驱和后继只有在遍历时候才能找到,所以线索化的实质就是遍历二叉树。

设置一个pre指针指向刚访问过的结点,p指向真正访问的结点,pre就为p的前驱。

代码实现:

void InThread(ThreadTree &p,ThreadTree &pre){
    if(p!NULL){
        InThread(p->lchild,pre);        //向左递归
        if(p->lchild==NULL){            //左子树为空
            p->lchild=pre;            //指向前驱
            p->Ltag=1;                //标记为线索
        }
        if(pre!=NULL&&pre->rchild==NULL){    //建立后继
            p->rchild=p;
            pre->Rtag=l;
        }
        pre=p;
        InThread(p->lchild,pre);
    }
}

通过中序遍历直接建立线索二叉树的主要算法:

void CreateInTread(ThreadTree T){
    ThreadTree pre = NULL;
    if(T!=NULL){
        InThread(T,pre);        //线索化
        pre->rchild=NULL;        //遍历处理最后一个结点
        pre->Rtag=1;
    }
}

线索二叉树遍历算法:

1)中序线索二叉树中中序序列下的第一个结点:

ThreadNode *Fisrtnode(ThreadNode *p){
    while(p->Ltag==0) p=p->lchild;    //最左边的结点(不一定是叶子结点)
    return p;
}

2)求中序线索二叉树的结点的后继结点:

思想:中序遍历的后继一定是在右边的,所以要么就是右孩子结点,要么就是右孩子的最左边结点。

ThreadNode *Nextnode(ThreadNode *p){
    if(p->Rtag==0) 
        return Firstnode(p->rchild);    //右孩子的最左边结点
    else 
        return p->rchild;    //右孩子

3)利用上述两个算法,可以写出不含头结点的中序线索二叉树遍历

思想和普通for循环类似

首先循环开始是找到头结点(Firstnode(T));

结束条件为p遇到空时候,因为是线索二叉树,所以空只会出现在最后一个结点的后面;

最后寻找下一个结点(Nextnode(p))

void Inorder(ThreadNode *T){
    for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
        visit(p);
}

2.8.3 先序线索二叉树

先序线索和中序线索类似,也是遍历进行构建线索。

1)先序线索二叉树构建

void PreThread(ThreadTree &p,ThreadTree &pre){
    if(p!=NULL){
        if(p->lchild==NULL){    //左孩子为空,则指向前驱
            p->lchild=pre;
            p->Ltag=1;
        }
        if(pre!=NULL&&pre->rchild==NULL){     //前驱的右孩子为空,前驱的结点指向该结点   
            pre->rchild=p;                       //作为后继
            pre->Rtag=1;
        }
        pre=p;            //该点线索构建完成,进行左右子树递归
        if(p->Ltag==0)
            preThread(p->lchild,pre);
        if(p->Rtag==0)
            preThread(p->rchild,pre);
    }
}

2)先序线索二叉树遍历

void PreOrder(ThreadTree &root){
    if(root!=NULL){
        ThreadNode *p=root;
        while(p!=NULL){
            while(p->Tlag==0){        //左子树不是线索,则访问左子树
                visit(p);
                p=p->lchild;
            }
            visit(p);        //此时p左指针为线索,但是还没有被访问,则访问
            p=p->rchild;    //去遍历后继,如果为空就结束了
        }
    }
}

2.8.4 后序线索二叉树

先左右递归,然后再连接线索,因为在后续遍历中,先访问左右两结点,再访问中间结点,构建线索也类似。

void PostThread(ThreadTree &p,ThreadTree &pre){
    if(p!=NULL){
        PostThread(p->lchild,pre);
        PostThread(p->rchild,pre);
        if(p->lchild==NULL){    //左孩子为空,则指向前驱
            p->lchild=pre;
            p->Ltag=1;
        }
        if(pre!=NULL&&pre->rchild==NULL){     //前驱的右孩子为空,前驱的结点指向该结点   
            pre->rchild=p;                       //作为后继
            pre->Rtag=1;
        }
        pre=p;            //该点线索构建完成,进行左右子树递归
    }
}

2.9 根据遍历重组二叉树结构

根据遍历重组二叉树,必须要中序遍历的结构,因为只有中序遍历才能区分左子树和右子树,从而得到二叉树的父子关系。所以采用先序和中序遍历、后续与中序遍历才能得到二叉树的结构。

不能运用先序和后续遍历构造二叉树的具体原因:

先序和后序在本质上都是将父节点与子结点进行分离,但并没有指明左子树和右子树的能力,因此得到这两个序列只能明确父子关系,而不能确定一个二叉树。 由二叉树的中序和前序遍历序列可以唯一确定一棵二叉树 ,由前序和后序遍历则不能唯一确定一棵二叉树。由二叉树的中序和后序遍历序列可以唯一确定一棵二叉树,由前序和后序遍历则不能唯一确定一棵二叉树。

具体例子:

假设有一颗树的先序遍历为1,2;后续遍历为2,1;则其二叉树的结构可能如下:

存在两种二叉树的结构,所以不能运用先序和后续遍历进行构造二叉树。

2.9.1 根据先序和中序重组二叉树

先序遍历:ABDHECFIMGJKL

中序遍历:DBHEAIMFCGKLJ

因为先序遍历首先都是访问根节点,由此可以做以下步骤:

1)首先,根据先序遍历,我们可以确定,结点A是最初的根结点;

2)由于先序遍历得知,A是最初的结点,于是可推测出中序遍历中A的左右为其左右子树的结点,如下图所示;

3)分成左右两个子树之后,又可以将其分为两个先序和中序遍历的子问题求解;

4)左边中序由第一个子先序遍历可以看出,初始根节点为B,右边中序由第二个子先序遍历可以看出,初始根节点为C,如图所示;

5)之后,ABCD四个结点的位置就确定好了;现在又编程三个先序,三个中序的子问题来解决;

6)与上述类似,第一个子问题的初始结点为H,第二个子问题的初始结点为F,第三个子问题的初始结点为G,如下图所示;

7)确定好了ABCDHFG的结点位置;还是三个子问题来解决;

8)第一个子问题E结点在H点后面,根据先序遍历(中左右)的顺序得知,E一定是H的右孩子结点,才会在先序遍历时候排在其右边;第二个子问题的初始结点为I,且在先序、中序遍历中F在I前面,I在M前面的,所以I是F的左结点,M是I的右结点;第三个子问题的初始结点为J,在先序、中序遍历中,G在J的前面,所以J为G的左孩子结点,在中序遍历中,K在L前面,而先序遍历L在K前面,所以K为J的左孩子结点,L为K的右孩子结点,如下图所示;

至此,二叉树结构还原完成。按照分治法的思想,就能对二叉树进行还原。

下图是实验平台构建还原的二叉树的结构。

2.9.2 根据中序和后序重组二叉树

后序遍历:DBEFCA

中序遍历:BDAECF

因为后序遍历最后都是访问根节点,由此可以做以下步骤:

1)首先,根据后序遍历,我们可以确定,结点A是最初的根结点;

2)由于后序遍历得知,A是最初的结点,于是可推测出中序遍历中A的左右为其左右子树的结点,如下图所示;

3)分成左右两个子树之后,又可以将其分为两个后序和中序遍历的子问题求解;

4)第一个子问题由后序遍历得知D在B的后面,中序中B在D前面初始根节点为B,D为B的右子树结点,第二个子问题由后序遍历可以看出,初始根节点为C,E、F分别为C的左右两个孩子结点,如图所示;

至此,二叉树结构还原完成。按照分治法的思想,就能对二叉树进行还原。

下图是实验平台构建还原的二叉树的结构。

3  树、森林

3.1 树的存储结构

下图是实验平台生成的三棵树,也可以称之为森林。

三棵树用孩子表示法分别为:A( B( H ,I) , C ,D( G) ,E( F), J);A( B( E ,G), C, D( F));A( B)

3.1.1 双亲表示法

假设以一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲结点在链表中的位置,其形式说明如下:

#define MAX_TREE_SIZE 100
typedef struct PTNode{    //结点结构
    int data;
    int parent;        //双亲位置域
}PTNode;
typedef struct{        //树结构
    PTNOde nodes[MAX_TREE_SIZE];
    int r,n;            //根的位置和结点数
}PTree;

左边第一列表示数组下标,第二列为数组元素,第三列为父亲结点的数组下标。

在这种存储结构利用了每个结点只有唯一双亲的性质,但是这种表示方法,求孩子结点时候需要遍历整个结构。

上图树在实验平台中的显示。

3.1.2 孩子表示法

定义:孩子链表示法是指同一双亲的孩子链成单链,每个节点带有指向孩子链的指针。树的所有结点都有唯一的父节点,但是每个结点都有不确定数量的子结点。每个结点由三部分组成:存储数据元素值的数据部分、指向它的第一个子结点的指针、指向它的兄弟结点的指针。孩子链表示法是树最常用的存储方式。

该图的孩子表示法的结构图如下:

使用该图结构存储普通树,既能快速找到指定节点的父节点,又能快速找到指定节点的孩子节点。

typedef struct CTNode{    //孩子结点
    int child;
    struct CTNode *next;
}*ChildPtr;
typedef struct{
    int data;
    ChildPtr firstchild;    //孩子链表头结点
}CTBox;
typedef struct{
    CtBox nodes[MAX_TREE_SIZE];
    int n,r;                //结点数和根的位置
}CTree

3.1.3 孩子兄弟表示法

定义:又称二叉树表示法,或二叉链表表示法。即以二叉链表作树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为firstch-ild域和nextsibling域。

在二叉链表中,各个结点包含三部分内容:

1)节点的值

2)指向孩子结点的指针

3)指向兄弟结点的指针

孩子指针域

数据域

兄弟指针域

该图的孩子兄弟表示法的结构图如下:

记住口诀:左孩子右兄弟(结点孩子结点化为二叉树左孩子结点,兄弟结点化为二叉树右孩子结点)

typedef struct CSNode{
    int data;
    struct CSNode *firstchild,*nextsibling;
}CSNode,* CSTree;

3.1.4 树的顺序存储

下图是实验平台显示树的顺序存储结果时候各个结点的位置和其孩子结点。

从实验平台可以直观的知道:

A的孩子孩子为B ,C ,D

B的孩子结点为E ,F

E不存在孩子结点

F的孩子结点为H

H不存在孩子结点

C不存在孩子结点

D的孩子结点为G

G的孩子结点为I ,J ,K

I不存在孩子结点

J不存在孩子结点

K不存在孩子结点

3.2 森林与二叉树的转化

 森林转化为二叉树可以把其中的森林当初树来看待,也就是上图中A,B,I互为兄弟结点,假设他们都是用一个结点NULL共同的子结点所生成的。之后就可以按照树的形式将其转化为二叉树。

口诀:左孩子右兄弟(结点孩子结点化为二叉树左孩子结点,兄弟结点化为二叉树右孩子结点)

下图是实验平台树转化为二叉树图。(树和二叉树均由括号表示法表示)

下图是实验平台二叉树转化为森林图。(树和二叉树均由括号表示法表示)

4 哈夫曼树

4.1 哈夫曼树定义

在许多应用中,树中结点常常被赋于一个表示某种意义的数值,称为该结点i的权。从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为

式中, 是第i个叶结点所带的权值,是该叶结点到根结点的路径长度。

下列是有关路径的一些概念:

1)路径:路径是指从树中一个结点到另一个结点的分支所构成的路线。

2)路径长度:路径长度是指路径上的分支数目。

3)树的路径长度:树的路径长度是指从根到每个结点的路径长度之和。 4)带权路径长度:结点具有权值,从该结点到根之间的路径长度乘以结点的权值,就是该结点的带 权路径长度。 5)树的带权路径长度(WPL):树的带权路径长度是指树中所有叶子结点的带权路径长度之和。

4.2 哈夫曼树的构造

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为、…、,则哈夫曼树的构造规则为:

(1) 将、…、看成是有n 棵树的森林(每棵树仅有一个结点);

(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;

(3)从森林中删除选取的两棵树,并将新树加入森林;

(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

下列是哈夫曼树的构造过程:

4.3 哈夫曼树的特点

1)权值越大的结点,距离根结点越近。

2)树中没有度为1的结点。这类树又叫作正则(严格)二叉树。

3)树的带权路径长度最短。

5 总结

树和二叉树是这一章节内容众多,而且知识点较难,需要读者多加学习,了解树和二叉树的具体用法和相关性质,大部分的性质虽然可以运用数学归纳法来推出,但是如果能够记住树和二叉树的性质,在写题或者编程时候可以更快的求解。而且在以后数据结构考试中,树和二叉树都是重难点,加强这一章节的学习至关重要。

参考文献:

[1] 苏小红,孙志刚,陈惠鹏等编著. C语言大学实用教程(第四版)[M]. 北京:电子工业出版社,2017.

[2] 严蔚敏,吴伟民. 数据结构[M]. 北京:清华大学出版社.

[3] 罗勇君, 郭卫斌.算法竞赛_入门到进阶[M].北京:清华大学出版社.

作者:

江西师范大学_姜嘉鑫; 江西师范大学_龚俊

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
折半查找和二叉排序都是解决查找问题的常用算法,在实现上有一些不同。 折半查找是一种基于有序序列的查找算法,它的时间复杂度为O(log n)。具体实现时,折半查找将待查找的元素与序列的中间元素进行比较,如果相等则返回,如果小于中间元素,则在左边继续查找,否则在右边继续查找。重复这个过程直到找到目标元素或者确定目标元素不存在。 二叉排序是一种基于二叉树结构的查找算法,它的时间复杂度取决于的平衡情况,通常为O(log n)。具体实现时,二叉排序将元素插入到中的合适位置,保证左子的元素小于当前节点,右子的元素大于当前节点。查找时从根节点开始,与当前节点进行比较,如果相等则返回,如果小于当前节点,则在左子中继续查找,否则在右子中继续查找。重复这个过程直到找到目标元素或者确定目标元素不存在。 在实际应用中,折半查找常用于静态查找,即数据集合不会发生变化的情况下;而二叉排序适用于动态查找,即数据集合可能随时发生变化的情况下。同时,在二叉排序中,为了避免的不平衡造成时间复杂度的恶化,可以采用平衡二叉树算法,如红黑、AVL等。 总体来说,折半查找和二叉排序都是非常常用的查找算法,具体选择哪种算法取决于数据集合的特点和实际应用的需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gongjunbox

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值