以下递归程序的最后一行是如何工作的

How the last line of following recursive program is working?

本文关键字:一行 工作 何工作 递归 程序 最后      更新时间:2023-10-16

我正在研究链表,然后我发现了一个递归反转链表的代码。这是C++代码。

void recursiveReverse(node*& head)
{
node* first;
node* rest;
/* checking for an empty list */
if (head == NULL)
return;   
first = head;  
rest  = first->next;
/* List has only one node */
if (rest == NULL)
return;   
recursiveReverse(rest);
first->next->next  = first;  
first->next  = NULL;          
/* fix the head pointer */
head = rest;              
}

我理解了除最后一行之外的整个代码。因为根据我的说法,在递归展开期间,其余指针的更新也与第一个指针类似,因此,在此代码结束时,头部不会指向最后一个节点。

这是我对以下代码的解释。

让我们以链表 1 -> 2 -> 3 为例。

  1. 最初,首先将存储头节点的地址,其余部分 将包含节点 2 的地址。
  2. 现在,由于 rest 不是 NULL,因此,将调用递归反向(rest)。
  3. 现在,第一个将指向节点
  4. 2,其余将指向节点 3。
  5. 同样,rest不是NULL,因此将调用递归反向(rest)。
  6. 现在,第一个将指向节点 3,其余将包含 NULL。
  7. 之后,递归将开始展开,首先将返回到节点 2,其余部分将返回到节点 3。
  8. 现在,语句 first -> next -> next = first;将导致节点 3 的下一部分指向节点 2,链表将变为1 -> 2 <- 3。节点 2 的下一部分将包含 NULL,并且由于 head = rest,因此,head 也将指向最后一个节点作为rest指针。
  9. 之后,first 将指向节点 1,
  10. 语句first -> next -> next = first;将导致节点 2 的下一部分指向节点 1,链表将变为 1 <-2 <- 3。节点 1 的下一部分将包含 NULL,语句head = rest将导致头部指向节点 2 而不是节点 3,因为其余部分(第一个 -> 下一个)当前位于节点 2。

任何人都可以解释一下我在解释此代码时的错误之处吗?

也许还有其他误解,但我假设基本的误解与您的解释有关"语句 head = rest 将导致头部指向节点 2 而不是节点 3,因为其余部分(首先是 -> 下一个)当前位于节点 2"。

最后,head将指向初始列表的最后一个节点。让我们考虑代码的简化/缩短部分:

rest  = head->next;
if (rest == NULL)  // end of list reached; head points tho the last node 
return;
recursiveReverse(rest);  // if the end is not reached, go forward with the next node (i.e. the value of head->next
head = rest;    // reset head to the (shared) value of rest. 

这是因为语句recursiveReverse(rest)会被一次又一次地调用,直到head->nextNULL,即到达列表的末尾。recursiveReverse的最后一次运行返回,因为head->next == NULL,在调用者中,变量rest指向最后一个节点。 现在请注意,"rest"在所有对recursiveReverse的调用中共享,因为它是通过引用传递的。因此,到目前为止调用的每个recursiveReverse实例都将调用语句head = rest,但是 - 由于rest在所有调用之间共享并且在递归调用后不会更改 - 语句head = rest将始终将head分配给相同的值rest,该值仍然是指向最后一个节点的值。

Puh - 希望这是全面的。

无论如何,使用通过引用传递的参数执行递归函数通常很难理解;通常,当递归函数管理其私有状态但使用返回值来协调结果时,事情会变得更容易。如果你把代码安排成你有node* reverseRecursive(node *current),你的代码会变得更容易理解。

它看起来像在步骤 8 中。 您忘记了void recursiveReverse(node*& head)中的head是参考。因此,当您递归调用recursiveReverse(rest);时,rest是通过引用传递的。这意味着,在递归内部,head是对调用函数中rest变量的引用。因此,当它被更改为指向递归内部的3时,调用函数中的rest也发生了变化。

如果这听起来令人困惑,那么在堆栈上绘制局部变量可能会有所帮助:

1) 最初,head将是指向传递给函数的节点的指针的引用,first将存储头节点的地址,rest将包含节点 2 的地址。

堆栈将如下所示(仅显示局部变量并忽略调用函数时已经存在的任何变量):

====================================
head:     reference to original head
first:    node 1
rest:     node 2

2)现在,既然休息不NULLrecursiveReverse(rest)就会被召唤。

3)现在,head将在调用函数中引用restfirst将指向节点2,rest将指向节点3。 堆栈将如下所示:

====================================
head:     reference to original head
first:    node 1
rest:     node 2      <---------------+
====================================  |
head:     reference to ---------------+
first:    node 2
rest:     node 3 

4)同样,休息不是NULL,因此recursiveReverse(rest)将被召唤。

5)现在,head将在最后一个调用函数中引用restfirst将指向节点3,rest将包含NULL。 堆栈将如下所示:

====================================
head:     reference to original head
first:    node 1
rest:     node 2      <---------------+
====================================  |
head:     reference to ---------------+
first:    node 2
rest:     node 3      <---------------+
====================================  |
head:     reference to ---------------+
first:    node 3
rest:     NULL 

6)由于restNULL我们只是返回,堆栈回到这个:

