我们要存储的树是:
双亲表示法存储在内存中的结构是:
此种方式找父节点好找,但是找子结点需要遍历整个数组。
删除某一个结点时:(思路)
1.该结点没有孩子,直接删除即可。
2.该结点有孩子,需要递归将孩子结点删除,然后再删除该结点。
删除时的小细节:
1.每删除一个元素时,将数组中的最后一个元素前移,节省存储空间,遍历数组时方便,遍历的元素少,但是存在问题。(F结点有子结点,F的子结点在未删除的情况,F结点进行了移动,导致F结点的下标发生了变化,继而导致 GHK与F结点失联。)
2.删除的时候,数组最后一个元素不再前移。而是在插入的时候,遍历数组,找到空闲位置然后进行插入。此种方式同样也不会造成数组存储空间的浪费。 而且不会出现孩子结点找不到父亲的情况。这种方式比前一种就是遍历数组找元素的时候,可能会多遍历几个元素。--> 但是从正确性的角度来说优于第一种。 此种方式n就不能代表数组中有效元素个数。
代码及结果截图:
#include<stdio.h>
#include<stdlib.h>
/*
树的存储: 双亲表示法(本质上用数组实现)
孩子表示法(数组+链表)
孩子兄弟表示法(用链表存储 左孩子右兄弟)
*/
#define ElemType char
#define null NULL
#define MaxSize 20
typedef struct PNode{
ElemType data; //数据元素
int parent; //双亲的位置
}PNode;
typedef struct PTree{
PNode nodes[MaxSize]; //树的双亲表示
int n; //结点数
}PTree;
void deleteRecursionNew(PTree *ptree,int index);
void deleteRecursion(PTree *ptree,int index);
//用双亲表示法初始化一棵树
PTree InitPTree(PTree ptree){
ElemType ch;
int j;
printf("请输出你要创建的树的结点个数:\n");
scanf("%d",&(ptree.n));
printf("请输入结点的值,以及该结点双亲位于数组的下标:\n");
for(int i = 0;i < ptree.n; i++){
getchar();
scanf("%c %d",&ch,&j);
ptree.nodes[i].data=ch;
ptree.nodes[i].parent=j;
}
return ptree;
}
//找父结点(O(n)) -->找到该元素的时间复杂度为O(n),真正找父节点的时间复杂度为O(1)
void findParent(PTree ptree){
ElemType ch;
printf("您要查找的是哪个元素的父结点\n");
getchar();
scanf("%c",&ch);
int findStatus = 0;
for(int i = 0 ; i < ptree.n ;i++){
if(ptree.nodes[i].data == ch){
if(ptree.nodes[i].parent == -1){
printf("这是树根元素,此元素没有父亲\n");
return;
}
//找到了
printf("该元素是%c,他的父元素是%c\n",ch,ptree.nodes[ptree.nodes[i].parent].data);
findStatus = 1;
break;
}
}
if(findStatus == 0 ){
printf("您要查找的元素不在这棵树中\n");
}
}
//找子节点(O(2n))--> 找到该元素的时间复杂度为O(n),真正找到子节点的时间复杂度也为O(n).
void findChild(PTree ptree){
char ch;
printf("您想要查找的哪个元素的子节点\n");
getchar();
scanf("%c",&ch);
int index = -1;
for(int i =0;i<ptree.n;i++){
if(ptree.nodes[i].data == ch){
index = i;
break;
}
}
if(index == -1){
printf("您要查找的元素不在这棵树中\n");
return;
}
for(int i = 0;i<ptree.n;i++){
if(ptree.nodes[i].parent == index){
printf("该元素是%c,该元素的直接子结点有%c\n",ch,ptree.nodes[i].data);
}
}
}
//添加一个结点 O(1)
void addPNode(PTree *ptree){
ElemType ch;
int j;
printf("您要添加的结点的值,以及该结点双亲位于数组的下标:\n");
getchar();
scanf("%c %d",&ch,&j);
if(ptree->n>=MaxSize){
printf("存储空间不足,插入元素失败");
return;
}
ptree->nodes[ptree->n].data = ch;
ptree->nodes[ptree->n].parent=j;
(ptree->n)++;
}
/*
此种删除方式:虽然删除结点后,把数组中最后的元素移到当前位置。节省存储空间。删除A,B,C结点结果正确,元素前移也正确。
但是删除R结点,按理说是删除所有结点,但是最后打印的数组中还剩下KGH 三个元素。
经分析原因如下:
1.删除A D E 元素时,GHK元素前移 顺序变为 G K H , 然后递归删除B结点, F元素前移。递归删除C元素。
发现A有个孩子是F,于是递归删除F结点,再删除F结点时,F的下标已经变了,此时GHK的parent是6,而不是2.
所以GKH元素未能进行递归删除。所以最后留下了GKH元素。
本质是由于F结点有子结点,F的子结点在未删除的情况,F结点进行了移动,导致F结点的下标发生了变化,继而导致
GHK与F结点失联。
*/
//双亲表示法时,删除某个元素及其子元素
void deletePNode(PTree *ptree){
printf("请输入您要删除的元素是:\n");
ElemType ch;
getchar();
scanf("%c",&ch);
int index = -1;
for(int i=0;i< ptree->n;i++){
if(ptree->nodes[i].data == ch){
index = i;
break;
}
}
//递归删除子元素
deleteRecursion(ptree,index);
printf("剩余的元素个数是%d\n",ptree->n);
//然后再删除此元素
if(ptree->n > 0 && index != ptree->n-1){
ptree -> nodes[index].data = ptree->nodes[(ptree->n)-1].data;
ptree ->nodes[index].parent = ptree->nodes[(ptree->n)-1].parent;
ptree->nodes[(ptree->n)-1].data = ' ';
ptree->nodes[(ptree->n)-1].parent= -2;
(ptree->n)--;
}else{
//删除的是最后一个元素 或者数组中已经没有元素可以挪动了
ptree->nodes[index].data=' ';
ptree->nodes[index].parent=-2;
}
}
//递归删除一个结点的所有子结点,index为要删除的结点在数组中的下标
void deleteRecursion(PTree *ptree,int index){
for(int i = 0 ; i < ptree->n ; i++){
//找到了要删除的元素
if(index == ptree->nodes[i].parent){
deleteRecursion(ptree,i);
//这里执行删除操作(将数组中的最后元素填充到此地,节省数组空间)
printf("递归删除的元素是%c\n",ptree->nodes[i].data);
//数组中有元素,且删除的不是最后一个元素
if(ptree->n > 0 && i != ptree->n-1){
ptree -> nodes[i].data = ptree->nodes[(ptree->n)-1].data;
ptree ->nodes[i].parent = ptree->nodes[(ptree->n)-1].parent;
ptree->nodes[(ptree->n)-1].data = ' ';
ptree->nodes[(ptree->n)-1].parent= -2;
(ptree->n)--;
//i--必须要有,因为有可能最后一个元素也符合条件, i-- 是保证新移动过来的这个元素也参与了比较。
i--;
}else{
//删除的是最后一个元素 或者数组中已经没有元素可以挪动了
ptree->nodes[i].data= ' ';
ptree->nodes[i].parent=-2;
//删除最后一个元素的时候,数组中元素的个数你也得减1啊
(ptree->n)--;
}
}
}
}
/*
第二种方案:
删除的时候,数组最后一个元素不再前移。而是在插入的时候,遍历数组,找到空闲位置然后进行插入。
此种方式同样也不会造成数组存储空间的浪费。 而且不会出现孩子结点找不到父亲的情况。
这种方式比前一种就是遍历数组找元素的时候,可能会多遍历几个元素。--> 但是从正确性的角度来说优于第一种。
此种方式n就不能代表数组中有效元素个数。
*/
void deletePNodeNew(PTree *ptree){
printf("请输入您要删除的元素是:\n");
ElemType ch;
getchar();
scanf("%c",&ch);
int index = -1;
for(int i=0;i< ptree->n;i++){
if(ptree->nodes[i].data == ch){
index = i;
break;
}
}
//递归删除子元素
deleteRecursionNew(ptree,index);
//删除完子元素之后,把该元素入数组
printf("最终删除的元素是%c\n",ptree->nodes[index].data);
ptree -> nodes[index].data = ' ';
ptree ->nodes[index].parent =-2;
}
void deleteRecursionNew(PTree *ptree,int index){
for(int i = 0 ; i < ptree->n ; i++){
//找到了要删除的元素
if(index == ptree->nodes[i].parent){
deleteRecursionNew(ptree,i);
printf("递归删除的元素是%c\n",ptree->nodes[i].data);
ptree -> nodes[i].data = ' ';
ptree ->nodes[i].parent =-2;
}
}
}
//配套的添加方法
void addPNodeNew(PTree *ptree){
ElemType ch;
int j;
printf("您要添加的结点的值,以及该结点双亲位于数组的下标:\n");
getchar();
scanf("%c %d",&ch,&j);
int flag = 0;
for(int i = 0;i< ptree->n;i++){
if(ptree->nodes[i].parent == -2){
flag = 1;
ptree->nodes[i].data = ch;
ptree->nodes[i].parent = j;
break;
}
}
//如果在数组有效元素的范围内,没有找到空闲空间,判断一下数组空间是否已满
if(flag == 0){
if(ptree->n>=MaxSize){
printf("数组空间已满,已经放不下元素了\n");
return;
}
ptree->nodes[ptree->n].data=ch;
ptree->nodes[ptree->n].parent=j;
ptree->n++;
return;
}
}
int main(int argc, char *argv[])
{
PTree ptree;
//初始化数组
for(int i = 0;i< MaxSize ;i++){
ptree.nodes[i].data=' ';
ptree.nodes[i].parent=-2;
}
//双亲表示法存储树
ptree = InitPTree(ptree);
//查找某个树结点的父亲结点
findParent(ptree);
//查找某个树结点的儿子结点
findChild(ptree);
//如果删除的这个结点有子结点,那我们应该递归的删除完子结点,然后再删除该结点。
//删除之前打印一遍数组
for(int i = 0;i<MaxSize ;i++){
printf("%c %d\n",ptree.nodes[i].data,ptree.nodes[i].parent);
}
deletePNodeNew(&ptree);
//删除之前打印一遍数组
for(int i = 0;i<MaxSize ;i++){
printf("%c %d\n",ptree.nodes[i].data,ptree.nodes[i].parent);
}
addPNodeNew(&ptree);
addPNodeNew(&ptree);
addPNodeNew(&ptree);
addPNodeNew(&ptree);
//添加完之后打印一遍数组
for(int i = 0;i<MaxSize ;i++){
printf("%c %d\n",ptree.nodes[i].data,ptree.nodes[i].parent);
}
return 0;
}