如何向后读取单链表?
Posted
技术标签:
【中文标题】如何向后读取单链表?【英文标题】:How to read a singly linked list backwards? 【发布时间】:2010-11-10 03:28:00 【问题描述】:我能想到的一种方法是反转列表然后阅读它。 但这涉及更改不好的列表。 或者我可以复制列表然后反转它,但这会使用额外的 O(n) 内存。 有没有更好的方法不使用额外的内存,不修改列表并在 O(n) 时间内运行
反向链表代码在c#中是这样的
Void Reverse (Node head)
Node prev= null;
Node current = head;
Node nextNode = null;
while (current!=null)
nextNode = current.Next;
current.Next = prev;
prev=current;
current = nextNode;
head = prev;
递归解是
void ReadBackWard (Node n)
if (n==null)
return;
else
ReadBackward(n.Next);
Console.WriteLine(n.Data);
【问题讨论】:
递归是你的朋友 @Neil:你能推荐一些使用递归的伪代码 但是递归使用 O(n) 内存 只有在使用 O(n) 额外内存的情况下,我们才能在 O(n) 时间内解决这个问题。请参阅下面的答案....谢谢大家的帮助....真的很棒,你们摇滚!!!.... 尼尔:检查我的递归实现 【参考方案1】:要使用 O(n) 内存和 O(n) 性能,请创建堆栈;在向前迭代时推动所有内容,然后弹出所有内容,产生结果。
要使用 O(n^2) 性能(但 O(1) 额外内存),每次都向前读取,向上读取最后一个节点之前的节点。
例子:
IEnumerable<T> Reverse (Node head)
Stack<Node> nodes = new Stack<Node>();
while(head != null)
nodes.Push(head);
head = head.Next;
while(nodes.Count > 0)
yield return nodes.Pop().Value;
【讨论】:
这相当于创建列表的反向副本。 这是一个更好的解决方案,但它使用相同的 O(n) 内存,这与拥有一个列表副本并反转它并读取它是一样的 不一定。您只需要将指针压入堆栈,而不是整个项目。 这与递归基本相同。唯一的区别是显式堆栈与递归的隐式堆栈。 通过递归,您通常还需要推送表示调用位置的附加状态。使用显式堆栈通常更有效。【参考方案2】:单链表的特点之一是它实际上是单链表。这是一条单向的街道,除非你把它变成别的东西(比如反向单链表、堆栈、双向链表......),否则没有办法克服它。一个人必须忠实于事物的本质。
如前所述;如果您需要双向遍历列表;你需要有一个双向链表。这就是双向链表的本质,它是双向的。
【讨论】:
+1 叹息。为什么构建 SO 的人所倡导的简单性却被如此忽视?【参考方案3】:你真的应该使用双向链表。
如果这是不可能的,我认为您最好的选择是构建一个已反转的列表副本。
如果列表太长,其他选项,例如依赖递归(有效地将列表复制到堆栈)可能会导致您用完堆栈空间。
【讨论】:
见标签 - “采访问题” :) 我认为将列表更改为双向链表不如提出其他机制来解决问题。【参考方案4】:如果内存不足,您可以反转列表,遍历它并再次反转它。或者,您可以制作一组指向节点的指针(或任何类似于 C# 中的指针)。
【讨论】:
【参考方案5】:还有第三种解决方案,这次使用O(log(n))
内存和O(n log(n))
时间,因此在 Marc 的回答中占据了两种解决方案之间的中间地带。
它实际上是二叉树 [O(log(n))
] 的逆序下降,除了在每一步都需要找到树的顶部 [O(n)
]:
-
将列表一分为二
递归到列表的后半部分
打印中点的值
递归到前半部分
这是Python中的解决方案(我不懂C#):
def findMidpoint(head, tail):
pos, mid = head, head
while pos is not tail and pos.next is not tail:
pos, mid = pos.next.next, mid.next
return mid
def printReversed(head, tail=None):
if head is not tail:
mid = findMidpoint(head, tail)
printReversed(mid.next, tail)
print mid.value,
printReversed(head, mid)
这可以使用迭代而不是递归来重铸,但要以清晰为代价。
例如,对于一个百万条目的列表,三个解决方案的顺序为:
解决方案内存性能 ========================================== Marc #1 4MB 100 万次操作 矿山 80B 2000 万次操作 Marc #2 4B 1 万亿次操作【讨论】:
@chrispy:具有n
节点的树需要O(n)
内存,而不是您提到的O(log n)
。我是不是理解错了?
@eSKay 代码在遍历列表好像有一个关联的树,实际上并不是在内存中创建树
@Lazer:忽略“树”这个词,并从分而治之的角度思考:如果您跟踪中间点,则可以同样有效地处理列表的后半部分作为上半场。在处理列表的第一个第二个时,如果您跟踪 3/4 点,您可以像处理第三个季度一样快地处理四个季度。然后在处理前半部分时,保留 1/4 点,这样您就可以像处理第一部分一样高效地处理第二部分。
漂亮的解决方案!【参考方案6】:
void reverse_print(node *head)
node *newHead = NULL, *cur = head;
if(!head) return;
// Reverse the link list O(n) time O(1) space
while(cur)
head = head->next;
cur->next = newHead;
newHead = cur;
cur = head;
// Print the list O(n) time O(1) space
cur = newHead;
while(cur)
printf(" %d", cur->val);
cur = cur->next;
// Reverse the link list again O(n) time O(1) space
cur = newHead;
while(cur)
newHead = newHead->next;
cur->next = head;
head = cur;
cur = newHead;
// Total complexity O(n) time O(1) space
【讨论】:
最好的反向打印算法,节省时间和空间【参考方案7】:假设你的单链表实现了 IEnumerable
var backwards = singlyLinkedList.Reverse();
您需要在代码文件的顶部添加一个using System.Linq;
指令才能使用 LINQ 的扩展方法。
【讨论】:
... 这正是 OP 所建议的,但想要一个更好的解决方案。仅仅因为您不自己分配额外的内存并不意味着它不会发生。 Reverse 是延迟加载的,在请求项目时执行。和OP不一样。【参考方案8】:创建堆栈并将所有元素推入堆栈的一种变体是使用递归(以及系统的内置堆栈),这可能不是生产代码的方式,但可以作为更好的(恕我直言)采访答案如下:
-
这表明你了解递归
代码更少,看起来更优雅
天真的面试官可能不会意识到有空间开销(如果是这种情况,您可能要考虑是否要在那里工作)。
【讨论】:
【参考方案9】:好吧,天真的解决方案是跟踪您当前所在的节点,然后从头开始迭代,直到找到该节点,始终保存您刚刚离开的节点。然后每次找到当前所在的节点时,生成刚刚离开的节点,将该节点保存为当前所在的节点,然后从头开始重新迭代。
这在性能方面当然会非常糟糕。
我相信一些更聪明的人有更好的解决方案。
伪代码(甚至有错误):
current node = nothing
while current node is not first node
node = start
while node is not current node
previous node = node
node = next node
produce previous node
set current node to previous node
【讨论】:
【参考方案10】:这很乱但有效:
class SinglyLinkedList
SinglyLinkedList next;
int pos;
SinglyLinkedList(int pos)
this.pos = pos;
SinglyLinkedList previous(SinglyLinkedList startNode)
if (startNode == this) return null;
if (startNode.next == this) return startNode;
else return previous(startNode.next);
static int count = 0;
static SinglyLinkedList list;
static SinglyLinkedList head;
static SinglyLinkedList tail;
public static void main (String [] args)
init();
System.out.println("Head: " + head.pos);
System.out.println("Tail: " + tail.pos);
list = head;
System.out.print("List forwards: ");
while (list != null)
System.out.print(list.pos + ",");
list = list.next;
list = tail;
System.out.print("\nList backwards: ");
while (list.previous(head) != null)
System.out.print(list.pos + ",");
list = list.previous(head);
static void init()
list = new SinglyLinkedList(0);
head = list;
while (count < 100)
list.next = new SinglyLinkedList(++count);
list = list.next;
tail = list;
【讨论】:
【参考方案11】:如果在显式堆栈程序中,我们只为每个节点的数据创建一个堆栈(而不是创建<Node>
类型的堆栈,我们创建<T>
类型的堆栈)不是更好吗?因为那时我们不需要存储节点的任何其他信息。
IEnumerable<T> Reverse (Node<T> head)
Stack<T> nodes = new Stack<T>();
while(head != null)
nodes.Push(head.data);
head = head.Next;
while(nodes.Count > 0)
yield return nodes.Pop();
【讨论】:
【参考方案12】:你可以在 O(n^2) 中读取它——每次去最后一个节点读取并打印出前一个节点
【讨论】:
【参考方案13】:有什么问题:
public void printBackwards(LinkedList sl)
ListIterator<Element> litr = sl.listIterator(sl.size());
Element temp;
while(litr.previousIndex() >= 0)
temp = litr.previous();
System.out.println(temp);
O(n) 性能,O(1) 内存,简单如 do-re-mi!
【讨论】:
投了反对票; C#中的链表实现为双向链表,OP要求单链表。以上是关于如何向后读取单链表?的主要内容,如果未能解决你的问题,请参考以下文章