树与二叉树
零.前言
本来上一章应该还有稀疏矩阵的十字链表存储
和广义表
。
但是广义表,更多的是偏向于题目,了解下概念,能计算就行了。广义表从某种程度来说,也算是一棵树。
一.树
1.定义
树是一个n(>=0)个结点的有限集。
2.性质
在任意一颗非空树中:
- 有且仅有一个特定的结点叫做
根结点root
- 当
n>1
时,其余结点可以分为m>0
个互不相交的有限集T1……Tm。其中,每一个集合的本身又是一颗树,并且被称为根的子树
。
所以,树的结构定义因该是一个递归的定义。
3.表示方式
除了我们常见的树状图表示法,还有:
- 嵌套集合表示法(a)
- 广义表表示法(b)
- 凹入表示法©
4.基本术语
- 树的
结点
包含一个数据元素及若干指向其子树的分支。 结点
拥有的的子树称为结点的度(Degree)
。- 度为0的结点,称为
叶子
或终端结点
。 - 度不为0的结点,称为
分支结点
或非终端结点
树的度
是树内各结点的度的最大值。- 结点的子树的根,称为该结点的
孩子
;相应的,该结点是孩子的双亲
。 - 同一个双亲的结点,它们互称为
兄弟
- 结点的层次从根开始定义,根为第一层,根的孩子在第二层。某结点在n层,那么其子树的根就在n+1层。双亲在同一层的结点叫做
堂兄弟
。 - 树中结点的最大层次称为树的
深度或高度
。 - 如果将各子树看作从左至右是有次序的,那么称该树是
有序树
,否则为无序树
。 - 在有序树中,最左边的子树的根称为
第一个孩子
,最右边的称为最后一个孩子
森林
是m>=0
棵互不相交的树的集合。对于树中每个结点而言,其子树的集合即位森林。
就逻辑结构而言,任意一棵树是一个二元组Tree=(root,F),其中root是数据元素,称作树的根结点;F是m>=0棵树的森林,F=(T1……Tm),其中Ti=(ri, Fi称做根root的第i棵子树。
二.二叉树
1.小介绍
如果研究一颗树,那么肯定要从最简单的情况开始研究。当我们每个结点有一个孩子的时候,那么这不就是链表吗?所以我们研究树,一般采用结点有两个孩子的树,叫做二叉树。
2.性质
这里给出一些性质而不给出证明。证明起来也很容易(大概)。
- 在二叉树的第
i
层上至多有2i-1个结点。 - 深度为k的二叉树最多有2k-1个结点(k>=1)。
- 对于任意一颗二叉树。如果其终端结点数为m,度为2的节点数为n,则m=n+1。
- 一颗深度为k,且有2k-1个结点的二叉树称为
满二叉树
。 - 对满二叉树进行编号,约定编号从根结点开始,从上至下,从左至右。当一颗树每一个结点都与这种方式的满二叉树的方式一一对应,称之为
完全二叉树
。它有两个特点: a)叶子结点只可能在层次最大的两层上出现。 b) 对任意结点,其右分支下的子孙最大层次为l,那么其左下分支下子孙的最大层次必为l或l+1. - 具有n个结点的完全二叉树的深度为log2n +1
- 如果有一颗有n个结点的完全二叉树,且遵循刚才描述的编号方法,对任意一结点i (1<=i<=n)有:a) 如果i=1,则结点i是二叉树的根,无双亲。如果i>1,则双亲结点的编号是i/2。b)如果2i>n,则结点无左孩子,否则其左孩子是结点2i。c)如果2i+1>n,则结点i无右孩子,否则其右孩子的编号是结点2i+1 。
3.二叉树的储存结构
3.1 顺序储存
顺序存储一般用于完全二叉树。如果用于非完全二叉树,可能会造成大量的空间浪费。
3.2 二叉链表
由结构来看,一个结点它除了数据域,还有左右指针域。那么最简单的链表就包含了这三个部分。可以理解为单向链表X2。
3.3 三叉链表
在三叉链表里,它比二叉链表多了一个双亲结点的指针。很类似于双向链表。
4.二叉树的遍历
4.1 定义
在二叉树的一些应用中,常常要求在树中查找具有某种特征的点,或者对树中全部结点逐一进行某种处理。这就提出了一个遍历二叉树
的问题。按某种搜索路径巡防树中的每个结点,使得每个结点只被访问一次。假设我们有一个1234567的完全二叉树,作为接下来三种遍历的例子。遍历都是基于对非空结点的操作。
4.2 先序遍历
- 访问根结点。
- 先序遍历左子树。
- 先序遍历右子树。
- 遍历结果:1245367
4.3 中序遍历
- 中序遍历左子树。
- 访问根结点。
- 中序遍历右子树。
- 遍历结果:4251637
4.4 后序遍历
- 后序遍历左子树。
- 后序遍历右子树。
- 访问根结点。
- 遍历结果4526731
三.编程原理
1.存储结构
在这篇文章中,我们采用最简单的二叉链表+遍历实现。首先我们还是创建一个数据域的结构体,然后一个结点的结构体,包含两个指向左右儿子结点的指针和一个数据域结构体。
2.存储(先序遍历)
我们通过先序遍历的方法,读入一颗树并构建出这棵二叉树。当然,为了知道每棵树的终端结点,我们需要把终端结点的儿子用虚结点,随意的字符表示。在此约定为#
,比如ABC
这棵深度为2的树,我们需要输入AB##C##
。
我们的遍历因该遍历到虚结点,如果字符为#
则,该结点值为NULL,如果非#
,则结点的数据域存入该字符,并创建儿子结点。
3.遍历输出
如果我们遇到了#
结点,那么就停止递归,这是我们的递归边界。
否则就按照某种遍历方式去遍历并输出结点字符即可。
四.代码实现
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef struct dataType{
char data;
}dataType;
typedef struct Btree{
dataType node;
struct Btree *lc;
struct Btree *rc;
}Btree;
int BtreeCreat(Btree **TreePtr); // 通过先序遍历的方式创建二叉树
int BtreeShowDLR(Btree *root); // 三种遍历
int BtreeShowLDR(Btree *root);
int BtreeShowLRD(Btree *root);
int main()
{
Btree *root = NULL;
printf("\nplase input the tree as DLR:\n");
BtreeCreat(&root);
printf("\nDLR is: ");
BtreeShowDLR(root);
printf("\nLDR is: ");
BtreeShowLDR(root);
printf("\nLRD is: ");
BtreeShowLRD(root);
return 0;
}
int BtreeCreat(Btree **TreePtr)
{
// 因为会对传入的结点的地址进行修改,所以需要用到二重指针
char ch;
ch = getchar(); //读入字符
if (ch == '#')
{
*TreePtr = NULL; // 如果这个字符是#,则将这个结点的地址赋为空
}
else //非空
{
*TreePtr = (Btree*)malloc(sizeof(Btree)); // 创建结点
if (!*TreePtr) return ERROR; // 创建失败退出
(*TreePtr)->node.data = ch; // 将结点的值赋值为字符
BtreeCreat(&((*TreePtr)->lc)); // 遍历创建左结点
BtreeCreat(&((*TreePtr)->rc)); // 遍历创建右结点
}
return OK;
}
int BtreeShowDLR(Btree *root) // 因为遍历不会对结点地址进行改变,所以不需要二重指针
{
if (root != NULL) // 如果结点不为空,说明它有值
{
printf("%c", root->node.data); // 输出根结点
BtreeShowDLR(root->lc); // 遍历左结点
BtreeShowDLR(root->rc); // 遍历右结点
}
// 为空没有任何操作
return OK;
}
int BtreeShowLDR(Btree *root)
{
if (root != NULL) // 遍历顺序不一样
{
BtreeShowLDR(root->lc);
printf("%c", root->node.data);
BtreeShowLDR(root->rc);
}
return OK;
}
int BtreeShowLRD(Btree *root)
{
if (root != NULL) // 遍历顺序不一样
{
BtreeShowLRD(root->lc);
BtreeShowLRD(root->rc);
printf("%c", root->node.data);
}
return OK;
}