【问题标题】:help rewriting in functional style帮助重写功能风格
【发布时间】:2011-03-01 15:39:24
【问题描述】:

我正在学习 Scala 作为我的第一个函数式语言。作为问题之一,我试图找到一种生成序列 S 最多 n 个位置的功能方法。定义 S 使得 S(1) = 1,并且 S(x) = x 在序列中出现的次数。 (我不记得这叫什么了,但我以前在编程书籍中看到过。)

实际上,序列如下所示:

  S = 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7 ...

我可以很容易地在 Scala 中使用这样的命令式样式生成这个序列:

  def genSequence(numItems: Int) = {
    require(numItems > 0, "numItems must be >= 1")
    var list: List[Int] = List(1)
    var seq_no = 2
    var no     = 2
    var no_nos = 0
    var num_made = 1

    while(num_made < numItems) {
      if(no_nos < seq_no) {
        list = list :+ no
        no_nos += 1
        num_made += 1
      } else if(no % 2 == 0) {
        no += 1
        no_nos = 0
      } else {
        no += 1
        seq_no += 1
        no_nos = 0
      }
    }
    list
  }

但我真的不知道如何在不使用 vars 和 while 循环的情况下编写此代码。

谢谢!

【问题讨论】:

  • 我只读了一点关于 Scala 的文章,但是这样的内容应该可以帮助您入门:Stream.continually(x).take(S(x)).toList。使用 this 作为其定义的一部分编写一个递归函数 - 我只是不太了解 Scala,无法告诉你它应该是什么。
  • 你确定这个序列是唯一的吗?我认为1, 0, 0, 0, ... 也符合您的条件。
  • 似乎是哥伦布的序列:oeis.org/A001462

标签: scala functional-programming


【解决方案1】:

到目前为止,Pavel 的答案最接近,但效率也很低。两个flatMaps 和一个zipWithIndex 在这里太过分了:)

我对所需输出的理解:

  • 结果至少包含一次所有正整数(从 1 开始)
  • 每个数字n 出现在输出(n/2) + 1 次中

正如 Pavel 正确指出的那样,解决方案是从 Stream 开始,然后使用 flatMap

Stream from 1

这会生成一个Stream,这是一个可能永无止境的序列,只会按需生成值。在这种情况下,它会一直生成 1, 2, 3, 4... 直至 Infinity(理论上)或 Integer.MAX_VALUE(实际上)

流可以被映射,就像任何其他集合一样。例如:(Stream from 1) map { 2 * _ } 生成偶数流。

您还可以在 Streams 上使用flatMap,允许您将每个输入元素映射到零个或多个输出元素;这是解决您的问题的关键:

val strm = (Stream from 1) flatMap { n => Stream.fill(n/2 + 1)(n) }

那么...这是如何工作的?对于元素3,lambda { n =&gt; Stream.fill(n/2 + 1)(n) } 将产生输出流3,3。对于前 5 个整数,您将获得:

1 -> 1
2 -> 2, 2
3 -> 3, 3
4 -> 4, 4, 4
5 -> 5, 5, 5
etc.

因为我们使用的是flatMap,所以这些将被连接起来,产生:

1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, ...

流被记忆,因此一旦计算出给定的值,它将被保存以供将来参考。但是,所有前面的值都必须至少计算一次。如果您想要完整的序列,那么这不会导致任何问题,但这确实意味着从冷启动生成S(10796) 会很慢! (与您的命令式算法共享的问题)。如果您需要这样做,那么到目前为止,没有一个解决方案可能适合您。

