前一节介绍了树的基本概念,这一节咱们就来看看树的表示方法。我们应该用什么样的存储方式来存储一棵树呢?我们目前学习了数据的顺序存储和链式存储,其实树就是以这两种基本存储方法构建起来的。
目录
🌟1.双亲表示法
💓2.孩子表示法
🌵3.孩子兄弟表示法
🌟1.双亲表示法
双亲表示法是用顺序表,也就是数组对树进行表示的。
每一个结点包含存储的数据与其父节点的数组下标,多个结点用一组连续的地址空间,即数组来存储。
我们来看这一棵树:
它的存储方式为:
详细说明:
- 因为根节点没有父节点,将其父节点数组下标设置为-1,根节点存储在顺序表的第1个位置;
- 数据元素2、3、4的父节点为1,父节点数组下标为1,分别存储在顺序表的2、3、4个位置;
- 数据元素5、6的父节点为2,父节点数组下标为2,分别存储在顺序表的第5、6个位置;
- 数据元素7的父节点为3,父节点数组下标为3,存储在顺序表的第7个位置
1.显然就可以写出,存储结点的代码:
typedef struct TreeNode
{
int data;//树中存放的数据
int parent;//父节点的位置
}Node;
2.初始化这棵树,也就是建立根节点:
void init(int n)
{
size = 0;
maxsize = 10;
/*创建根节点*/
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = n;//根结点存储元素
new_node->parent = -1;//根结点的没有父节点,赋为-1
node[size++] = new_node;//将根结点添加到数组中,size+1
}
3.通过以上的详细说明可以写出建立树,也就是插入树的结点:
- 首先要明确在哪里插入,也就是找到插入结点的父节点
- 然后在顺序表中依次添加内容
void insert(int n,int p)
{
if (size == maxsize)
{
printf("已存满,添加失败\n");
}
else
{
//先要找到有没有指定的父亲结点
int i = find(p);
if (i = -1)
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = n;
new_node->parent = p;//它的父亲结点元素为p
node[size++] = new_node;//在数组中依次向后添加
}
else
{
printf("没有此父节点,添加失败\n");
}
}
}
优缺点:
优点:若要找到某个结点的父节点,可以很容易找到,因为顺序表已经存好了每个节点的父节点位置;
缺点:但是要找某个结点的孩子节点,那就比较麻烦了,需要去遍历整个结构。
完整代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct TreeNode
{
int data;//树中存放的数据
int parent;//父节点的位置
}Node;
Node* node[10];//顺序表表示
int size;//当前元素个数
int maxsize;//元素总个数
void init(int);//初始化并建立根节点
void insert(int, int);//建立孩子结点在指定父亲结点下
int find(int);//查找该结点的父节点
void init(int n)
{
size = 0;
maxsize = 10;
/*创建根节点*/
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = n;//根结点存储元素
new_node->parent = -1;//根结点的没有父节点,赋为-1
node[size++] = new_node;//将根结点添加到数组中,size+1
}
void insert(int n,int p)
{
if (size == maxsize)
{
printf("已存满,添加失败\n");
}
else
{
//先要找到有没有指定的父亲结点
int i = find(p);
if (i = -1)
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = n;
new_node->parent = p;//它的父亲结点元素为p
node[size++] = new_node;//在数组中依次向后添加
}
else
{
printf("没有此父节点,添加失败\n");
}
}
}
int find(int p)
{
//如果该元素p等于数组中的某元素,则说明找到了,返回其在数组中的位置
for (int i = 0; i < size; i++)
{
if (p == node[i]->data)
return i;
}
//否则,返回-1
return -1;
}
int main()
{
init(1);
insert(2, 1);
insert(3, 1);
insert(4, 1);
insert(5, 2);
insert(6, 2);
insert(7, 3);
for (int i = 0; i < size; i++)
{
printf("父节点为%d ", node[i]->parent);
printf("该结点为%d\n", node[i]->data);
}
return 0;
}
输出结果:
💓2.孩子表示法
刚刚的双亲表示法,因为没有存储孩子结点的位置,所以找孩子结点比较麻烦,于是前辈们又想出来了另外一种方法,叫孩子表示法,这种方法融合了顺序存储与链式存储结构,可以很快的找到孩子们。
我们先来看看这棵树:
它的存储方式:
详细说明:
添加一个数据(插入一个结点)
- 既要在数组中依次添加新的数据
- 也要在其父节点后面用头插法插入
- 添加第一个数据1,作为根节点,加入到数组中第1个位置,它没有父节点;
- 添加第二个数据2,其父节点为1,加入2到数组的第2个位置,在其父节点1后插入第1个孩子;
- 添加第三个数据3,其父节点为1,加入3到数组的第3个位置,在其父节点1后插入第2个孩子;
- 添加第四个数据4,其父节点为1,加入4到数组的第4个位置,在其父节点1后插入第3个孩子;
- 添加第五个数据5,其父节点为2,加入5到数组的第5个位置,在其父节点2后插入第1个孩子;
- 添加第六个数据6,其父节点为2,加入6到数组的第6个位置,在其父节点2后插入第2个孩子;
- 添加第七个数据7,其父节点为3,加入7到数组的第7个位置,在其父节点3后插入第1个孩子。
在插入的过程中,我们可以看出来:
- 插入孩子结点采用头插法,即先插入的元素到了后面,后插入的元素在前面
- 数组的类型与孩子结点的类型相同,也是一个结构体,包含一个数据域,一个指针域
1.显然可以写出,存储结构代码
typedef struct LinkList
{
int data;//存放数据
struct LinkList* next;
}Node;
2.初始化根节点
void init(int n)
{
size = 0;
maxsize = 10;
//其实生成一个new_node,然后再赋值给数组也行
node[size] = (Node*)malloc(sizeof(Node));
node[size]->data = n;
node[size]->next = NULL;//根节点暂时没有孩子,next为空
size++;
}
3.先存入数组,然后插入到父节点后面
/*将数据n插入到结点p下面*/
//即将n作为p的孩子结点
void insert(int n, int p)
{
if (size == maxsize)
{
printf("已存满,插入失败\n");
}
else
{
int i = find(p);
if (i != -1)
{
//先将孩子结点放入到数组中
node[size] = (Node*)malloc(sizeof(Node));
node[size]->data = n;
node[size]->next = NULL;//此结点暂时没有孩子结点
size++;
//再将结点插入到父节点next中
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = n;
new_node->next = node[i]->next;//类似于头插法
node[i]->next = new_node;//将新插入的孩子结点放在父节点后一个位置,即父节点原有孩子结点的前面
}
else
printf("没有找到此父节点,插入失败\n");
}
}
完整代码
#include<stdio.h>
#include<stdlib.h>
typedef struct LinkList
{
int data;//存放数据
struct LinkList* next;
}Node;
Node* node[10];//存储结点的数组
int size;//数组中元素的个数
int maxsize;
void init(int);//初始化操作
void insert(int, int);//构建树
int find(int);//找到父节点
void init(int n)
{
size = 0;
maxsize = 10;
//其实生成一个new_node,然后再赋值给数组也行
node[size] = (Node*)malloc(sizeof(Node));
node[size]->data = n;
node[size]->next = NULL;//根节点暂时没有孩子,next为空
size++;
}
int find(int p)
{
for (int i = 0; i < size; i++)
{
if (p == node[i]->data)
return i;
}
return -1;
}
/*将数据n插入到结点p下面*/
//即将n作为p的孩子结点
void insert(int n, int p)
{
if (size == maxsize)
{
printf("已存满,插入失败\n");
}
else
{
int i = find(p);
if (i != -1)
{
//先将孩子结点放入到数组中
node[size] = (Node*)malloc(sizeof(Node));
node[size]->data = n;
node[size]->next = NULL;//此结点暂时没有孩子结点
size++;
//再将结点插入到父节点next中
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = n;
new_node->next = node[i]->next;//类似于头插法
node[i]->next = new_node;//将新插入的孩子结点放在父节点后一个位置,即父节点原有孩子结点的前面
}
else
printf("没有找到此父节点,插入失败\n");
}
}
int main()
{
init(1);
insert(2, 1);
insert(3, 1);
insert(4, 1);
insert(5, 2);
insert(6, 2);
insert(7, 3);
for (int i = 0; i < size; i++)
{
printf("父节点为%d ", node[i]->data);
Node* temp = node[i]->next;
while (temp != NULL)
{
printf("孩子结点为%d ", temp->data);
temp = temp->next;
}
printf("\n");
}
}
输出结果:
🌵3.孩子兄弟表示法
这种表示方式也可以很快找到孩子结点,不过有点绕,要耐心理解嗷~
我们先来看看这棵树,这也是它的存储方式:
每个结点包含了数据域、孩子指针域、兄弟指针域
注意:这里的2、3、4互为兄弟,也都是1的孩子;5、6互为兄弟,也都是2的孩子。
详细说明:
这种插入方式:
- 同样也要先找到在谁的后面插入,即找到父节点
- 然后再看看父节点的孩子指针空不空
- 空的话就插入到父节点的孩子指针域里,如果这个位置有结点的话(孩子指针域不空),就要插入到这个孩子结点的兄弟指针域里,插入方式还是头插法。
- 添加根节点1,其孩子、兄弟指针域为空;
- 添加结点2,其父节点为1,父节点的孩子指针域为空,就插入到此位置(将父节点1的孩子指针指向2),2的孩子、兄弟指针为空;
- 添加结点3,其父节点为1,父节点的孩子指针域不空,就插入到其孩子结点的兄弟指针域里(将孩子结点2的兄弟指针指向3),3的孩子、兄弟指针为空;
- 添加结点4,其父节点为1,父节点的孩子指针域不空,就插入到其孩子结点的兄弟指针域里(将孩子结点2的兄弟指针指向4),4的孩子指针为空、兄弟指针指向原来的3(这里的图就有一点问题,4应该在3的上面)
- 添加结点5,其父节点为2,父节点的孩子指针域为空,就插入到此位置(将父节点2的孩子指针指向5),5的孩子、兄弟指针为空;
- 添加结点6,其父节点为2,父节点的孩子指针域不空,就插入到其孩子结点的兄弟指针域里的(将孩子结点5的兄弟指针指向6),6的孩子、兄弟指针为空;
- 添加结点7,其父节点为3,父节点的孩子指针域为空,就插入到此位置(将父节点3的孩子指针指向7),7的孩子、兄弟指针为空;
1.显然,它的存储方式:
typedef struct ChildSibling {
int data;//当前结点的数据
struct ChildSibling* child;//指向孩子的指针
struct ChildSibling* sibling;//指向兄弟的指针
}Node;
2.初始化根节点:
void Init(int key)
{
root = (Node*)malloc(sizeof(Node));
root->data = key;
root->child = NULL;
root->sibling = NULL;
}
3.先插入到孩子指针域里,满了就插入到孩子结点的兄弟指针域那里:
/*
int key 待插入的数据
int parent 数据的父节点
*/
void insert(int key, int parent)
{
//定位父节点
temp = get_node(root, parent);
if (temp == NULL)
{
printf("没有此结点,插入失败\n");
}
else
{ //左孩子不为空,就插入到左孩子的兄弟结点当中,类似头插法(兄弟结点可能已经有结点了)
if (temp->child != NULL)
{
temp = temp->child;
Node* node = (Node*)malloc(sizeof(Node));
node->data = key;
node->child = NULL;
node->sibling = temp->sibling;
temp->sibling = node;
}
//左孩子为空,就插入到左孩子的位置
else
{
Node* node = (Node*)malloc(sizeof(Node));
node->child = NULL;
node->sibling = NULL;
node->data = key;
temp->child = node;
}
}
}
注意:前面两种方法因为有数组,所以可以通过访问数组下标的方式找到父节点,而孩子兄弟表示法没有用数组,那该怎么找父节点呢?
只有一个一个找了,这里用递归来实现(后面的前、中、后序遍历树也要用到递归的方法嗷~)
Node* get_node(Node* node, int parent)
{
if (node->data == parent)
{
return node;
}
if (node->child != NULL)
{
get_node(node->child, parent);
}
if (node->sibling != NULL)
{
get_node(node->sibling, parent);
}
return NULL;
}
那这里就不给出完整代码了,弄懂了原理,自己动手试试吧!