在“任何”类型列表的末尾添加一个元素(Int,Double ...)

时间:2019-01-23 17:17:30

标签: scala list recursion

我正在尝试在“任何”类型列表(List [Any])的末尾添加一个元素。 我想使用递归函数来构建它,其思想是“如果需要此元素,则在迭代结束后,将其追加到列表中”。 在下面的代码中,想法是“如果元素'elem'在'l'的开头,则我有一个列表'l',我将其位置(保存在'index'中)添加为列表'ret的下一个元素'。否则,我将检查下一个元素并且什么都不做(我使用'l :: support ...'只是为了将返回类型与List [Any]匹配。)当'l'为空(或Nil)时给我清单“ ret”。 “ ret”的末尾是包含在列表“ l”中找到的所有元素“ elem”的位置的列表。 这非常重要:我正在构建的列表实际上是“ ret”,这是递归函数的返回!

我尝试使用“ ::”,“ +:”,“:+”,但是它们没有用。每次“错误:值::不是类型参数Any的成员”时,错误都是相同的。

object find{
    def support[Any](l:List[Any],elem:Any,index:Int,ret:List[Any]):List[Any]={
        if (l==Nil) ret;
        else if(l.head==elem) (l :: support(l.tail,elem,index+1,ret :: (index.asInstanceOf[Any])));
        else (l :: support(l.tail,elem,index+1,ret));       
    }
    def findIndices[Any](l:List[Any],x:Any):List[Any]={//I want to find all the elements "elem" in the list "l"
        val a:List[Any]=support(l,x,0,List[Any]());//I'll build my list using an auxiliary function "support"
        a;
    }
}
object main extends App{
    val l:List[Int]=List(1,2,2,2,2,3,4,5)
    val x:Int=2;
    val f:List[Int]=find.findIndices(l,x)
    println(f)
}

我对所有可行的解决方案都开放,但请首先尝试回答我的问题,并说明您的操作方式以及原因。 我正在学习这种语言,我来自C和Java。

3 个答案:

答案 0 :(得分:2)

TL; DR一线解决方案:

val list = List(1,2,2,2,2,3,4,5)
list.zipWithIndex.filter(_._1 == 2).map(_._2) //  List(1, 2, 3, 4)

或使用大小写符号命名变量:

list.zipWithIndex.filter { case(value,index) => value == 2 } map { case(value,index) => index }

或使用collect方法组合过滤器和地图:

list.zipWithIndex.collect { case (value,index) if value == 2 => index }

递归

如果您确实需要使用递归,则有一种简单的方法和一种困难的方法来执行此操作,并且看起来您正在尝试以困难的方式进行操作。在这种情况下,困难的方法很有意义,但是我将首先通过简单的方法来帮助您了解我在做什么以及为什么。

因此有一个列表

val list = List(1,2,2,2,2,3,4,5) // named list instead of l because l looks like 1.

我们想要一个函数findIndices,以使findIndices(list,2)返回List(1,2,3,4)

我将首先像这样定义findIndices

def findIndices(list: List[Any], element: Any): List[Any] = ???

现在有几件事我想立即更改。首先,您的示例使用一种类型的事物列表,因此,这似乎是使用参数化类型的绝佳机会。

def findIndices[T](list: List[T], element: T): List[Any] = ???

第二件事是,无论列表中的内容是什么,索引的结果列表都是整数,因为索引是整数。

def findIndices[T](list: List[T], element: T): List[Int] = ???

所以现在我准备开始使用该方法的主体。我知道这需要递归,而递归函数的一般形式是:

  1. 检查终止情况。
  2. 处理非终止案件。

所以我的函数看起来像这样:

def findIndices[T](list: List[T], element: T): List[Int] = {
  // if (terminating case) return termination
  // else if (check for next index of element) return index plus recursive call
  // else return recursive call
}

填充空白可以为我们提供以下内容:

def findIndices[T](list: List[T], element: T): List[Int] = {
  if (list.isEmpty) List.empty[Int]
  else if (list.head == element) 0 :: findIndices(list.tail, element)
  else findIndices(list.tail, element)
}

不幸的是,这段代码有一个错误。我们正在根据不断缩短的列表而不是原始列表计算索引。我们可以通过使用越来越短的列表版本来跟踪索引的偏移量来解决此问题:

