【问题标题】:Reverse Traversing singly linked list in C++C ++中的反向遍历单链表
【发布时间】:2013-07-01 08:27:52
【问题描述】:

最近我在一次采访中被问到这个问题。我所能做的就是从一个从0到9开始的链表从9到1遍历。代码如下:

#include <iostream>

typedef struct node {                                                               
      int data;               // will store information
      node *next;             // the reference to the next node
};  
node *head;

int printList(node *traverse) {
    if (traverse->next == NULL) {
        return -1;
    }
    traverse=traverse->next;
    printList(traverse);

    cout << traverse->data << endl;
    return 0;
}

int main() {
    node *temp = NULL;
    node *begin = NULL;      
    for (int i = 0; i < 10; i++) {
        temp = new node;
        temp->data = i;
        if (begin == NULL) {
            begin = temp;
        }
        if (head != NULL) {
            head->next = temp;
        }
        head = temp;
        head->next = NULL;
    }
    head = begin;
    printList(head);
    return 0;
}

1) 如何使用 printList() 递归函数打印 0(第一个元素)?

2) 如何用 while 循环替换 printList() 递归函数?

3) 如果在采访中被问到,main() 函数是否有正确的节点初始化和插入?

【问题讨论】:

  • 我的错误。删除评论以防止混淆。

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


【解决方案1】:

它们是实现这一目标的四种可能方法,每种方法都有其优点。

递归

void print_list(node* traverse)
{
    if (traverse == NULL) return;
    print_list(traverse->next);
    std::cout << traverse->data << std::endl;
}

这可能是第一个想到的答案。但是,进程堆栈大小是有限的,因此递归的限制非常低。一个大列表会引发堆栈溢出。这在 C++ 中不是一个很好的设计。

迭代

void print_list(node *n)
{
    using namespace std;
    deque<node*> res;
    for(;n != NULL; n = n->next) res.push_back(n);
    for_each(res.rbegin(), res.rend(), [](node* n){cout << n->data << endl;});
}

当然,如果你想让它成为迭代方式,你将需要自己(在进程堆上)堆叠节点指针,而不是将这个作业委托给调用堆栈。此方法可让您打印更大的列表,并且计算时间为 O(n)。但是,它的内存使用量为 O(n),但您已经有一个使用 O(n) 内存的列表。所以这应该不是问题。但是,您可能确实需要避免内存消耗。这给我们带来了下一个想法。

双重迭代

void print_list(node *head)
{
    node* last = NULL;
    while(last != head)
    {
        node* current = head;
        while(current->next != last)
            current = current->next;
        std::cout << current->data << std::endl;
        last = current;
    }
}

这似乎是一个愚蠢的解决方案,因为它具有 O(n^2) 复杂度,但这是计算复杂度。它具有 O(1) 内存复杂度,并且根据实际上下文和确切问题,它可能是您需要的答案。但是这种 O(n^2) 复杂性需要付出很多代价。特别是如果 n 太大,您想避免另一个 O(n) 分配。这让我们想到了最后一个想法。

破解容器

void print_list(node *head)
{
    node* last = NULL;
    for(node* next; head != NULL; head = next)
    {
        next = head->next;
        head->next = last;
        last = head;
    }
    for(node* next; last != NULL; last = next)
    {
        next = last->next;
        last->next = head;
        head = last;
        cout << last->data << endl;
    }
}

您首先修改容器,然后以新顺序进行迭代。在单链表上,您可以只反转链接,然后在再次反转链接的同时进行反向迭代。它的美妙之处在于它在计算中保持 O(n),在内存中保持 O(1)。问题是您在执行此操作时使整个容器无效:您的输出操作不会使列表保持不变:这不是异常安全的:如果您的操作在迭代中间失败,则列表不再有效。这可能是也可能不是问题,具体取决于问题。

