【问题标题】:How can this linked list reversal with recursion work?这个带有递归的链表反转如何工作?
【发布时间】:2021-07-12 00:58:41
【问题描述】:

我正在研究一个反转给定链表的函数的递归实现:

static Node reverse(Node head) {
    if(head == null) {
        return head;
    }

    // last node or only one node
    if(head.next == null) {
        return head;
    }

    Node newHeadNode = reverse(head.next);

    // change references for middle chain
    head.next.next = head;
    head.next = null;

    // send back new head node in every recursion
    return newHeadNode;
}

public static void main(String[] args) throws IOException {
    LinkedList llist = new LinkedList();
    llist.insertNode(20);
    llist.insertNode(4);
    llist.insertNode(15);
    llist.insertNode(85);
      
    Node llistReversed = reverse(llist.head);
    printSinglyLinkedList(llistReversed, " ");
}

所以链表节点为:85 15 4 20

我以为在调用reverse 之后head.next 会指向值为15 的第二个节点?如果是这样,这段代码如何(递归地)反转这个链表?

【问题讨论】:

  • 您提供的代码没有定义reverse。请提供足够的代码来了解这是什么意思,并提供构建示例列表并进行调用的代码。
  • @trincot 抱歉,这是我找到的解决方案之一,提前致谢; geeksforgeeks.org/…
  • 我明白了。我已经编辑了您的问题,从该来源添加了更多代码。
  • @trincot 非常感谢。我认为我最大的问题是;在我看来 (head.next == null) { return head;}​​ 指向 15。但我理解你的评论说它指向 15,但由于它不为空,所以它进入列表的尾部,但我如果没有某种迭代,看不到这将如何发生。所以在我看来,要么递归调用一直迭代直到它达到空值,要么 head.next 不断地自行迭代直到它找到一个空值!这就是我卡住的地方。谢谢
  • “但我理解你的评论”:不确定你指的是上述哪个 cmets。或者这是对我的回答的评论?在这种情况下,您能否在我的答案下方发表评论?

标签: java c# recursion linked-list singly-linked-list


【解决方案1】:

要查看它是否正常工作,请从只有一个节点的列表开始。很明显,在这种情况下,节点将由以下if 语句按原样返回:

if (head.next == null) {
    return head;
}

这确实是你所期望的:一个节点的列表的反转不应该改变该列表的任何内容;头部应该引用相同的单个节点。

感应

我们现在可以继续分析 2 个节点、3 个节点、4 个节点、5 个节点等的算法。但我们可以在这里使用归纳证明:

假设我们有一个大小大于 1 的列表,如下所示:

 head
  ↓              
┌───────────┐    ┌───────────┐                        ┌───────────┐
│ value: 85 │    │ value: 15 │                        │ value: 20 │
│ next: ———————→ │ next: ———————→ ...more nodes...——→ │ next:null │
└───────────┘    └───────────┘                        └───────────┘ 

然后我们将执行:

Node newHeadNode = reverse(head.next);

head->next 是对第二个节点的引用,即值为 15 的节点。此引用被传递给 reverse 的递归调用。

现在reverse 的每次执行都有自己的执行上下文,在更深层次的上下文中,我们得到了head 变量的新实例。它使用作为参数传递的值进行初始化。所以在这种情况下 that head 指的是值为 15 的节点。

对于reverse 的递归执行上下文,这是列表的第一个 节点(因为它不知道85 的节点),它自己的head 变量引用它:

                  head
                   ↓              
                 ┌───────────┐                        ┌───────────┐
                 │ value: 15 │                        │ value: 20 │
                 │ next: ———————→ ...more nodes...——→ │ next:null │
                 └───────────┘                        └───────────┘ 

我们现在将假设此调用将正确地反转那个较短的列表,并将返回旧的尾节点,它已成为新的头部(值为 20):

                  head                                (returned)
                   ↓                                    ↓        
                 ┌───────────┐                        ┌───────────┐
                 │ value: 15 │                        │ value: 20 │
                 │ next:null │ ←——...more nodes... ←——————— :next │
                 └───────────┘                        └───────────┘ 

执行return,更深的执行上下文消失,返回的引用赋值给outer执行上下文中的newHeadNode,其中head仍然指的是值为85的节点:

 head                                                  newHeadNode
  ↓                                                     ↓
┌───────────┐    ┌───────────┐                        ┌───────────┐
│ value: 85 │    │ value: 15 │                        │ value: 20 │
│ next: ———————→ │ next:null │ ←——...more nodes... ←——————— :next │
└───────────┘    └───────────┘                        └───────────┘ 

注意我们如何假设

  • 该较短列表中的所有链接现在都有效地指向“另一条路”;
  • head 之后的节点已成为尾节点,其next 属性设置为null
  • 新的头是前一个尾节点,递归函数调用返回对它的引用。

然后执行:

head.next.next = head;

这反映在这里:

 head                                                  newHeadNode
  ↓                                                     ↓