def findIndices[T](list: List[T], element: T, offset: Int = 0): List[Int] = {
  if (list.isEmpty) List.empty[Int]
  else if (list.head == element) offset :: findIndices(list.tail, element, offset + 1)
  else findIndices(list.tail, element, offset + 1)
}

此方法按预期工作...仅适用于小列表。对于非常大的列表,我们将发生堆栈溢出。解决此问题的方法是使方法尾部递归,因此程序在每次调用时都无需跟踪堆栈。这就是您在问题中试图做的事情。我称其为困难方式,但是一旦有了非尾递归函数,将其转换为尾递归函数实际上是相当简单和机械的。

顶级功能参数保持不变:

def findIndices[T](list: List[T], element: T, offset: Int = 0): List[Int] = ???

在第一个函数中,我们定义了一个新函数。此新功能将具有与旧功能相同的参数,外加一个累加器。该累加器将使我们能够将算法的中间结果向下传递到下一个递归函数调用,因此程序不必维护调用堆栈即可跟踪每个中间结果。

外部函数唯一要做的就是用初始参数调用内部函数。

def findIndices[T](list: List[T], element: T, offset: Int = 0): List[Int] = {

  def findIndicesAcc(list: List[T], element: T, acc: List[Int], offset: Int = 0): List[Int] = {
    // do logic here
  }

  findIndicesAcc(list, element, List.empty[Int])
}

累加器功能内部的逻辑与原始功能非常相似。它将仅利用累加器参数,而不是将中间结果留在堆栈中。

def findIndices[T](list: List[T], element: T, offset: Int = 0): List[Int] = {

  def findIndicesAcc(list: List[T], element: T, acc: List[Int], offset: Int = 0): List[Int] = {
    if (list.isEmpty) acc
    else if (list.head == element) findIndicesAcc(list.tail, element, offset + 1, offset :: acc)
    else findIndicesAcc(list.tail, element, offset + 1, acc)
  }

  findIndicesAcc(list, element, List.empty[Int])
}

此功能可以正常工作,但是我们可以做一些最终的簿记和清理工作。首先,我们可以摆脱最外层的偏移参数,并删除内层偏移参数的默认值。其次,我们可以向内部函数添加一个@tailrec批注,以确保我们已经解决了堆栈溢出问题。第三,如果索引在返回列表中的顺序很重要,我们可以对累加器的输出调用反向。

import scala.annotation.tailrec

def findIndices[T](list: List[T], element: T): List[Int] = {

  @tailrec
  def findIndicesAcc(list: List[T], element: T, offset: Int, acc: List[Int]): List[Int] = {
    if (list.isEmpty) acc
    else if (list.head == element) findIndicesAcc(list.tail, element, offset + 1, offset :: acc)
    else findIndicesAcc(list.tail, element, offset + 1, acc)
  }

  findIndicesAcc(list, element, 0, List.empty[Int]).reverse
}

有关尾递归的更多信息

尾部递归是指递归函数,其中递归调用是该函数中最后发生的事情。有时,您必须仔细查看是否有尾递归。例如,代码片段

else if (list.head == element) offset :: findIndices(list.tail, element, offset + 1)

不是尾部递归的,因为在返回offset之前,必须将findIndices放在else if (list.head == element) { val tail = list.tail val newOffset = offset + 1 val recursiveResult = findIndices(tail, element, newOffset) return offset :: recursiveResult } 的结果之前。如果我们将代码片段的每个操作分成几行,则可以更加清楚:

findIndices

当这样分解代码时,我们可以看到在递归else if (list.head == element) findIndicesAcc(list.tail, element, offset + 1, offset :: acc)调用返回之后,还需要做更多的工作。

另一方面,代码片段

else if (list.head == element) { val tail = list.tail val newOffset = offset + 1 val combinedList = offset :: acc return findIndicesAcc(tail, element, newOffset, combinedList) }

是尾递归的。当我们将操作分为几行时,我们得到

findIndicesAcc

我们可以清楚地看到offset调用是最后发生的事情。

在第一种情况下,程序被迫维护整个调用堆栈,因为它需要记住函数的每个先前迭代中{{1}}的值。在linux机器上,我们通常要使用8 MB的堆栈。对于非常长的列表,我们最终会耗尽所有8 MB的堆栈,这会导致堆栈溢出异常。