====================================
head:     reference to original head
first:    node 1
rest:     node 2      <---------------+
====================================  |
head:     reference to ---------------+
first:    node 2
rest:     node 3 

7)现在,语句first->next->next = first;将导致节点3的next部分指向节点2,链表将变得1 -> 2 <- 3。节点 2 的下一部分将包含NULL。由于head是对调用函数中rest的引用,因此head = rest将使head引用rest指向与本地rest相同的节点:

====================================
head:     reference to original head
first:    node 1
rest:     node 3      <---------------+ (head = rest; made *this* rest
====================================  |  be equal to the *local* rest)
head:     reference to ---------------+
first:    node 2
rest:     node 3

8)之后,我们返回,first将再次指向节点1,语句first->next->next = first;将导致节点2的下一部分指向节点1,链表将变得1 <- 2 <- 3。节点 1 的下一部分将包含NULL,语句head = rest将导致head引用的指针指向节点 3,因为rest在步骤 7 中更改为指向节点 3。

我们初学者应该互相帮助:)

在我看来,对于像我们这样的初学者来说,理解该功能是如何工作的并不容易。

因此,最好使用方案来弄清楚它的工作。

如果列表不包含节点或仅包含一个节点,则没有什么可以逆转的。

此代码片段对应于此结论。

/* checking for an empty list */
if (head == NULL)
return;   
first = head;  
rest  = first->next;
/* List has only one node */
if (rest == NULL)
return; 

现在让我们假设该列表正好包含两个节点。在这种情况下,它看起来像

--------    ----------    ----------------   
| head | -> |   A  |B| -> |   B  |nullptr|
--------    ----------    ----------------

此列表按以下方式分为以下部分

--------    ----------
| head | -> |   A  |B|
--------    ----------

--------    ----------
| first| -> |   A  |B|
--------    ----------
--------    ----------------   
| rest | -> |   B  |nullptr|
--------    ----------------

此代码片段对应于此结论

first = head;  
rest  = first->next;

现在函数递归地调用自己

recursiveReverse(rest);

事实上,这个新列表需要

--------    ----------------   
| rest | -> |   B  |nullptr|
--------    ----------------

由于这个新列表只包含一个节点,因此该函数只返回。

由于原始列表必须反转,因此head必须包含rest的值,即head必须指向节点"B"

此代码片段对应于此结论

head = rest;

但是在这种情况下,我们将得到

--------    ----------------   
| head | -> |   B  |nullptr|
--------    ----------------

但是此列表仅包含一个节点"B"。所以在执行此语句之前

head = rest;

我们需要将列表与节点"A"一起附加。

正如我们所看到的那样,指针first指向节点"A"

--------    ----------
| first| -> |   A  |B|
--------    ----------

那么我们可以执行以下操作

first->next->next  = first; 

这导致

--------     ----------    -----------   
| first | -> |   A  |B| -> |   B  | A|
--------     ----------    -----------

另一方面,我们有

--------    ----------    -----------   
| rest | -> |   B  |A| -> |   A  | B|
--------    ----------    -----------

然后在此声明之后

first->next  = NULL;

我们会得到

--------     ----------------  | -----------   
| first | -> |   A  |nullptr|  | |   B  | A|-----------
--------     ----------------  | -----------          |
^                V
--------                             |             -----------------   
| rest | ----------------------------              |   A  | nullptr|
--------                                           -----------------

现在确实是时候发表声明了

head = rest;

我们会得到

--------     ----------------  | -----------   
| first | -> |   A  |nullptr|  | |   B  | A|-----------
--------     ----------------  | -----------          |
^                V
--------                             |             -----------------   
| head | ----------------------------              |   A  | nullptr|
--------                                           -----------------

那就是列表颠倒了。

如果列表包含两个以上的节点,则在拆分原始列表后,指针first将指向原始列表的第一个节点,该节点必须是反向列表中的最后一个节点。反过来,第一个节点将指向下一个节点,该节点在反向列表中将是最后一个节点。

使用此代码片段

first->next->next  = first;  
first->next  = NULL;  

我们可以将其放在反向列表中的最后一个节点之后。我们需要做的就是将head设置为存储在指针rest中的值,因为指针rest是反向列表的头部,指针first指向的节点附加到该列表。

head = rest;

仅此而已。

这是一个演示程序

#include <iostream>
struct node
{
int value;
node *next;
};
void push( node * &head, int value )
{
head = new node { value, head };
}
std::ostream & out( node * const &head, std::ostream &os = std::cout )
{
for ( node *current = head; current != nullptr; current = current->next )
{
os << current->value << ' ';
}
return os;
}
void recursiveReverse( node * &head )
{
if ( head != nullptr && head->next != nullptr )
{
node *first = head;
node *rest  = head->next;

recursiveReverse( rest );

first->next->next  = first;  
first->next  = nullptr;          
head = rest;
}       
}
int main() 
{
node *head = nullptr;
const int N = 10;
for ( int value = 0; value < N; value++ )
{
push( head, value );
}
out( head ) << std::endl;
recursiveReverse( head );
out( head ) << std::endl;
return 0;
}

它的输出是

9 8 7 6 5 4 3 2 1 0 
0 1 2 3 4 5 6 7 8 9