问题描述
我很好奇什么基本情况可用于递归释放循环链表,将链表的头部作为唯一参数传递。我最初认为基本情况可能是 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->next
和 head
相同,则销毁 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) 假设列表为空(head
和 temp
为 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);
}
}