问题描述
假设我们有一个树数据结构:
#ifndef TREE_H
#define TREE_H
#include <stdarg.h>
typedef struct tree {
char *tag;
struct tree **children;
int num_children;
} tree;
tree *leaf(char *tag);
tree *node(char *tag,int num_children,...);
#endif
以此,我们可以通过以下语法上令人愉悦的方式定义一棵树:
#include "tree.h"
int main() {
tree *test =
node("Tywin",2,node("Cersei",leaf("Joffrey"),leaf("Tommen")
),leaf("Tyrion")
);
return 0;
}
现在,我想到了实现此目标的两种可能方法。当我们定义节点和叶子时,我们可以复制所有数据,如下所示:
#include "tree.h"
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
tree *leaf(char *tag) {
tree *res = malloc(sizeof(*res));
size_t tag_length = strlen(tag) + 1;
res->tag = malloc(tag_length * sizeof((*res->tag)));
strcpy(res->tag,tag);
res->children = NULL;
res->num_children = 0;
return res;
}
tree *node(char *tag,...) {
tree *res = malloc(sizeof(*res));
size_t tag_length = strlen(tag) + 1;
res->tag = malloc(tag_length * sizeof((*res->tag)));
strcpy(res->tag,tag);
res->children = malloc(num_children * sizeof(*res->children));
va_list ap;
va_start(ap,num_children);
tree *current_child;
for (int i = 0; i < num_children; ++i) {
*(res->children + i) = malloc(sizeof(**(res->children + i)));
current_child = va_arg(ap,tree *);
memcpy(*(res->children + i),current_child,sizeof(*current_child));
}
va_end(ap);
res->num_children = num_children;
return res;
}
但是,与此相关的是,node
的中间调用(如创建Tyrion和Cersei的调用)会导致内存泄漏。另外,即使您可以某种方式清理泄漏,您分配的内存也可能比“应有”的更多。因此,除了复制指针之外,显然还有另一种选择:
#include "tree.h"
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
tree *leaf(char *tag) {
tree *res = malloc(sizeof(*res));
res->tag = tag;
res->children = NULL;
res->num_children = 0;
return res;
}
tree *node(char *tag,...) {
tree *res = malloc(sizeof(*res));
res->tag = tag;
res->children = malloc(num_children * sizeof(*res->children));
va_list ap;
va_start(ap,num_children);
for (int i = 0; i < num_children; ++i) {
*(res->children + i) = va_arg(ap,tree *);
}
va_end(ap);
res->num_children = num_children;
return res;
}
然后,您可以定义一个递归清除方法,该方法在完成后将调用您的顶级对象(例如,递归free
并设置为NULL
指针,因此如果是{{ 1}} d两次,就可以了。但是我对此的担心是,您希望用户以后不会更改基础数据,即不能保证此数据结构是不变的。
问题:
解决方法
- 您定义的内容可以称为“侵入式”数据结构,因为您将层次结构数据(
children
)和有效负载(tag
)存储在每个tree
实例中。几乎没有任何“规范”的方式来定义这种结构-定义通常取决于需求。一些教科书和开放库可能完全以此方式定义了发束,某些代码可能提供了自定义内存分配器,以使分配的节点在内存中保持彼此靠近。
考虑其他选项,最好将节点有效负载和实际层次结构数据分开。例如,如果您的树仅是(完整的)二叉树,则可以避免将节点的左/右兄弟索引存储在数组中,并且完全避免分配/指针。如果您需要不需太多节点插入/删除的k进制树,则可以查看LCRS tree representation来表示层次结构,并将与节点关联的所有数据(标签和其他内容)存储在单独的位置。
我的answer有一个关于看似相似的关于简单数据结构的问题。您可以看看它。
也就是说,您的表示形式(考虑到没有错误和正确的递归cleanup
例程)在C语言中是有效的树表示形式。
- 在C中,您可以选择在修改树时锁定所有内容或应用“写时复制”策略。这再次取决于要求。您是否必须搜索树,是否经常更新结构或仅重新计算/重新分配节点数据?您是仅使用CPU在本地处理此数据,还是需要与GPU / DSP共享树或通过网络传输树?这些问题可以帮助您选择所需的表示形式和同步原语。否则,就没有一种“标准的”方法可以一次性使所有内容都是线程安全的/不可变的,并且一直使用。我可能会提出的一个建议是,使用LCRS时,您每次需要修改树结构时,都可以以“功能方式”编程并从旧的“计算”新的层次结构数组。