在第二种情况下,所有相关数据都传递给递归函数的下一个迭代。在以前的函数调用中,程序不需要跟踪任何内容。编译器能够检测到这一点,并将代码优化为基本上一个循环。在这种情况下,无需维护调用堆栈,因此也没有堆栈溢出异常的风险。

最后一个警告,我尽可能仔细地检查了这段代码,但是在编写此代码时我没有使用Scala编译器的权限,因此我对任何错别字表示歉意。

答案 1 :(得分:1)

我认为您应该对Scala中的泛型有所了解。特别地,调用通用参数<leader>d使其掩盖标准类型Any的想法是一个非常糟糕的想法。它还可能有助于学习模式匹配,这是替换某些Any语句的强大工具。我相信您想要的代码如下所示:

if/else if/else

您可以尝试使用此代码online here

请注意,我做了一些改进,但是还有更多的改进可能。例如,通常将这种object find { @tailrec def support[A](l: List[A], elem: A, index: Int, ret: List[Int]): List[Int] = l match { case Nil => ret case head :: tail if head == elem => support(tail, elem, index + 1, ret :+ index) // _ meas here we ignore head anyway so don't need a variable for that case _ :: tail => support(tail, elem, index + 1, ret) } def findIndices[A](l: List[A], x: A): List[Int] = { //I want to find all the elements "elem" in the list "l" support(l, x, 0, List.empty[Int]) //I'll build my list using an auxiliary function "support" } } 方法放在support内,因此无法从外部使用。另外,findIndices:+上非常慢。通常,通过将结果列表追加到开头,然后在末尾List一次来构建结果列表会更快。

答案 2 :(得分:1)

完全手工制作的解决方案

def reverse[T](list: List[T]): List[T] = {
  @annotation.tailrec
  def loop(remaining: List[T], acc: List[T]): List[T] = remaining match {
    case Nil => acc
    case x :: xs => loop(remaining = xs, acc = x :: acc)
  }
  loop(remaining = list, acc = Nil)
}

def findIndices[T](elem: T, list: List[T]): List[Int] = {
  @annotation.tailrec
  def loop(remaining: List[T], acc: List[Int], currentIndex: Int): List[Int] = remaining match {
    case Nil => acc
    case x :: xs =>
      val newAcc = if (x == elem) currentIndex :: acc else acc
      loop(remaining = xs, newAcc, currentIndex + 1)
  }
  reverse(loop(remaining = list, acc = Nil, currentIndex = 0))
}

使用标准的高阶函数

def findIndicesStd[T](elem: T, list: List[T]): List[Int] =
  list.zipWithIndex.filter(_._1 == elem).map(_._2)

结果

findIndices(elem = 5, list = List(1, 5, 3, 5, 8, 5, 5, 10))
// res0: List[Int] = List(1, 3, 5, 6)

findIndicesStd(elem = 5, list = List(1, 5, 3, 5, 8, 5, 5, 10))
// res1: List[Int] = List(1, 3, 5, 6)

说明

您可以随意问尽可能多的问题,我将在本部分中添加答案。

但是,我会回答几个我相信您现在会得到的答案:

  • @annotation.tailrec是什么意思?: 在代码中,什么都没有。它是一个编译器批注,它告诉编译器检查给定的函数是否逐尾递归,如果不发出警告,则更像是一种最佳实践,以确保该函数不会炸毁堆栈

  • 为什么用T而不是Any T在这里表示泛型,就像类型占位符所说的那样,适用于“任何”类型。另一方面,Any是一种具体类型(一切的超级类型)。它们看起来很相似,但是泛型确保您不会丢失类型信息(如果您拥有一个Int列表,并且您reverse会返回一个Int列表,而不是Any列表)

  • 为什么要反转结果? 列表很适合添加前缀,但是“可怕”适合添加前缀-通常,将它们迭代两次以进行转换,而另一个迭代则总是更好

  • (除非您像List的内部实现那样使用突变,但是如果您不小心使用它,可能会破坏程序)。

  • _._1_._2是什么意思?: 第一个下划线用于匿名功能,它表示第一个(在本例中为下划线)参数_1_2.类上的方法(如您所见,因为它们被点Tuple调用),它们访问第一个和元组的第二个元素