使用递归从列表中的最后一个查找第n个元素

时间:2015-05-27 02:13:04

标签: c haskell recursion tail-recursion

我们可以使用静态变量解决这个问题,例如C,如下面的代码段(like the function found in this page)。

static int i = 0;
if(head == NULL)
   return;
getNthFromLast(head->next, n);
if(++i == n)
  {THIS IS THE NTH ELEM FROM LAST
   DO STUFF WITH IT.
  }
  1. 我试图看看我是否可以使用尾调用递归来解决这个问题, 和NO静态变量/全局变量。

  2. 我正在努力学习Haskell 想知道如何以纯粹的功能方式实现它,而不使用Haskell的length!!x !! ((length x) - K)

  3. 首先问一下,我们如何在C中做到这一点,使用递归和无静态/全局变量,只是为了得到一些想法。

    任何建议/指示将不胜感激。

    感谢。

5 个答案:

答案 0 :(得分:3)

链接页面介绍了如何使用双指解决方案解决问题;有点令人惊讶的是,他们不是简单地编写递归版本,这将简单明了。 (根据我的经验,当有一个简单明了的版本同样有效时,你不会通过提供棘手和模糊的代码来取得成功。但我认为有些访问者会重视晦涩的代码。)

所以,双指解决方案是基于这样的观察:如果我们有两个指针进入列表(两个手指),并且它们总是n元素分开,因为我们总是将它们串联起来,然后当前导手指到达列表末尾时,尾随手指将指向我们想要的元素。这是一个简单的尾递归:

Node* tandem_advance(Node* leading, Node* trailing) {
  return leading ? tandem_advance(leading->next, trailing->next)
                 : trailing;
}

对于初始情况,我们需要前导手指从列表的开头是N个元素。另一个简单的尾递归:

Node* advance_n(Node* head, int n) {
  return n ? advance_n(head->next, n - 1)
           : head;
}

然后我们只需将两者放在一起:

Node* nth_from_end(Node* head, int n) {
  return tandem_advance(advance_n(head, n + 1), head);
}

(我们最初前进n+1,以便结尾的第0个节点将是最后一个节点。我没有检查所需的语义;可能n可能是正确的。 )

答案 1 :(得分:2)

在Haskell中,双指解决方案似乎是显而易见的方式。如果请求的元素不存在,则此版本将以各种方式出错。我将把它作为练习来解决这个问题(提示:编写返回drop值的lastMaybe版本,然后将计算与{{1}一起串起来})。请注意,这会使列表的最后一个元素从结尾开始为第0个。

>>=

如果你想手动进行一些递归,No_signal表示会提供更好的性能,

nthFromLast :: Int -> [a] -> a
nthFromLast n xs = last $ zipWith const xs (drop n xs)

我们不打算用手书写-- The a and b types are different to make it clear -- which list we get the value from. lzc :: [a] -> [b] -> a lzc [] _ = error "Empty list" lzc (x : _xs) [] = x lzc (_x : xs) (_y : ys) = lzc xs ys nthFromLast n xs = lzc xs (drop n xs) ,因为图书馆中相当简单的版本是最好的。与本答案中的第一个解决方案或“反向,然后索引”方法不同,使用drop的实现只需要分配一定量的内存。

答案 2 :(得分:1)

我认为你的代码丢失了

getNthFromLast(list *node_ptr, int n) {

就在顶部。 (!!)

您的递归版本会在其调用堆栈帧中跟踪node_ptr,因此基本上非尾递归。此外,它继续展开堆栈(返回堆栈的调用帧),同时递增i并仍然检查其等同于n,甚至之后从最后发现了它的目标 n 节点;所以它有效。

这将是一个迭代版本,确实可以作为尾递归编码,在前进的道路上做事情,因此可以在达到目标后立即停止。为此,我们从一开始就打开 n - 间隙,而不是在达到结束之后。我们向前推进,而不是像你的递归版本那样向后计数。这是这里已经提到过的两点指导方法。

在伪代码中,

end = head; 
repeat n: (end = end->next); 
return tailrec(head,end)->payload;

其中

tailrec(p,q) { 
    if(NULL==q): return p; 
    else: tailrec(p->next, q->next);
}

这是从1开始的,假设为n <= length(head)。 Haskell代码已在另一个答案中给出。

此技术称为tail recursion modulo cons,或此处 modulo payload - 访问。

答案 3 :(得分:1)

library(dplyr)
df %>% 
    group_by(group) %>% 
    slice(seq(N))

因为Haskell很懒惰,所以这应该足够高效

如果你不想使用nthFromLast lst n = reverse lst !! n ,你必须自己重新定义它,但这很愚蠢。

答案 4 :(得分:0)

典型的迭代策略使用两个指针并在开始移动另一个之前运行一个指针到n - 1。 使用递归,我们可以通过添加第三个参数来使用堆栈从列表末尾开始计数。为了保持使用的清洁,我们可以创建一个静态辅助函数(在这个意义上它意味着只在编译单元中可见,与具有函数范围的静态变量完全不同的概念)。

static node *nth_last_helper(node* curr, unsigned int n, unsigned int *f) {
  node *t;
  if (!curr) {
    *f = 1;
    return NULL;
  }
  t = nth_last_helper(curr->next, n, f);
  if (n == (*f)++) return curr;
  return t;
}

node* nth_last(node* curr, unsigned int n) {
  unsigned int finder = 0;
  return nth_last_helper(curr, n, &finder);
}

或者我们实际上可以在没有额外参数的情况下进行计数,但我认为这不太清楚。

static node *nth_last_helper(node* curr, unsigned int *n) {
  node *t;
  if (!curr) return NULL;
  t = nth_last_helper(curr->next, n);
  if (t) return t;
  if (1 == (*n)--) return curr;
  return NULL;
}

node* nth_last(node* curr, unsigned int n) {
  return nth_last_helper(curr, &n);
}

请注意,我使用了无符号整数,以避免为&#34;负nth last&#34;选择语义。列表中的值。

然而,这些都不是尾递归的。为实现这一目标,您可以更直接地将迭代解决方案转换为递归解决方案,如rici's answer中所示。