1.树的定义与性质

(1)树(tree)的概念

在数据结构中,树则是用来概括传递关系的一种数据结构。为了简化,数据结构中把树枝分叉处、树叶、树根抽象为结点(node),其中树根抽象为根结点(root),且对一棵树来说最多存在一个根结点;把树叶概括为叶子结点(leaf),且叶子结点不再延伸出新的结点;把茎干和树枝统一抽象为(edge),且一条边只用来连接两个结点(一个端点一个)。这样,树就被定义为由若干个结点和若干条边组成的数据结构,且在树中的结点不能被边连接成环。在数据结构中,一般把根结点置于最上方(与现实中的树恰好相反),然后向下延伸出若干条边到达子结点(child)(从而向下形成子树(subtree)),而子结点叉向下延伸出边并连接一些结点…直至到达叶子结点,看起来就像是把现实中的树颠倒过来的样子。

 

(2)概念和性质

由于机考的性质,读者不需要对树的许多理论知识都了如指掌,下面只给出几个比较实用的概念和性质,希望读者能把它们记住,其中性质①⑤经常被用来出边界数据:

①树可以没有结点,这种情况下把树称为空树(empty tree)。

②树的层次(layer)从根结点开始算起,即根结点为第一层,根结点子树的根结点为第二层,以此类推。

③把结点的子树棵数称为结点的(degree),而树中结点的最大的度称为树的度(也称为树的宽度)。

④由于一条边连接两个结点,且树中不存在环,因此对有n个结点的树,边数一定是n。且满足连通、边数等于顶点数减1的结构一定是一棵树。

⑤叶子结点被定义为度为0的结点,因此当树中只有一个结点(即只有根结点)时,根结点也算作叶子结点。

⑥结点的深度(depth)是指从根结点(深度为1)开始自顶向下逐层累加至该结点时的深度值:结点的高度(height)是指从最底层叶子结点(高度为1)开始自底向上逐层累加至该结点时的高度值。树的深度是指树中结点的最大深度,树的高度是指树中结点的最大高度。对树而言,深度和高度是相等的,但是具体到某个结点来说深度和高度就不一定相等了。

⑦多棵树组合在一起称为森林( forest),即森林是若干棵树的集合。

 

 

2.二叉树的递归定义

(1)二叉树的递归定义

①要么二叉树没有根结点,是一棵空树

②要么二叉树由根结点、左子树、右子树组成,且左子树和右子树都是二叉树。

递归定义就是用自身来定义自身。一个递归函数必须存在两个概念:递归边界和递归式,其中递归式用来将大问题分解为与大问题性质相同的若干个小问题,递归边界则用来停止无休止的递归。那么二叉树的递归定义也是这样:一是递归边界,二是递归式。二叉树中任何一个结点的左子树既可以是一棵空树,也可以是一棵有左子树和右子树的二叉树;结点的右子树也既可以是一棵空树,又可以使一棵有左子树和右子树的二叉树,这样直到到递归边界,递归定义结束。

 

(2)特殊的二叉树

满二叉树:每一层的结点个数都达到了当层能达到的最大结点数。

完全二叉树:除了最下面一层之外,其余层的结点个数都达到了当层能达到的最大结点数,且最下面一层只从左至右连续存在若干结点,而这些连续结点右边的结点全部不存在。

 

3.二叉树的存储结构与基本操作

(1)二叉树的存储结构

一般来说,二叉树使用链表来定义。和普通链表的区别是,由于二叉树每个结点有两条出边,因此指针域变成了两个,分别指向左子树的根结点地址和右子树的根结点地址。如果某个子树不存在,则指向NULL,其他地方和普通链表完全相同,因此叉把这种链表叫作二叉链表,其定义方式如下:

struct node{
typename data; //数据域
node* lchild; // 指向左子树根结点的指针
node* rchild; // 指向右子树根结点的指针
};

由于在二叉树建树前根结点不存在,因此其地址一般设为NULL:

node* root = NULL;

而如果需要新建结点(例如往二叉树中插入结点的时候),就可以使用下面的函数:

node* newNode(int v) {
node* Node = new node; //申请一个node型的地址空间
Node->data = v; //结点权值为v
Node->lchild = Node->rchild = NULL; //初始化状态下没有左右孩子
return Node; //返回新建结点的地址
}

二叉树的常用操作有以下几个:二叉树的建立,二叉树结点的查找、修改、插入与删除,其中删除操作对不同性质的二叉树区别比较大,因此不在本节介绍。本节主要介绍査找、修改、插入、建树的通用思想。

 

(2)二叉树结点的查找、修改

査找操作是指在给定数据域的条件下,在二叉树中找到所有数据域为给定数据域的结点,并将它们的数据域修改为给定的数据域。

需要使用递归来完成查找修改操作。还记得二叉树的递归定义吗?其中就包含了二叉树递归的两个重要元素:递归式和递归边界。在这里,递归式是指对当前结点的左子树和右子树分别递归,递归边界是当前结点为空时到达死胡同。例如查找修改操作就可以用这样的思路,即先判断当前结点是否是需要查找的结点:如果是,则对其进行修改操作;如果不是,则分别往该结点的左孩子和右孩子递归,直到当前结点为NULL为止。程序代码:

void search(node* root,int x,int newdata){
if(root == NULL){
return; //空树,死胡同(递归边界)
}
if(root->data == x) { //找到数据域为x的结点,把它修改成newdata
root->data = newdata;
}
search(root->lchild,x,newdata); //往左子树搜索x(递归式)
search(root->rchild,x,newdata); //往右子树搜索x(递归式)
}

(3)二叉树结点的插入

二叉树结点的插入位置就是数据域在二叉树中査找失败的位置。而由于这个位置是确定的,因此在递归查找的过程中一定是只根据二叉树的性质来选择左子树或右子树中的一棵子树进行递归,且最后到达空树(死胡同)的地方就是查找失败的地方,也就是结点需要插入的地方。程序代码:

// insert函数将在二叉树中插入一个数据域为x的新结点
// 注意根结点指针root要使用引用,否则插入不会成功
void insert(node* &root,int x){
if(root == NULL){ //空树,说明查找失败,也即插入位置(递归边界)
root = newNode(x);
return;
}
if(由二叉树的性质,X应该插在左子树){
insert(root->lchild,x); //往左子树搜索(递归式)
} else {
insert(root->rchild,x); //往右子树搜索(递归式)
}
}

在上述代码中,很关键的一点是根结点指针root使用了引用&。引用的作用是在函数中修改root会直接修改原变量。这么做的原因是,在insert函数中新建了结点,并把新结点的地址赋给了当层的root。如果不使用引用,root = new node这个语句对root的修改就无法作用到原变量(即上一层的root- >lchild与root- >rchild)上去,也就不能把新结点接到二叉树上面,因此insert函数必须加引用。(因为search函数中修改的是指针root指向的内容,而不是root本身,而对指针实现的结点内容的修改是不需要加引用的)

判断是否要加引用:

一般来说,如果函数中需要新建结点,即对二叉树的结构做出修改,就需要加引用;如果只是修改当前已有结点的内容,或仅仅是遍历树,就不用加引用。至于判断不出来的情况,不妨直接试一下加引用和不加引用的区别再来选择。

最后再特别提醒一句,在新建结点之后,务必令新结点的左右指针域为NUL,表示这个新结点暂时没有左右子树。

(4)二叉树的创建

二叉树的创建其实就是二叉树结点的插入过程,而插入所需要的结点数据域一般都会由题目给出,因此比较常用的写法是把需要插入的数据存储在数组中,然后再将它们使用insert函数一个个插入二叉树中,并最终返回根结点的指针root。而等读者熟悉之后,可能更方便的写法是直接在建立二叉树的过程中边输入数据边插入结点。程序代码:

//二叉树的建立
node* Create(int data[],int n) {
node* root = NULL; //新建空根结点root
for(int i=0;i<n;i++){
insert(root,data[i]); //将data[0]~data[n-1]插入二叉树中
}
return root; //返回根结点
}

(5)*root == NULL和root == NULL的区别

*root == NULL的含义是获取地址root指向的空间的内容;root == NULL说明地址root是否为空,也即确定是否存在这个结点。读者需要明白root==NULL与*root=NUL的区别,也即结点地址为NULL与结点内容为NULL的区别(也相当于结点不存在与结点存在但没有内容的区别)这在写程序时是非常重要的,因为在二叉链表中一般都是判定结点是否存在,所以一般都是root==NULL。

 

(6)完全二叉树的存储结构

对完全二叉树来说,除了采用二叉链表的存储结构外,还可以有更方便的存储方法。对棵完全二叉树,如果给它的所有结点按从上到下、从左到右的顺序进行编号(从1开始),就会得到类似于下图所示的编号顺序。

树与二叉树_二叉树

通过观察可以注意到,对完全二叉树当中的任何一个结点(设编号为x),其左孩子的编号一定是2x,而右孩子的编号一定是2x+1。也就是说,完全二叉树可以通过建立一个大小为2^k的数组来存放所有结点的信息,其中k为完全二叉树的最大高度,且1号位存放的必须是根结点(根结点不能存在下标为0处)。这样就可以用数组的下标来表示结点编号,且左孩子和右孩子的编号都可以直接计算得到。

事实上,如果不是完全二叉树,也可以视其为完全二叉树,即把空结点也进行实际的编号工作。但是这样做会使整棵树是一条链时的空间消耗巨大(对k个结点就需要大小为2的数组),因此很少采用这种方法来存放一般性质的树。不过如果题目中已经规定是完全二叉树,那么数组大小只需要设为结点上限个数加1即可,这将会大大节省编码复杂度。

除此之外,该数组中元素存放的顺序恰好为该完全二叉树的层序遍历序列而判断某个结点是否为叶结点的标志为:该结点(记下标为root)的左子结点的编号root*2大于结点总个数n(不需要判断右子结点);判断某个结点是否为空结点的标志为:该结点下标root大于结点总个数n。

 

下一节:​​二叉树的遍历——先序遍历、中序遍历、后序遍历​

再下一节:​​二叉树的遍历——层次遍历​