【问题标题】: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) 时间内解决这个问题。请参阅下面的答案....谢谢大家的帮助....真的很棒,你们摇滚!!!....
  • 尼尔:检查我的递归实现

标签: c# singly-linked-list


【解决方案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)]:

    1. 将列表一分为二
    2. 递归到列表的后半部分
    3. 打印中点的值
    4. 递归到前半部分

    这是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,你可以利用 LINQ 的反向扩展方法:

    var backwards = singlyLinkedList.Reverse();
    

    您需要在代码文件的顶部添加一个using System.Linq; 指令才能使用 LINQ 的扩展方法。

    【讨论】:

    • ... 这正是 OP 所建议的,但想要一个更好的解决方案。仅仅因为您不自己分配额外的内存并不意味着它不会发生。
    • Reverse 是延迟加载的,在请求项目时执行。和OP不一样。
    【解决方案8】:

    创建堆栈并将所有元素推入堆栈的一种变体是使用递归(以及系统的内置堆栈),这可能不是生产代码的方式,但可以作为更好的(恕我直言)采访答案如下:

    1. 这表明你了解递归
    2. 代码更少,看起来更优雅
    3. 天真的面试官可能不会意识到有空间开销(如果是这种情况,您可能要考虑是否要在那里工作)。

    【讨论】:

      【解决方案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】:

          如果在显式堆栈程序中,我们只为每个节点的数据创建一个堆栈(而不是创建&lt;Node&gt; 类型的堆栈,我们创建&lt;T&gt; 类型的堆栈)不是更好吗?因为那时我们不需要存储节点的任何其他信息。

          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要求单链表。
              猜你喜欢
              • 2018-07-22
              • 1970-01-01
              • 1970-01-01
              • 2015-11-17
              • 2023-03-24
              • 2013-02-15
              • 1970-01-01
              • 2016-08-29
              • 1970-01-01
              相关资源
              最近更新 更多