码迷,mamicode.com
首页 > 编程语言 > 详细

数据结构与算法:二叉树

时间:2016-06-18 11:39:41      阅读:255      评论:0      收藏:0      [点我收藏+]

标签:

二叉树是一种非常常见并且实用的数据结构,它结合了有序数组与链表的优点。在二叉树中查找数据与在数组中查找数据一样快,在二叉树中添加、删除数据的速度也和在链表中一样高效,所以有关二叉树的相关技术一直是程序员面试笔试中必考的知识点。

基础知识

二叉树(Binary Tree)也称为二分树、二元树、对分树等,它是n(n>=0)个有限元素的集合。该集合或者为空,或者由一个称为根(root)的元素及两个不想交的、被分别称为左子树和右子树的二叉树组成。当集合为空时,称该二叉树为空二叉树。

在二叉树中,一个元素也称为一个结点。二叉树的递归定义:二叉树或者是一棵空树,或者是一棵由一个根结点和两棵互不相交的分别称做根结点的左子树和右子树所组成的非空树,左子树和右子树又同样都是一棵二叉树。

基本概念

以下是一些常见的二叉树的基本概念:
(1)结点的度。结点所拥有的子树的个数称为该结点的度
(2)叶结点。度为0的结点称为叶结点,或者称为终端结点
(3)分枝结点。度不为0的结点称为分支节点,或者称为非终端结点。一棵树的结点除叶结点以外,其余的都是分支结点。
(4)左孩子、右孩子、双亲。树中一个结点的子树的根结点称为这个结点的孩子。这个结点称为它孩子结点的双亲。具有同一个双亲的孩子结点互称为兄弟。
(5)路径、路径长度。如果一棵树的一串结点n1,n2,…,nk有如下关系:结点ni时ni+1的父节点(1<=i<k),就把n1,n2,…,nk称为一条由n1~nk的路径。这条路径的长度是k-1
(6)祖先、子孙。在树中,如果有一条路径从结点M~结点N,那么M就称为N的祖先,而N称为M的子孙
(7)结点的层数。规定树的根结点的层数为1,其余结点的层数等于它的双亲节点的层数加1。
(8)树的深度。树中所有结点的最大层数称为树的深度。
(9)树的度。树中各结点度的最大值称为该树的度,叶子结点的度为0。
(10)满二叉树。在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称为满二叉树
(11)完全二叉树。一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1<=i<=n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。完全二叉树的特点是:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。

性质

二叉树的基本性质如下
性质1:一棵非空二叉树的第i层上最多有2^(i-1)个结点(i>=1)

性质2:一棵深度为k的二叉树中,最多具有 2^k - 1个结点,最少有k个结点

性质3:对于一棵非空的二叉树,度为0的结点(即叶子结点)总是比度为2的结点多一个,即如果叶子结点数为n0,度为2的结点数为n2,则有 n0 = n2 + 1.

证明:用n0表示度为0(叶子结点)的结点总数,用n1表示度为1的结点总数,n2表示度为2的结点总数,n表示整个完全二叉树的结点总数,则n=n0+n1+n2.根据二叉树和树的性质,可知 n=n1+2*n2+1 (所有结点的度数之和 +1 = 结点总数),根据两个等式可知 n0 + n1 +n2 = n1+2*n2 +1 ,所以, n2 = n0-1,即 n0 = n2 + 1. 所以 n = n0 + n1 + n2。

性质4:具有n个结点的完全二叉树的深度为 log2 n + 1(log以2为底,n的对数,向下取整)

证明:根据性质2,深度为k的二叉树最多只有 2^k -1 个结点,且完全二叉树的定义是与同深度的满二叉树前面编号相同,即它的总结点数n位于k层和k-1层满二叉树容量之间,即2^(k-1)-1 < n <= 2^(k-1) - 12^(k-1) <= n < 2^k,三边同时取对数,于是有 k-1 <= log2n < k因为k是整数,所以深度为 性质4所述。

性质5:对于具有n个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的结点,有:
(1)如果i>1,则序号为i的结点的双亲节点的序号为 i/2;如果 i=1,则序号为i的结点 是根结点,无双亲结点。
(2)如果2i<=n,则序号为i的结点的左孩子结点的序号为2i;如果2i>n,则序号为i的结点无左孩子。
(3)如果2i+1 <= n,则序号为i的结点的右孩子结点的序号为2i+1;如果2i+1>n,则序号为i的结点无右孩子

此外,若对二叉树的根结点从0开始编号,则相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为 2i+1,右孩子的编号为 2i+2。