┌───────────┐    ┌───────────┐                        ┌───────────┐
│ value: 85 │    │ value: 15 │                        │ value: 20 │
│ next: ———————→ │           │                        │           │
│           │ ←——————— :next │ ←——...more nodes... ←——————— :next │
└───────────┘    └───────────┘                        └───────────┘ 

还有:

head.next = null;

这是有道理的,因为如果列表已经反转,那么头部现在是尾部,并且在尾部节点之后应该没有其他节点:

 head                                                  newHeadNode
  ↓                                                     ↓
┌───────────┐    ┌───────────┐                        ┌───────────┐
│ value: 85 │    │ value: 15 │                        │ value: 20 │
│ next: null│ ←——————— :next │ ←——...more nodes... ←——————— :next │
└───────────┘    └───────────┘                        └───────────┘ 

最后:

return newHeadNode;

所以...我们看到,如果我们的假设是正确的,那么我们就正确地反转了列表。

当我们验证它适用于具有 1 个节点的列表时,我们现在还发现,如果它适用于大小为 ? 的列表,它也适用于大小为 ?+1 的列表,我们有证据表明它适用于任何列表。

head如何“移动”

您可以想象变量head 在每次更深层次的递归调用时将如何“行走”到下一个节点,直到它到达列表中的最后一个节点。然后基本情况开始,不再发生递归。当执行从递归中回溯时,head 似乎会以相反的方向回到它开始的位置。

尽管这样看可能会有所帮助,但这并不完全正确:reverse 的每个执行上下文都有自己的 head 版本,实际上永远不会移动。它是一个恒定的参考。然而,由于每个递归调用都将head->next 作为参数,存在于更深执行上下文中的新head 变量将使用该next 节点进行初始化。所以reverse 的每个单独的执行上下文都有一个head 变量,它引用了一个不同的 节点。并且每当reverse 的一次调用的执行结束时,执行将退回到先前的执行上下文,在该上下文中我们再次处理变量head,该变量没有移动并且仍然引用与递归之前的情况相同的节点打电话。

【讨论】:

  • 我在中间和最后添加了一些解释。请让我知道这是否阐明了它的工作原理。
【解决方案2】:

对于通常的单链表,是的,head.next 指向或引用包含 15 的节点。在递归方法的第一次调用中,即。在下一次调用中,它将是 4,然后是 20。

查看第一次调用(其中head.next 指向15)reverse(head.next) 在将15 4 20 反转为20 4 15 后返回新的head,即指向20 节点的指针或引用。

该方法的工作原理:递归调用后,15 节点指向 85 节点,而 85 节点则为空。请注意,我们在这些操作中仍然使用旧的head。这确保了 85 在 20 4 15 之后附加,因此完成了反向操作。最后,我们从递归调用中得到的新头,对 20 的引用,被返回。

两个建议:在纸上画出来。并在您的调试器中运行它。

【讨论】:

    【解决方案3】:

    这将有助于查看包括reverse 的方法签名的完整代码,但我假设head 实际上不是整个列表的头部,而是在递归调用反向时传递的一部分列表的头部

    因此,除了 null 检查,它用于在到达列表末尾时停止递归,因为递归方法所做的第一件事就是调用自己传递列表中的下一个节点,它有效地遍历所有到达列表末尾的方式然后随着堆栈展开而开始返回

    因此,该方法的每次迭代都会建立对 head 的引用,该引用是列表中的特定节点,并且尚未修改其下一个的下一个节点。通过使自己成为下一个之后的下一个,它将自己移动到列表的末尾,但可能更容易认为链接的方向是相反的:

    A>B>C>D
    

    如果我们一直到 D 然后返回一个(因为 D 的下一个为空)所以我们在 C 处,通过使 C 的下一个 (D) 的下一个等于 C,我们将 D 指向 C 服务扭转C和D之间的联系

    A>B>C><D
    

    C 仍然指向 D,但现在 D 又指向 C

    然后我们执行head.next = null,这会阻止 C 指向 D..

      null
        ᐱ
    A>B>C<D
    

    ..但这通常是无意义的操作,因为它即将被再次覆盖..

    ..当我们回到 B 并让 B 的 nextnext 指向 B 时,这是一种说法“让 C 的下一个指向 B”

    A>B><C<D
    

    我们刚刚将 C 指向 null 是没有实际意义的,因为我们只是将 C 指向 B,但它在一种情况下确实有用..

    .. 最终我们将到达 A,点 A 的下一个 (B) 回到 A,然后将 A 的下一个 (B) 设置为 null,我们确实需要这样做,因为没有任何东西需要处理。如果我们没有在 A 的 next 上设置 null,我们将永远无法到达列表的末尾,因为它会循环(B 指向 A 指向 B 指向 A 等)

    null 
     ᐱ
     A<B<C<D
    

    我们的列表是相反的

    最后请注意,在压缩到列表末尾之后,我们有效地将 D 作为 newHeadNode 一路返回并将其返回,以便它可以将其捕获为新头(否则我们将无法访问所有列表A除外)

    【讨论】:

    猜你喜欢
    • 2021-07-27
    • 2018-07-11
    • 2020-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多