【讨论】:

    【解决方案2】:

    以下代码产生的序列与您的完全相同:

    val seq = Stream.from(1)
            .flatMap(Stream.fill(2)(_))
            .zipWithIndex
            .flatMap(p => Stream.fill(p._1)(p._2))
            .tail
    

    但是,如果您想生成Golomb sequence(符合定义,但与您的示例代码结果不同),您可以使用以下内容:

    val seq = 1 #:: a(2)
    def a(n: Int): Stream[Int] = (1 + seq(n - seq(seq(n - 2) - 1) - 1)) #:: a(n + 1)
    

    您可以查看my article 以获取更多关于如何以函数式样式处理数字序列的示例。

    【讨论】:

    • 正如 Kevin 正确指出的那样,在重构的 sn-p 中,两个 flatMap 和一个 zipWithIndex 是多余的。此外, Stream 是一种过度杀伤力,因为计算不引用先前计算的值。所以“def it = Iterator.from(1).flatMap(n => Iterator.fill(n/2 + 1)(n))”是更高效的重构。然而,重构不足以产生正确的哥伦布序列。第二个代码 sn-p 使用带有递归关系的 Stream 来实现目标。
    • 如果有记忆,你仍然需要在重构版本中使用Stream,因为Iterator 不提供flatMap 操作。不过在这一点上我可能是错的(在我的手机上检查并不容易,scaladoc 对触摸屏不友好)
    • @Kevin Iterator 确实有 flatMap 方法。
    【解决方案3】:

    以下是将您的代码翻译成更实用的样式:

    def genSequence(numItems: Int): List[Int] = {
      genSequenceR(numItems, 2, 2, 0, 1, List[Int](1))
    }
    
    
    def genSequenceR(numItems: Int, seq_no: Int, no:Int, no_nos: Int, numMade: Int, list: List[Int]): List[Int] = {
     if(numMade < numItems){
       if(no_nos < seq_no){
         genSequenceR(numItems, seq_no, no, no_nos + 1, numMade + 1, list :+ no)
       }else if(no % 2 == 0){
         genSequenceR(numItems, seq_no, no + 1, 0, numMade, list)
       }else{
         genSequenceR(numItems, seq_no + 1, no + 1, 0, numMade, list)
       }
      }else{
        list
      }
    }
    

    genSequenceR 是递归函数,它在列表中累积值并根据条件调用具有新值的函数。与 while 循环一样,它在 numMade 小于 numItems 时终止并将列表返回给 genSequence。

    这是您的代码的一个相当基本的功能翻译。它可以改进,并且通常使用更好的方法。我建议尝试通过模式匹配来改进它,然后在此处使用其他使用 Stream 的解决方案。

    【讨论】:

      【解决方案4】:

      这是来自 Scala tyro 的尝试。请记住,我不太了解 Scala,我不太了解问题,也不太了解您的算法。

      def genX_Ys[A](howMany : Int, ofWhat : A) : List[A] = howMany match {
          case 1 => List(ofWhat)
          case _ => ofWhat :: genX_Ys(howMany - 1, ofWhat)
      }
      
      def makeAtLeast(startingWith : List[Int], nextUp : Int, howMany : Int, minimumLength : Int) : List[Int] = {
          if (startingWith.size >= minimumLength) 
            startingWith 
          else 
            makeAtLeast(startingWith ++ genX_Ys( howMany, nextUp), 
                       nextUp +1, howMany + (if (nextUp % 2 == 1) 1 else 0), minimumLength)
      }
      
      def genSequence(numItems: Int) =  makeAtLeast(List(1), 2, 2, numItems).slice(0, numItems)
      

      这似乎可行,但请重新阅读上面的警告。特别是,我确定有一个库函数可以执行genX_Ys,但我找不到它。

      编辑可能是

      def genX_Ys[A](howMany : Int, ofWhat : A) : Seq[A]  = 
         (1 to howMany) map { x => ofWhat }
      

      【讨论】:

      • 你要找的库函数是List.fill
      【解决方案5】:

      这里是对 Golomb 序列定义的一个非常直接的“翻译”:

      val it = Iterator.iterate((1,1,Map(1->1,2->2))){ case (n,i,m) =>
          val c = m(n)
          if (c == 1) (n+1, i+1, m + (i -> n) - n)
          else (n, i+1, m + (i -> n) + (n -> (c-1)))
      }.map(_._1)
      
      println(it.take(10).toList)
      

      三元组 (n,i,m) 包含实际数字 n、索引 i 和映射 m,其中包含 n 必须重复的频率。当我们的 n 在地图中的计数器达到 1 时,我们增加 n(并且可以从地图中删除 n,因为不再需要它),否则我们只需减少 n 在地图中的计数器并保留 n。在每种情况下,我们都会将新的对 i -> n 添加到映射中,稍后将用作计数器(当后续 n 达到当前 i 的值时)。

      [编辑]

      想了想,我意识到我不需要索引,甚至不需要查找(因为“计数器”已经处于“正确”的顺序),这意味着我可以将 Map 替换为 Queue:

      import collection.immutable.Queue
      
      val it = 1 #:: Iterator.iterate((2, 2, Queue[Int]())){
        case (n,1,q) => (n+1, q.head, q.tail + (n+1))
        case (n,c,q) => (n,c-1,q + n)
      }.map(_._1).toStream
      

      迭代器在从 2 开始时可以正常工作,所以我必须在开头添加一个 1。第二个元组参数现在是当前 n 的计数器(取自队列)。当前计数器也可以保存在队列中,所以我们只有一对,但是由于复杂的队列处理,所以不太清楚发生了什么:

      val it = 1 #:: Iterator.iterate((2, Queue[Int](2))){
        case (n,q) if q.head == 1 => (n+1, q.tail + (n+1))
        case (n,q) => (n, ((q.head-1) +: q.tail) + n)
      }.map(_._1).toStream 
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-02-12
        • 1970-01-01
        • 2011-03-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多