数据结构(树、二叉树)

定义——

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

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

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

    对于树的定义还需要强调两点:

        1、n>0时,根节点是唯一的,不可能存在多个根节点

        2、m>0时,子树的个数没有限制,但他们一定是互不相交的

结点分类——

    结点的度:结点拥有的子树数称为节点的度。

    叶节点(终端结点):度为0的结点。

    分支节点(非终端结点):度不为0的结点,除根节点,分支节点也称为内部结点。

    树的度:是树内部各个结点度的最大值。

结点之间的关系——

    结点的子树的根称为该结点的孩子,该结点称为孩子的双亲。

    同一个双钱的孩子之间互称为兄弟。

    结点的祖先是从根结点到该结点所经过分支的所有结点。

树的其他相关概念——

    结点的层次是从根开始定义起,根为第一层,根的孩子为第二层,以此类推。某结点在第i层,则子树就在第i+1层。

    双亲在同一层的结点互为堂兄弟。

    树中结点最大的层次称为树的深度或高度。

    如果将树中结点的各个子树看成从左往右是有次序的,不能互换的,则称为有序树,否则称为无序树。

    森林是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合就是森林。

线性结构和树结构的比较——
        线性结构:第一个元素——无前驱;最后一个元素无后继;中间元素——一个前驱一个后继
        树结构:根节点——无双亲,唯一;叶节点——无孩子,可以多个;中间结点——一个双亲多个孩子。
树的抽象数据类型
    相对于线性结构,树的操作就完全不同了,基本常用操作:
        
    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棵子树。

树的存储结构——

    双亲表示法、孩子表示法、孩子兄弟表示法。

    1、双亲表示法(双亲角度考虑)

        在每个结点中,附设一个指示器指示其双亲结点在数组中的位置。

        结点结构:data|parent

            data为数据域,parent为指针域,指示双节结点在数组中的下标,根节点无双亲,存储-1。

        根据结点的parent指针很容易找到其双亲的位置,时间复杂度为O(1),找到-1,代表找到了根节点,但是要知道其孩子是什么,就要遍历整个结构。

    2、孩子表示法(孩子角度考虑)

    每个树中的结点可能有多棵子树,可以考虑用多重链表。每个结点有多个指针域,其中每个指针指向一棵子树的根结点,这种方法叫做多重链表表示法。

        假设指针域的个数为树的度,就会存在资源浪费。假设每个结点的指针域个数为结点的度,则每个结点的数据结构就不一样了,维护成本太高了。因此

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

    两种结构:

        child|next:child数据域,存储某结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针

        data|firstchild:data是数据域,用来存储结点的数据信息,firstchild是头指针域,存储该结点的孩子链表的头指针

    data|firstchild顺序结构存储结点信息及头指针,child|next链式结构,存储孩子结点指针

    3、孩子兄弟表示法(兄弟结点考虑)

        任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟存在也是唯一的。因此设置两个之神,分别指向该结点的第一个孩子和该结点的右兄弟。结点结构:data|firstchild|rightsib

        data是数据域,first child为指针域,存储该结点的第一个孩子的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。

二叉树——

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

    二叉树特点:

        每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。

        左子树和右子树是有顺序的,次序不能任意颠倒

        即使树中只有一棵子树,也要区分他是左子树还是右子树

    二叉树的五种基本形态:

        空二叉树

        只有一个根节点

        根节点只有左子树

        根节点只有右子树

        根节点既有左子树又有右子树

    特殊二叉树:

        1、斜树。所有的结点都只有左子树的二叉树叫左斜树。只有右子树的二叉树叫右斜树

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

            满二叉树的特点:

                叶子只能出现在最下一层。出现在其它层就不可能达成平衡

                非叶子结点的度一定是2,否则就是缺胳膊少腿了

                在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多

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

            完全二叉树的特点:

                叶子结点只能出现在最下两层

                最下层的叶子一定集中在左部连续位置

                倒数二层,若有叶子结点,一定都在右部连续位置

                如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况

                同样结点数的二叉树,完全二叉树的深度最小

    判断二叉树是否是完全二叉树:

        在给每个结点按照二叉树的结构逐层顺序编号,如果编号出现空挡,就说明不是完全二叉树,否则就是。