有关二叉树的例题

题目

例题1: 一棵完全二叉树上有1001个结点,其中叶子结点的个数是多少?

例题2:如果根的层次为1,具有 61个结点的完全二叉树的高度为多少?

例题3:在具有100个结点的树中,其边的数目为多少?

解析

例题1:二叉树的公式: n = n0 + n1 + n2 = n0+n1+(n0-1) = 2*n0 + n1 -1.而在完全二叉树中,n1只能取0或1.若n1 = 1,则 2*n0 = 1001 , 可推出n0为小数,不符合题意;若 n1 =0, 则 2*n0-1 = 1001, 则 n0 = 501.所以答案为501.

例题2:如果根的层次为1,具有61个结点的完全二叉树的高度为多少?
根据二叉树的性质,具有n个结点的完全二叉树的深度为 log2n + 1 (log以2为底n的对数),因此含有61个结点的完全二叉树的高度为 log2n + 1 (log以2为底n的对数),即应该为6层。所以答案为6.

例题3:在具有100个结点的树中,其边的数目为多少?
在一棵树中,除了根结点之外,每一个节点都有一条入边,因此总边数应该是 100-1,即99条。所以答案为 99

递归实现二叉树的遍历

二叉树的先序遍历的思想是从根结点开始,沿左子树一直走到没有左孩子的结点为止,依次访问所经过的结点,同时所经结点的地址进栈,当找到没有左孩子的结点时,从栈顶退出该结点的双亲的右孩子。此时,此结点的左子树已访问完毕,再用上述方法遍历该结点的右子树,如此重复到栈空为止。

二叉树中序遍历的思想是从根结点开始,沿左子树一直走到没有左孩子的结点为止,并将所经结点的地址进栈,当找到没有左孩子的结点时,从栈顶退出该结点并访问它。此时,此结点的左子树已访问完毕,再用上述方法遍历该结点的右子树,如此重复到栈空为止。

二叉树后序遍历的思想是从根结点开始,沿左子树一直走到没有左孩子的结点为止,并将所经结点的地址第一次进栈,当找到没有左孩子的结点时,此结点的左子树已访问完毕,从栈顶退出该结点,判断该结点是否为第一次进栈。如果是,再将所经结点的地址第二次进栈,并沿该结点的右子树一直走到没有右孩子的结点为止;如果不是,则访问该结点。此时,该结点的左右子树都已完全遍历,且令指针p=NULL,如此重复直到栈空为止。

已知先序遍历和中序遍历,如何求后序遍历

一般数据结构都有遍历操作,根据需求的不同,二叉树一般有以下几种遍历方式:先序遍历、中序遍历、后序遍历和层序遍历

(1)先序遍历:如果二叉树为空,遍历结束。否则,第一步,访问根结点;第二步,先序遍历根结点的左子树;第三步,先序遍历根结点的右子树。
(2)中序遍历:如果二叉树为空,遍历结束。否则,第一步,中序遍历根结点的左子树;第二步,访问根结点;第三步,中序遍历根结点的右子树。
(3)后序遍历:如果二叉树为空,遍历结束。否则,第一步,后序遍历根结点的左子树;第二步,后续遍历根结点的右子树;第三步,访问根结点
(4)层次遍历:从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问

技术分享

图13-15 的各种遍历结果如下:
先序遍历 ABDHIEJCFG
中序遍历 HDIBJEAFCG
后序遍历 HIDJEBFGCA
层次遍历 ABCDEFGHIJ

例如,先序序列为 ABDECF, 中序序列为 DBEAFC。求后序序列。

首先先序遍历树的规则为根左右,可以看到先序遍历序列的第一个元素必为树的根结点,则A就为根结点。再看中序遍历为左根右,再根据根结点A,可知左子树包含元素为 DBE,右子树包含元素为FC。然后递归求解左子树(左子树的先序为 BDE,中序为 DBE),递归求解右子树(即右子树的先序为 CF,中序为 FC)。如此递归到没有左右子树为止。所以,树结构如图 13-16所示。

通过上面的例子可以总结出用先序遍历和中序遍历来求解二叉树的过程,步骤如下:
(1)确定树的根结点。树根是当前树中所有元素在先序遍历中最先出现的元素,即先序遍历的第一个节点就是二叉树的根。

(2)求解树的子树。找到根在中序遍历的位置,位置左边是二叉树的左孩子,位置右边是二叉树的右孩子,若根结点左边或右边为空,则该方向子树为空;若根结点左边和右边都为空,则根结点已经为叶子结点。

