二叉树的逻辑结构、存储结构和基本操作

二叉树

逻辑结构

见纸质笔记P130!!!

存储结构

顺序存储结构

二叉树的顺序存储结构只适合存储完全二叉树!!!

完全二叉树
#define MaxSize 100
struct TreeNode{
    Elemtype value;  //节点中的数据元素
    bool isEmpty; //该节点是否为空
};

// 初始化二叉树  初始化时结点标记为空
void InitTree(TreeNode &T){
    for(int i=0;i<MaxSize;i++) t[i].isEmpty=true;
}

定义一个长度为MaxSize的数组t,按照从上至下,从左至右的顺序依次存储完全二叉树的各个节结点。

⭐常见的基本操作:

  • i的左孩子 – 2 i 2i 2i
  • i的右孩子 – 2 i + 1 2i+1 2i+1
  • i的父节点 – ⌊ i / 2 ⌋ \lfloor i/2\rfloor i/2
  • i所在的层次 – ⌈ l o g 2 ( n + 1 ) ⌉ 或 ⌊ l o g 2 n ⌋ + 1 \lceil log_2(n+1)\rceil 或 \lfloor log_2n\rfloor+1 log2(n+1)⌉log2n+1

若完全二叉树中共有n个结点,则

  • 判断i是否有左孩子? – 2 i < = n ? 2i<=n? 2i<=n?
  • 判断i是否有右孩子? – 2 i + 1 < = n ? 2i+1<=n? 2i+1<=n?
  • 判断i是否为叶子/分支节点 – i > ⌊ n / 2 ⌋ ? i>\lfloor n/2\rfloor? i>n/2⌋?
普通二叉树

⭐二叉树的顺序存储中,一定要把二叉树的节点编号与完全二叉树对应起来!!

  • i的左孩子 – 2 i 2i 2i
  • i的右孩子 – 2 i + 1 2i+1 2i+1
  • i的父节点 – ⌊ i / 2 ⌋ \lfloor i/2\rfloor i/2

非完全二叉树中共有n个结点,则

  • 判断i是否有左孩子 --if(t[2i].isEmpty)
  • 判断i是否有右孩子 --if(t[2i+1].isEmpty)

⭐⭐⭐最坏情况:高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2h-1个存储单元

链式存储结构⭐⭐

实际使用当中,常常用到链式存储结构!!

typedef struct BiTNode{
    Elemtype data;
    struct BiTNode *lchild, *rchild;
}BiTNode,*BiTree;

// 若实际应用中经常用到寻找某结点的父节点,可以在结构体中多加一个parent指针指向父节点
typedef struct BiTNode{
    Elemtype data;
    struct BiTNode *lchild, *rchild;
    struct BiTNode *parent;
}BiTNode,*BiTree;

二叉树的遍历

二叉树的递归特性:

1.要么是空二叉树

2.要么就是由“根节点+左子树+右子树”组成的二叉树

二叉树的先序遍历(根左右)

⭐对应算术表达式的前缀表达式

//先序遍历,遵循根左右原则
void PreOdder(BiTree T){
    if(T!=NULL){
     	visit(T);
        PreOrder(T->lchild);
        PreOrder(T->rchild);
    }
}

二叉树的中序遍历(左根右)

⭐对应算术表达式的中缀表达式

//中序遍历
void InOrder(BiTree T){
    if(T!=NULL){
        InOrder(T->lchild);
        visit(T);
        InOrder(T->rchild);
    }
}

二叉树的后续遍历(左右根)

⭐对应算术表达式的后缀表达式

// 后序遍历
void PostOrder(BiTree T){
    if(T!=NULL){
        PostOrder(T->lchild);
        PostOrder(T->rchild);
        visit(T);
    }
}

二叉树的层序遍历(详情可见队列的应用!)

算法思想:

1.初始化一个辅助队列

2.根节点入队

3.若队列非空,则将队头元素进行出队操作,访问对头元素,并将其左右子树以此入队(若左右子树存在)

4.重复3直至队列为空

算法实现

// 定义树的结构体
typedef struct BiTree{
    ElemType data;
    struct BiTree *lchild,rhild;
}BiTNode,*BiTree;

