数据结构与算法:二叉树

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

  • 基础知识

    • 基本概念
    • 性质
    • 有关二叉树的例题
      • 题目
      • 解析
  • 递归实现二叉树的遍历
  • 已知先序遍历和中序遍历如何求后序遍历
    • 引申已知中序遍历和后序遍历求先序遍历
  • 非递归实现二叉树的后序遍历
    • 如何使用非递归方法实现二叉树的先序遍历与中序遍历
  • 使用非递归算法求二叉树的深度
  • 霍夫曼编解码

基础知识

二叉树(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,则走左子树的根结点,否则走向右子树的根结点,一旦到达叶子结点,便译出代码多对应的字符。然后又重新从根结点开始继续译码,直到二进制编码结束。

时间: 2024-12-28 21:25:54

数据结构与算法:二叉树的相关文章

javascript数据结构与算法--二叉树(插入节点、生成二叉树)

javascript数据结构与算法-- 插入节点.生成二叉树 二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * * * */ /*用来生成一个节点*/ function Node(data, left, right) { this.data = data;//节点存储的数据 this.left = left; this.right = right; this.show = show; } function sh

[数据结构与算法] 二叉树及其遍历方式

声明:原创作品,转载时请注明文章来自SAP师太技术博客:www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将追究法律责任!原文链接:http://www.cnblogs.com/jiangzhengjun/p/4289830.html 一.数据结构分类 (一)按逻辑结构 集合(无辑关系) 线性结构(线性表):数组.链表.栈.队列 非线性结构:树.图.多维数组 (二)按存储结构 顺序(数组)储结构.链式储结构.索引储结构.散列储结构 二.二叉树相关性质

数据结构与算法--二叉树(一)

1 基于二叉链表的有序二叉树 1.1 问题 BST是Binary Search Tree的缩写,译为二叉搜索树,或有序二叉树,是二叉树的一种,它的定义如下: 1)或者是一棵空树: 2)或者是具有下列性质的二叉树: I) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值: II) 若右子树不空,则右子树上所有结点的值均大于它的根结点的值: III)左.右子树也分别为二叉排序树: BST在查找一个结点或插入一个结点时,具有极大的优势,速度非常快.是一种基础性数据结构,广泛应用于更加抽象的集合

数据结构与算法 —— 二叉树

二叉树 定义: 来自于百度百科. 在计算机科学中,二叉树是每个节点最多有两个子树的树结构.通常子树被称作"左子树"(left subtree)和"右子树"(right subtree).二叉树常被用于实现二叉查找树和二叉堆. 二叉树的每个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒.二叉树的第i层至多有2^{i-1}个结点:深度为k的二叉树至多有2^k-1个结点:对任何一棵二叉树T,如果其终端结点数为n_0,度为2的结点数为n_

小甲鱼数据结构和算法-----二叉树的构建和前序遍历

题目要求:建立二叉树并输出每个字符所在的层数.如下图要求输出 A 在第一层 B.C 在第二层 D.E在第三层 代码如下: #include <stdio.h> #include <stdlib.h> typedef struct BiTNode { char data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree; // 创建一棵二叉树,约定用户遵照前序遍历的方式输入数据 void CreateBiTree(BiTree *T)

数据结构和算法——二叉树

树1.树的优点有序数组: 查找很快,二分法实现的查找所需要的时间为O(logN),遍历也很快,但是在有序数组中插入,删除却需要先 找到位置, 在把数组部分元素后移,效率并不高. 链表: 链表的插入和删除都是很快速的,仅仅需要改变下引用值就行了,时间仅为O(1),但是在链表中查找数据却需要遍历所有的元素, 这个效率有些慢了.树的优点: 树结合了有序数组和链表的优点,可以实现快速的查找,也可以快速的删除,查找. 树的一些专用术语: 路径: 顺着连接节点的边从一个节点到另一个节点的,所经过的所有节点的

python数据结构与算法——二叉树结构与遍历方法

先序遍历,中序遍历,后序遍历 ,区别在于三条核心语句的位置 层序遍历  采用队列的遍历操作第一次访问根,在访问根的左孩子,接着访问根的有孩子,然后下一层 自左向右一一访问同层的结点 # 先序遍历 # 访问结点,遍历左子树,如果左子树为空,则遍历右子树, # 如果右子树为空,则向上走到一个可以向右走的结点,继续该过程 preorder(t):    if t:       print t.value       preorder t.L       preorder t.R # 中序遍历 # 从根

java数据结构和算法------二叉树基本操作

1 package iYou.neugle.tree; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class Binary_Tree<T> { 7 private Tree tree = new Tree(); 8 9 class Tree { 10 public T data; 11 public Tree left; 12 public Tree right; 13 } 14 15 public

js数据结构和算法---二叉树

原文: https://segmentfault.com/a/1190000000740261 //前序遍历 function preOrder(node) { if (node != null) { node.style.background = "black"; setTimeout(function () { preOrder(node.children[0]); },1500); setTimeout(function () { preOrder(node.children[1

js数据结构与算法——二叉树

function BinaryTree(){ var Node = function(key){ this.key = key; //值 this.left = null; //左箭头 this.right = null; //右箭头 } //根节点 var root = null; var insertNode = function(oldNode,newNode){ if(newNode.key < oldNode.key){ if(oldNode.left === null){ oldNo