【问题标题】:single linked list interview question单链表面试题
【发布时间】:2010-11-17 07:35:32
【问题描述】:

假设我们有一个字符串:MALAYALAM,每个字符都是每个节点的数据部分,所以我们将有一个大小为 9 的列表。我们如何知道该列表是否为回文。

约束:

  • 我们不知道列表长度。
  • 不要对整个列表或一半列表使用临时内存(数组或堆栈或其他列表)。使用少量临时字符是可以接受的。
  • 列表修改是可以的,只要我们在操作结束时有原始列表。

我想到的解决方案很少,想与大家讨论。而且是单链表。

感谢和 Rgds, 〜卡尔文

【问题讨论】:

  • 是的,已编辑。我的解决方案:采用快速和慢速 ptrs,将列表的一半反转并与另一半进行比较。并反转反转的一半,使列表看起来像原始列表。我正在寻找更好的解决方案。
  • 我看到该解决方案的唯一问题是它仍然使用 O(logn) 存储。
  • @axel22,我不这么认为。原始列表的存储不被视为用于解决方案的存储。重新链接列表的一半不需要额外的存储空间。
  • @Dialeticus - 你说得对,约束说不使用 O(n) 存储,但没有提到 O(logn) 存储。不过,我同意你关于重新链接的观点,我认为这就是这个问题想要的答案。
  • @calvin,你的想法很可能就是他们想要的。干得好。

标签: linked-list


【解决方案1】:

我的解决方案:取快速和慢速指针,将列表的一半反转并与另一半进行比较。并反转反转的一半,使列表看起来像原始列表。我正在寻找更好的解决方案。

编辑:因为我找不到更好的解决方案。

