树是一种重要的数据结构,其在文件系统和数据库系统中应用广泛,可以提高排序和检索的效率。
二叉树是一种最基础最典型的树,常用于教学和研究树的特性。
下面将二叉树的概念、性质和遍历做一个小结,方便大家共同学习,欢迎纠错补充。
一.概念及性质
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;
}
若二叉树为空,则空操作返回;否则按照“左孩子-根结点-右孩子”的顺序进行访问。
递归算法,按概念操作。如下:
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;
}
若二叉树为空,则空操作返回;否则按照"左孩子-右孩子-根节点"的顺序进行访问。
递归算法,按概念操作。如下:
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;
}
若二叉树为空,则空操作返回;否则从树的第一层(也就是根节点)开始访问,从上到下逐层遍历,在同一层中,按从左到右的顺序对节点逐个访问。
考虑使用队列:每一层从左到右依次入队,出队顺序和入队一致。另外每出队(访问)一个节点后,就将其左右孩子节点入队到队尾。
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;
}