1、基本概念
树是树型结构的简称,它是一种重要的非线性数据结构。
树的表示:通常使用广义表表示方法,即每棵树的根作为由子树构成的表的名字而放在表的前面,如下图的树对应的广义表表示为:
A(B(D,E(H,I),F),C(G))
结点的度:树中每个结点具有的非空子树数或者说后继结点数被定义为该结点的度。(如上图中,B结点度为3,A和E结点度都为2,C结点度为1,其余结点度均为0)
树的度:树中所有结点的度的最大值被定义为该树的度。(如上图中树的度为3)
叶子结点:度等于0的结点称为叶子结点或终端结点。
分支结点:度大于0的结点称为分支结点或非终端结点。每个结点的分支数就是该结点的度数。
在一棵树中,每个结点的子树的根(或者说每个结点的后继)被称为孩子结点,该结点被称为父亲结点。
结点的层数从树根开始定义,根结点为第一层,它的孩子结点为第二层,以此类推。树中结点最大层数称为树的深度或高度。上图树的深度为4.
二叉树:是指树的度为2的有序树。
满二叉树:当二叉树中的每一层都满时(结点数为2^(i-1)),则称此树为满二叉树。
完全二叉树:二叉树中,除最后一层外,其余层都是满的,并且最后一层或者是满的,或者是在最右边缺少连续若干个结点,则称此树为完全二叉树。
理想平衡二叉树:二叉树中,除最后一层外,其余层都是满的,并且最后一层的结点可以任意分布,则称此树为理想平衡二叉树。理想平衡二叉树包含满二叉树和完全二叉树。
2、二叉树的存储结构
a、顺序存储结构
顺序存储一棵二叉树,首先对每个结点编号,然后以编号为下标,把各结点存储到一堆数组中。
如下图为一个二叉树及其对应的顺序存储结构
二叉树顺序存储结构对于存储完全二叉树是合适的,但对于一般二叉树来说不合适,浪费存储空间,故一般使用下面的链接存储结构。
b、链接存储结构
每个结点设置三个域:左指针域、值域、右指针域。
结点类型定义:
struct BTreeNode //结点类型定义 { ElemType data; //值域 struct BTreeNode* left; //左指针域 struct BTreeNode* right; //右指针域 };
如下图为一个二叉树及其对应的链接存储结构
3、二叉树的遍历
a、前序遍历:访问根结点的操作在遍历左、右子树之前,上图前序遍历序列为:A,B,C,D,E,F,G
b、中序遍历:访问根结点的操作在遍历左子树之后和右子树之前,上图中序遍历序列为:C,B,D,A,E,G,F
c、后序遍历:访问根结点的操作在遍历左、右子树之后,上图后序遍历序列为:C,D,B,G,F,E,A
d、按层遍历算法:即按照从上到下、同一层从左到右的次序访问各结点,上图按层遍历次序为:A,B,E,C,D,F,G
具体实现见后面的详细程序。
4、二叉树的操作和运算
下面通过详细的程序展现二叉树的操作和运算,按上图二叉树作为例子,广义表表示为:A(B(C,D),E(,F(G)))
#include<stdio.h> #include<stdlib.h> #define QueueMaxSize 20 //定义队列数组长度 #define StackMaxSize 10 //定义栈数组长度 typedef char ElemType; //定义ElemType类型 struct BTreeNode //结点类型定义 { ElemType data; //值域 struct BTreeNode* left; //左指针域 struct BTreeNode* right; //右指针域 }; //1、初始化二叉树 void InitBTree(struct BTreeNode** BT) { *BT = NULL; //把树根指针置空 } //2、建立二叉树,采用广义表表示的输入法,如:A(B(C,D),E(,F(G))) void CreateBTree(struct BTreeNode** BT, char* string) { struct BTreeNode* p; struct BTreeNode* s[StackMaxSize]; //定义s数组作为存储根结点的指针的栈使用 int top = -1; //栈顶指针置为-1,表示空栈 int k; //k作为处理结点的标志,k=1处理左子树,k=2处理右子树 int i = 0; //用i扫描数组string中存储的二叉树广义表字符串,初值为0 *BT = NULL; //把树根指针置空,即从空树开始建立二叉树 while (string[i]) { switch (string[i]) { case ' ':break; case '(': { if (top == StackMaxSize - 1) { printf("栈空间太小,需增加StackMaxSize!\n"); exit(1); } top++; s[top] = p; k = 1; break; } case ')': { if (top == -1) { printf("二叉树广义表字符串错!\n"); exit(1); } top--; break; } case ',':k = 2;break; default: { p = malloc(sizeof(struct BTreeNode)); p->data = string[i]; p->left = p->right = NULL; if (*BT == NULL) *BT = p; else { if (k == 1) s[top]->left = p; else s[top]->right = p; } } }//switch end i++; }//while end } //3、检查二叉树是否为空 int BTreeEmpty(struct BTreeNode* BT) { if (BT == NULL) return 1; else return 0; } //4、求二叉树深度 int BTreeDepth(struct BTreeNode* BT) { if (BT == NULL) return 0; else { int dep1 = BTreeDepth(BT->left); //计算左子树深度 int dep2 = BTreeDepth(BT->right);//计算右子树深度 if (dep1 > dep2) return dep1 + 1; else return dep2 + 1; } } //5、从二叉树中查找值为x的结点,若存在则返回元素存储位置,否则返回空值(算法类似于前序遍历) ElemType* FindBTree(struct BTreeNode* BT, ElemType x) { if (BT == NULL) return NULL; else { if (BT->data == x) return &(BT->data); else { ElemType* p; if (p = FindBTree(BT->left, x)) return p; if (p = FindBTree(BT->right, x)) return p; return NULL; } } } //6、输出二叉树,可在前序遍历的基础上修改。采用广义表输出格式:A(B(C,D),E(,F(G))) void PrintBTree(struct BTreeNode* BT) { if (BT != NULL) { printf("%c", BT->data); //输出根结点的值 if (BT->left != NULL || BT->right != NULL) { printf("("); PrintBTree(BT->left); //输出左子树 if (BT->right != NULL) printf(","); PrintBTree(BT->right); //输出右子树 printf(")"); } } } //7、清除二叉树,使之变为一棵空树,算法类似于后序递归遍历 void ClearBTree(struct BTreeNode** BT) { if (*BT != NULL) { ClearBTree(&((*BT)->left));//删除左子树 ClearBTree(&((*BT)->right));//删除右子树 free(*BT); //释放根结点 *BT = NULL; //置根指针为空 } } //8、前序遍历 void Preorder(struct BTreeNode* BT) { if (BT != NULL) { printf("%c,", BT->data); Preorder(BT->left); Preorder(BT->right); } } //9、中序遍历 void Inorder(struct BTreeNode* BT) { if (BT != NULL) { Inorder(BT->left); printf("%c,", BT->data); Inorder(BT->right); } } //10、后序遍历 void Postorder(struct BTreeNode* BT) { if (BT != NULL) { Postorder(BT->left); Postorder(BT->right); printf("%c,", BT->data); } } //11、按层遍历 //按层遍历算法需要使用一个队列,开始时把整个树的根结点入队,然后每从队列中删除一个结点并输出该结点时, //都把它的非空的左右孩子结点入队,当队列为空时算法结束。 //算法中,队列的最大长度不会超过二叉树中相邻两层的最大结点数, //所以在提前在程序开始处定义最大队列长度QueueMaxSize大于队列的最大长度,就无需考虑队满溢出的事了 void Levelorder(struct BTreeNode* BT) { struct BTreeNode* p; struct BTreeNode* q[QueueMaxSize];//定义队列所使用的数组空间,元素类型为指向结点的指针类型 int front = 0; int rear = 0; if (BT != NULL)//将树根指针入队 { q[rear] = BT; rear = (rear + 1) % QueueMaxSize; } while (front != rear)//当队列非空时执行循环 { p = q[front];//保存队首元素 front = (front + 1) % QueueMaxSize;//删除队首元素,使队首指针指向队首元素 printf("%c,", p->data);//输出队首元素 if (p->left != NULL)//若结点存在左孩子,则左孩子结点指针入队 { q[rear] = p->left; rear = (rear + 1) % QueueMaxSize; } if (p->right != NULL)//若结点存在右孩子,则左孩子结点指针入队 { q[rear] = p->right; rear = (rear + 1) % QueueMaxSize; } } } //主函数 void main() { struct BTreeNode* bt; char b[50]; ElemType x, *px; InitBTree(&bt); printf("输入二叉树广义表字符串:\n"); scanf("%s", b); CreateBTree(&bt, b); PrintBTree(bt); printf("\n"); printf("前序:"); Preorder(bt); printf("\n"); printf("中序:"); Inorder(bt); printf("\n"); printf("后序:"); Postorder(bt); printf("\n"); printf("按层:"); Levelorder(bt); printf("\n"); printf("输入一个待查找的字符:\n"); scanf(" %c", &x); //格式串中的空格可以跳过任何空白符 px = FindBTree(bt, x); if (px) printf("查找成功:%c\n", *px); else printf("查找失败\n"); printf("二叉树的深度为:"); printf("%d\n", BTreeDepth(bt)); ClearBTree(&bt); }
输出结果:
程序中的建立二叉树和按层遍历部分用到了广义表、栈和队列的知识,而且细节性较强,需额外细心。
5、树的基本概念及存储结构
这里指度大于等于3的树,通常称为多元树或多叉树。
树的顺序存储适合满树的情况,否则将非常浪费存储空间。所以通常使用链接存储结构。
树的链接存储结构通常采用如下三种方式:
(1) 标准方式
在这种方式中,树中的每个结点除了包含有存储数据元素的值域外,还包含有k个指针域,用来分别指向k个孩子结点,或者说,用来分别链接k棵子树,其中k为树的度。结点的类型可定义为:
struct GTreeNode { ElemType data;//结点值域 struct GTreeNode* t[k];//结点指针域t[0]~t[k-1] };
(2) 广义标准方式
广义标准方式是在标准方式的每个结点中增加一个指向其双亲结点的指针域。结点的类型可定义为:
struct PGTreeNode { ElemType data; //结点值域 struct PGTreeNode* t[k]; //结点指针域t[0]~t[k-1] struct PGTreeNode* parent; //双亲指针域 };(3)二叉树形式
这种表示首先将树转换为对应的二叉树形式,然后再采用二叉链表存储这棵二叉树。但无法表示任一结点中缺少前面孩子,又存在后面孩子那样的有序树。
一般情况下使用标准方式存储树。
下面是一个三叉树及其对应的链接存储结构(标准方式),广义表表示为:a(b(,e,f(,j)),c,d(g(k,,l),h,i))
(注意:在缺少前面子树而存在后面子树的情况时,广义表表示时,空子树后面的逗号不能省略)
6、树的操作和运算
树的操作和运算与二叉树类似,可以在熟练二叉树的操作和运算后进行比较。
下面用一个实例程序具体展现上图中树的操作和运算
#include<stdio.h> #include<stdlib.h> #define kk 3 //定义树的度 #define MS 10 //定义符号常量在建立树存储结构时指定栈空间的大小 #define MQ 10 //定义符号常量在树的按层遍历算法中指定队列空间的大小 typedef char ElemType; struct GTreeNode //树的链接存储标准方式的结点类型定义 { ElemType data; //结点值域 struct GTreeNode* t[kk]; //结点指针域t[0]~t[kk-1] }; //1、建立树的存储结构,采用广义表的表示法,如:a(b(,e,f(,f)),c,d(g(k,,l),h,i)) //假定结点值仍为字符类型char,算法与二叉树类似 //设置两个栈,s栈用来存储指向根结点的指针,以便孩子结点向双亲结点链接之用, //d栈用来存储待链接的孩子结点的序号,以便能正确地链接到双亲结点的指针域 //下面是根据广义表字符串string所给出的k叉树建立对应的存储结构 void CreateGTree(struct GTreeNode** GT, char* string) { struct GTreeNode* s[MS]; //MS常量为存储结点指针的栈数组长度 int d[MS]; int top = -1; //top作为两个栈的栈顶指针,初始值0表示栈空 struct GTreeNode* p; //定义p为指向树结点的指针 int i = 0, j; //用i指示扫描字符串数组a中的当前字符位置 *GT = NULL; //初始将树根指针置空 while (string[i]) { switch(string[i]) { case ' ':break; case '(': { top++; s[top] = p; //p指针进s栈,0进d栈, d[top] = 0; //表明待扫描的孩子结点将被链接到s栈顶元素所指结点的第一个指针域 break; } case ')':top--;break; //让s和d退栈 case ',':d[top]++;break;//让待读入的孩子结点链接到s栈顶元素所指结点的下一个指针域 default: { p = malloc(sizeof(struct GTreeNode)); p->data = string[i]; for (j = 0; j < kk; j++) //kk表示树的深度 p->t[j] = NULL; if (*GT == NULL) //使p结点成为树根结点(if)或链接到双亲结点对应的指针域(else) *GT = p; else s[top]->t[d[top]] = p; } }//switch end; i++; //准备处理下一个字符 } } //2、树的先根遍历 void PreRoot(struct GTreeNode* GT)//先根遍历一棵k叉树 { int i; if (GT != NULL) { printf("%c,", GT->data); //访问根结点 for (i = 0; i< kk ; i++) PreRoot(GT->t[i]); //递归遍历每一个子树 } } //3、树的后根遍历 void PostRoot(struct GTreeNode* GT) { int i; if (GT != NULL) { for (i = 0; i < kk; i++) PostRoot(GT->t[i]); //递归遍历每一个子树 printf("%c,", GT->data); //访问根结点 } } //4、树的按层遍历 void LayerOrder(struct GTreeNode* GT)//按层遍历由GT指针所指向的k叉树 { struct GTreeNode* p; int i; struct GTreeNode* q[MQ]; //定义一个队列用的数组q,MQ常量为队列数组长度 int front = 0, rear = 0; //定义队首指针和队尾指针,初始均置0表示空队 if (GT != NULL) //将树根指针进队 { q[rear] = GT; rear = (rear + 1) % MQ; } while (front != rear)//当队列非空时执行循环 { p= q[front]; front = (front + 1) % MQ; //使队首指针指向队首元,素删除队首元素 printf("%c,",p->data); //输出队首元素所指结点的值 for (i = 0; i < kk; i++) //非空的孩子结点指针依次进队 if (p->t[i] != NULL) { q[rear] = p->t[i]; rear = (rear + 1) % MQ; } } } //5、从树中查找结点值 ElemType* FindGTree(struct GTreeNode* GT, ElemType x) { if (GT==NULL) return NULL; //树空返回空指针 else { ElemType* p; int i; if (GT->data == x) return &(GT->data); //查找成功返回结点值域的地址 for (i = 0; i < kk; i++) //向每棵子树继续查找,返回得到的值域地址 if (p = FindGTree(GT->t[i], x)) return p; return NULL;//查找不成功返回空指针 } } //6、输出树,可在先根遍历的基础上修改。采用广义表输出格式:a(b(,e,f(,f)),c,d(g(k,,l),h,i)) void PrintGTree(struct GTreeNode* GT) { if (GT != NULL) { int i; printf("%c", GT->data); //输出根结点的值 for (i = 0; i < kk; i++)//判断GT结点是否有孩子 if (GT->t[i] != NULL) break; if (i < kk) //有孩子时执行递归 { printf("("); PrintGTree(GT->t[0]); //输出第一棵树 for (i = 1; i < kk; i++)//输出其余子树 { printf(","); PrintGTree(GT->t[i]); } printf(")"); } } } //7、求树深度 int GTreeDepth(struct GTreeNode* GT) { int i, max; //max用来保存已求过的子树中的最大深度 if (GT == NULL) return 0; max = 0; for (i = 0; i < kk; i++) { int d = GTreeDepth(GT->t[i]); //计算一棵子树深度 if (d > max) max = d; } return max + 1; //返回非空树的深度,它等于各子树的最大深度加1 } //8、清除二叉树,使之变为一棵空树,算法类似于后序递归遍历 void ClearGTree(struct GTreeNode** GT) { if (*GT != NULL) { int i; for (i = 0; i < kk; i++) ClearGTree(&((*GT)->t[i]));//删除子树 free(*GT); //释放根结点 *GT = NULL; //置根指针为空 } } //主函数 void main() { char ch; struct GTreeNode* gt = NULL; char b[50]; //定义一个用于存放k叉树广义表的字符数组 printf("输入一棵%d叉树的广义表字符串:\n", kk); scanf("%s", b); CreateGTree(>, b); printf("先根遍历结果:"); PreRoot(gt); printf("\n"); printf("后根遍历结果:"); PostRoot(gt); printf("\n"); printf("按层遍历结果:"); LayerOrder(gt); printf("\n"); printf("按广义表形式输出的树为:"); PrintGTree(gt); printf("\n"); printf("树的深度为:%d\n", GTreeDepth(gt)); printf("输入待查找的一个字符:"); scanf(" %c", &ch);//格式串中的空格可以跳过任何空白符 if (FindGTree(gt, ch)) printf("查找成功!\n"); else printf("查找失败!\n"); ClearGTree(>); }
运行结果: