接着上一次对非线性逻辑数据结构树的内容,开启对二叉树的深入复习和总结。首先还是先回顾一下几个重要的概念:
一、回顾
1. 满二叉树与完全二叉树
满二叉树指的是除了叶子节点外所有的节点都有两个子节点。这样可以很容易的计算出满二叉树的深度,要掌握满二叉树的一些性质。
完全二叉树则是从满二叉树继承而来,指的所有的节点按照从上到下,从左到右的层次顺序依次排列所构成的二叉树称之为完全二叉树。所以可以想象,对于深度为h的完全二叉树,前h-1层可以构成深度为h-1的满二叉树,而对于第h层则是从左到右连续排列的。后面讲到的二叉树的存储结构当采用顺序结构时,为了能够记录二叉树节点有一个前驱和两个后继节点,以及方便插入等采用了完全二叉树的结构,将没有的节点当作虚节点,构建满二叉树,然后从上到下,从左到右依次存入即可。所以,了解满二叉树的性能是非常重要的。当然对于二叉树的存储,采用链式结构会更加的合适,后面会单独介绍。
有了这个性质,对于节点数为553的完全二叉树,可以很容易的计算出它的深度与每一层的节点数:
553 = (512-1)+ 42,所以它的深度为10,而第10层的节点数为42个,而前9层的二叉树结构构成深度为9的满二叉树。
2. 二叉树的存储
前面已经讲过了二叉树的顺序存储结构,是按照完全二叉树的结构进行的编排。这里讲解链式存储结构。
用链式存储结构存储二叉树是很自然的,节点除了数据域之外,设置了left和right两个指针域,分别存放该节点的左右孩子的地址。二叉树的链式存储结构成为二叉链表。其中root是指向根的指针,空树时root为NULL。
二叉树的二叉链表存储结构:
typedef struct tnode
{
DataType data;
struct tnode * left, * right;
}TNode;
当左孩子结点没有时,left = NULL,同理,当右孩子结点没有时,right = NULL。所以从插入、删除以及查找等各种角度上来讲,使用链式存储结构都比使用顺序存储结构要好很多。所以,以后对于二叉树的顺序存储结构只具有对比意义,在实际中都是采用链式存储结构。
二、二叉树的遍历
顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次,得到树中所有结点的一个线性排列,将树的结点线性化(定义一个次序)。“访问”的含义可以很广,如:输出结点的信息等。遍历是算法的基础,在线性结构中,遍历的顺序与逻辑顺序一样,所以遍历没有单独研究,而非线性遍历则复杂的多,方法也不止一种,而且算法直接与某种遍历相关联,因此把算法设计与遍历联系在一起研究。
层次遍历:从上到下、从左到右访问各结点;其示意图如下:
先序(前序)遍历:先访问根结点,然后分别先序遍历左子树、右子树;
中序遍历:先中序遍历左子树,然后访问根结点,最后中序遍历右子树;
后序遍历:先后序遍历左、右子树,然后访问根结点。
对于下图所示的简单的二叉树,采用不同的遍历方法,得到线性排列分别为:
层次遍历序列:A B C D
先序遍历序列:A B D C
中序遍历序列:B D A C
后序遍历序列:D B C A
三、二叉树的遍历算法
1. 递归算法
那么对于先序遍历的递归算法如下:
<span style="font-family:SimSun;font-size:18px;"><span style="font-size:18px;">void PreOrder(CNode *pNode) { if(pNode!=NULL) { printf("%lf ",pNode->data); PreOrder(p->lchild); PreOrder(p->rchild); } }</span></span>那么对于中序遍历的递归算法如下:
<span style="font-family:SimSun;font-size:18px;"><span style="font-size:18px;">void PreOrder(CNode *pNode) { if(pNode!=NULL) { PreOrder(p->lchild); printf("%lf ",pNode->data); PreOrder(p->rchild); } }</span></span>类似的,对于后续遍历的递归算法如下:
<span style="font-family:SimSun;font-size:18px;"><span style="font-size:18px;">void PreOrder(CNode *pNode) { if(pNode!=NULL) { PreOrder(p->lchild); PreOrder(p->rchild); printf("%lf ",pNode->data); } }</span></span>
2. 递归算法的评估
递归函数结构清晰,代码简洁,隐蔽了复杂的细节,而且许多算法由于本身就是递归定义的,所以用递归实现更加直接更方便,但是递归函数有以下不足:
(1) 执行效率低;
(2) 一次递归调用可能产生一层接一层的递归,连续地压栈也许会超出可用堆栈空间的范围;
(3) 递归函数的参数表特殊,通常要比非递归函数所需的参数要多;
(4) 递归设计不是面向对象的方法;
(5) 有些语言不允许使用递归调用。
当强调算法设计,运行时对时间和空间的要求合理,就可以使用递归。
对于使用递归和迭代这两种实现循环的方式,可以参考我的另一篇博文《迭代是人,递归是神(迭代与递归的总结:比较)》,里面有对迭代和递归详细的分析与对比。
***********************************************************************************************************************************************************************
2015-8-2