如何释放 DAG-like 数据结构

How to free DAG-like data structure

我有以下 DAG(有向无环图)数据结构:

struct dag {
    struct dag **children;  // Array of pointers to DAGs
    int n_children;         // Number of children
};

我希望这是一个 DAG 而不仅仅是一棵树,这意味着给定 DAG 的某些 children 可能是相同的。 这就在删除函数中引发了一个问题:

void del(struct dag *d)
{
    if (d != NULL) {
        for(int i = 0; i < d->n_children; i++) {
            del(d->children[i]);
        }
        free(d->children);
        free(d);
    }
}

事实上,我最终释放了已经释放的东西,如下例所示(这会引发分段错误)。此代码创建两个 DAG aba 没有 children,b 有两个 children,它们都是 a.

void main()
{
    struct dag *a, *b, **b_children;

    a = malloc(sizeof(*a));
    a->children = NULL;
    a->n_children = 0;

    b_children = malloc(sizeof(a) * 2);
    b_children[0] = a;
    b_children[1] = a;

    b = malloc(sizeof(*b));
    b->children = b_children;
    b->n_children = 2;

    del(b);
}

这种方法有什么缺陷?我可以编写一个 del 函数来实现我想要的功能吗?

对于有向无环图,节点可能被多个父节点引用,这需要额外的簿记来处理生命周期。

对此有多种方法:

  • 您可以在每个节点中添加一个引用计数,为引用该节点的每个额外指针递增它,并在指针不再引用该节点时递减它,特别是当父节点要引用该节点时被释放。引用计数很难正确可靠地实现,并且如果您的图形变得更加复杂,即:如果存在实际循环,则它不是一个完整的解决方案。

  • 您可以维护已分配节点的链表,并在释放树时释放完整列表。这种方法允许任意节点交叉引用,但不允许释放单个节点。您一次只能释放整棵树。您仍然可以从树中删除单个节点,但它们将保持分配在列表中,直到您释放整个树。另请注意,开销是等效的:一个额外的指针而不是:每个节点需要一个整数。

根据您要用这棵树解决的问题的具体情况,一种方法可能比另一种更合适。

在这两种情况下,您都应该使用函数来分配、更新和删除节点。

这是一个带有引用计数的版本:

#include <stdio.h>
#include <stdlib.h>

static int total_dag_nodes = 0;  // for debugging

struct dag {
    int refs;
    int n_children;         // Number of children
    struct dag **children;  // Array of pointers to DAGs
};

struct dag *new_dag(int n) {
    struct dag *s = malloc(sizeof *s);
    if (s == NULL)
        return NULL;
    total_dag_nodes++;
    s->refs = 1;
    s->n_children = 0;
    s->children = NULL;
    if (n > 0) {
        s->n_children = n;
        s->children = calloc(sizeof(*s->children), n);
    }
    return s;
}

void free_dag(struct dag *s) {
    if (s && --s->refs <= 0) {
        for (int i = 0; i < s->n_children; i++)
            free_dag(s->children[i]);
        free(s);
        total_dag_nodes--;
    }
}

int set_dag_child(struct dag *s, int n, struct dag *child) {
    if (s == NULL || n < 0 || n >= s->n_children) {
        /* invalid argument */
        return -1;
    }
    struct dag *prev = s->children[n];
    s->children[n] = child;
    if (child != NULL)
        child->refs++;
    /* delay freeing the previous node to avoid freeing it
       if it is identical to new one. */
    free_dag(prev);
    return 0;
}

void print_dag(const char *name, struct dag *s) {
    printf("%s: %p", name, (void*)s);
    if (s) {
        printf(" [refs:%d] {", s->refs);
        for (int i = 0; i < s->n_children; i++)
            printf(" %p", (void*)s->children[i]);
        printf(" }");
    }
    printf(";\n");
}