【讨论】:

    【解决方案2】:

    以下应该在 O(n) 中完成

    1. 递归到列表末尾,保留头指针。
    2. 退出递归循环时,将当前与头部进行比较,并将头部移动到 head.next 直到您用完字符或发现不匹配。

    您可以通过计算列表的长度并在达到列表的一半后停止比较来改进此解决方案。

    如果字符串中的字符数大于允许的最大堆栈深度,这将不起作用。在这种情况下,更改列表将按如下方式工作...

    找出列表的长度。

    • 不断反转链接列表中的链接,直到到达中间...... 一旦你到达中间,
    • 您将有一半的列表指向开头,其余的将指向结尾。
    • 运行一个while循环直到结束并比较相应的字符并再次反转链接......

    【讨论】:

    • 约束是:不要为整个列表或一半列表使用临时内存(数组或堆栈或其他列表)。使用少量临时字符是可以接受的。如果我正确理解了第一个替代方案,那么您实际上可以使用堆栈分配的帧来存储列表。这仍然是一个 O(n) 空间解决方案。然而,倒转链接的第二种解决方案解决了这个问题。
    • 它从来没有说过你不能使用递归。我没有使用任何临时内存,但同时不应仅仅因为方法的帧是在堆栈中分配的,就停止递归。这样你就不能调用任何方法。我想在这样的问题中,人们不应该在代码中分配任何单独的内存。
    • 想象一下这个列表有 9000 个项目而不是 9 个或 900000 个。什么堆栈可以容纳它?
    • 它说你不能使用堆栈。从计算的角度来看,无论是使用编程模型提供的编程堆栈、自定义内存分配提供的基于堆的堆栈,还是在静态内存中分配的静态大小的堆栈,都算作使用计算机中的额外内存。我认为这就是问题的想法。
    • 我同意其他cmets。您不应使用超过恒定数量的堆栈空间,因为这是 OP 要求的。
    【解决方案3】:

    您可以使用随机化在 O(n) 时间和 O(1) 空间内完成。

    第 1 步:计算字符串的哈希值,例如整个字符串的指纹。

    第 2 步:反转链表。

    第 3 步:计算反转字符串的哈希值。

    第 4 步:将链表反转为其原始顺序。

    可以在 O(n) 时间和 O(1) 空间内完成链表的反转,如下所示:

    rev(head) {
      prev = nil;
      curr = head;
      next = head.next;
      while (curr != nil) {
        curr.next = prev;
        prev = curr;
        curr = next;
        next = curr.next;
      }
    }
    

    【讨论】:

    • 设计哈希是这里的关键挑战。我们可以通过计算哈希到前半部分和哈希到后半部分的反转并比较这两者来优化您的解决方案。而且我不知道如果列表长度为 5000 效率如何?
    • 您可以将字符串视为一个数字,并对大整数使用任何散列方案。例如,您可以使用 Rabin-Karp 指纹识别之类的方法来计算哈希,尽管它通常用于计算滑动窗口上的哈希,但它工作得非常好。由于它在 O(n) 时间内运行,因此它的运行速度将与任何其他算法一样快,并且在适度的常数因子内。
    【解决方案4】:

    遍历列表一次以找出它的长度

    然后你可以检查是否字符 i == 字符长度 - i for i=0 to length/2

    在 O(n^2) 时间内运行并使用 O(1) 存储

    【讨论】:

    • 更正:for (i = 0; i < length/2; i++) 就够了。由于 OP 只提到了空间限制,因此它是最好和最简单的解决方案。
    • 啊,是的,我的方法是每对检查两次,谢谢指正
    • 如何在 lengh-i 获得角色?
    • 好吧,在第一次通过列表获得“长度”之后,您从字符 0 开始并跟踪您当前所在的字符。假设你在字符 k;要获得字符 n > k,您需要遵循链表中的 n - k 个指针
    • 所以,如果我们想要在 (length-i) 处的字符,我们必须再次解析列表或存储一些位置。由于不允许存储,我们将解析多次(从头开始或从第 i 个位置),我正确吗?
    【解决方案5】:

    这是一个蛮力算法,希望你明白。

    <pseudocode>
    
    begin = list.begin();
    end = list.end();
    
    while (begin < end) {
       previous = iterator_that_points_to(list, end);
       if (*begin != *previous)
          return false;
    
       begin++;
       end = previous;
    }
    return true;
    
    </pseudocode>
    

    虽然 iterator_that_points_to 的代码(请忍受我的糟糕命名)是:

    Node* iterator_that_points_to(CharList const& theList, Node* to_find)
    {
       Node* iterator = theList.rootNode;
       while (iterator.next != 0) {
          if (iterator.next == to_find)
             return iterator;
    
          iterator = iterator->next; 
       }
       return 0; // should never happen
    }
    

    【讨论】:

      【解决方案6】:

      根据他们告诉您的要求,这是他们想要的解决方案。

      1) 找到列表的中点。您可以通过使用两个指针并增加两个节点和第二个节点增加一个节点来做到这一点。当第一个指针到达末尾时,您的第二个指针将位于列表的中间节点。

      2) 将链表从中间反转到末尾。您可以在线性时间内完成此操作。

      3) 现在比较两个列表。

      4) 通过再次反转来恢复先前反转的列表。

      最好的想法是编写一个函数来在线性时间内反转列表并将其用作 ispalindrome 函数的助手。这会清理代码并使其更易于在白板上进行管理。

      【讨论】:

      • 你确定这会持续增加空间复杂度吗?
      • 绝对是的。此解决方案不需要额外的存储空间,除了在反转列表时使用临时 NODE 指针。
      【解决方案7】:

      也许你可以做一个分而治之的解决方案:

      1. 遍历列表一次以确定其长度 - O(n)
      2. 再次浏览列表以将光标定位在一半上 - 你现在有光标 A 和 B - O(n)
      3. 比较A是否与B相反:

        • 如果剩余长度大于 一些常量,将 A 拆分为游标 通过遍历A1和A2,做同样的事情 对于 B1 和 B2 - O(n);比较 A1 和 B2,A2和B1同理

        • 如果剩余长度小于 一些恒定的,只是蛮力 比较它们 - 将 B 复制到数组中 并向后阅读比较它 与 A - O(1)

      请注意,步骤 3 应重复O(logn) 次,因此解决方案的复杂度为O(nlogn)

      更详细的复杂性分析:

      f(const) = const
      
      f(n) = f(n/2) + f(n/2) + C*n
      

      使用替换(n = 2^mf(2^m) = g(m))来求解这个方程。这个递归的解决方案应该产生与 n*logn 相同的复杂度类。

      由于递归调用,空间复杂度为O(logn),但这似乎没有违反任何约束。但是可以修改解决方案,使其不使用递归调用,而是使用循环 - 你应该只存储递归深度和该深度的位置(想象你绘制了递归树 - 你只需要 2 个整数来存储位置在那棵树上知道你的下一步是什么)。

      【讨论】:

        【解决方案8】:

        这个看起来很酷。

        假设你有一个链接列表 Node1->Node2->Node3->--->Noden。

        让 sum1=sum2=0

        你所要做的就是遍历列表一次并计算

                   sum1 = (sum1+Nodei->data)*2.
        
                   sum2 += Nodei->data*2^i.
        

        并比较它们是否相等

        如果相等

           palindrome 
        

        否则

           not a palindrome. 
        

        时间复杂度:O(n)

        空间复杂度:O(1)

        【讨论】:

          【解决方案9】:
          palindrome = true; //assume initially that it is a palindrome
          count = 1;
          q = p; // use a temporary pointer for traversing to end
          while (q->next) { q = q->next; count ++; } //count the number of elements in the list
          do{
             if (p->data != q->data) {palindrome = false; break;}//compare first and last elements of the list under consideration
             p = p->next; count -  = 2; //consider a new linked list with the two end-points cut off
             q = p; for (j = 0; j < count; j++) q = q=>next; 
          } while (count > 0);
          

          【讨论】: