【问题标题】:Best practice for shifting a sequence in a circular manner以循环方式移动序列的最佳实践
【发布时间】:2012-01-16 07:10:58
【问题描述】:

我必须实现一种数组或序列或列表,它支持循环转发和回绕元素的最便宜的方式。看这个例子:

Original sequence: 1 2 3 4 5

Forwarded once: 5 1 2 3 4
Forwarded twice: 4 5 1 2 3

反向绕组相同但相反。什么是最便宜和最具 Scala 风格的实现方式?在 Java 中,我可以使用 LinkedList,它会做得很好......但是,我找不到 Scala 的任何明确答案。

此外,它还必须易于按索引替换任何给定元素,如在 LinkedList 中。

更新:

对于最快但不那么惯用的算法变体(你知道什么时候需要它),请参阅 Petr Pudlák 的回答!!!

【问题讨论】:

  • this 可能对你有用
  • 我认为“转桶”可能比“流通”更能表达您的意图...
  • @fortran 我只从逻辑电路中知道这个术语。因此它是一个特定的实现。你确定这适用于变速操作?
  • @ziggystar 我认为你是对的,我可能混淆了循环移位的硬件术语......无论如何,动词应该是 shift,而不是 circulate (这就是我在标题中发现的奇怪之处)。干杯。

标签: arrays scala sequences


【解决方案1】:

不可变的实现

ring buffer 是一对指向此序列的 IndexedSeq 和 Int 指针。我提供不可变版本的代码。请注意,并非所有可能有用的方法都已实现;就像改变 IndexedSeq 内容的突变器一样。

使用此实现,移位只是创建一个新对象。所以效率很高。

示例代码

class RingBuffer[A](val index: Int, val data: IndexedSeq[A]) extends IndexedSeq[A] {
  def shiftLeft = new RingBuffer((index + 1) % data.size, data)
  def shiftRight = new RingBuffer((index + data.size - 1) % data.size, data)
  def length = data.length
  def apply(i: Int) = data((index + i) % data.size)
}

val rb = new RingBuffer(0, IndexedSeq(2,3,5,7,11))

println("plain: " + rb)
println("sl: " + rb.shiftLeft)
println("sr: " + rb.shiftRight)

输出

plain: Main(2, 3, 5, 7, 11)
sl: Main(3, 5, 7, 11, 2)
sr: Main(11, 2, 3, 5, 7)

与可变实现的性能比较

OP 提到如果您需要性能,您应该查看可变实现(例如this answer)。一般情况下这是不正确的。一如既往:这取决于。

不可变

  • update:O(log n),基本就是底层IndexedSeq的更新复杂度;
  • shifting:O(1),还涉及创建一个新对象,这可能会花费一些周期

可变

  • 更新:O(1),数组更新,尽快
  • shifting:O(n),每个元素你都要碰一次;由于常数因素,原始数组的快速实现可能仍会胜过小型数组的不可变版本

【讨论】:

  • 有人知道为什么println 将数据结构显示为Main 吗?我已经使用scala 命令将它作为脚本运行。
  • 来自TraversableLike的方法stringPrefix将集合的前缀定义为最后一个.和第一个$之间的类名的子字符串。我现在无法检查,但我认为由于scala 命令类加载魔术,您的类会收到类似Main$object$$iw$$iw$RingBuffer 的名称,它给出前缀Main。在 REPL 中试试这个:settings.unwrapStrings = false; class Foo; println(new Foo);
  • 你使用的是什么版本的scala?在 REPL 的 2.9.0.1 中,它打印为 plain: (2, 3, 5, 7, 11)。您可以使用override def stringPrefix = "RingBuffer" 获得正确的前缀。
  • 这真的很酷,因为data 在内存中只需要一次,所有RingBuffers 都可以共享一个副本(假设不变)。
【解决方案2】:
scala> val l = List(1,2,3,4,5)
l: List[Int] = List(1, 2, 3, 4, 5)

scala> val reorderings = Stream.continually(l.reverse).flatten.sliding(l.size).map(_.reverse)
reorderings: Iterator[scala.collection.immutable.Stream[Int]] = non-empty iterator

scala> reorderings.take(5).foreach(x => println(x.toList))
List(1, 2, 3, 4, 5)
List(5, 1, 2, 3, 4)
List(4, 5, 1, 2, 3)
List(3, 4, 5, 1, 2)
List(2, 3, 4, 5, 1)

【讨论】:

  • 好主意!功能性。有效率吗?
  • 我觉得reorderings 的类型很奇怪。 Iterator 的 Streams?
