数据结构(八)—— 树的相关学习

一、树的定义以及相关结构名词解释

树是n(n>=0)个结点的有限集。若n=0,则称其为空树。若n>0,则它满足如下两个条件:(1)有且仅有一个特定的成为根(Root)的结点;(2)其余结点可分为m(m>=0)个互不相关的有限集T1、T2、T3……Tm,其中每一个集合本身又是一颗树,并称为根的子树。

相关结构的名词可以看下图:

森林:是把m(m>=0)课互不相交的树的集合。吧啊根节点删除树就变成了森林。一棵树可以看成是一个特殊的森林。给森林的鸽子树加上一个双亲结点,森林就变成了树。树与森林的概念如下图所示:

二、二叉树的定义和相关性质

二叉树是n(n>=0)个结点的有限集。若n=0,则称其为空二叉树。若n>0,则它满足如下两个条件:(1)有且仅有一个特定的成为根(Root)的结点;(2)二叉树分为左右子树,有且只有两个子树,其子树也如此。

二叉树跟树的区别:二叉树不是树的特殊情况,这是两个概念。

二叉树结点的子树要区分左子树和右子树,即使只有一颗子树,也需要如此区分,说明它是左子树,还是右子树。

树当结点只有一个孩子是,就无须区分它是左还是右的次序。因此二者是不同的,这也是二叉树和树的最主要差别。

二叉树的性质:

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

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

性质一二,可以通过如下图进行数数结点的方式理解。

性质3:对任何一颗二叉树T如果其叶子数为n_0度为2的结点数n_2为则n_0 = n_2 -1

性质3,是通过对树的总边数B从下往上数,和从上往下数的边数是一样的和结点总数n的关系,以及度为2的结点和度为1的结点来推算出叶子结点的个数,用以下图来进行理解。

性质:具有n个结点的完全二叉树的深度为\left \lfloor log_2n \right \rfloor+1

性质4,首先认识下完全二叉树的概念,以及相关的特点。完全二叉树是深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树(满二叉树就是二叉树每一层上的结点数都是最大节点数)中编号为1~n的结点一一对应时,称之为完全二叉树。下图为满二叉树和完全二叉树的图像。

性质5:如果对一颗有n个结点的完全二叉树(深度为\left \lfloor log_2n \right \rfloor+1)的结点按层序编号 (从1层到第\left \lfloor log_2n \right \rfloor+1层,每层从左到右),则对任意一结点i(1 \leq i \leq n)有如下:

如果 i=1,则结点是二叉树的根,无双亲;如果 i>1,则其双亲是\left \lfloor i/2 \right \rfloor结点。

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

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

根据性质5的相关定理,可以根据如下图进行验证:

三、二叉树的存储与操作 

  1. 二叉树的顺序存储

按照满二叉树的结点层次编号,一次存放如二叉树中的数据元素,而空的结点为0便可以进行线性表的存储。如下面两个图所示:

其代码实现可以如下表述:

#define MAXTSIZE 100
typedef int TElemType;
typedef TElemType SqBiTree[MAXTSIZE];
SqBiTree bt;
  1. 二叉树的链式存储

链式存储包括存储二叉树结点的左右孩子,以此来确定结点的下一个位置,方便遍历和其他操作。其结构如下图所示

其代码实现可以如下表述:

typedef struct BiTNode {
    TElemType data;
    struct BiTNode* lchid, * rchild;   //左右孩子指针
}BiTNode,*BiTree;

链式存储的结果可以抽象成下图:

比较两种存储方式的优缺点:

顺序存储,比较容易进行查找操作,适合存放满二叉树。但因为树有许多的空分支,也得存进去所以造成极大的空间浪费,而且进行添加或删除等操作时,需要移动的元素过于多,造成的时间浪费比较严重。

链式存储,对空间的利用比较充足,进行插入删除等操作时更加方便,而且对树的遍历提供比较方便的便利方法,可以更好的存储树这种结构。但是结点的空指针域也是存在着浪费的现象,但却后面的线索二叉树却可以进行更好的利用。

  1. 二叉树的遍历

二叉树的遍历常见是规定先左后右的方式,从而有三种情况:

  • DLR —— 先序遍历(先遍历根节点在遍历左子树最后遍历右子树)

  • LDR —— 中序遍历(先遍历左子树在遍历根节点最后遍历右子树)

  • LRD —— 后序遍历(先遍历左子树在遍历右子树最后遍历根节点)

例如:对下图进行遍历

  • DLR —— 先序遍历 :abcdefgh

  • LDR —— 中序遍历 :cbedaghf

  • LRD —— 后序遍历:cedbhgfa

遍历都是个通过一个递归的思想进行的,所以在代码实现方面都是通过递推的干事进行的。如下进行代码实现:

