【问题标题】:Why does it matter which pointer is changing the next element in a linked list?为什么哪个指针改变链表中的下一个元素很重要?
【发布时间】:2015-09-25 17:06:43
【问题描述】:

我一直在努力应对一些 HackerRank 挑战。这是一个反转链表。我想递归地尝试一下,我认为我做得很好。但是,由于一个微小的变化,我的代码无法正常工作:

void Reverse(Node *&head)
{
  if (head == NULL)
      return;

  Node *link = head->next;

  if (link == NULL)
      return;

   Reverse(link);
   link->next = head;
   head->next = NULL;
   head = link;
}

所以这行不通。我将问题追溯到递归调用reverse(link) 之后的第一行,如果我将该行更改为head->next->next = head;,程序将通过所有测试。如果我们认为link 等于head->next,我不明白link->nexthead->next->next 之间的区别。 为什么哪个指针指向头部很重要?

我的逻辑:

Given:
--------------------------------------
h     l
|     |
[1]->[2]->[3]
--------------------------------------


link->next = head;
should

 h    l
 |    |
[1]<-[2] [3]


just like

head->next->next = head;

 h   l
 |   |
[1]<-[2] [3]

because head->next-next == link->next

我的逻辑不正确吗?我有一种感觉,这与我递归调用一个通过引用传递的函数这一事实有关。但是当我追查到它时,我看不出这对这个问题有什么影响。

谢谢

【问题讨论】:

  • 但是对Reverse的递归调用改变了link的值(它是通过引用传递的)。在那之后,link 不再是 head-&gt;next

标签: c++ recursion linked-list singly-linked-list


【解决方案1】:

逻辑上有一点点错误。

如果是回溯 step-b-step 形式 NULL 它变得可见。

(假设 Head 是参考并且只关注您的递归算法。)

[H] = head
[x] = NULL

[H] = [1]

[1] -> [2] -> [3] -> [X] // here link == NULL
                      ^

Pop 0 : 函数返回,前一个函数栈继续

Now when it return the control is at

[1] -> [2] -> [3] -> [X] // The function returns to previous call
               ^

现在下一行是

link->next = head; 

i.e. [3]->next = head;
i.e.   [3] -> next = [H]
i.e.   [3] -> [1]

head->next = NULL;

i.e.   [3] -> [1] -> [X]   

head = link

i.e [H]=[3]

流行音乐 1

现在函数返回到这里

[2] -> [3] -> [1] -> [X]
^      [H]

重复相同的步骤

link->next = head; 

i.e. [2]->next = head;
i.e.   [2] -> next = [H]
i.e.   [2] -> [3]

head->next = NULL;

i.e.   [2] -> [3] -> [X]   // now here we lost access to one
                           // but since its still in recurrsion stack
                           // so no problem

head = link

i.e [H]=[2]

流行音乐 2

现在函数返回到这里

[X]->[1]->[X]
      ^
[2] -> [3] -> [X]
[H]

重复相同的步骤

link->next = head; 

i.e. [1]->next = head;
i.e.   [1] -> next = [H]
i.e.   [1] -> [2]

目前为止正确的是错误

head->next = NULL;


i.e.   [2] -> [X]   // now here we lost access to [3]
head = link

i.e [H]=[1]

我们这里得到的是什么链表

[1]->[2]->[x]     [x]->[3]->[X]
[H]
 ^

[3] 泄露,无法访问

Pop 3:最终弹出到原始函数调用

这里 linkhead 相同,即[1] 一切都崩溃了

link->next = head; 

i.e. [1]->next = head;
i.e.   [1] -> next = [H]
i.e.   [1] -> [1]

head->next = NULL;

i.e.   [1] -> [X]   // now here we lost access to [1]
head = link
[H]=[1]

所以最后我们丢失了链表

[1]->[X]   [X]->[2]->[X] [X]->[3]->[X]

【讨论】:

    【解决方案2】:

    问题是Reverse 不是一个普通的函数。它需要一个引用参数。

    通常,如果我们有一些变量,比如xy,它们拥有相同的值或对象,以及一个函数f,我们调用f(x)f(y) 都没有关系。

    但是如果f 不是真正的函数,而是实际上是一个类似函数的句法运算符,它可以访问xy 位置,它会覆盖它,那么它确实是否使用xy 可能很重要。这很重要,因为调用者继续依赖 xy 的值,它们在调用者的持续执行中扮演着不同的角色。

    在您的(原始)程序中,link 变量在 Reverse(link) 调用之前等于 head-&gt;next。但是这个调用修改了link,这样调用之后,关系就不再一样了。如果需要使用link的优先值,则必须使用head-&gt;next

    避免混淆的一种方法是对类似函数的抽象使用清晰的命名,这些抽象会对其参数做一些奇怪的事情,例如破坏它们的值。这适用于引用函数以及预处理器宏。例如,如果您看到increment(x),您会被大声警告x 可能被其后继者覆盖:increment 是一个引用函数,或者可能是一个预处理器宏。而如果您看到get_account_balance(a),您可能不会期望它会覆盖变量a。如果是这样,那可能是难以发现的错误的根源。

    其次,不言而喻,您应该避免不必要地使用引用参数。您的函数没有理由有一个破坏其参数的递归接口。

    如何在不使用引用参数的情况下编写反转代码是让函数返回反转列表。如果您仍然需要基于引用的 API,您可以为此提供额外的功能:

    node *rev_list_destructively(node *list)
    {
      if (list == NULL) {
        return NULL; // reversing empty list results in empty list
      } else if (list->next == NULL) {
        return list; // reversing one element list is that list itself
      } else {
        node *rest = list->next;
        node *rest_reversed = rev_list_destructively(rest);
    
        rest->next = list;
        list->next = NULL;
    
        return rest_reversed;
      }
    }
    
    void rev_list_in_place(node *&list)
    {
      list = rev_list_destructively(list);
    }
    

    研究递归是如何工作的。如果我们有一个至少包含两个元素的列表(list-&gt;next 不是 NULL),我们可以调用列表的其余部分 rest 并反转它,捕获返回值,这为我们提供了反转的其余部分。我们所要做的就是将头节点附加到它的尾部,我们就完成了。

    我们利用了原来的 rest 变量继续指向同一个节点这一事实,并且该节点现在位于列表的末尾:因此 rest 指针现在指示尾节点。

    尾节点是我们需要操作以扩展尾部的节点。我们通过rest-&gt;next = list 做到这一点。现在rest 指向倒数第二个节点,list 是尾节点。 list 当然,一直持续指向进入函数的列表的原始头节点。最后,我们必须使用list-&gt;next = NULL 以空值终止新的尾节点。当然,我们的返回值是我们从递归调用中捕获的反向剩余列表。

    例子:

    values on entry into recursive case:
      list:  (1 2 3 4)
      rest:  (2 3 4)
    
    after recursion:
      rest_reversed: (4 3 2)
      rest: (2) 
      list: (1 2)  (list->next still points to the (2) node).
    
    after rest->next = list:
      rest_reversed: (4 3 2 1 2 1 2 1 2 1 ...)   -- list is cyclic!!!
      rest: (2 1 2 1 2 1 2 1 2 1 ...)
      list: (1 2 1 2 1 ...)
    
    after list->next = NULL
      rest_reversed: (4 3 2 1) -- the cycle is broken!
      rest: (2 1)
      list: (1)
    
    we return rest_reversed: (4 3 2 1).
    

    【讨论】:

      【解决方案3】:

      是的 - 你的逻辑有点错误。改变 head 的值会破坏算法。这个想法是,在每次返回后,head 和 link 都会恢复它们的旧值。

      试试:

      void Reverse(Node* head) // Just pass a pointer
      {
        if (head == NULL)
            return;
      
        Node *link = head->next;
      
        if (link == NULL)
            return;
      
         Reverse(link);
         link->next = head;
         head->next = NULL;
         // head = link;  Don't change head
      }
      

      但这还是有一个问题——列表反转后如何设置新的头部?您需要捕获指向原始列表中最后一个元素的指针。

      像这样改变:

      void Reverse(Node* p, Node** newHeadAddr)
      {
          // Will only happen if list is empty
          if (p == NULL) return;
      
          Node *link = head->next;
      
          // If the end is reached
          if (link == NULL) 
          {
               *newHeadAddr = p;
               return;
          }
      
          Reverse(link, newHead);
      
          // link now points to last element in reversed list
          // p points to current element
          // so add p to the reversed linked list
          link->next = p;
          p->next = NULL;
      }
      

      并像这样使用它:

      Node* newHead = nullptr;
      Reverse(head, &newHead);
      head = newHead;
      

      关于逻辑:下面是递归调用进行时变量行为的示例,即每次调用时 p 和 link 指向的内容的描述。希望对您有所帮助。

      假设您有一个包含三个元素的列表。

      第一次通话时:

      First call:
      p is a pointer element[0] (i.e. head)
      link is a pointer to element[1]
      

      第二次通话时:

      Second call:
      p is a pointer to element[1]
      link is a pointer to element[2]
      ---------------------------------------
      First call:
      p is a pointer to element[0] (i.e. head)
      link is a pointer to element[1]
      

      在第三次通话时:

      Third call:
      p is a pointer to element[2]
      link is a null pointer
      ---------------------------------------
      Second call:
      p is a pointer to element[1]
      link is a pointer to element[2]
      ---------------------------------------
      First call:
      p is a pointer to element[0] (i.e. head)
      link is a pointer to element[1]
      

      由于链接为 nullptr,调用将返回并且您有:

      Second call:
      p is a pointer to element[1]
      link is a pointer to element[2]
      ---------------------------------------
      First call:
      p is a pointer to element[0] (i.e. head)
      link is a pointer to element[1]
      

      现在代码可以做

      link->next = p;     // element[2]->next = element[1]
      p->next = nullptr;  // element[1]->next = nullptr;
      

      原来有

      element[2]->element[1]->nullptr
      

      然后返回:

      First call:
      p is a pointer element[0]
      link is a pointer to element[1]
      

      现在代码可以做

      link->next = p;     // element[1]->next = element[0]
      p->next = nullptr;  // element[0]->next = nullptr;
      

      所以你有

      element[2]->element[1]->element[0]->nullptr
      

      你就完成了。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-06-01
        相关资源
        最近更新 更多