(3)对二叉树的左、右孩子分别进行步骤(1)(2),直到求出二叉树结构为止。

技术分享

引申,已知中序遍历和后序遍历,求先序遍历

第一步确定树的跟,树根是当前树中所有元素在后序遍历中最后出现的元素。
第二步求解树的子树,找出根结点在中序遍历中的位置,根左边的所有元素就是左子树,根右边的所有元素就是右子树,如果根结点左边或右边为空,则该方向子树为空;若根结点左边和右边都为空,则根结点已经为叶子结点。
第三步递归求解树,将左子树和右子树分别看成一棵二叉树。重复以上步骤,直到所有的结点完成定位。该过程 与根据先序序列和中序序列求解树的过程类似,略有不同。

需要注意的是,如果知道先序和后序遍历序列,是无法构建二叉树的。例如,先序序列为 ABDECF,后序序列为 DEBFCA,此时只能确定根结点,而对于左右子树的组成不确定。

非递归实现二叉树的后序遍历

后序遍历可以用递归实现,程序中递归的调用就是保存函数的信息在栈中。一般情况下,能用递归解决的问题都可以用栈解决,知识递归更符合人们的思维方式,代码相对而言也更简单,但不能说明递归比栈的方式更快、更节省空间,因为在递归过程中都是操作系统来帮助用栈实现存储信息。下面用栈来实现二叉树的后序遍历。

栈的思想是“先进后出”,即首先把根结点入栈(这时栈中有一个元素),根结点出栈的时候再把它的右左孩子入栈(这时栈中有两个元素,注意是“先进右后进左”,不是“先进左后进右”),再把栈顶出栈(也就是左孩子),再把栈顶元素的右左孩子入栈,此过程一直执行直到栈为空,出栈的元素按顺序排列就是这个二叉树的先序遍历。

用栈来解决二叉树的后序遍历是最后输出父亲结点,先序遍历是在结点出栈时入栈右左孩子。显然,对于后序遍历,不应该在父亲结点出栈时,才把右左孩子入栈,应该在入栈时就把右左孩子一并入栈。在父亲结点出栈时,应该判断右左孩子是否已经遍历过(是否执行过入栈),那么就应该由一个标记来判断还在是否遍历过。

下面借用二叉树的结构体来定义一个适用于这个算法的新结构体

typedef struct stackTreeNode
{
    BTree treeNode;
    int flag;
} *pSTree;

结构体中,flag为标志位,0表示左右孩子没有遍历 2表示左右孩子遍历完,具体实现代码如下:

int lastOrder( BTree root )
{
    stack< pSTree > stackTree;
    pSTree sTree = ( pSTree) malloc( sizeof(struct stackTreeNode) );
    sTree->treeNode = root;
    sTree->flag = 0;
    stackTree.push( sTree );
    while( !stackTree.empty() )
    {
        psTree tmptree = stackTree.top();
        if(tmptree->flag == 2)
        {
            cout << tmptree->treeNode->data << " ";
            stackTree.pop();
        }
        else
        {
            if(tmptree->treeNode->rchild)
            {
                pSTree sTree = (pSTree) malloc( sizeof(struct stackTreeNode) );
                sTree->treeNode = tmptree->treeNode->rchild;
                sTree->flag = 0;
                stackTree.push(sTree);
            }
            tmptree->flag++;
            if( tmptree->treeNode->lchild )
            {
                PSTree sTree = (pSTree)malloc(sizeof(struct stackTreeNode));
                sTree->treeNode = tmptree->treeNode->lchild;
                sTree->flag = 0;
                stackTree.push(sTree);
            }
            tmptree->flag++;
        }
    }
    return 1;
}

如何使用非递归方法实现二叉树的先序遍历与中序遍历

将二叉树的先序遍历递归算法转化为非递归算法的方法如下:
(1)将二叉树的根结点作为当前节点。
(2)若当前结点非空,则先访问该结点,并将该结点进栈,再将其左孩子结点作为当前结点,重复步骤(2),直到当前结点为空为止。
(3)若栈非空,则栈顶结点出栈,并将当前结点的右孩子结点作为当前结点
(4)重复步骤(2)(3),直到栈为空且当前结点为空为止。

将中序遍历递归算法转化为非递归算法的方法如下:
(1)将二叉树的根结点作为当前结点。
(2)若当前结点非空,则该结点进栈并将其左孩子结点作为当前结点,重复步骤(2),直到当前结点为空为止。
(3)若栈非空,则将栈顶结点出栈并作为当前结点,接着访问当前结点,再将当前结点的右孩子结点作为当前结点。
(4)重复步骤(2)(3),直到栈为空且当前为空为止。

