递归释放循环单向链表? 您询问了传入一个参数的问题删除head->next,而不是head与你的方法比较

问题描述

我很好奇什么基本情况可用于递归释放循环链表,将链表的头部作为唯一参数传递。我最初认为基本情况可能是 if (head->next == head) { return NULL; } 可能足以防止 head->next 指向自身,但情况似乎并非如此(字面和比喻上)。在这里递归调用后,最后一个节点 free(Head) 没有被释放。

typedef struct node
{
    int data;
    struct node *next;
} node;

    // temp stores the original head of the list
    node *recursive_destroyer(node *head,node *temp)
    {
        if (head->next == temp) 
            return NULL;
    
        recursive_destroyer(head->next,temp);
    
        free(head);
        head = NULL;
    
        return NULL;
    }

解决方法

我没有在另一个释放整个列表的函数中将 head 设置为等于 recursive_destroyer 函数。这是那个函数:

LinkedList *destroy_list(LinkedList *list)
{
    node *temp;

    if (list == NULL)
        return NULL;

    if (list->head == NULL)
        return NULL;

    temp = list->head;
    
    // was not setting list->head equal to this function.
    // causing the original head to never become NULL
    list->head = recursive_destroyer(list->head,temp);

    if (list->head != NULL)
        printf("Error,head not freed.. \n");

    free(list);

    return NULL;
}

也可以传递一个指向 list->head 的指针以避免将 list->head 设置为等于函数。

,

您询问了传入一个参数的问题

我想大多数人都跳过了你的第一句话,而是跳到了你的代码上。你在帖子里问:

我很好奇什么基本情况可用于递归释放循环链表,将链表的头部作为唯一参数传递。 ...

您继续解释您尝试过的方法:

我最初认为基本情况可能是 if (head->next == head) { return NULL; } 可能足以防止 head->next 指向自身,但情况似乎并非如此......

您提供了一个代码示例,但它传入了两个参数。

删除head->next,而不是head

此答案解决了您第一句话中的问题。接下来将与您的方法进行简短比较。

检查 head->next 是否指向 head 是一个很好的停止情况,但这意味着您的递归函数需要在每次迭代时删除和销毁 head->next,然后递归处理相同的列表。

如果 head->nexthead 相同,则销毁 head,您就完成了。

我认为从这个函数返回值没有任何意义,所以我删除了它。

void recursive_destroyer(node *head) {

    if (head == NULL) return;

    if (head->next == head) {
        destroy(head);
        return;
    }

    node *tmp = head->next;
    head->next = head->next->next;
    destroy(tmp);

    recursive_destroyer(head);
}

请注意,递归函数不再需要第二个参数。

与你的方法比较

您的示例代码中存在一些导致错误行为的问题。还有其他一些答案已经深入解决了这些问题。但是,我确实想指出,您应该尽可能选择尾递归

尾递归是同级调用的特例。兄弟调用是指一个函数调用另一个函数,然后立即返回。在下面的示例中,function_A() 正在对 function_B()

进行同级调用
void function_B () { puts(__func__); }

void function_A (bool flag) {
    if (flag) {
        function_B();
        return;
    }
    puts(__func__);
}

同级调用可以被编译器优化为重用当前函数的栈帧来进行同级调用。这是因为在兄弟返回后不需要调用者的当前函数状态。

尾递归调用可以用同样的方式优化。因此,优化后的尾递归调用与普通循环具有相同的内存占用。而实际上,如果优化器检测到兄弟调用是递归调用,则不是对自身执行函数调用,而是将尾递归转换为跳转到函数开头。大多数 C 编译器都可以执行这种优化。您可以自己手动执行此优化,并轻松将尾递归函数转换为循环。

如果您正在使用 C 编译器的优化功能,并且它支持尾递归优化,那么没有技术理由更喜欢循环而不是尾递归。如果您的软件团队发现阅读递归代码令人困惑,那么首选循环只是为了使代码更易于理解。

,