int PreOrderTraverse(BiTree T)   //先序遍历
{
    if (T == NULL) return 1;
    else {
        visit(T);
        PreOrderTraverse(T->lchild);
        PreOrderTraverse(T->rchild);
    }
}
int InOrderTraverse(BiTree T)    //中序遍历
{
    if (T == NULL) return 1;
    else {
        InOrderTraverse(T->lchild);
        visit(T);
        InOrderTraverse(T->rchild);
    }
}
int PostOrderTraverse(BiTree T)  //后序遍历
{
    if (T == NULL) return 1;
    else {
        PostOrderTraverse(T->lchild);
        PostOrderTraverse(T->rchild);
        visit(T);
    }
}

除了常见的三种遍历方法外,还有一个层次遍历的方法,顾名思义就是一层一层进行遍历。

上图的层次遍历结果为:abfcdgeh

层次遍历算法的思想是队列的思想,将根节点进入队列,出队时候将其左右孩子进入队列在循环此操作,以上图为例,

a进入队列,出队是 b f 进入队列,

b出队时, c d进入队列,此时队列中元素有 f c d

f出队时,g进入队列,此时队列中元素有 c d g

c出队时,没有进入队列,此时队列元素有 d g

d出队时,e 进入队列,此时队列元素有 g e

g出队时,h 进入队列,此时队列元素有 e h

后面没有进入了,全部出队。结果就为:a b f c d g e h

其代码实现如下:

 void LevelOrder(BiTree T)
 {
     BiTNode* p;
     SqQueue* qu;
     InitQueue(qu);
     enQueue(qu, T);
     while (!QueueEmpty(qu)) {
         deQueue(qu, p);
         if (p->lchild != NULL)
             enQueue(qu, p->lchild);
         if (p->rchild != NULL)
             enQueue(qu, p->rchild);
     }
​
 }
  1. 二叉树的建立

二叉树的建立,二叉树的建立就是将树的各个结点录入到链表中,而如果直接是以一种遍历顺序去建立的话,对于遍历结果一样而树的模样会出现不是唯一结果,因此将树的空节点也同样需要记录,从而可以保证生成的树是唯一的树。下面以先序遍历的结果生成树为例。

以#表示空的结点,这棵树的先序遍历的结果:ABC##DE#G##F###

其遍历的算法实现可以如下代码所示:

int CreateBiTree(BiTree T) {
    char ch = 0;
    scanf("%c", & ch);
    if (ch == "#") T = NULL;
    else {
        if (!(T = (BiTree)malloc(sizeof(BiTNode)))) {
            exit(0);
        }
        T->data = ch;
        CreateBiTree(T->lchild);
        CreateBiTree(T->rchild);
    }
    return 1;
}

5.二叉树的其他操作算法

计算二叉树的深度:判断空树,如果不是空,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1.

代码实现如下:

int Depth(BiTree T) {
    int n = 0, m = 0;
    if (T == NULL) return 0;
    else {
        m = Depth(T->lchild);
        n = Depth(T->rchild);
        if (m > n) return (m + 1);
        else return (n + 1);
    }
}

计算二叉树结点的总数:判断空树,不为空,节点个数为左子树的节点个数+右子树的节点个数。

代码实现如下:

int NodeCount(BiTree T) {
    if (T == NULL) return 0;
    else
        return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}

计算二叉树叶子的总数:判断空树,不为空,为左子树的叶子结点个数+右子树的叶子结点个数。

代码是实现如下:

int LeafCount(BiTree T) {
    if (T == NULL) return 0;
    if (T->lchild == NULL && T->rchild == NULL) return 1;
    else return LeafCount(T->lchild) + LeafCount(T->rchild);
}

四、线索二叉树

线索二叉树,为了查找前驱后继的结点更加方便,也为了把空的左右孩子空间利用起来,所以产生了线索二叉树的概念。其构建是将左孩子空的指向其前驱结点,右孩子空的指向后继结点,从而可以更加方便得进行查找。而有些结点没有前驱或者后继,因此还需要一个标记位来实现,所以线索二叉树的结构就如下图所示:

其结构代码如下所示:

typedef struct BiThrNode {
    TElemType data;
    int ltag, rtag;
    struct BiThrNode* lchild, * rchild;   //左右孩子指针
}BiThrNode, * BiThrTree;

其构建的二叉树根据遍历的不同而构成的线索二叉树同样也不一样。如下图所示:

先序线索二叉树:其前驱后继是根据先序遍历排序后的结果来确定的。

 中序线索二叉树:其前驱后继是根据中序遍历排序后的结果来确定的。

后序线索二叉树:其前驱后继是根据后序遍历排序后的结果来确定的。

 如果增设了一个头结点,那么ltag = 0,lchild指向根节点,rtage = 1,rchild指向遍历序列的最后一个结点,那么遍历序列中的第一个结点的lc域和最后一个结点的rc域就都指向头结点了,就围成一个圈了。如下图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值