【解决方案3】:

我自己也需要这样的手术,就是这样。方法rotate 将给定的索引序列(数组)向右旋转(负值向左移动)。该过程是就地的,因此不需要额外的内存并且修改了原始数组。

它根本不是 Scala 特定的或函数式的,它的目的是非常快。

import annotation.tailrec;
import scala.collection.mutable.IndexedSeq

// ...

  @tailrec
  def gcd(a: Int, b: Int): Int =
    if (b == 0) a
    else gcd(b, a % b);

  @inline
  def swap[A](a: IndexedSeq[A], idx: Int, value: A): A = {
    val x = a(idx);
    a(idx) = value;
    return x;
  }

  /**
   * Time complexity: O(a.size).
   * Memory complexity: O(1).
   */
  def rotate[A](a: IndexedSeq[A], shift: Int): Unit =
    rotate(a, 0, a.size, shift);
  def rotate[A](a: IndexedSeq[A], start: Int, end: Int, shift: Int): Unit = {
    val len = end - start;
    if (len == 0)
      return;

    var s = shift % len;
    if (shift == 0)
      return;
    if (s < 0)
      s = len + s;

    val c = gcd(len, s);
    var i = 0;
    while (i < c) {
      var k = i;
      var x = a(start + len - s + k);
      do {
        x = swap(a, start + k, x);
        k = (k + s) % len;
      } while (k != i);
      i = i + 1;
    }
    return;
  }

