树是一种非线性的,一对多的数据结构.不同于前面所学的链表队列栈等,一个树的节点后可以连接多个节点.树有且只有一个特点的根节点,其他的节点又可以分为根节点的子树.满足"树中还有树",所以树具有递归性.树中的每一个角色叫做节点,根节点很特殊他没有前驱节点,如果一个节点没有后继那么称为叶子节点.
树的存储结构
树有三种存储结构:1.双亲表示法,2.孩子表示法,3.孩子兄弟表示法.
双亲表示法
双亲表示法,就是每个节点除了记录这个节点的名字,还要记录这个节点的双亲节点.那么就可以用一个结构体数组来记录,结构体包含两个数据一个是节点数据,以及双亲节点在这个数组中的下标.
#include<stdio.h>
#include<stdlib.h>
#define maxx 100
struct Node {
char data;//节点名字
int fa;//双亲节点所在下标
}t[maxx];
int sum = 0;
void InitTree(char root) {
t[0].data = root;//根节点
t[0].fa = -1;//根节点没有双亲节点
sum++;
}
int Find(char fx) {
for (int i = 0; i < sum; i++) {
if (t[i].data == fx) {
return i;
}
}
return -1;//未找到
}
void Insert(char fx, char fa) {
if (sum == maxx) {
printf("树已满不能插入\n");
return;
}
t[sum].data = fx;
t[sum].fa = Find(fa);
sum++;
return;
}
int main() {
int n;
scanf("%d", &n);
getchar();
char root, x, fx;
scanf("%c", &root);
InitTree(root);
for (int i = 0; i < n - 1; i++) {
getchar();
scanf("%c %c", &x, &fx);
Insert(x, fx);
}
getchar();
scanf("%c",&x);
//找x的父亲
int xi = Find(x);
if (xi == -1) {
printf("树中不存在节点%c\n", x);
return 0;
}
int fxi = t[xi].fa;
if (fxi == -1) {
printf("%c是根节点\n",x);
}
else {
printf("%c是%c的子节点\n",x,t[fxi].data);
}
//找x的子节点
for (int i = 0; i < sum; i++) {
if (t[i].fa == xi) {
printf("%c是%c的子节点\n", t[i].data, x);
}
}
return 0;
}
在用一个全局变量sum来记录书中元素的数量,同时当元素插入树中通过更新sum记录新的节点.
1.初始化树,需要读入根节点而根节点root是没有前驱的,所以把根节点的双亲节点下标更新为-1,然后节点总数sum也得更新.
2.查找函数,需要读入要查找的节点数据fx,让遍历结构体数组的所有数据域,如果t[i].data == fx那么就找到了目标节点,返回此时的下标i.全部循环结束后还没有找到的话就返回-1.
3.插入函数,插入一个新的节点需要输入节点数据和这个节点的双亲结点,插入前先判断树是否已满(sum == maxx).先更新结构体数组中sum位置的数据域为fx,再把sum位置的双亲结点下标更新.这里需要借助Find函数,在结构体数组中找到fa的位置再返回下标并记录.最后更新sum即可.
树的基础操作就只有这些,双亲表示法是一种基于线性表的线性结构.
主函数中先输入要读入的元素个数n,c语言中scanf会在读入信息后加上一个换行符,为了确保后面输入的正常要用getchar去除.再读入root结点然后对树进行初始化,然后用insert函数把各个结点插入树中,因为根节点是单独处理的,所以循环的次数为n-1.读入一个要操作的结点x.
找x结点的父亲:用Find函数查找x在数组的下标并用xi记录.如果xi == -1说明书没有这个结点.再通过访问结构体数组的xi位置找到x的双亲结点在数组中的下标用fxi记录.如果fxi == -1那么说明x是根节点没有双亲结点.如果fxi 不等于 -1 那么他的双亲结点就是数组中fxi位置的数据.
找x结点的孩子:只需要遍历数组的父亲结点下标,如果t[i].fa == xi那么这个结点就是x的孩子结点.
孩子表示法
孩子表示法,就是每个节点需要记录这个结点还有这个结点的子节点,一个结点只能有一个前驱所以双亲表示法可以用线性结构来一一对应,而一个结点可以有多个后继那么光用线性表是无法记录的.所以我们可以引入一个链表来记录结点的所有子节点,既可以不用拘束于线性结构的大小,又方便增删.
#include<stdio.h>
#include<stdlib.h>
#define maxx 100
typedef struct chNode {
int chi;//孩子的下标
struct chNode* next;//孩子节点的后继
}chNode;
struct Tree {
char data;
chNode* son;//孩子链表的头节点
}t[maxx];
int sum = 0;
void InitTree(char root) {
t[0].data = root;
t[0].son = NULL;
sum++;
}
int Find(char fx) {
for (int i = 0; i < sum; i++) {
if (t[i].data == fx) {
return i;
}
}
return -1;
}
void Insert(char x, char fx) {
t[sum].data = x;
t[sum].son = NULL;
//把记录x的节点插入fx的儿子链表中
int i = Find(fx);
chNode* s = (chNode*)malloc(sizeof(chNode));
s->chi = sum;
s->next = t[i].son;
t[i].son = s;
sum++;
}
int main() {
int n;
scanf("%d", &n);
getchar();
char root, x, fx;
scanf("%c", &root);
InitTree(root);
for (int i = 0; i < n - 1; i++) {
getchar();
scanf("%c %c", &x, &fx);
Insert(x, fx);
}
getchar();
scanf("%c", &x);
int j = Find(x);
//找孩子
chNode* p = t[j].son;
if (p == NULL) {
printf("p是叶子节点无子节点\n");
}
else {
printf("该节点的孩子节点有:");
while (p != NULL) {
printf("%c ", t[p->chi].data);
p = p->next;
}
printf("\n");
}
//找父亲节点
int flag = 0;
for (int i = 0; i < sum; i++) {
p = t[i].son;
while (p != NULL) {
if (t[p->chi].data == x) {
printf("该节点的父亲是%c\n", t[i].data);
flag = 1;
break;
}
p = p->next;
}
if (flag == 1) {
break;
}
}
if (flag != 1) {
printf("该节点是根节点\n");
}
return 0;
}
我们需要用两个结构体来表示这棵树,第一个是孩子结点的结构体,类似于链表结点数据域记录这个结点在线性表中的下标,而指针域记录后继节点的地址.树的主体还是一个结构体数组,数据域记录这个结点的数据,而指针域记录他的孩子链表的头节点.再用一个全局变量sum记录树中元素个数.
1.初始化树,需要录入根节点的数据root,把下标为0的位置的数据域更新为root,并将他的孩子链表头指针置空.最后要更新sum.
2.查找函数只需要输入目标节点的信息fx再遍历线性表的数据域,如果t[i].data == fx直接返回此时的下标i即可.如果循环结束了说明没有找到目标节点就返回-1.
3.插入函数,需要输入fx节点和他的子节点x.先更新结构体数组sum位置的数据域,再将指针域置空(插入的节点暂时没有子节点).然后我们要把x插入到fx的孩子链表中.先用i记录fx在线性表中的位置,然后malloc一个节点s,s节点在线性表中的下标为sum.因为孩子链表是一个单向链表,所有显然用头插法会更方便.把s节点插入孩子链表中,再把结构体数组的孩子链表头节点更新为s(t[i].son = s;).
孩子表示法是用线性结构记录所有节点,再用链式结构记录所有的孩子节点.
主函数中输入的部分和双亲表示法无异,用j记录节点x在线性表中的位置.
找x的孩子:直接找到x的孩子链表的头节点然后遍历输出即可.chNode* p = t[j].son,用节点p记录链表头节点,如果p == NULL那么说明这个链表为空x没有孩子节点.
找x的父亲节点:用flag记录是否找到x的父亲节点,遍历每一个线性表后挂的每一个链表,如果某个链表节点的数据域是x,那么说明这个链表是属于x父亲节点的子节点链表,直接找到此时下标i的数据域t[i].data即是x的父亲节点.找到父亲节点后需要更新flag为1然后跳出循环.如果循环结束后flag还是等于0,那么说明x是根节点没有父亲节点.
孩子兄弟表示法
每一个节点我们只记录这个节点的长子以及他右边的兄弟节点,这样一颗不规则的n叉树可以被改造为一颗二叉树.二叉树的结构更规范更清晰更便于搜索.
#include<stdio.h>
#include<stdlib.h>
typedef struct Node {
char data;
struct Node* ch;//长子地址
struct Node* bro;//右边第一个兄弟地址
}Node,*TreeList;
TreeList InitTree(char root) {
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存申请失败\n");
return;
}
s->data = root;
s->ch = NULL;
s->bro = NULL;
return s;
}
Node* Find(TreeList t, char x) {
if (t->data == x) {
return t;
}
if (t->ch != NULL)//左子树不为空 左边找
{
Node* ans = Find(t->ch, x);
if (ans != NULL) {
//找到了
return ans;
}
}
if (t->bro != NULL)//右子树不为空 右边找
{
Node* ans = Find(t->bro, x);
if (ans != NULL) {
//找到了
return ans;
}
}
return NULL;
}
TreeList Insert(TreeList r, char x, char fx) {
//把数据x插入到二叉链表中,并且x的父亲是fx
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) {
printf("内存申请失败\n");
return;
}
s->data = x;
s->ch = NULL;
s->bro = NULL;
//把节点s插入到二叉链表中
//先找到fx所在位置
Node* f = Find(r, fx);
//判断s是不是长子
if (f->ch == NULL) {
//是长子
f->ch = s;
}
else {
//不是长子
Node* t = f->ch;
while (t->bro != NULL) {
t = t->bro;
}
t->bro = s;
}
return r;
}
int main()
{
int n;
scanf("%d", &n);
getchar();
char root, x, fx;
scanf("%c", &root);
TreeList r = InitTree(root);//声明根指针r 初始化一个根结点 r指向该根结点
for (int i = 1; i <= n - 1; i++)
{
getchar();
//读入数据x 及其父亲fx
scanf("%c %c", &x, &fx);
r = Insert(r, x, fx);
}
getchar();
scanf("%c", &x);
//找x的孩子
Node* p = Find(r, x);
if (p->ch == NULL)
{
printf("该结点没有孩子\n");
}
else
{
printf("该结点的孩子有: \n");
p = p->ch;
while (p != NULL)
{
printf("%c ", p->data);
p = p->bro;
}
printf("\n");
}
return 0;
}
孩子兄弟表示法是一个纯粹的链式结构,用一个二叉链表来记录这棵树.用一个结构体来封装树的结点,记录这个结点的数据,以及长子的地址,还有右边第一个兄弟的地址.同时用typedef给结构体取两个别名来区分普通结点和根节点.
1.初始化树,需要输入根节点的数据.malloc一个新的结点s,判空后把s的数据域更新为root, 并将s的长子地址以及右边第一个兄弟的地址置空.最后返回s.这个s就是根节点.
2.查找函数,需要输入树的根节点以及目标节点的数据x.前文提到树是具有递归性的,二叉树的结构更加标准,所有我们这里用递归的方法来查找整棵二叉树.先判断根节点是否是目标节点,再分别判断左右子树中是否有目标节点.因为Find函数的返回值是一个Node* 指针所以在查找过程中用ans节点来记录find结果.要理解递归的过程,可以不用层层展开,而是直接把这个函数当成一个结果带入进去理解.如果找到目标节点那么ans != NULL直接返回ans节点即可,如果都没有找到的话最后返回NULL.
3.插入函数,需要输入根节点r以及节点x还有x的父亲fx.先malloc一个节点s并将x的数据保存到s节点中.然后将s的孩子地址和兄弟地址置空.先找到fx在二叉链表的位置,判断s是否是fx的长子如果是的话直接放在fx的孩子后继中.如果x不是fx的长子那么需要遍历fx长子的兄弟链表并将s插入到链表的最末端.最后返回头节点r.
主函数的输入与前文相同.用节点p来记录要操作的目标节点x.
找x的孩子:如果x的长子后继为空,那么说明x没有孩子节点.反之直接遍历输出x长子的兄弟链表,这些全都是x的孩子节点.
树是一种全新的数据结构,其逻辑方式和前面的数据结构都有所不同.