二叉树

定义

二叉树不是线性的数据结构,而是树型结构,它的特点是每个结点之多有两颗子树(即二叉树不存在出度大于2的结点),并且二叉树子树有左右之分,其次序不能任意颠倒。二叉树有5条非常重要的性质以及4种遍历方法。前者经常在笔试题中出现,后者经常会在机试,面试中出现。

二叉树

性质

  • 性质1: 在二叉树的第i层上至多有2i-1(i≥1);

利用数学归纳法证明此性质:
①. 当i=1时,只有一个根结点,显然2i-1 = 21-1 = 1是对的;
②. 归纳假设: 当i=k (1< k < n)的时候,满足二叉树第k层上至多有2k-1个结点。
③. 由归纳假设出发,那么第k层的每个结点在k+1层至多有2个对应结点,因此k+1层的结点数为2k-1 = 2k-1*2 = 2k = 2k+1-1;k+1层的结点数满足性质,完成证明。

  • 性质2: 深度为k的二叉树的至多有2k-1个结点。

利用性质1很好证明:因为第i层的结点数至多有2i-1个,因此从1到k层结点至多有 i=1k2i1=2k1 个结点数。

  • 性质3: 对任何一颗二叉树T,如果其叶节点数为n0,出度为2的结点数为n2,则有n0=n2+1;

证明:设n1为树中出度为1的结点数,根据二叉树定义,每个结点至多两个子树,因此每个结点至多出度为2,所以二叉树上总结点数

n = n0+n1+n2 …… (1)

又因为除了根节点,每个结点的入度为1,因此入度为1的结点总数

B = n-1 …… (2)

并且一个出度为2的结点必然对应有2个入度为1的结点,出度为1的结点,比如对应有1个入度为1的结点。根据这个性质,可以得出

B = 2n2 + n1 ……. (3)

由(2),(3)可得:

n = 2n2 + n1 + 1 …….(4)

整理(1),(4)式可得:

n0=n2+1

  • 性质4: 具有n个结点的完全二叉树的深度为 [log2n]+1 ;

满二叉树的定义:一颗深度为k且有2k-1个结点的二叉树称为满二叉树。下图中(a)为满二叉树

完全二叉树的定义: 一颗深度为k且有n个结点,当且仅当每个结点与深度为k的满二叉树一一对应称为完全二叉树。下图中(b)为完全二叉树,(c), (d)为非完全二叉树。

完全二叉树

证明性质4:根据性质2和完全二叉树的定义,一颗深度为k的完全二叉树的总结点数n有约束关系:

2k-1 ≤ n < 2k

则同时取log2为底,整理之后得到如下关系表达式

k-1 ≤ log2n < k

因为k是整数,所以k=[log2n]+1;

  • 性质5: 一颗完全二叉树其结点按照从上到下,从左到右顺序从1开始编号,那么如果某个a结点存在左孩子结点b,则必然有b=2a; 如果某个结点a存在右孩子c,则必然有c=2a+1;【PS:这个性质不做证明】

二叉树的存储方案

数组存储

由于二叉树每个结点之间都存在数学关系,因此选择数组存储在理论上也行得通。比如如下的数组存储方案{1,2,3}表示1为根节点,2和3分别为1的左右孩子。但是数组存储方案有很大的问题就是,在最坏情况下,当二叉树形成右孩子单链的时候,存储k个结点所需要的数组大小为2k-1;这样对内存空间是极大的浪费,因此平时基本不使用这种方式存储。

链式存储

链式存储就是我们最熟悉的结构,一个结点包括三个域:数据域,左孩子指针域和右孩子指针域。数据域存储该结点存放的元素信息,左孩子指针域指向左子树的根结点,右孩子指针域指向右子树的根结点。这种存储方案,内存空间是根据需求动态变化的,平时最常用的一种存储方式,又称为二叉链表存储二叉树。另外,有时候一个结点会包含四个域,除了以上所说的三个之外,还有一个parent域是用来存储当前结点的父亲结点是谁?这种方式就是所说的三叉链表存储二叉树。

相关操作以及时间复杂度分析

先序遍历

二叉树的先序、中序、后序都需要用到递归或者栈的数据结构。先序遍历的总体步骤如下:

(1). 访问当前节点的数据;
(2). 如果当前节点存在左孩子,则遍历左子树;
(3). 如果当前节点存在右孩子,则遍历右子树;

遍历的时间复杂度为O(n);

中序遍历

中序遍历的总体步骤如下:

(1). 如果当前节点存在左孩子,则遍历左子树;
(2). 访问当前节点的数据;
(3). 如果当前节点存在右孩子,则遍历右子树;

遍历的时间复杂度为O(n);

后序遍历

后序遍历的总体步骤如下:

(1). 如果当前节点存在左孩子,则遍历左子树;
(2). 如果当前节点存在右孩子,则遍历右子树;
(3). 访问当前节点的数据;

遍历的时间复杂度为O(n);

层次遍历

层次遍历需要用到队列的知识,初始化空队列,将根节点放入队列,将每次访问前,先将队列的队头元素进行出队操作,访问队头结点,如果该结点存在左右孩子,都将它们进行入队操作,因为它们肯定属于下一层。直到队列为空,则完成遍历。

