用于反转链表的非迭代等价物

时间:2012-09-04 12:55:33

标签: c++

我正在阅读RobertSedwick撰写的算法书中的列表遍历。功能定义如下所示。提到有可能遍历和删除函数可以有迭代计数器部分,但traverseR不能有。我的问题为什么traverseR不能有迭代计数器部分?如果递归调用不是函数的结束,即在遍历中那么我们就不能迭代,我的理解是对的吗?

感谢您的时间和帮助。

void traverse(link h, void visit(link))
  { 
    if (h == 0) return;
    visit(h); 
    traverse(h->next, visit);
  }
void traverseR(link h, void visit(link))
  { 
    if (h == 0) return;
    traverseR(h->next, visit);
    visit(h); 
  }
void remove(link& x, Item v)
  { 
    while (x != 0 && x->item == v) 
      { link t = x; x = x->next; delete t; }
    if (x != 0) remove(x->next, v); 
  }

6 个答案:

答案 0 :(得分:5)

traverseR使用调用堆栈来存储指向列表中所有节点的指针,以便在调用堆栈展开时以相反的顺序访问它们。

为了在没有调用堆栈的情况下(即非递归地)执行此操作,您将需要一些其他类似堆栈的数据结构来存储这些指针。

其他函数只是在当前节点上工作并继续前进,在递归函数调用返回后不需要存储任何东西。这意味着尾部递归可以用循环替换(通过修改代码,或者,根据编译器,让它确定可能并自行进行转换)。

答案 1 :(得分:4)

假设列表是单链接的,则无法以反向顺序迭代访问它,因为没有从节点到前一节点的指针。

traverseR的递归实现基本上是它隐含地反转列表并以正向顺序访问它。

答案 2 :(得分:1)

您可以使用堆栈编写traverseR的迭代版本:在循环中从一个节点迭代到另一个节点,推送堆栈上的节点。当你到达列表的末尾然后,在另一个循环中,弹出并访问你访问过的节点。

但他的基本上就是递归版本的功能。

答案 3 :(得分:1)

可以以相反的顺序遍历单个链接列表,仅具有O(1)额外空间 - 即,没有先前访问过的节点的堆栈。然而,它有点棘手,而且根本不是线程安全的。

这样做的诀窍是从头到尾遍历列表,在你这样做时将其反转,然后将其移回到开头,在回来的路上再次反转它。

由于它是一个链表,因此将其反转到相当简单:当你到达一个节点时,保存其next指针的当前值,并用上一个节点的地址覆盖它。 list(有关更多详细信息,请参阅代码):

void traverseR(node *list, void (*visit)(node *)) { 
    node *prev = nullptr;
    node *curr = list;
    node *next;

    if (!curr)
        return;

    // Traverse forwards, reversing list in-place as we go.
    do {
        next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    } while (curr->next);

    // fix up so we have a fully reversed list
    curr->next = prev;
    prev = nullptr;

    // Traverse the reversed list, visiting each node and reversing again
    do { 
        visit(curr);
        next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    } while (curr->next);
}

几乎与链接列表有关的任何事情,我觉得有必要补充一点(至少是IMO)它们几乎总是被视为纯粹的智力练习。在实际代码中使用它们通常净损失。您通常最终会得到缓慢,易碎且难以理解的代码,并且通常会浪费相当多的内存(除非您存储在每个节点中的数据非常大,指针通常可以使用与数据一样多的空间本身)。

答案 4 :(得分:0)

  

我的问题为什么traverseR不能有迭代计数器部分?如果递归调用不是函数的结束,即在遍历中那么我们就不能迭代,我的理解是对的吗?

正确。函数traverseremove以对自己的调用结束。它们是尾递归函数。 traverseR对自身的调用不在函数的末尾; traverseR不是尾递归。

递归通常需要创建并稍后销毁堆栈帧。通过将递归更改为迭代,可以使用尾递归函数完全避免此开销。大多数编译器都能识别尾递归函数并将递归转换为迭代。

答案 5 :(得分:0)

根据迭代的含义,可以编写traverseR的迭代版本。如果您受限于通过列表进行单次遍历,则无法进行。但如果你可以牺牲很多处理时间就可以完成。它在经典速度与内存权衡中确实使用较少的内存。

void traverseRI(link h, void visit(link))
{
    if (h == 0) return;

    link last = 0;

    while (last != h)
    {
        link test = h;
        while (test->next != last)
        {
            test = test->next;
        }

        visit(test);
        last = test;
    }
}