二叉树的性质——

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

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

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

            度为1的结点数为n1,结点总数n=n0+n1+n2;分支线数量为n-1=n1+2n2;两式消去n与n1得n0=n2+1;

    性质4:具有n个结点的完全二叉树的深度为[log2n]+1([x]表示不大于x的最大整数)

        满二叉树结点n=2^k-1,由于完全二叉树叶子结点只出现在最后两层且结点序号和满二叉树对应位置的序号一样,因此n>2^(k-1)-1,2^(k-1)-1<n<=2^k-1,由于n为整数,本来n大于左边的值,左边+1,因此就>=,所以2^(k-1)<=n<2^k,两边取对数,得k-1<=log2n<k,由于k也是整数,k=[log2n]+1;

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

        *如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则双亲是结点[i/2]

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

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


二叉树的存储结构

    二叉树的顺序存储结构:

        二叉树的顺序存储结构就是用一维数组存储二叉树的结点,并且结点的存储的位置,也就是数组的下标要能体现结点之间的逻辑关系。顺序存储结构只用于完全二叉树,因为完全二叉树定义的严格,结点序号。

    二叉链表(链式存储结构):

        二叉树每个结点最多有两个孩子,所以为他设计一个数据域两个指针域,lchild|data|rchild

二叉树遍历——

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

    二叉树的遍历方法:

        1、前序遍历:规则是若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树

        2、中序遍历:规则是若树为空,则空操作返回,否则从根节点开始(并不是先访问根节点)中序遍历根节点的左子树,然后才是根节点,最后中序遍历右子树

        3、后序遍历:规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根节点

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

    前序遍历(递归算法):

        c语言:

            void PreOrderTraverse(BiTree T){

                if(T==null)return ;

                printf("%c",T->data);

                PreOrderTraverse(T->lchild);

                PreOrderTraverse(T->rchild);

            }

        java:           

            public void PreOrderPreverse(BiTNode node) {
if (node == null)
return;
else {
System.out.print(node.getData() == null ? "" : node.getData()
+ "      ");
PreOrderPreverse(node.getLchild());
PreOrderPreverse(node.getRchild());
}
}

    中序遍历(递归算法):

            void InOrderTraverse(BiTree T){

                if(T==null)return;

                InOrderTraverse(T->lchild);

                printf("%c",T->data);

                InOrderTraverse(T->rchild);

            }

    后序遍历:        

         void InOrderTraverse(BiTree T){

                if(T==null)return;

                InOrderTraverse(T->lchild);              

                InOrderTraverse(T->rchild);

                printf("%c",T->data);

            }

    已知前序遍历和中序遍历序列,可以唯一确定一棵二叉树。

    已知后序遍历和中序遍历序列,可以唯一确定一棵二叉树。(必须要有中序)

二叉树建立——

    建立二叉树,也是利用递归原理。只是在原来打印结点的地方改成了生成结点,给结点赋值的操作。为了让每个结点确认是否都有左右孩子对二叉树进行拓展:将二叉树中每个结点的空指针引出一个虚结点,其值唯一特定值,比如“#”。

    前序遍历生成二叉树:

        c语言:

        void CreateBiTree(BiTree *T){

            TElemType ch;

            scanf("%c",&ch);

            if(ch=='#'){

                *T=null;

            }

            else{

                *T=(BiTree)malloc(sizeof(BiTNode));

                if(!*T)exit(OverFlow);

                (*T)->data=ch;

                CreateBiTree(&(*T)->lchild);//构造左子树

                CreateBiTree(&(*T)->rchild);//构造右子树

            }

        }

        java语言:

            public BiTNode PreCreateBiTree(BiTNode node) {
Scanner sc = new Scanner(System.in);
String ch = sc.nextLine();
if (ch.equals("#")) {
node = null;
} else {
node.setData(ch);
node.setLchild(new BiTNode());
node.setRchild(new BiTNode());
PreCreateBiTree(node.getLchild());
PreCreateBiTree(node.getRchild());
}
return node;

}