时间复杂度为O(n);

建树

不同的遍历方式对应着不同的建树方式,对于用数组实现的二叉树建议用层次遍历建立树。就是将按照层次遍历结果安排好的数据一个个读入数组即可。而对于链式表示的二叉树先序,中序,后序均可,建树过程就是对应的先序,后序,中序遍历的过程。
时间复杂度为O(n);

实现代码

数组存储

/*
 * 因为层次遍历需要用到队列的数据结构,所以我这里用了C++的STL库中的队列
 */

#include<stdio.h>
#include<string.h>
#include<queue>
using namespace std;

#define MAX_SIZE 100
#define EMPTY -1

typedef int SqlBTree[MAX_SIZE];

void create_tree(SqlBTree btree, int n)
{
    for(int i=1;i<=n;i++)
        scanf("%d", btree+i); // 注意0位置不要放数据,否则根据左孩子结点是父亲结点2被的关系在根节点不成立
}


void pre_order(SqlBTree btree, int i)
{
    if(btree[i] != EMPTY){
       printf("%d ", btree[i]);
       pre_order(btree, 2*i);
       pre_order(btree, 2*i+1);
    }
}

void in_order(SqlBTree btree, int i)
{
    if(btree[i] != EMPTY){
        in_order(btree, 2*i); // 递归遍历左孩子
        printf("%d ", btree[i]);
        in_order(btree, 2*i+1); // 递归遍历右孩子
    }
}

void last_order(SqlBTree btree, int i)
{
    if(btree[i] != EMPTY){
       last_order(btree, 2*i); // 递归遍历左孩子
       last_order(btree, 2*i+1); // 递归遍历右孩子
       printf("%d ", btree[i]);
    }
}

/*
 * 因为建立树的时候就是按照层次遍历建立的,偷懒点的办法,直接可以遍历数组,就是层次遍历的结果
 * 但是我还是按照借用队列数据结构来实行严格的层次遍历
 */
void level_order(SqlBTree btree, int i)
{

    // 准备工作,将根结点放入队列
    if(btree[i] == EMPTY) return;

    queue<int> cppqueue;
    cppqueue.push(i);

    while(!cppqueue.empty()){
        int next = cppqueue.front();
        cppqueue.pop();
        printf("%d ", btree[next]);
        if(btree[2*next] != EMPTY)   cppqueue.push(2*next);
        if(btree[2*next+1] != EMPTY)  cppqueue.push(2*next+1);
    }

}

int main()
{
    SqlBTree btree;
    memset(btree, EMPTY, sizeof(btree)); // 初始化全为EMPTY;
    create_tree(btree, 10);
    printf("pre order: ");
    pre_order(btree, 1);
    printf("\n");
    printf("in order: ");
    in_order(btree, 1);
    printf("\n");
    printf("last order: ");
    last_order(btree, 1);
    printf("\n");
    printf("level order: ");
    level_order(btree, 1);
    printf("\n");

    return 0;
}

链式存储


/*
 * 因为层次遍历需要用到队列的数据结构,所以我这里用了C++的STL库中的队列
 */

#include<stdio.h>
#include<stdlib.h>
#include<queue>
using namespace std;

#define END -1


typedef struct TNode *TNodePtr;

typedef struct TNode{
    int data;
    TNodePtr left;
    TNodePtr right;
}TNode, *TNodePtr;

/*
 * 先序建树,用END作为输入结束
 */
void create_tree(TNodePtr *node)
{
    int input;
    scanf("%d", &input);
    if(input == END) *node = NULL;
    else {
        (*node) = (TNodePtr)malloc(sizeof(TNode));
        (*node)->data = input;
        create_tree(&((*node)->left));
        create_tree(&((*node)->right));
    }


}

void pre_order(TNodePtr node)
{
    if(node){
        printf("%d ", node->data);
        pre_order(node->left);
        pre_order(node->right);
    }
}

void in_order(TNodePtr node)
{
    if(node){
        in_order(node->left);
        printf("%d ", node->data);
        in_order(node->right);
    }
}

void last_order(TNodePtr node)
{
    if(node){
         last_order(node->left);
         last_order(node->right);
         printf("%d ", node->data);
    }
}

void level_order(TNodePtr node)
{
    // 准备工作,将根结点放入队列
    if(!node) return;

    queue<TNodePtr> cppqueue;
    cppqueue.push(node);

    while(!cppqueue.empty()){
        TNodePtr next = cppqueue.front();
        cppqueue.pop();
        printf("%d ", next->data);
        if(next->left)   cppqueue.push(next->left);
        if(next->right)  cppqueue.push(next->right);
    }
}

int main()
{

    TNodePtr root;
    create_tree(&root);

    printf("pre order: ");
    pre_order(root);
    printf("\n");
    printf("in order: ");
    in_order(root);
    printf("\n");
    printf("last order: ");
    last_order(root);
    printf("\n");
    printf("level order: ");
    level_order(root);
    printf("\n");
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值