2

树(Tree)是n(n>=0)个结点的有限集. n=0时称为空树。 在任意一棵非空树中: (1)有且仅有一个特定的称为根(Root)的结点; (2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、......、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

clipboard.png

上图,T1={B,E,F,K,L},T2={C,G},T3={D,H,I,J,M};T1、T2和T3都是根A的子树,且本身也是一棵树

结点分类:
度(Degree): 结点拥有的子树数称为结点的度。
叶结点(Leaf)或终端结点: 度为0的结点。
非终端结点或分支结点:度不为0的结点。

除了根结点之外,分支结点也称为内部结点。
树的度:树内各结点中,其中一个结点的度的最大值就是树的度。

结点之间关系
孩子(child)和双亲(Parent):结点的子树的根称为该结点的孩子;相应地,该结点称为孩子的双亲。
兄弟(sibling):同一个双亲的孩子之间互称兄弟。
结点的祖先: 是从根到该结点所经过分支上的所有结点。
子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。

其他概念
结点的层次(level): 结点的层次从根结点开始定义,根为第一层,根的孩子为第二层。
堂兄弟:双亲在同一层的结点互为堂兄弟。
树的深度(Depth)或高度:树中结点的最大层次称为树的深度或高度。
有序树:树中结点的各子树从左至右是有次序的,不能互换的。
无序树:非有序树。
森林(Forest):是m(m>=0)棵互不相交的树的集合。

树的抽象数据类型

ADT 树(tree)
Data
    树是一个根结点和若干棵子树构成。树中结点具有相同数据类型及层次关系。   
Operation 
     InitTree(*T);                         //构造空树T
     DestroyTree(*T);                    //销毁树T
     CreateTree(*T,definition);            //按definition中给出树的定义来构造树
     ClearTree(*T);                        //若树T存在,则将树T清为空树  
     TreeEmpty(T);                        //若T为空树,返回true,否则返回false
     TreeDepth(T);                        //返回T的深度
     Root(T);                            //返回T的根结点
     Value(T,cur_e);                        //cur_e是树T中一个节点,返回此结点的值
     Assign(T,cur_e,value);                //给树T的结点cur_e赋值为value
     Parent(T,cur_e);                    //若cur_e是树T的非根结点,则返回它的双亲,否则返回空
     LeftChild(T,cur_e);                    //若cur_e是树T的非叶结点,则返回它的最左孩子,否则返回空
     RightSibling(T,cur_e);                //若cur_e有右兄弟,则返回它的右兄弟,否则返回空
     InsertChild(*T,*p,i,c);                //其中p指向树T的某个结点,i为所指结点p的度加上1,非空树c与T不相交,操作结果为插入c为树T中p指结点的第i棵子树
     DeleteChild(*T,*p,i);                //其中p指向树T的某个结点,i为所指向结点p的度,操作结果为删除T中,p所指结点的第i棵子树

endADT

树的存储结构

双亲表示法

假设一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示双亲结点的链表中的位置。

类似数据库表结构来存储部门层次结构。利用了顺序存储的结构。其中缺点也能看到,能快速定位,但不好扩展。

clipboard.png

如用双亲表示法来表示上图的树结构:

下标 data parent
0 A -1
1 B 0
2 C 0
3 D 1
4 E 2
5 F 2
6 G 3
7 H 3
8 I 3
9 J 4

以下是双亲表示法的结点结构:

#define MAX_TREE_SIZE

typedef int TElemType;
typedef struct PTNode{                //结点结构
    TElemType data;                    //结点数据
    int parent;                        //双亲位置
} PTNode;

typedef struc{
    PTNode node{MAX_TREE_SIZE};        //结点数组
    int r,n;                        //根的位置和结点数
}

根据parent指针(此指针并非一定是C语言的指针,C语言的坐标也可以称为指针)很容易找到它的双亲结点,所用时间复杂度为O(1).
如果我们想知道某结点的孩子是什么,如D的结点是什么,就需要遍历这个数据(并非啥广度深度遍历,就是直接暴力遍历数组),这样负责度为O(N),所以我们给结点结构增加最左边孩子域fristchild。

typedef struct PTNode{                //结点结构
    TElemType data;                    //结点数据
    int parent;                        //双亲位置
    int fristchild;                    //最左边孩子域
} PTNode;

这样也仅仅是解决0个或1个孩子结点的树结构中,找结点孩子的问题。当2个或2个以上孩子的结点情况下,就再增加一域:右兄弟域。

typedef struct PTNode{                //结点结构
    TElemType data;                    //结点数据
    int parent;                        //双亲位置
    int fristchild;                    //最左边孩子域
    int rightsib;                    //右兄弟域
} PTNode;

这样就能解决树中,找结点的孩子问题。这里引申一个结论:存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合,是否方便,时间复杂度好不好等

孩子表示法

先说孩子表示发之前,我们先看一种表示法:多重表表示法

每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重表表示法。
多重表表示法的缺点在于,由于每个结点的孩子数是不确定,如果预先在每个结点都开辟好一定的空间来存储指向孩子的指针,则会浪费空间。如果需要实时计算孩子的大小,则运算了大。
但如果是特定在二叉树的情境下,这个多重表示法是比较适合。

由于多重表表示法的缺点,引申出 孩子表示法:

clipboard.png

把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放一个一维数组中。

孩子表示法,需设计两钟 结点结构。

#define MAX_TREE_SIZE 100
typedef struct CTNode{                //孩子结点
    int child;                        //结点在结点数组中的位置
    struct CTNode *next;            //指向下一个兄弟结点
} *ChildPtr;

typedef struct{                        //表头结点
    TElemType data;                    
    ChildPtr firstchild;            //指向孩子链表
} CTBox;

typedef struct{                        //树结构
    CTBox nodes[MAX_TREE_SIZE];        //结点数组
    int r,n;                        //根的位置和结点数
}

孩子兄弟表示法

任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。

typedef struct CSNode{
    TElemType data;
    struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;

这个表示法的最大好处是将一课复杂的树变成一棵二叉树。

树的存储结构

本章也仅仅描述树有哪些表示法。别混淆了,这里说的大概念的树,没特指二叉树。遍历也没说,那应该树的遍历如前序遍历、中序遍历和后序遍历是属于二叉树的遍历方式。下面会说。
看来这章仅仅是为了把树描述的更完善些,从而引申出其其中最特特别的树:二叉树

二叉树

二叉树(Binary)是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和又子树的二叉树组成。

clipboard.png

二叉树特点

  1. 每个结点最多有两棵子树,也就是二叉树不存在度大于2的结点。
  2. 二叉树是有序树,左子树和右子树是有顺序的,次序不能任意颠倒。
  3. 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
  4. 二叉树的五种基本形态:
    4.1 空二叉树
    4.2 只有一个根结点
    4.3 根结点只有左子树
    4.4 根结点只有右子树
    4.5 根结点既有左子树又有右子树

特殊二叉树

斜树顾名思义,斜树一定要是斜的。所有结点都只有左子树的二叉树叫左斜树;所有结点都是只有右子树的二叉树叫右斜树;这两种统称为斜树。

clipboard.png
clipboard.png

满二叉树在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层次上,这样的二叉树称为满二叉树。

clipboard.png

完全二叉树对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。

clipboard.png

二叉树的性质

以下结论记下就好了,没看过推理证明过程的可以去看看。不然就靠记忆力记住,因为看了推理证明,依然会忘记推理证明过程。可以先把结论先用熟,后面再去记忆推理证明过程,反正记住以下是经过严格证明所得出来的结论就是了。

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

i>=1说明不能是空树,根据上面的说的定义,根是第一层,则根这一层是2^(1-1)=2^0=1个结点。

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

这里得澄清两个点,一是性质1说的是一层至多有多少个结点,而性质2是说整棵树至多有多少个结点。二是这里说的深度,依然和上面说的定义一样:树中结点的最大层次称为树的深度或高度。
也就是说如果树有1层,则2^1-1=1个结点
也就是说如果树有2层,则2^2-1=3个结点
如此类推

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

回顾下概念:叶结点(Leaf)或终端结点是度为0的结点

性质4: 具有n个结点的完全二叉树的深度为floor(log_2 n)+1,floor函数是指不大于x的最大整数

性质5: 如果对一棵有n个结点的完全二叉树(其深度为floor(log_2 n)+1)的结点按层序编号(从第1层到第floor(log_2 n)+1层,每层从左到右),对任一结点i(1<=i<=n)有:
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲的结点floor(i/2)。
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3.如果2i+1>n,则结点i无右孩子;否则其右孩子的结点2i+1。

二叉树的存储结构

二叉树的顺序存储结构

就是用一维数组存储二叉树中的结点,并且结点在树的位置跟数组的下标对应起来。
这种结构,如果对于满二叉树、完全二叉树来说,是比较好,空间利用率高。但对于斜树来说,利用率就非常低了。

clipboard.png

上图(完全二叉树)对应的顺序存储结构是:

clipboard.png

而一下图非完全二叉树、满二叉树及其应的顺序存储结构:

clipboard.png

二叉链表

二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的相反,我们称这样的链表叫做二叉链表。这个也是上诉所说的多重表表示法。

clipboard.png

结构如下

typedof struct BiTNode                     //结点结构
{
    TElemType data;                        //结点数据域
    struct BiTNode *lchild, *rchild;    //左右孩子指针域
} BiTNode, *BiTree;

二叉树的遍历

先理一下游戏规则:

二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

clipboard.png

根据上述游戏规则,则遍历此树有以下流行的玩法:
1.前序遍历

若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。遍历顺序为:ABDGHCEIF

2.中序遍历

若二叉树为空,则空操作返回,否则中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。遍历顺序为:GDHBAEICF

3.后序遍历

若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历左右子树,最后是访问根结点。遍历顺序为:GHDBIEFCA

4.层序遍历

若二叉树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。遍历顺序为:ABCDEFGHI

对于计算机来说,计算机它只会循环、判断等方式来处理,也就是说它只会处理线性序列,而上述四种遍历方法,其实就是把树中的结点变成某种意义的线性序列。

二叉树的遍历实现

前序遍历、中序遍历、后序遍历都是采用递归实现,极其简洁明了(当然明了是指对代码很熟悉或对递归概念很熟悉的人儿)。
前序遍历实现

void PreOrderTraverse(BiTree T){        //注意T在这里是个指针,指着根结点
    if(T==NULL)
        return;
    printf("%c",T->data);                //输出节点信息
    PreOrderTraverse(T->lchild);        //递归遍历左子树
    PreOrderTraverse(T->rchild);        //递归遍历右子树
}

中序遍历实现

void InOrderTraverse(BiTree T){            //注意T在这里是个指针,指着根结点
    if(T==NULL)
        return;
    InOrderTraverse(T->lchild);            //递归遍历左子树
    printf("%c",T->data);                //输出节点信息
    InOrderTraverse(T->rchild);            //递归遍历右子树
}

哈哈,你没看过,中序和前序遍历的不同就是一条语句挪了下位置而已。后序遍历也是这样:

后序遍历实现

void PostOrderTraverse(BiTree T){        //注意T在这里是个指针,指着根结点
    if(T==NULL)
        return;
    PostOrderTraverse(T->lchild);        //递归遍历左子树
    PostOrderTraverse(T->rchild);        //递归遍历右子树
    printf("%c",T->data);                //输出节点信息
}

树、森林与二叉树的转换

在数学上,两个子树或N个子树都是等价的,或者说N个子树的情况下都可以用二叉树实现。这样我们就可以利用二叉树的性质和算法来解决问题。

树转换为二叉树

步骤如下:
1.加线。在所有兄弟节点之间加一条线。
2.去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3.层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。

clipboard.png

clipboard.png

森林转换为二叉树

1.把每个树转为二叉树
2.第一棵二叉树不懂,从第二棵二叉树开始,依次把后一个棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了又森林转换来的二叉树。

参考:《大话数据结构》
  


电脑杂技集团
208 声望32 粉丝

这家伙好像很懂计算机~