树(Tree)的定义及基本概念
树的定义
树(Tree)是个结点的有限集合T,它满足两个条件:
- 有且仅有一个特定的称为根(Root)的节点;
- 其余的节点分为个互不相交的有限合集,其中每一个集合又是一棵树,并称为其根的子树。
表示方法:树形表示法,目录表示法。
树的基本概念
一个节点的子树的个数称为该节点的度数。
一颗树的度数是指该树中节点的最大度数。
度数为零的节点称为树叶或终端节点。
度数不为零的节点称为分支节点。
一个节点系列,并满足是的父节点,就称为一条从到的路径,路径的长度为,即路径中的边数。
路径中前面的节点是后面节点的祖先,后面节点是前面节点的子孙。
节点的层数等于父节点的层数加一,根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度。
若树中每个节点的各个子树的排列为从左到右,不能交换,即兄弟之间是有序的,则该树称为有序树。
棵互不相交的树的集合称为森林。
树去掉根节点就成为森林,森林加上一个新的根节点就成为树。
树的逻辑结构
树中任何节点都可以有零个或多个直接后继节点(子节点),但至多只有一个直接前趋节点(父节点),根节点没有前趋节点,叶节点没有后继节点。
二叉树
二叉树的逻辑结构
二叉树是个节点的有限集合,或者是空集 ,或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成 严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
二叉树的性质
二叉树第层上的节点最多为个。
深度为的二叉树最多有个节点。
满二叉树
深度为时,有个节点的二叉树。
完全二叉树
只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。
具有n个节点的完全二叉树的深度为
或
二叉树的存储结构
二叉树的顺序存储
完全二叉树节点的编号方法是从上到下,从左到右,根节点为1号节点,设完全二叉树的节点数为,某节点编号为:
- 当(不是根节点)时,有父节点,其编号为;
- 当时,有左孩子,其编号为,否则没有左孩子,本身是叶节点;
- 当时,有右孩子,其编号为 ,否则没有右孩子;
- 当为奇数且不为1时,有左兄弟,其编号为,否则没有左兄弟;
- 当为偶数且小于时,有右兄弟,其编号为,否则没有右兄弟。
有个节点的完全二叉树可以用有个元素的数组进行顺序存储,节点号和数组下标一一对应,下标为零的元素不用。
利用以上特性,可以从下标获得节点的逻辑关系。不完全二叉树通过添加虚节点构成完全二叉树,然后用数组存储,这要浪费一些存储空间。
二叉树的链式存储
定义一个二叉树节点类型结构体bitree,每个结点包括三个域:
- 数据域data:存放每个节点的数据;
- 左孩子指针域left:存放指向左孩子的指针,如果没有左孩子,则为NULL;
- 右孩子指针域right:存放指向右孩子的指针,如果没有右孩子,则为NULL。
在头文件tree.h中定义二叉树结构体:
typedef char data_t;
typedef struct node_t {
data_t data;//二叉树节点数据域
struct node_t *left;//二叉树节点左孩子指针域
struct node_t *right;//二叉树节点右孩子指针域
}bitree;//二叉树节点类型别名
二叉树的四种基本遍历算法
遍历的含义
遍历是指沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。
二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径进行遍历的问题。
由于二叉树的递归性质,遍历算法也是递归的。
对于下面这棵树的遍历,可以通过什么算法实现呢?
遍历算法:先序遍历
先序遍历,是指先访问树根,再访问左子树,最后访问右子树。
先序遍历算法:
若二叉树为空树,则空操作,否则:
- 访问根节点:
- 先序遍历左子树;
- 先序遍历右子树。
这是一个递归算法,不断遍历左子树和右子树,直到左子树和右子树均为NULL,结束遍历,依次所访问的节点,即为遍历结果。
为了实现算法,在定义了上述bitree结构体后,声明一个数据类型为bitree的指针函数tree_create()和先序遍历函数preorder();
在tree.h文件中声明树的创建函数和先序遍历函数:
bitree *tree_create();//创建二叉树函数
void preorder(bitree *r);//先序遍历函数
在tree.c中实现树的创建函数:tree_create()
#include <stdio.h>
#include <stdlib.h>
#include "linkqueue.h"//队列头文件,在层次遍历过程中需要用到队列
bitree *tree_create() {
data_t ch;
bitree *r;
scanf("%c", &ch);//获取用户输入的字符
if (ch == '#') {
return NULL;//如果获取到的字符是‘#’则返回NULL
}
//给二叉树的节点分配内存空间,如果分配失败,则返回NULL
if ((r = (bitree *)malloc(sizeof(bitree))) == NULL) {
printf("malloc failed.\n");
return NULL;
}
//将获取到的字符存入二叉树节点的data域中,并递归创建左子树和右子树
r->data = ch;
r->left = tree_create();
r->right = tree_create();
return r;
}
在tree.c中实现先序遍历函数:preorder()
void preorder(bitree *r) {
//传入参数验证,同时也是退出递归的条件
if (r == NULL) {
return;
}
//打印访问到的节点存储的值
printf("%c", r->data);
//递归遍历被访问的节点的左子树和右子树
preorder(r->left);
preorder(r->right);
}
先序遍历代码看似简单,但其中蕴含一个非常重要的思想,就是递归:
递归是指,原问题的解总是依赖于子问题的解决,在二叉树的先序遍历过程中,要想遍历完整个树,在访问了节点数据data之后,总是依赖于左子树和右子树的是否遍历完成,而左子树和右子树是否遍历完成,同样依赖于它们的左子树和右子树是否遍历完成,以此类推,直到传入preorder()函数的参数为NULL,再返回,完成遍历。
使用递归算法,一定要明确退出递归的条件,否则构成死循环,在这个算法中,退出递归的条件就是r为NULL。
遍历算法:中序遍历
中序遍历,是指先访问左子树,再访问树根,最后访问右子树。
若二叉树为空树,则空操作,否则:
- 中序遍历左子树;
- 访问根节点;
- 中序遍历右子树。
在tree.h文件中声明中序遍历函数inorder():
void inorder(bitree *r);//中序遍历函数
在tree.c文件中实现中序遍历函数inorder():
void inorder(bitree *r) {
//传入参数验证,如果为NULL,返回,也是递归终止条件
if (r == NULL) {
return;
}
inorder(r->left);//递归遍历左子树
printf("%c", r->data);//访问并打印节点data值
inorder(r->right);//递归遍历右子树
}
在这里同样用到递归,退出条件依然是r为NULL,只是是先进行左子树的递归,再打印节点的值,再遍历右子树。
遍历算法:后序遍历
后序遍历,是指先访问左子树,再访问右子树,最后访问树根。
若二叉树为空树,则空操作,否则:
- 后序遍历左子树;
- 后序遍历右子树。
- 访问根节点。
在tree.h文件中声明后序遍历函数postorder():
void postorder(bitree *r);//声明后序遍历函数
在tree.c文件中实现后序遍历函数postorder():
void postorder(bitree *r) {
//传入参数验证,如r为NULL,返回,递归终止条件
if (r == NULL) {
return;
}
postorder(r->left);//递归遍历左子树
postorder(r->right);//递归遍历右子树
printf("%c", r->data);//访问节点data值并打印
}
遍历算法:层次遍历
二叉树的层次遍历,是指是从左往右依次访问每层结点的过程。对于顺序表存储的二叉树,层次遍历较容易实现,只需逐层访问存储的结点。对于链表存储的二叉树,可利用队列实现层次遍历。
链表存储的二叉树层次遍历算法:
- 根结点入队,出队并访问;
- 将其左右孩子入队,出队并访问;
- 重复此过程直至队列为空。
在tree.h文件中声明后序遍历函数layerorder():
void layerorder(bitree *r);//层次遍历函数声明
在linkqueue.h文件中定义节点和队列结构体,并声明相关函数:
#include "tree.h"
typedef bitree * datatype;//给bitree起别名datatype
//定义node结构体,并起别名listnode和linklist
typedef struct node {
datatype data;
struct node *next;
}listnode, *linklist;
//定义队列结构体,并起别名linkqueue
typedef struct {
linklist front;//队头
linklist rear;//队尾
}linkqueue;
linkqueue *queue_create();//声明创建队列函数
int enqueue(linkqueue *lq, datatype x);//声明入队函数
datatype dequeue(linkqueue *lq);//声明出队函数
int queue_empty(linkqueue *lq);//声明队列是否为空判断函数
int queue_clear(linkqueue *lq);//声明队列清空函数
linkqueue *queue_free(linkqueue *lq);//声明释放队列函数
在linkqueue.c文件中实现队列的相关函数:
实现队列的创建函数queue_create():
#include <stdio.h>
#include <stdlib.h>
//#include "tree.h"
#include "linkqueue.h"
linkqueue *queue_create() {
linkqueue *lq;
if ((lq = (linkqueue *)malloc(sizeof(linkqueue))) == NULL) {
printf("malloc linkqueue failed.\n");
return NULL;
}
lq->front = lq->rear = (linklist)malloc(sizeof(listnode));
if (lq->front == NULL) {
printf("malloc front failed.\n");
return NULL;
}
lq->front->data = 0;
lq->front->next = NULL;
return lq;
}
实现入队函数enqueue():
int enqueue(linkqueue *lq, datatype x) {
linklist p;
if (lq == NULL) {
printf("lq is NULL.\n");
return -1;
}
p = (linklist)malloc(sizeof(listnode));
if (p == NULL) {
printf("malloc front failed.\n");
return -1;
}
p->data = x;
p->next = NULL;
lq->rear->next = p;
lq->rear = p;
return 0;
}
实现出队函数dequeue():
datatype dequeue(linkqueue *lq) {
linklist p;
if (lq == NULL) {
printf("lq is NULL.\n");
return NULL;
}
p = lq->front;
lq->front = p->next;
free(p);
p = NULL;
return (lq->front->data);
}
实现队列判断函数queue_empty():
int queue_empty(linkqueue *lq) {
if (lq == NULL) {
printf("lq is NULL.\n");
return -1;
}
return (lq->front == lq->rear ? 1: 0);
}
实现队列清空函数queue_clear():
int queue_clear(linkqueue *lq) {
linklist p;
if (lq == NULL) {
printf("lq is NULL.\n");
return -1;
}
while (lq->front->next) {
p = lq->front;
lq->front = p->next;
//printf("clear free: %d\n", p->data);
free(p);
p = NULL;
}
return 0;
}
实现队列释放函数queue_free():
linkqueue *queue_free(linkqueue *lq) {
linklist p;
if (lq == NULL) {
printf("lq is NULL.\n");
return NULL;
}
while (lq->front) {
p = lq->front;
lq->front = p->next;
//printf("free: %d\n", p->data);
free(p);
}
free(lq);
lq = NULL;
return 0;
}
在tree.c文件中实现层次遍历函数layerorder():
void layerorder(bitree *r) {
//声明一个队列lq
linkqueue *lq;
//创建队列lq,入创建失败直接返回
if ((lq = queue_create()) == NULL)
return;
//验证传入参数r
if (r == NULL)
return;
//打印访问到的节点数值,并让该节点入队
printf("%c", r->data);
enqueue(lq, r);
//当队列不为空,不断执行左孩子、右孩子的访问,入队和出队
while (!queue_empty(lq)) {
r = dequeue(lq);
if (r->left != NULL) {
printf("%c", r->left->data);
enqueue(lq, r->left);
}
if (r->right != NULL) {
printf("%c", r->right->data);
enqueue(lq, r->right);
}
}
//确认清空队列并释放内存
queue_clear(lq);
queue_free(lq);
}
遍历算法测试:test.c文件
#include <stdio.h>
#include <stdlib.h>
#include "tree.h"
int main(int argc, const char *argv[])
{
bitree *r;
if((r = tree_create()) == NULL)
return -1;
//调用先序遍历函数
preorder(r);
puts("");
//调用中序遍历函数
inorder(r);
puts("");
//调用后序遍历函数
postorder(r);
puts("");
//调用层次遍历函数
layerorder(r);
puts("");
return 0;
}
运行结果
将树图1左孩子、有孩子缺失的部分用‘#’填充,运行程序后,输入A#BCEH###FI##J##D#GK###,得到以下结果: