4.1 树
4.1.1 树的基本概念
树是一种非线性结构,其严格的数学定义是:如果一组数据中除了第一个节点(第一个节点称为根节点,没有直接前驱节点)之外,其余任意节点有且仅有一个直接前驱,有零个或多个直接后继,这样的一组数据形成一棵树。这种特性简称为一对多的逻辑关系。
从树根生长,逐级分支
非空树的特性:
有且仅有一个根节点
没有后继的结点称为“叶子结点”
有后继的结点称为“分支结点”
除了根节点外,任何一个结点都有且仅有一个前驱
4.1.2 树的基本术语
对于一棵树来说,有如下基本术语:
根(root):
树的第一个节点,没有直接前驱。如上图中的A。
2.双亲节点(parent):
某节点的直接前驱称为该节点的双亲节点,或成为父节点。例如上图中A是B的父节点。
孩子节点(child):
某节点的直接后继称为该节点的孩子节点。例如上图中B、C、D均为A的孩子节点。
节点的层次(level):
根节点所在的层次规定为第1层,其孩子所在的层次为第2层,后代节点以此类推。比如上图中节点E的层次是3。
节点的度(degree):
一个节点拥有的孩子节点的总数,称为该节点的度。比如上图中节点B的度为2。
叶子(leaf):
一棵树中度等于0的节点,被称为叶子,又称为终端节点。比如上图中K、L、F、G、M、I、J均为叶子。
树的高度(height):
一棵树中所有节点的层次的最大值,称为这棵树的高度,又称为树的深度。比如上图的树的高度为4。
有序树与无序树:
一棵树中,如果某个节点的孩子节点之间是有次序的,则称这棵树为有序树,反之称为无序树。
有序树的定义:若将树中每个节点的各子树看成是从左到右有次序的(即不能互换),则称该树为有序树(Ordered Tree)
无序树的定义:若将树中每个结点的各子树从左到右是没有次序的(即可以互换),则称该树为无序树
4.2 二叉树
4.2.1 二叉树的基本概念
二叉树是n ( n ≥ 0 )个结点的有限集合:
①或者为空二叉树,即n = 0。
②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:
①每个结点至多只有两棵子树
②左右子树不能颠倒(二叉树是有序树)。
4.2.2 二叉排序树
二叉排序树:
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树
左子树上所有结点的关键字均小于根结点的关键字;
右子树上所有结点的关键字均大于根结点的关键字;
左子树和右子树又各是一棵二叉排序树。
4.2.3 二叉树的实现
定义管理结构体:需要定义左结点和右结点
typedef struct b_tree{
int data;
struct b_tree *lchild;//左节点
struct b_tree *rchild;//右节点
}node,*p_node;
树的初始化:插入一个根结点,然后将左右结点指向空
p_node new_node (int data){
p_node newnode = malloc(sizeof (node));
if(newnode == NULL){
printf("newnode malloc fail\n");
return NULL;
}
//把数据放入新节点
newnode->data = data;
//左右子树都为NULL
newnode->lchild = NULL;
newnode->rchild = NULL;
return newnode;//将根节点返回
}
二叉树的插入:对比左右结点,若大于根结点插右边,小于根节点插左边
4.2.4 二叉树的遍历
所谓遍历,就是按某种规律访问每一个节点。对于之前的线性表而言,遍历算法很简单,就是从头跑到尾,因为线性表是一对一的关系。但是树状结构是非线性的,因此从根节点开始遍历所有节点可以有多种不同的算法,常见的有:
- 前序遍历(根左右):根节点 - 左子树 - 右子树
- 中序遍历(左根右):左子树 - 根节点 - 右子树
- 后序遍历(左右根):左子树 - 右子树 - 根节点
- 按层遍历:从上到下,从左到右依次访问节点
其中需要注意的是,前中后序遍历,都是递归算法。以前序遍历为例,当访问完根节点,进而要访问左子树时,由于左子树本身一棵二叉树,因此也需要进行前序遍历,也是先访问左子树的根节点,然后再依次访问左子树的左子树和左子树的右子树。比如:
前序遍历的序列是:F - [BADCE] - [GIH]
其中,F是根节点,而BADCE是左子树,GIH是右子树。
对左子树的访问,也符合前序遍历的定义,即:B - [A] - [DCE]
以此类推,对上述二叉树而言:
中序遍历的序列是:[ABCDE] - F - [GHI]
后序遍历的序列是:[ACEDB] - [HIG] - F
至于按层遍历,就按照字面意思理解即可,序列是:FBGADICEH
代码实现:
//前序遍历
void RootLR_foreach(p_node root){
if(root == NULL){
return;
}
//输出根结点
printf("%d ",root->data);
//遍历左节点
RootLR_foreach(root->lchild);
//遍历右节点
RootLR_foreach(root->rchild);
}
//中序遍历
void LRootR_foreach(p_node root){
if(root == NULL){
return;
}
//输出根结点
//遍历左节点
LRootR_foreach(root->lchild);
printf("%d ",root->data);
//遍历右节点
LRootR_foreach(root->rchild);
}
//后序遍历
void LRRoot_foreach(p_node root){
if(root == NULL){
return;
}
//输出根结点
//遍历左节点
LRRoot_foreach(root->lchild);
//遍历右节点
LRRoot_foreach(root->rchild);
printf("%d ",root->data);
}
注意,层序遍历需要用到队列的思想:
//层序遍历
void NLR_foreach(p_node root) {
if (root == NULL) {
return;
}
// 创建一个队列
Queue queue;
initQueue(&queue);
// 根节点入队
enqueue(&queue, root);
while (!isEmptyQueue(queue)) {
// 出队节点并访问
p_node temp = dequeue(&queue);
printf("%d ", temp->data);
// 左子节点入队
if (temp->lchild != NULL) {
enqueue(&queue, temp->lchild);
}
// 右子节点入队
if (temp->rchild != NULL) {
enqueue(&queue, temp->rchild);
}
}
// 清空队列
clearQueue(&queue);
}