我正在阅读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);
}
答案 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不能有迭代计数器部分?如果递归调用不是函数的结束,即在遍历中那么我们就不能迭代,我的理解是对的吗?
正确。函数traverse
和remove
以对自己的调用结束。它们是尾递归函数。 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;
}
}