使用非递归算法求二叉树的深度

计算二叉树的深度,一般都是用后序遍历,采用递归算法,先计算出左子树的深度,再算出右子树的深度,最后取较大者加1即为二叉树的深度

typedef struct Node
{
    char data;
    struct Node *LChild;
    struct Node *RChild;
    struct Node *Parent;
}BNode,*BTree;

//后序遍历求二叉树的深度递归算法
int PostTreeDepth( BTree root )
{
    int left,right, max;
    if( root!=NULL )
    {
        left = PostTreeDepth( root->LChild );   
        right = PostTreeDepth( root->RChild );
        max = left > right ? left : right ;
        return (max+1);
    }
    else
        return 0;
}

如果直接将该算法改成非递归形式是非常繁琐和复杂的。考虑到二叉树深度与深度的关系,可以有下面两种非递归算法实现求解二叉树深度。

方法一:先将算法改成先序遍历再改写非递归形式。先序遍历算法:遍历一个结点前,先算出当前结点时在哪一层,层数的最大值就等于二叉树的深度。

int GetMax( int a,int b )
{
    return a>b?a:b;
}
int GetTreeTreeHeightPreorder( const BTree root )
{
    struct Info
    {
        const BTree TreeNode;
        int level;
    }
    deque<Info> dq; //双端队列,可以在两端进行插入和删除元素
    int level = -1;
    int TreeHeight = -1;
    while(1)
    {
        while(root)
        {
            ++level;
            if(root->RChild)
            { 
                Info info = { root->RChild, level };
                dq.push_back( info ); // 尾部插入一数据
            }// end if
            root = root->LChild;
        }//while(root)
        TreeHeight = GetMax( TreeHeight, level );
        if( dq.empty())
            break;
        const Info&info = dq.back();// 返回最后一个数据
        root = info.TreeNode;
        level = info.level;
        dq.pop_back(); // 删除最后一个数据
    }
    return TreeHeight;
}

方法二:修改上面提到的迭代算法。上例中,所用到辅助栈(或双端队列)的大小达到的最大值减去1就等于二叉树的深度。因而只需记录在往辅助栈放入元素后(或者在访问结点数据时),辅助栈的栈大小达到的最大值

int GetTreeHeightPostorder( const BTree root )
{
    deque<const BTree> dq; // 双端队列
    int TreeHeight = -1;
    while(1)
    {
        //先序将左子树入栈
        for( ;root!=NULL; root=root->LChild )
            dq.push_back( root );
        //dq.size()辅助栈的大小
        TreeHeight = GetMax( TreeHeight, (int)dq.size()-1 );
        while(1)
        {
            if(dq.empty()) return TreeHeight;
            const BTree parrent = dq.back();
            const BTree Rchild = parrent->RChild;
            if( RChild&& root!=RChild )
            {
                root = RChild;
                break;
            }
            root = parrent;
            dq.pop_back();
        }
    }
    return TreeHeight;
}

霍夫曼编解码

霍夫曼编码用到一种叫做“前缀编码”的技术,即任意一个数据的编码都不是另一个数据编码的前缀。而最优二叉树,即霍夫曼树(带权路径长度最小的二叉树)就是一种实现霍夫曼编码的方式。霍夫曼编码的过程就是构造霍夫曼树的过程,构造霍夫曼树的相应算法如下:
(1)有一组需要编码且带有权值的字母,如a(4) b(8) c(1) d(2) e(11)。括号内分别为各字母相对应的权值。
(2)选取字母中权值较小的两个 c(1) d(2) 组成一个新二叉树,其父节点的权值为这两个字母权值之和,记为 f(3) ,然后将该结点加入到原字母序列中去(不包含已经选择的权值最小的两个字母),则剩下的字母为 a(4) b(8) e(11) f(3)
(3)重复进行步骤(2),直到所有字母都加入到二叉树中为止。(编码一般是左0, 右1)

技术分享
技术分享

霍夫曼树的解码过程与编码过程正好相反,从根结点触发,逐个读入编码内容;如果遇到0,则走左子树的根结点,否则走向右子树的根结点,一旦到达叶子结点,便译出代码多对应的字符。然后又重新从根结点开始继续译码,直到二进制编码结束。

技术分享

数据结构与算法:二叉树

标签:

原文地址:http://blog.csdn.net/omenglishuixiang1234/article/details/51694948

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!