【讨论】:

    【解决方案4】:

    我解决 Scala 问题的方法是先在 Haskell 中解决它们,然后再进行翻译。 :)

    reorderings xs = take len . map (take len) . tails . cycle $ xs
      where len = length xs
    

    这是我能想到的最简单的方法,它通过反复“左移”生成所有可能的移位列表。

    ghci> reorderings [1..5]
    [[1,2,3,4,5],[2,3,4,5,1],[3,4,5,1,2],[4,5,1,2,3],[5,1,2,3,4]]
    

    这个概念相对简单(对于那些熟悉函数式编程的人来说)。首先,cycle 原始列表,产生一个可以从中提取的无限流。接下来,将该流拆分为一个流流,其中每个后续流都丢弃了前一个流的第一个元素 (tails)。接下来,将每个子流限制为原始列表的长度 (map (take len))。最后,将流的流限制为原始列表的长度,因为只有 len 可能的重新排序 (take len)。

    现在让我们在 Scala 中执行此操作。

    def reorderings[A](xs: List[A]):List[List[A]] = {
      val len = xs.length
      Stream.continually(xs).flatten // cycle
        .tails
        .map(_.take(len).toList)
        .take(len)
        .toList
    }
    

    我们只需要对 cycle 使用一个小的解决方法(不确定 Scala 标准库是否提供循环,尽管我惊喜地发现它们提供了 tails)和一些 toLists(Haskell 列表 是惰性流,而 Scala 是严格的),但除此之外,它与 Haskell 完全相同,据我所知,行为完全相同。你几乎可以认为 Scala 的 . 表现得像 Haskell 的,除了流动方向相反。

    还请注意,这与 dhg 的解决方案几乎相同,除了没有反向,这(从好的方面)使其更有效,但(在不利的方面)以“倒退”顺序提供周期,而不是“前进” "的顺序。

    【讨论】:

    • 一个有趣的想法!坦率地说,我现在正在努力掌握 FP,我最初选择学习的语言是 Scala 和 Haskell,最后是 Clojure。到目前为止,我对 Haskell 的了解也让我认为这是一个很好的实践——在其中制定解决方案,然后转换为 Scala)))但我在 FP 中还远非完美,所以感谢您提到与一个例子。这会让我更加意识到这一点!
    【解决方案5】:

    @dhg 和@Roman Zykov 版本的完美结合:

    scala> val l = List(1,2,3,4,5)
    l: List[Int] = List(1, 2, 3, 4, 5)
    
    scala> val re = Stream continually (l ++ l.init sliding l.length) flatten
    re: scala.collection.immutable.Stream[List[Int]] = Stream(List(1, 2, 3, 4, 5), ?)
    
    scala> re take 10 foreach println
    List(1, 2, 3, 4, 5)
    List(2, 3, 4, 5, 1)
    List(3, 4, 5, 1, 2)
    List(4, 5, 1, 2, 3)
    List(5, 1, 2, 3, 4)
    List(1, 2, 3, 4, 5)
    List(2, 3, 4, 5, 1)
    List(3, 4, 5, 1, 2)
    List(4, 5, 1, 2, 3)
    List(5, 1, 2, 3, 4)
    

    【讨论】:

      【解决方案6】:

      有一个非常简单的解决方案:

      val orderings = List(1,2,3,4,5)
      (orderings ++ orderings.dropRight(1)).sliding(orderings.length).toList
      
      List(List(1, 2, 3, 4, 5), List(2, 3, 4, 5, 1), List(3, 4, 5, 1, 2), List(4, 5, 1, 2, 3), List(5, 1, 2, 3, 4))
      

      【讨论】:

        【解决方案7】:

        我的看法:

        @tailrec
         def shift(times:Int, data:Array[Int]):Array[Int] = times match {
                case t:Int if(t <= 0) => data;
                case t:Int if(t <= data.length) => shift(0, (data++data.take(times)).drop(times)) 
                case _ => shift(times % data.length, data);
        }
        

        【讨论】:

          【解决方案8】:

          这是序列的一种可能解决方案

          class ShiftWarper( seq: Seq[ Int ] ) {
            def shiftLeft: Seq[ Int ] = {
              if ( seq.isEmpty ) {
                seq
              } else {
                seq.tail :+ seq.head
              }
            }
            def shiftRight: Seq[ Int ] = {
              if ( seq.isEmpty ) {
                seq
              } else {
                seq.last +: seq.init
              }
            }
          }
          implicit def createShiftWarper( seq: Seq[ Int ] ) =
              new ShiftWarper( seq ) 
          
          def shift_n_Times(
            times: Int,
            seq: Seq[ Int ],
            operation: Seq[ Int ] => Seq[ Int ] ): Seq[ Int ] = {
            if ( times > 0 ) {
              shift_n_Times(
                times - 1,
                operation( seq ),
                operation )
            } else {
              seq
            }
          } 
          
          val initialSeq = ( 0 to 9 )
          
          ( initialSeq shiftLeft ) shiftRight
          shift_n_Times(
            5,
            initialSeq,
            initialSeq => new ShiftWarper( initialSeq ).shiftRight )
          

          【讨论】:

            【解决方案9】:

            这是另一个简单的 scala 解决方案,用于将 Stream 向右或向左移动任意数量。 “循环”无限重复流,然后“移位”找到正确的切片。 "posMod" 允许您移动大于 xs.length 的索引,但实际上不会在无限 Stream 中偏离超过 xs.length 的元素:

            scala> def posMod(a:Int, b:Int) = (a % b + b) % b
            
            scala> def cycle[T](xs : Stream[T]) : Stream[T] = xs #::: cycle(xs)
            
            scala> def shift[T](xs:Stream[T], x: Int) = cycle(xs)
                      .drop(posMod(x, xs.length))
                      .take(xs.length)
            

            然后:

            scala> shift(Stream(1,2,3,4), 3).toList
            --> List[Int] = List(4, 1, 2, 3)
            
            scala> shift(Stream(1,2,3,4), -3).toList
            --> List[Int] = List(2, 3, 4, 1)
            
            scala>  shift(Stream(1,2,3,4), 30000001).toList
            --> List[Int] = List(2, 3, 4, 1)
            

            【讨论】:

            • 我认为我的左移正数和右移负数与问题的措辞相反,“前进”向右移动,但你明白了。
            【解决方案10】:

            我的提议:

            def circ[A]( L: List[A], times: Int ): List[A] = {
                if ( times == 0 || L.size < 2 ) L
                else circ(L.drop(1) :+ L.head , times-1)
            }
            
            val G = (1 to 10).toList
            println( circ(G,1) ) //List(2, 3, 4, 5, 6, 7, 8, 9, 10, 1)
            println( circ(G,2) ) //List(3, 4, 5, 6, 7, 8, 9, 10, 1, 2)
            println( circ(G,3) ) //List(4, 5, 6, 7, 8, 9, 10, 1, 2, 3)
            println( circ(G,4) ) //List(5, 6, 7, 8, 9, 10, 1, 2, 3, 4)
            

            【讨论】:

              【解决方案11】:

              您可以实例化一个包含 Array(A) 和您需要的旋转步数 (S) 的函数:

              def rotate(A: Array[Int], S: Int): Int = { (A drop A.size - (S % A.size)) ++ (A take A.size - (S % A.size)) }
              
              rotate(Array(1, 2, 3, 4, 5), 1)
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-06-11
                • 2018-09-23
                • 2012-09-20
                • 1970-01-01
                • 2019-03-16
                • 1970-01-01
                相关资源
                最近更新 更多