您的代码不起作用。它将保持单个分配不变。

考虑循环链表[1]。如果你调用 recursive_destroyer(head,head),它不会释放任何东西。正确的递归代码是

void destroy_helper(node* const current,node* const original) {
    if (current->next != original) destroy_helper(current->next,original);
    free(current);
}

void destroy(node* const list) {
    // null-check necessary since otherwise current->next is UB in destroy_helper
    if (list) destroy_helper(list,list);
}

如果我们想把它变成迭代代码,我们必须首先将destroy_helper函数修改为尾递归:

void destroy_helper(node* const current,node* const original) {
    node* const next = current->next;
    free(current);
    if (next != original) destroy_helper(next,original);
}

然后我们可以将其重写为循环:

void destroy(node* const list) {
    if (list) {
        node* current = list;
        do {
            node* next = current->next;
            free(current);
            current = next;
        } while (current != list);
    }
}

编辑:

为了证明我的代码实际上释放了所有内容,我们可以将 free 替换为以下函数:

void free_with_print(node* ptr) {
    printf("Freeing node with value %d\n",ptr->data);
    free(ptr);
}

一个简单的例子:

int main() {
    node* node1 = malloc(sizeof *node1);
    node1->data = 1;
    node1->next = node1;
    
    node* node2 = malloc(sizeof *node2);
    node2->data = 2;
    node2->next = node1;
    node1->next = node2;
    destroy(node1);
}

使用迭代版本打印

Freeing node with value 1
Freeing node with value 2

正如预期的那样。用你的原始代码打印尝试同样的事情

Freeing node with value 1

正如预期的那样,您的代码不会释放两个节点之一,而我的代码会释放两个节点。

,

对于这样的代码,您可以(并且应该)在头脑中进行“单步调试”,以说服自己它应该按预期工作。这是一项非常重要的学习技能。

让我们尝试 3 种情况:

a) 假设列表为空(headtemp 为 NULL)。在这种情况下,由于尝试在 if (head->next == temp) 中使用 NULL 指针,它会在 head->next 处崩溃。

b) 假设列表中有一项。在这种情况下,if (head->next == temp) 为真,因为它是一个循环链表,所以它从第一次调用返回而不释放任何东西。

c) 想象一下这个列表有 2 个项目。在这种情况下,if (head->next == temp) 对于第一次调用为假,对于第二次调用为真;所以第二次调用不会释放任何东西,第一次调用会释放列表的原始头部。

我们可以从中推断出,列表中的最后一项永远不会被释放(但如果它不是最后一项,则列表原始头部的第一项将被释放)。

要解决您始终可以释放该项目的问题,例如:

    if (head->next == temp) {
        free(head);
        return NULL;
    }

这很混乱,因为您正在复制代码(并且可以反转条件以避免这种情况)。如果 head 始终指向原始 head 并且 temp 是临时的,也会更容易阅读。此外(如评论中所述)完成后返回 NULL 没有意义。重构代码会给你类似的东西:

void recursive_destroyer(node *head,node *temp)
{
    if (head->next != temp) {
        recursive_destroyer(head,temp->next);
    }
    free(temp);
}

然而;如果列表最初为空,这仍然会崩溃。为了解决这个问题,我会做一个包装函数,比如:

void recursive_destroyer(node *head) {
    if(head != NULL) {
        recursive_destroyer_internal(head,head);
    }
}

static void recursive_destroyer_internal(node *head,node *temp)
{

最后一个问题是递归是不好的(由于所有额外的函数调用往往会变慢,并且当你用完堆栈空间时有崩溃的风险,并且通常最终更难以人们阅读);特别是如果/当编译器不能自己进行“尾调用优化”将其转换为非递归循环时。要解决您不应该使用递归的问题。例如:

void destroy(node *head) {
    node *original_head = head;
    node *temp;

    if(head != NULL) {
        do {
            temp = head;
            head = head->next;
            free(temp);
        } while(head != original_head);
    }
}