线索二叉树——

    对于一个有n个结点的二叉链表,每个结点有两个指针域,一个指向左孩子,一个指向右孩子,所以一共有2n个指针域。而n个结点的二叉树一共有n-1条分支线数,也就是说,其实是存在2n-(n-1)=n+1个空指针域。这些空指针域不存储任何信息其实是对资源的浪费。而对于中序遍历可以知道一个结点的前驱和后继,但是每次都要进行遍历才知道,基于此,线索二叉树将空指针域用来表示结点前驱和后继。

    线索二叉树:指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。

    线索二叉树:一个结点lchild为空则指示结点前驱,rchild为空则用来指示结点后继,因此线索二叉树等于是把一棵二叉树转换为一个双向链表。

    对二叉树以某种次序遍历使其称为线索二叉树的过程是线索化。

    数据结构:lchild | ltag | data | rtag | rchild

    线索化的实质就是将二叉链表中的空指针改为指向前驱或者后继线索。由于前驱和后继的信息只有在遍历该二叉树时才能看到,所以线索化过程就是在遍历过程中修改空指针的过程。

    线索二叉树的时间复杂度为O(n)。

    如果所有的二叉树需经常遍历或者查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。

    中序遍历中序线索化

        C语言:

            BiThrTree pre;//全局变量,始终指向刚刚访问过的结点

            void InThreading(BiThrTree p){

                InThreading(p->lchild);

                if(!p){

                    if(p->lchild){

                        p.ltag=1;//1代表前驱线索

                        p.lchild=pre;

                    }

                    if(pre->rchild){//因为p的后继还没有访问到,所以只能对前驱结点pre的右指针判断

                        pre.rtag=1;

                        pre.rchild=p;

                    }

                    pre=p;

                }

                InThreading(p->lchild);

            }

    JAVA语言:

        private BiThrTree pre = null;
public void InThreading(BiThrTree p) {
if (p != null) {
InThreading(p.getLchild());
if (p.getLchild() == null) {
p.setLtag(1);
p.setLchild(pre);
}
if (pre != null && pre.getRchild() == null) {
pre.setRtag(1);
pre.setRchild(p);
}
pre = p;
InThreading(p.getRchild());
}

}

    中序遍历线索二叉树:

        和双向链表一样,在二叉树线索链表上添加一个头结点,头结点lchild指向根节点,rchild指向中序遍历的最后一个结点。中序遍历的第一个结点的lchild和最后一个结点的rchild均指向头结点。这样既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。

        Status InOrderTraverse(BiThrTree T){

            BiThrTree p;

            p=T->lchild;

            while(p!=T){

                while(p->lTag==0)p=p->lchild;

                printf("%c",p->data);

                while(p->Thread==1&&p->right!=T){//如果他是线索,并且不指向头结点,指向头结点则就是最后一个了,也没有数据要打印

                    p=p->rchild;//直接打印后继

                    printf("%c",p->data);

                }

                p=p->rchild;//线索遍历跳出循环以后,肯定是有右孩子的,所以指向右孩子

            }

        }

        java语言:

            public void InOrderTraverse_Thr(BiThrTree T) {
BiThrTree p = T.getLchild();// 指向根节点
while (p != T)// p==T时,二叉线索树有可能是空树或者是循环结束了
{
while (p != null && p.getLtag() == 0)
p = p.getLchild();// 查找中序遍历第一个结点
if (p != null)
System.out.print(p.getData() + "   ");// 打印中序遍历第一个结点的数据
// 对后继结点打印
while (p != null && p.getRtag() == 1 && p.getRchild() != T) {
p = p.getRchild();
System.out.print(p.getData() + "   ");
}
if (p != null)
p = p.getRchild();// 进入右子树根
}

}