【讨论】:

    【解决方案2】:

    有一个用while循环反向遍历列表的老技巧。

    你沿着正向走循环,但是当你离开每个节点时,你会反转链接——即,你得到它的当前值(指向下一个节点的指针),然后将它设置为包含一个指针到 previous 节点。当您到达列表的末尾时,您现在有一个反向的单链表 - 即,跟随指针将带您回到列表的开头。

    然后你回到开头,打印出每个节点的值,并(再次)反转链接,这样当你完成后,列表就像它开始一样,链接从头到尾。

    但是请注意,这可能会导致(举一个明显的例子)多线程代码出现问题。从头到尾再回到开头的整个列表遍历必须被视为一个单一的原子操作——如果两个线程试图同时遍历列表,将会发生非常糟糕的事情。同样,使这个异常安全也可能具有一定的挑战性。

    IOW,这些很少能有效解决实际问题(但通常链表也是如此)。然而,它们是向面试官展示你可以玩愚蠢的链表技巧的有效方法。

    【讨论】:

    • 还有一个技巧,它使用 XOR 将下一个和上一个指针存储在一个指针中 (en.wikipedia.org/wiki/XOR_linked_list)。
    • @ChuckCottrill:我怀疑它是我原创的,但我在维基百科存在之前就写过that trick。 :-)
    • @ChuckCottrill:正如我所说,我很确定它不是我原创的(尽管它很丑,但如果它真的是我的,我可能会否认它的所有知识)。
    • 我曾经在一次采访中被问到这个问题,我的回答是你为什么要做这么脆弱的事情?
    【解决方案3】:

    如果你的列表是 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:

    1) 你想输出反转的列表,你的 printList 应该是这样的:

    int printList(node* traverse)
    {
        if (!traverse)
            return (-1);
        printList(traverse->next);
        std::cout << traverse->data << std::endl;
        return (0);
    }
    

    2) 使用 while 循环实际上是不可能的,除非您做一些非常丑陋的事情,例如将每个节点的数据连接到您将打印的结果字符串的开头。

    3) 你的主要对我来说似乎很奇怪。我不明白为什么你的“头”变量是全局的,为什么不是主要的?

    最后但同样重要的是,为什么不使用 std::list?

    【讨论】:

    • head 是全局的,因为我认为其他功能可能需要全局访问。这不是一个大问题。关于std::list,因为这是一个面试题,所以他们要求你实现而不是使用C++ STL
    • 嗯,那么这是一道C题。如果我在面试中问这个问题,我希望候选人的回答是“好吧,使用标准容器”,这是一个理智的反应。
    • @dreamstate 它可能在一个while循环中,只要你将所有节点指针保存在一个类似堆栈的容器中。这正是你的递归方法所做的,事实上,将节点指针堆积在 process 堆栈上......与进程堆相比,它的大小有限。
    • @lip 是的,这就是我所说的,你可以用丑陋的东西来做到这一点。是的,使用堆可以让你有更多的空间(我们真的需要它吗?),但是堆分配,意味着对于每个元素,像 sbrk(2) 这样的系统调用只会将执行时间乘以 100(我不知道,但是很多)。
    • @dreamstate 你应该尝试分析它。我什至无法在递归版本上获得 10% 的加速。为指针分配空间并没有那么昂贵。
    【解决方案4】:

    我在采访中被问到一个包含这个的问题。它旨在同时从两端遍历single list。因此,扭转单一列表不是一种选择。此外,内存空间复杂度应为O(1)。我尝试使用 O(n^2) 时间复杂度的嵌套循环来解决。但是,更有效的方法是使用@chuckcottrill 和@jerry-coffin 提到的XOR linked list。通过对节点的 prev 和 next 指针应用xor 操作,可以将单个列表转换为 XOR 链表。异或链表基于异或运算的以下性质。

    a^a^b = b(左边的顺序不重要)

    让我们考虑单个列表中的一个节点及其相邻节点:

    • X:上一个节点的地址,Y:下一个节点的地址
    • 转换为 XOR 列表时 Y' = X^Y (Y': Y 的新值)
    • 在 XOR 列表上反向遍历时 Y^(Y') =Y^Y^X=X

    所以我们可以到达前一个节点(X)并进行反向遍历。以下代码将单链表转换为异或链表并进行反向遍历(同时也可以进行正向遍历):

        node* curr = head;
        node* prev = NULL;
        node* next= curr->next;
    
        while (curr) {
            next = curr->next;
            // apply xor to prev and next   
            curr->next = (node*)((uintptr_t)(prev)^(uintptr_t)(next));
            // move pointers forward
            prev = curr;
            curr = next;
        }
        // prev becomes tail node 
        
        // -- Reverse Traversal --
        curr = prev ; // curr points to tail 
        prev = NULL;
        node* temp;
        
        while (curr) {
            cout << curr->data << " ";
            temp = curr;
            // apply xor to prev and next  
            curr = (node*)((uintptr_t)(prev)^(uintptr_t)(curr->next));
            prev = temp;
        }
    

    【讨论】:

      【解决方案5】:

      如果我错了,请纠正我。

      此解决方案使用迭代方法。一个额外的“previous”指针用于维护节点,该节点最终将以列表的相反顺序跟随节点。

      公共静态节点反向(节点头){

          Nodes temp;
      
          Nodes previous=null;
          while(head!=null)
          {
              temp=head.next;
              head.next=previous;
              previous=head;
              head=temp;
          }
          return previous;
      

      }

      【讨论】:

        【解决方案6】:

        1) 改变

        if (traverse->next == NULL)
        

        if (traverse == NULL)
        

        2)

        while(traverse != NULL) {
            // print sth
            traverse = traverse->next;
        }
        

        3) 对我来说似乎没问题。为什么要在 main 之外声明 head?

        【讨论】:

        • 1 ) codepad.org/tqDgsXUk 分段错误 2) codepad.org/YiSfIMEz 不是倒序
        • 1) 采用dreamstate 的更详细答案,基本相同 2) 正确。使用 while 循环,您将需要一个堆栈(后进先出),在遍历链表时填充该堆栈。然后你有另一个循环,而堆栈不为空并从堆栈中弹出(你基本上用递归调用做什么)。
        【解决方案7】:
        traverse->next = traverse;
        

        您可以使用的可能解决方案。另一种可能,

        traverse = (traverse->next)-    (traverse);
        

        但您必须对上溢/下溢进行错误检查。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-07-28
          • 2018-11-23
          相关资源
          最近更新 更多