• 数据结构(三):非线性逻辑结构-二叉树


    接着上一次对非线性逻辑数据结构树的内容,开启对二叉树的深入复习和总结。首先还是先回顾一下几个重要的概念:

    一、回顾

    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

  • 相关阅读:
    理解 Go interface 的 5 个关键点
    volcano networkpolicy
    通过 iptables log 日志追踪 Kubernetes 网络
    Network Policy Enforcement
    calico 容器编排之Kubernetes多租户网络隔离
    Kubernetes的原生多租户解决方案
    第四章 计算机基础知识详解
    vue兄弟组件通信
    vue父子组件通信
    Safari浏览器css兼容
  • 原文地址:https://www.cnblogs.com/huty/p/8519281.html
Copyright © 2020-2023  润新知