int main() {
    struct dag *a = new_dag(0);
    struct dag *b = new_dag(2);

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);
    print_dag("a", a);
    print_dag("b", b);

    set_dag_child(b, 0, a);
    set_dag_child(b, 1, a);

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);
    print_dag("a", a);
    print_dag("b", b);

    free_dag(a);    // removes the reference from a to the first dag

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);
    print_dag("b[0]", b->children[0]);
    print_dag("b", b);

    free_dag(b);    // should remove all references to both dags

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);

    return 0;
}

输出:

total_dag_nodes=2                                                                                           a: 0x7f885f600000 [refs:1] { };
b: 0x7f885f600010 [refs:1] { 0x0 0x0 };

total_dag_nodes=2                                                                                           a: 0x7f885f600000 [refs:3] { };
b: 0x7f885f600010 [refs:1] { 0x7f885f600000 0x7f885f600000 };

total_dag_nodes=2                                                                                           b[0]: 0x7f885f600000 [refs:2] { };
b: 0x7f885f600010 [refs:1] { 0x7f885f600000 0x7f885f600000 };

total_dag_nodes=0

这是使用全局列表的替代方法:

#include <stdio.h>
#include <stdlib.h>

static int total_dag_nodes = 0;  // for debugging
static struct dag *dag_list;

struct dag {
    struct dag *next;       // linked list for maintenance
    int n_children;         // Number of children
    struct dag **children;  // Array of pointers to DAGs
};

struct dag *new_dag(int n) {
    struct dag *s = malloc(sizeof *s);
    if (s == NULL)
        return NULL;
    total_dag_nodes++;
    s->next = dag_list;
    dag_list = s;
    s->n_children = 0;
    s->children = NULL;
    if (n > 0) {
        s->n_children = n;
        s->children = calloc(sizeof(*s->children), n);
    }
    return s;
}

void free_dag(struct dag *s) {
    /* nothing to do because tag could be referenced elsewhere */
}

void free_all_dags(void) {
    struct dag *p;
    while ((p = dag_list) != NULL) {
        dag_list = p->next;
        free(p->children);
        free(p);
        total_dag_nodes--;
    }
}

void print_dag(const char *name, struct dag *s) {
    printf("%s: %p", name, (void*)s);
    if (s) {
        printf(" {");
        for (int i = 0; i < s->n_children; i++)
            printf(" %p", (void*)s->children[i]);
        printf(" }");
    }
    printf(";\n");
}

int main() {
    struct dag *a = new_dag(0);
    struct dag *b = new_dag(2);

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);
    print_dag("a", a);
    print_dag("b", b);

    b->children[0] = a;
    b->children[1] = a;

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);
    print_dag("a", a);
    print_dag("b", b);

    free_dag(a);  // individual frees do nothing

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);
    print_dag("b[0]", b->children[0]);
    print_dag("b", b);

    free_all_dags();

    printf("\ntotal_dag_nodes=%d\n", total_dag_nodes);

    return 0;
}

输出:

total_dag_nodes=2                                                                                           a: 0x7f8d7b402650 { };
b: 0x7f8d7b402670 { 0x0 0x0 };

total_dag_nodes=2                                                                                           a: 0x7f8d7b402650 { };
b: 0x7f8d7b402670 { 0x7f8d7b402650 0x7f8d7b402650 };

total_dag_nodes=2                                                                                           b[0]: 0x7f8d7b402650 { };
b: 0x7f8d7b402670 { 0x7f8d7b402650 0x7f8d7b402650 };

total_dag_nodes=0

基本上,您需要实现垃圾回收算法。这有两个基本选择

  • 引用计数

    基本上,只需向每个节点添加一个引用计数,并在每次 add/remove 引用时 increment/decrement 它。当引用计数降为 0 时,释放节点。只要你没有循环(DAG 没有),这就很好用

  • mark/sweep

    您需要一种在访问节点时标记节点的方法(如果您可以可靠地取消标记所有节点,可能只需要一个位,或者一个 'id of last traversal' 值)。然后,为了释放您的 DAG,您访问所有标记它们的节点,如果它们之前没有标记(标记阶段),则将它们添加到 'to be freed' 列表中。然后,通过 'to be freed' 列表释放它们(扫描阶段)