定义
二叉树不是线性的数据结构,而是树型结构,它的特点是每个结点之多有两颗子树(即二叉树不存在出度大于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=1k2i−1=2k−1 个结点数。
- 性质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");
}