树、深二叉树的转换——

    1、树转换为二叉树    

            1】加线,在所有的兄弟结点之间加一条线

            2】去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线

            3】层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。

    2、森林转换为二叉树

        1】把每个树转换成二叉树

        2】第一课二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根节点作为前一棵二叉树的根节点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了有森林转换来的二叉树。

    3、二叉树转换为树

        1】加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子的节点、右孩子的右孩子的右孩子结点。。。,反正就是左孩子的n个右孩子结点都作为此节点的孩子。将该结点与这些右孩子结点用线连接起来。

        2】去线。删除原二叉树中所有结点与其右孩子结点的连线。

        3】层次调整。使之结构层次分明。

    4、二叉树转换为森林

        1】从根节点开始,若右孩子存在,则把右孩子结点的连线删除,在查看分离后的二叉树,若右孩子存在,则连线删除。。。直到所有右孩子连线都删除为止,得到分离的二叉树

        2】再将每棵分离后的二叉树转换为树即可。

树与二叉树的遍历——

    1、树的遍历分为两种。

        1】先根遍历,即先访问树的根节点,然后依次先根遍历根的每棵子树

        2】另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根的节点

    2、森林的遍历也分两种

        1】前序遍历:先访问森林中第一棵树的根节点,然后再依次先根遍历根的每棵子树,再依次用同样的方式遍历除去第一棵树的剩余树构成的森林

        2】后续遍历:是先访问森林树中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根节点,再依次同样的方式遍历除去第一棵树的剩余树构成的森林。

    森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结构相同。

赫夫曼树及其应用——

    1、路径和路径长度。在一棵树中,从一个结点往下可以达到的孩子或子孙结点之间的通路称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根节点到第L层结点的路径长度为L-1。树的路径长度就是从树根到每一结点的路径长度之和。

    2、结点的权及带权路径长度。若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。

    3、树的带权路径长度。树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL,其中带权路径长度WPL最小的二叉树称作赫夫曼树。

    赫夫曼树的构造:

        1、先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即A5,E10,B15,D30,C40

        2、取头两个最小权值的结点作为一个新结点N1的两个子结点,注意相对较小的是左孩子,这里就是A为N1的左孩子,E为N1的右孩子。新结点的权值为两个叶子权值的和5+15=20

        3、将N1替换A与E,插入有序序列中,保持从大到小的排列,即N115,B15,D30,C40

        4、重复步骤2,将N1与B作为一个新结点N2的两个子结点。N2的权值=15+15=30

        5、将N2替换N1与B,插入有序序列中,保持从小到大排列。即:N230,D30,C40

        6、重复步骤2,将N2和D作为新结点N3的两个子结点,N3的权值=30+30=60

        7、将N3替换为N2和D,插入有序序列中,保持从小到大排列,即C40,N360

        8、重复步骤2,将C与N3作为一个新结点T的两个子结点,由于T即是根结点,完成赫夫曼树的构造。

    构造赫夫曼树的赫夫曼算法描述:

        1、根据给定的n个权值{w1,w2,...wn}构成n棵二叉树的集合F={T1,T2,...,Tn},    其中每棵二叉树Ti中只有一个带权为w1根节点,其左右子树均为空。

        2、在F中选取两棵根结点的权值最小的树作为左右字数构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。

        3、在F中删除这两棵树,同时将新得到的二叉树加入F中

        4、重复2、3步骤,知道F只含一棵树为止。这棵树便是赫夫曼树。

    赫夫曼编码:

        若要设计长短不等的编码,则必须是任一字符编码都不是另一个字符编码的前缀,这种编码称作前缀编码。

        一般,设需要编码的字符集为{d1,d2,...,dn},各个字符在电文中出现的次数或者频率集合为{w1,w2,...,wn},以w1,w2,...,wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,有分支代表1,则从根节点到叶子结点所经过的路径分支组成的0和1的序列便称为该结点对应字符的编码,这就是赫夫曼编码。


        

假设ABCDEFG六个字母出现频率一次为27,8,15,30,5合起来是100。构造赫夫曼树如上图左树。左分支用0编码,右分支用1编码,得到上图ABCDEF的编码。

    

    

    


        

    

   

        

        





猜你喜欢

转载自blog.csdn.net/cmwenxin1992/article/details/80036446