二叉树:性质及遍历

        树是一种重要的数据结构,其在文件系统和数据库系统中应用广泛,可以提高排序和检索的效率。

        二叉树是一种最基础最典型的树,常用于教学和研究树的特性。

        下面将二叉树的概念、性质和遍历做一个小结,方便大家共同学习,欢迎纠错补充。


一.概念及性质

1.1 概念 

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

      

        从定义来看,二叉树有五种基本形态:

        a)空二叉树

        b)只有一个跟节点

        c)根节点只有左子树

        d)根节点只有右子树

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

      

        从树中节点的组成特点来看,有几种特殊二叉树:

        a)斜树

        顾名思义,二叉树是斜的:左斜树,所有节点都只有左子树;右斜树,所有节点都只有右子树。

        斜树特点明显,每层都只有一个节点,节点的个数与二叉树的深度相同。

        b)满二叉树

        树中所有的分支节点都有左子树和右子树,并且所有叶子都在同一层上。

        简单来说,非叶子节点的度都是2,叶子都在同一层。

        c)完全二叉树

        不套用书上的概念,简单理解就是,把给定的x个节点,按照满二叉树的形式,层数从上到下,每一层从左到右连续填到对应的位置,直到把节点填完。这里重要的一点就是,填的时候一定要严格遵循前面的规则,不能跨越位置不填。

        清楚了定义,可以想到其有一些特殊的性质:叶子节点只会出现在最下面两层(也可能是最下一层,此时的完全二叉树就是满二叉树);最下层的叶子左连续,倒数二层的叶子(若存在)右连续;不存在一个节点只有右子树的情况;相同节点数的二叉树,完全二叉树的深度最小。


1.2 性质

        二叉树具有一些需要我们理解并记住的性质,方便我们更好地使用它。本文省略这些性质的证明,感兴趣的话可以查阅相关书籍。

        a)在二叉树的第i层上至多有 2^(i-1) 个节点(i >= 1)。

        b)深度为k的二叉树至多有2^k - 1个节点(k >= 1)。

        c)对任何一棵二叉树T,度为2的节点数n2,度为0的节点数n0,则n0 = n2 + 1。(n个节点共 n-1 条边:n-1=n1+2*n2;又n=n0+n1+n2,联立两式即得出结论。)

        d)具有n个节点的完全二叉树的深度为L(log2)n_| + 1。(下取整)


二.二叉树的遍历

        有一次面试的时候被问到二叉树的深度优先遍历 ,当时脑子里完全没有思路:深度优先遍历是先序遍历、中序遍历和后序遍历中的哪一种呢?!其实,深度优先遍历和广度优先遍历是图遍历 中的概念,如果放到树中来用:先序遍历就是一种深度优先遍历;层序遍历就是一种广度优先遍历。为什么说是“一种”,因为先序和层序都是从左到右(一般习惯上)访问孩子结点,而深度和广度中的访问左右不定(要么都是从左到右,要么都是从右到左访问孩子结点)。

        如果我们约定了先左后右的习惯,二叉树的遍历主要分为四种:先序、中序、后序和层序遍历

        下面是各遍历算法的实现(递归、非递归)。

        首先定义二叉树的二叉链表节点结构:


struct BTNode
{
    int data;                      //这里取整型
    struct BTNode *pLeft;          //左孩子指针
    struct BTNode *pRight;         //右孩子指针
}


2.1 先序遍历

若二叉树为空,则空操作返回;否则按照“根结点-左孩子-右孩子”的顺序进行访问

递归算法,按概念来操作就行。如下:


void PreTraverse(BTNode *pT)
{
    if(NULL != pT)
    {
        printf("%d ", pT -> data);
        PreTraverse(pT -> pLeft);
        PreTraverse(pT -> pRight);
    }
}


非递归算法,要模拟前面的递归算法(本身函数调用的时候使用的就是栈),我们想到使用:先将根节点入栈,然后根节点出栈并访问根节点;再将根节点的右孩子、左孩子入栈,然后出栈一个节点并访问;重复前面的过程,直到栈为空为止。如下:


void PreTraverse(BTNode *pT)
{
    if(NULL == pT) 
        return; 
    stack<struct BTNode *> s; 
    s.push(pT); 
    struct BTNode *pTN = NULL; 

    while(!s.empty()) 
    { 
        pTN = s.top(); 
        cout << pTN -> data << " "; 
        s.pop(); 
        if(NULL != pTN -> pRight)          //一定先入栈右孩子 
            s.push(pTN -> pRight); 
        if(NULL != pTN -> pLeft) 
            s.push(pTN -> pLeft); 
    } 

    cout << endl;
}


2.2 中序遍历

若二叉树为空,则空操作返回;否则按照“左孩子-根结点-右孩子”的顺序进行访问

