找到K-th到单链表的最后一个元素

时间:2014-08-22 17:45:58

标签: java

我目前正在阅读“Crack the Coding Interview”一书(第5版)。其中存在一个特殊的问题,即实现一种算法来查找单个链表的最后一个元素的kth。这个问题讨论了各种方法。我对递归解决方案感到特别困惑。它如下(无耻地从书中复制):

public class IntWrapper {
    public int value = 0;
}

LinkedListNode nthToLastR2(LinkedListNode head, int k, IntWrapper i){
    if(head == null){
        return null;
    }
    LinkedListNode node = nthToLastR2(head.next, k, i);
    i.value = i.value + 1;
    if(i.value == k) {      // We've found the kth element
        return head;
    }
    return node;
}

我的问题是:当我们找到特定节点时,我们将返回LinkedListNode,即使我们找到该特定节点,我们也会返回LinkedListNode。所以没有办法告诉任何其他节点的第k个节点。任何人都可以告诉我,如果我的观察是否正确?如果是,我们如何才能使其正确(当然,除了在System.out.println之前执行return head因为那只是打印节点而不是返回它)?

3 个答案:

答案 0 :(得分:2)

这个递归函数的技巧是递归调用首先发生 - 这意味着当调用执行后的任何代码时,我们已经在最后一个节点。一旦我们结束,我们的函数将返回null,因为没有任何东西(头部现在是null)。我们的callstack的最终函数返回null,这意味着node = null。现在函数检查当前节点是否是第k个节点(使用累加器i)。

有两种可能的情况:

  1. 我们不在正确的节点:我们的函数只返回node,我们记得null。因此,对于callstack中的下一个更高的函数,除了i更大之外,事情看起来就好像它已经结束了。

  2. 我们位于正确的节点:我们返回head,而不是node(请记住node = null)。在这种情况下,我们会再次传播回调用堆栈,但这一次而不是node在每一层都是null,它是head,这是我们想要的节点。最终我们回到顶部,并返回正确的元素。

  3. 所以,简单地说,函数一直返回null直到找到元素,然后它继续返回元素。

    糟糕的mspaint图片:

    recursive case

答案 1 :(得分:2)

简短回答:您的观察结果不正确,示例按原样运行。对于扩展的第一个简单的回忆例子:

因子:n! = n*(n-1)*(n-2)...*1

递归函数:

int fact(int n) {
    if (n==1) {
       return 1;
    } else {
       return n*fact(n-1);
    }
}

所以基本上这个函数调用自己直到它到达n==1的点,然后在结果到达你调用的那个之后得到一个结果。所以基本上

fact(3) = 3*fact(2) = 3*2*fact(1) = 3*2*1

在您的示例中也是如此:

首先通过列表中的下一个元素

调用自己来运行列表直到结束
LinkedListNode node = nthToLastR2(head.next, k, i);

当它到达最后一个节点时,它开始返回东西

if(head == null){
    return null;
}

只有这样,在该呼叫之前的最后一个才能进入

i.value = i.value + 1;

并从最后一个节点开始计数。

然后通过检查

检查它是否具有最后一个kth节点
if(i.value == k) { 

如果它有它返回头部。

如果它还没有返回从之前返回的那个。例如k=4和七个节点

 head: 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> null 
                                           | returning
    i= 7    6    5    4   3    2    1      v
       3 <- 3 <- 3 <- 3 <-.-null<- null <- null

每个步骤都是对同一函数的独立评估,不知道是谁调用它并等待它调用的函数返回的东西。

答案 2 :(得分:0)

我不确定你使用的书是那么好。他们在没有getter / setter的情况下访问成员变量。你会在15年前看到Java,但现在是一个很大的禁忌。他们正处于第5版(2011年),但似乎他们懒得没有更新他们的例子,因为Java惯例正在发展。

他们的递归解决方案非常复杂。我在下面写了一个替代的递归解决方案。它保留最新访问节点的队列。该队列保持尽可能小。我正在使用Java 8的Optional,其中返回的空值表示列表太短。

   Optional<LinkedListNode> recur(
                              LinkedListNode currentNode, 
                              int nFromLast, 
                              Queue<LinkedListNode> latestNodes) {

        if (currentNode == null) { // went past the last node
            if (latestNodes.size() > nFromLast)
                return Optional.of(latestNodes.remove());
            else // the queue is too short
                return Optional.empty();
        } else {             
            if (latestNodes.size() == (nFromLast + 1))
                latestNodes.remove();
            latestNodes.add(currentNode);
            return recur(currentNode.getNext(), nFromLast, latestNodes);
        }
    }

最初使用空队列调用它:

recur(list, k, new ArrayDeque()) 

[警告:此段落处于高级水平。]

似乎我的解决方案通过保留队列来浪费内存,但是他们的解决方案将项目保留在堆栈中的内存中。我的解决方案是尾递归,而他们不是。不幸的是,由于JVM没有针对尾递归进行优化,因此暂时不是优势。 JVM还为我的方法保留堆栈的所有内容(包括队列的所有副本)。至少我认为我的解决方案更简单。

相关问题