// 定义存放树的结点的队列 使用链式队列更好些
typedef struct LinkQuene{
    LinkNode *front,*rear;
}LinkQuene;

// 定义队列结点
typedef struct LinkNode{
    BiTNode *data;
    struct LinkNode *next;
}LinkNode;

// 层序遍历
void LevelOrder(BiTree T){
    LinkQuene Q;
    InitQuene(Q);
    BiTree p; //这里是想表达p是一颗子树的头结点	
    EnQuene(Q,T);
    while(!IsEmpty(Q)){
        DeQuene(Q,p);
        visit(p);
        if(p->lchild!=null) EnQuene(p->lchild);
        if(p->rchild!=null) EnQuene(p->rchild);
    }
}

线索二叉树

基本概念

由于一棵树中包含2n个指针,而只有n-1(因为除根节点外的每个结点的头上都有一个指针)个指针被利用,所以还有n+1个指针尚未被利用,我们可以使用这些指针来存储该结点在特定的遍历序列(前序、中序、后序)中的前驱和后继节点!

A
B
C
D
E
F
NULL
NULL
G

Eg:中序遍历:DGBEAFC,除D和C外,每个结点都有自己的前驱和后继!可以使用指针将他们连接起来。而左右结点分别指向他们的前驱/后继结点的话,我们称之为线索

存储结构

相较于普通的二叉树,线索二叉树多出两个变量,ltag和rtag

typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag; //左右线索标志
}ThreadNode,*ThreadTree;

l c h i l d l t a g d a t a r t a g r c h i l d \begin{array}{|c|c|c|c|} \hline lchild & ltag & data & rtag & rchild\\ \hline \end{array} lchildltagdatartagrchild

l t a g = { 0 , l c h i l d 域指示结点的左孩子 1 , l c h i l d 域指示结点的前驱 r t a g = { 0 , r c h i l d 域指示结点的右孩子 1 , r c h i l d 域指示结点的前驱 ltag=\begin{cases} 0, lchild域指示结点的左孩子\\ 1,lchild域指示结点的前驱 \end{cases}\\ rtag=\begin{cases} 0, rchild域指示结点的右孩子\\ 1,rchild域指示结点的前驱 \end{cases} ltag={0,lchild域指示结点的左孩子1,lchild域指示结点的前驱rtag={0,rchild域指示结点的右孩子1,rchild域指示结点的前驱

通过中序遍历对二叉树进行线索化

// 通过中序遍历对二叉树进行线索化
void InThread(ThreadTree &p,ThreadTree &pre){
    if(p!=NULL){
        InThread(p->lchild,pre);
        visit(p,pre); // 在王道书中没有用函数,直接将函数部分写在了这里!!!
        InThread(p->rchild,pre);
    }
}
// 访问p结点
void visit(ThreadNode *p,ThreadTree &pre){
    if(p->lchild==NULL){
        p->lchild=pre;
        p->ltag=1;
    }
    if(pre!=NULL&&pre->rchild==NULL){
        pre->right=p;
        pre->rtag=1;
    }
    pre=p;
}

//通过中序遍历创建中序线索二叉树
void CreateInThread(ThreadTree T){
    ThreadTree pre=NULL;
    if(T!=NULL){
        InThread(T,pre);
        if(pre->rchild==NULL){
            pre->rtag=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;
            p->rtag=1;
        }
        if(p->ltag==0) // ltag无非就0和1,如果等于0就可以继续访问左子树,但是等于1的话,就会折回去访问它的根节点根节点再访问根的左子树,造成循环!!为了解决这个问题,我们命令如果左子树已经被线索化,即ltag=1,那么我们就直接不让他访问左子树!⭐⭐
        	PreThread(p->lchild,pre);
        PreThread(p->rchild,pre);
    }
}

void CreatePreThread(ThreadTree T){
    ThreadTree pre=NULL;
    if(T!=NULL){
        PreThread(T,pre);
        if(pre->rchild==NULL){ //先序中序遍历最后一个一定是最右边的结点,其右子树肯定是空的,所以可以直接不要判断,但是后续遍历不行,后序遍历最后一个肯定是根节点,直接让根节点的右子树等于空相当于直接把右分支砍掉了
            pre->rtag=1;
        }
    }
}

通过序遍历对二叉树进行线索化(王道书风格)

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;
    }
}

void CreatePostThread(ThreadTree T){
    ThreadTree &pre;
    if(T!=NULL){
        PostThread(T,pre);
        if(pre->rchild==NULL){ //如果为空 说明根结点只有左子树!
            pre->rtag=1;
        }
    }
}

在线索二叉树中找前驱后继

中序线索二叉树

前驱
// 求中序遍历中的第一个结点
ThreadNode *FirstNode(ThreadNode *p){
    while(p->ltag==0) p=p->lchild; //中序左根右,肯定是最左下角的结点
    return p;
}
// 找到中序遍历的最后一个结点
ThreadNode *LastNode(ThreadNode *p){
    while(p->rtag==0) p=p->rchild; //跟上边一样,这个函数的作用会在找p结点的前驱结点中用到!!!
    return p;
}

//找前驱结点
ThreadNode *PreNode(ThreadNode *p){
    if(p->ltag==1) return p->lchild;
    else return LastNode(p->lchild);
}
后继
//找后继结点
ThreadNode *NextNode(ThreadNode *p){
    if(p->rtag==1) return p->rchild;
    else return FirstNode(p->rchild);// p结点的后续结点一定是右子树要访问的第一个结点!!!因为根据左根右  p是根  则左p(左根右)!!!
}
遍历
// 可以根据刚刚写的函数来进行中序遍历,首先p等于中序的第一个结点,然后以此找其后续!!
void InOrder(ThreadNode *T){
    for(ThreadNode *p=FirstNode(T);p!=NULL;p=NextNode(p))
        visit(p);
}

先序线索二叉树

前驱(需要借助三叉链表)
后继
//按照先序的规则,根左右。
//若p为根且有👈结点和👉结点,则p的后继节点一定是左结点
//若p为根且没有左节点,则p的后继节点是右节点
ThreadNode *FindNext(ThreadNode *p){
    if(rtag=1) return p->rchild;
    else{
        if(p->lchild==NULL) return p->rchild;
        else return p->lchild;
    }
}

后续线索二叉树

前驱
// 若ltag不为0,则说明一定有左子树,
// 根据左右根原则,若右子树存在,根节点的前驱结点一定是右子树的根节点,若不存在,则一定为左子树的根节点
ThreadNode *FindPre(ThreadNode *p){
    if(ltag==1) return p->lchild;
    else{
        if(p->rchild==NULL) return p->lchild;
        else return p->rchild;
    }
}
后驱(需要借助三叉链表)

树的存储结构

双亲表示法

顺序存储结点数据,结点中保存父节点在数组中的下标

**优点:**找父节点方便

**缺点:**找孩子不方便

#define MAX_TREE_SIZE 100;
typedef struct{
    ElemType data;
    int parent;  // 双亲的位置
}PTNode;  //每个结点的结构,包括数据和父节点的存储位置

typedef struct{
    PTNode nodes[MAX_TREE_SIZE];
    int n;
}PTree;

在这里插入图片描述

孩子表示法

顺序存储结点数据,结点中保存孩子表头指针(顺序+链式存储)

优点:找孩子方便

缺点:找父节点不方便

struct CTNode{
    int child; //孩子结点在数组中的位置
    struct CTNode *next;   //下一个孩子
};
typedef struct{
    ElemType data;
    struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
    CTBox nodes[MAX_TREE_SIZE];
    int n,r; //结点数和根的位置
}CTree;

在这里插入图片描述

孩子兄弟表示法(常用!!⭐)

用二叉链表存储树–左孩子右兄弟

孩子兄弟表示法存储的树,从存储视角来看形态上和二叉树类似

考点:树与二叉树的相互转换。本质就是用孩子兄弟表示法存储树。

typedef struct CSNode{
    ElemType data;
    struct CSNode *firstchile;//第一个孩子
    struct CSNode *nextsibling;//右兄弟指针
}CSNode,*CSTree;

在这里插入图片描述

  • 26
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值