递归算法,按概念操作。如下:


void InTraverse(BTNode *pT)
{
    if(NULL != pT)
    {
        InTraverse(pT -> pLeft);
        printf("%d ", pT -> data);      
        InTraverse(pT -> pRight);
    }
}


非递归算法,只要一个栈顶节点的左子树不为空,就将其左子树也入栈;否则,访问该栈顶节点,并将其右子树入栈。如下:

void Intraverse(BTNode *pT)
{
    stack<struct BTNode *> s;
    struct BTNode *pTN = pT;

    while(NULL != pTN || !s.empty())
    {
        if(NULL != pTN)
        {
            s.push(pTN);
            pTN = pTN -> pLeft;
        }
        else
        {
            pTN = s.top();
            cout << pTN -> data << " ";
            s.pop();
            pTN = pTN -> pRight;
        }
    }

    cout << endl;
}


2.3 后序遍历
若二叉树为空,则空操作返回;否则按照"左孩子-右孩子-根节点"的顺序进行访问。

递归算法,按概念操作。如下:


void PostTraverse(BTNode *pT)
{
    if(NULL != pT)
    {
        PostTraverse(pT -> pLeft);     
        PostTraverse(pT -> pRight);
        printf("%d ", pT -> data); 
    }
}


非递归算法。如下:

方法一:一个栈实现

对任一节点P,先将其入栈;如果P不存在左右孩子,则可直接访问它,或者P存在左右孩子,但它们都已被访问过了,则同样直接访问P;若非上述两种情况,则将P的右孩子和左孩子依次入栈。这样就保证了每次取栈顶元素的时候,左孩子在右孩子的前面被访问,左右孩子都在根节点前面被访问。如下:


void PostTraverse(BTNode *pT)
{
    if(NULL == pT)
        return;
    stack<struct BTNode *> s;
    BTNode *pCur;                      //当前访问的节点
    BTNode *pPre = NULL;              //前一次访问的节点
    
    s.push(pT);
    while(!s.empty())
    {
        pCur = s.top();
       
       //当前节点没有左右孩子或有但都已被访问过了
        if((NULL == pCur -> pLeft && NULL == pCur -> pRight) ||
          (NULL != pPre && (pPre == pCur -> pLeft || pPre == pCur -> pRight)))
        {
            cout << pCur -> data << " ";
            s.pop();
            pPre = pCur;
        }
        else
        {
            if(NULL != pCur -> pRight)
                s.push(pCur -> pRight);
            if(NULL != pCur -> pLeft)
                s.push(pCur -> pLeft);
        }
    }

    cout << endl;
}
 

 

方法二:两个栈实现

后序遍历可以看成下面遍历的逆过程:先访问某个节点,然后遍历其右孩子,再遍历其左孩子,这个过程逆过来就是后序遍历。算法如下:

a.Push根节点到第一个栈s中。

b.从第一个栈s中Pop出一个节点,并将其Push到第二个栈output中。

c.然后Push该节点的左孩子和右孩子到第一个栈s中。

d.重复过程b和c直到栈s为空。

e.完成后,所有结点已经Push到栈output中,且按照后序遍历的顺序存放,直接全部Pop出来即是二叉树后序遍历的结果。


void PostTraverse(BINode *pT)
{  
    if (NULL == pT)
        return;  
    stack<struct BTNode*> s, output;  
    s.push(pT); 
 
    while (!s.empty())
    {  
        struct BTNode *pCur = s.top();  
        output.push(pCur);  
        s.pop();  
        if (pCur -> pLeft)  
            s.push(pCur->pLeft);  
        if (pCur -> pRight)  
            s.push(pCur -> pRight);  
    }  
      
    while (!output.empty()) 
    {  
        cout << output.top() -> data << " ";  
        output.pop();  
    } 
 
    cout << endl;  
}
 
 


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

考虑使用队列:每一层从左到右依次入队,出队顺序和入队一致。另外每出队(访问)一个节点后,就将其左右孩子节点入队到队尾。


void LevelOrder(BTNode *pT)
{  
    if(NULL == pT)
        return;

    queue<struct BTNode *> queue;  
    struct BTNode *pCur;
    queue.push(pT);  
 
    while(!queue.empty())
    {  
        //取队头元素  
        pCur = queue.front();  
        //访问p指向的结点  
        cout << pCur -> data << " ";  
        //队头元素出队  
        queue.pop();  

        //左子树不空,将左子树入队  
        if(pCur -> pLeft != NULL)
            queue.push(pCur -> pLeft);  
        //右子树不空,将右子树入队  
        if(pCur -> pRight != NULL)  
            queue.push(pCur -> pRight);  
    }  

    cout << endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值