树的表达方式:
之前,我们介绍的所有的数据结构都是线性存储结构。本章,我们所介绍的树的结构是⼀种⾮线 性的存储结构。存储的是具有⼀对多的关系的数据元素的集合。
树的概念:
图中可以看⻅⼀个使⽤树形结构存储的⼀个集合,这个集合就是{A,B,C.......}。对于数据A来 说,和数据B、C、D有关系。对于数据B来说,和E,F,G有关系。这就是⼀对多的关系。 我们将⼀对多的关系的集合中的数据元素按照图中的形式进⾏存储,整个存储形状在逻辑结果上 ⾯看,类似于实际⽣活中倒着的树,所以就将这种结构称之为树形结构。
树的结点:
结点:使⽤树结构存储的每⼀个数据元素都被称为“结点”。例如图中的A就是⼀个结点。
根结点:有⼀个特殊的结点,这个结点没有前驱,我们将这种结点称之为根结点。
⽗结点(双亲结点)、⼦结点和兄弟结点:对于ABCD四个结点来说,A就是BCD的⽗结点,也称 之为双亲结点。⽽BCD都是A的⼦结点,也称之为孩⼦结点。对于BCD来说,因为他们都有同⼀个爹,所 以它们互相称之为兄弟结点。
叶⼦结点:如果⼀个结点没有任何⼦结点,那么此结点就称之为叶⼦结点。
结点的度:结点拥有的⼦树的个数,就称之为结点的度。
树的度:在各个结点当中,度的最⼤值。为树的度。
树的深度或者⾼度:结点的层次从根结点开始定义起,根为第⼀层,根的孩⼦为第⼆层。依次类 推
树的存储结构:
双亲表示法
顺序表表示形式
结点的定义:
结点中包含数据域和父结点的下标
typedef struct TreeNode {
int data;//树中存放的真实的数据
int parent;//父节点 -1代表没有父节点
}Node;
全局变量:
创建一个结构体数组从而用顺序表表示树
/*全局变量*/
Node* node[5];//父亲表示法的顺序表表示
int size= 0;;//当前元素的个数
int maxSize = 5;;//元素的总个数
void insert_root(int);//建立根节点
void insert_child(int,int);//插入元素
int find_parent(int);
创建根结点:
先创建一个根结点
再将key写入数据域
因为根结点无父结点,所以父亲域写入-1
之后将结构体数组的第一个1位置写入次结点
size再往后移动
/*
创建根节点
key 根节点的关键字
*/
void insert_root(int key)
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = key;
new_node->parent = -1;
node[size] = new_node;
size++;
}
查找父结点的下标:
传入父结点的数值
通过对树的遍历循环来寻找
找到则返回下标
最终没找到返回下标-1
/*
找到父节点的下标 返回-1代表没找到
*/
int find_parent(int parent)
{
for (int i = 0; i < size; i++) {
if (parent == node[i]->data)
{
return i;
}
}
return -1;
}
插入元素:
传入的参数是关键词key和父亲结点的值parent
先判断元素是否满了
如果没满,先调用查找函数来查找父结点的下标
判断父亲结点是否找到
如果找到了
就新建一个结点
其数据域写入key
之后让其父亲域指向其父亲结点
再将此结点放入结构体数组中
size往后移动一位
/*
插入元素
int key 关键字
int parent 父节点的值
*/
void insert_child(int key, int parent)
{
if (size == maxSize)
{
//元素已满 要么提示 要么扩容
}
else
{
//判断一下 是否有这个父节点
int parent_index = find_parent(parent);
if (parent_index == -1)
{
//没有该父节点
}
else
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = key;
new_node->parent = parent_index;
node[size] = new_node;
size++;
}
}
}
总代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct TreeNode {
int data;//树中存放的真实的数据
int parent;//父节点 -1代表没有父节点
}Node;
/*全局变量*/
Node* node[5];//父亲表示法的顺序表表示
int size= 0;;//当前元素的个数
int maxSize = 5;;//元素的总个数
void insert_root(int);//建立根节点
void insert_child(int,int);//插入元素
int find_parent(int);
/*
创建根节点
key 根节点的关键字
*/
void insert_root(int key)
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = key;
new_node->parent = -1;
node[size] = new_node;
size++;
}
/*
插入元素
int key 关键字
int parent 父节点的值
*/
void insert_child(int key, int parent)
{
if (size == maxSize)
{
//元素已满 要么提示 要么扩容
}
else
{
//判断一下 是否有这个父节点
int parent_index = find_parent(parent);
if (parent_index == -1)
{
//没有该父节点
}
else
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = key;
new_node->parent = parent_index;
node[size] = new_node;
size++;
}
}
}
/*
找到父节点的下标 返回-1代表没找到
*/
int find_parent(int parent)
{
for (int i = 0; i < size; i++) {
if (parent == node[i]->data)
{
return i;
}
}
return -1;
}
最后,我们不难看出,顺序表结构下双亲表示法的树的功能无非初始化,查找和插入这三种。
优缺点说明
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的 结点都存有它双亲的位置。这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结 点,所⽤的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。可如果我们要知道结点的 孩⼦是什么,对不起,请遍历整个结构才⾏。
这真是麻烦,能不能改进⼀下呢?当然可以。我们增加⼀个结点最左边孩⼦的域,不妨叫它⻓子域,这样就可以很容易得到结点的孩⼦。如果没有孩子的结点,这个长子域就设置为-1。
对于有0个或1个孩⼦结点来说,这样的结构是解决了要找结点孩子的问题了。甚⾄是有2个孩 ⼦,知道了长子是谁,另⼀个当然就是次子了。另外⼀个问题场景,我们很关注各兄弟之间的关系,双亲表示法⽆法体现这样的关系,那我们怎么办?嗯,可以增加⼀个右兄弟域来体现兄弟关系,也就是说, 每⼀个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。
但如果结点的孩子很多,超过了2个。我们⼜关注结点的双亲、⼜关注结点的孩子、还关注结点的兄弟,⽽且对时间遍历要求还⽐较高,那么我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。存储结构的设计是⼀个⾮常灵活的过程。⼀个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否⽅便,时间复杂度好不好等。
孩子表示法
结点的定义:
结点包含数据域和孩子域
指针域用来指向当前结点的孩子
typedef struct LinkList {
int data;//存放数据
struct LinkList* next;
}Node;
全局变量:
Node* node_array[100];//存储结点的数组
int size;//数组中元素的个数
初始化并建立根结点:
传入待写入根结点数据域的关键词key
之后将size赋值为0
将第一个结点的位置先申请一块空间
再将key写入数据域
再将孩子域指向NULL
/*初始化 并且建立根节点*/
void Init(int key)
{
size = 0;
//将新的结点添加到数组当中
node_array[size] = (Node*)malloc(sizeof(Node));
//给新节点赋值
node_array[size]->data = key;
node_array[size]->next = NULL;
size++;
}
查找父结点:
传入关键字key
之后通过遍历查找
找到返回对应下标
没找到则返回-1
int find_parent(int parent)
{
for (int i = 0; i < size; i++)
{
if (node_array[i]->data == parent) {
return i;
}
}
return -1;
}
插入结点:
传入父结点的值parent和关键字key
之后再当前结构体size位置申请一块空间
之后给新结点赋值
顺便将其孩子结点指向空
之后是调用查找函数查找父结点的下标
先判断是否找到
如果找到则
创建一个结点
将数据域写入key,孩子域指向父亲域的孩子域(即头插法)
再让其父亲的孩子域指向当前结点
/*
int parent 父节点的值
int key 孩子结点的值
*/
void creat_tree(int parent, int key)
{
//先将孩子结点添加到数组当中
node_array[size] = (Node*)malloc(sizeof(Node));
//给新节点赋值
node_array[size]->data = key;
node_array[size]->next = NULL;
size++;
//找到父节点
int index = find_parent(parent);
if (index == -1)
{
}
else
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = key;
new_node->next = node_array[index]->next;
node_array[index]->next = new_node;
}
}
总代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct LinkList {
int data;//存放数据
struct LinkList* next;
}Node;
Node* node_array[100];//存储结点的数组
int size;//数组中元素的个数
void Init(int);//初始化操作
void creat_tree(int, int);//构建树
int find_parent(int);//找到父节点
int main()
{
Init(1);
creat_tree(1, 2);
creat_tree(1, 3);
creat_tree(1, 4);
creat_tree(2, 5);
creat_tree(2, 6);
creat_tree(3, 7);
for (int i = 0; i < size; i++)
{
printf("父节点为%d", node_array[i]->data);
Node* temp = node_array[i]->next;
while (temp != NULL)
{
printf("孩子结点为%d", temp->data);
temp = temp->next;
}
printf("\n");
}
}
/*初始化 并且建立根节点*/
void Init(int key)
{
size = 0;
//将新的结点添加到数组当中
node_array[size] = (Node*)malloc(sizeof(Node));
//给新节点赋值
node_array[size]->data = key;
node_array[size]->next = NULL;
size++;
}
/*
int parent 父节点的值
int key 孩子结点的值
*/
void creat_tree(int parent, int key)
{
//先将孩子结点添加到数组当中
node_array[size] = (Node*)malloc(sizeof(Node));
//给新节点赋值
node_array[size]->data = key;
node_array[size]->next = NULL;
size++;
//找到父节点
int index = find_parent(parent);
if (index == -1)
{
}
else
{
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = key;
new_node->next = node_array[index]->next;
node_array[index]->next = new_node;
}
}
int find_parent(int parent)
{
for (int i = 0; i < size; i++)
{
if (node_array[i]->data == parent) {
return i;
}
}
return -1;
}
孩子兄弟表示法
二叉链表的定义:
结点中包含数据域data,第一个孩子的指针域child,兄弟的指针域sibling
之后初始化一个指针,指向根结点的指针,同时也标记了整棵树
再初始化一个临时指针t
//二叉链表的结点结构
typedef struct ChildSibling{
int data;//数据
struct ChildSibling* child;// 第一个孩子指针域
struct ChildSibling* sibling;//兄弟指针域
}Node;
Node* root;//指向根节点的指针,同时也标记整棵树
Node* t;//临时指针
初始化建立根结点:
将root指针指向的结点申请一块空间
将key写入数据域
之后将孩子指针域指向空
兄弟指针域指向空
//初始化,建立根节点
void Init(int key)
{
root=(Node*)malloc(sizeof(Node));
root->data =key;
root->child =NULL;
root->sibling =NULL;
}
查找父结点:
传入指向树的指针r和父结点的数据parent
先判断r的属于域是否为parent
如果是则返回r
再判断孩子指针域是否为空
不为空则创建指针x,让他为以child为根结点的子树的指针
再次调用此查找方法(递归)
判断如果x不为空,且x的数据域为parent,则返回x
之后再判断r的兄弟指针域是是否为空
不为空则创建指针x,标记以r的兄弟sibling为根结点的子树
再次调用此查找方法(递归)
判断如果x不为空,且x的数据域为parent,则返回x
最后如果还是没找到则返回空
//在以r为根的树中,查找数据为parent的结点
//递归
Node* getNode(Node* r,int parent)
{
if(r->data ==parent)//1
{
return r;
}
if(r->child !=NULL)//2
{
Node* x=getNode(r->child,parent);//调1
if(x!=NULL&&x->data ==parent)
{
return x;
}
}
if(r->sibling !=NULL)//3
{
Node* x=getNode(r->sibling ,parent);//调2
if(x!=NULL&&x->data ==parent)
{
return x;
}
}
return NULL;
}
结点插入函数:
key是插入的数据,parent是key的父亲结点的数据
先将临时指针t接受查找函数getNode返回的地址
再判断地址是否为空
若不为空则创建指针p,指向申请的一块结点
并将结点的数据域写入key
之后判断t的孩子指针域是否为空(此时t为待插入结点的父亲结点)
不为空则说明key不是parent的第一个孩子
则将t的孩子指针赋给t(此时t为第一个孩子的结点)
之后让p结点的兄弟域指针指向t的兄弟域指针所指向的
再让t的兄弟域指针指向p
再将p的孩子域指针指向空(头插法实现)
如果为空则说明是第一个孩子
那么t的孩子域指针指向p
p的兄弟域指针和孩子域指针都指向空
//插入 :key是插入的数据,parent是key的父亲结点的数据
void insert(int key,int parent)
{
t=getNode(root,parent);
if(t!=NULL)
{
Node* p=(Node*)malloc(sizeof(Node));
p->data=key;
if(t->child !=NULL)//key 不是parent的第一个孩子
{
t=t->child ;
p->sibling =t->sibling;
t->sibling =p;
p->child =NULL;
}
else{//key 是parent的第一个孩
t->child =p;
p->child =NULL;
p->sibling=NULL;
}
}
else{
//
}
}
总代码:
#include<stdio.h>
#include<stdlib.h>
//二叉链表的结点结构
typedef struct ChildSibling{
int data;//数据
struct ChildSibling* child;// 第一个孩子指针域
struct ChildSibling* sibling;//兄弟指针域
}Node;
Node* root;//指向根节点的指针,同时也标记整棵树
Node* t;//临时指针
//初始化,建立根节点
void Init(int key)
{
root=(Node*)malloc(sizeof(Node));
root->data =key;
root->child =NULL;
root->sibling =NULL;
}
//在以r为根的树中,查找数据为parent的结点
//递归
Node* getNode(Node* r,int parent)
{
if(r->data ==parent)//1
{
return r;
}
if(r->child !=NULL)//2
{
Node* x=getNode(r->child,parent);//调1
if(x!=NULL&&x->data ==parent)
{
return x;
}
}
if(r->sibling !=NULL)//3
{
Node* x=getNode(r->sibling ,parent);//调2
if(x!=NULL&&x->data ==parent)
{
return x;
}
}
return NULL;
}
//插入 :key是插入的数据,parent是key的父亲结点的数据
void insert(int key,int parent)
{
t=getNode(root,parent);
if(t!=NULL)
{
Node* p=(Node*)malloc(sizeof(Node));
p->data=key;
if(t->child !=NULL)//key 不是parent的第一个孩子
{
t=t->child ;
p->sibling =t->sibling;
t->sibling =p;
p->child =NULL;
}
else{//key 是parent的第一个孩
t->child =p;
p->child =NULL;
p->sibling=NULL;
}
}
else{
//
}
}
//主函数自行写出
这种表示法,给查找某个结点的某个孩⼦带来了⽅便,只需要通过firstchild找到此结点的⻓⼦, 然后再通过⻓⼦结点的rightsib找到它的⼆弟,接着⼀直下去,直到找到具体的孩⼦。当然,如果想找某 个结点的双亲,这个表示法也是有做陷的,那怎么办呢?
对,如果真的有必要,完全可以再增加⼀个parent指针域来解决快速查找双亲的问题,这⾥就不 再细谈了。