有效检查Scala流中的​​值存在(以功能方式)

时间:2015-05-14 09:59:27

标签: algorithm scala functional-programming

我已经来过this question on reddit - 我对此感到非常困惑,但我不知道(似乎没有令人满意的答案)。我敢在这里复制一下:

假设我们有一个流,每个元素取决于它的前身,例如siimple伪随机序列,例如:

def neum(a:Int): Stream[Int] = Stream.iterate(a)(a => (a*a/100)%10000)

这是von-Neumann的随机函数来自问题所引用的exercise

从给定值开始,我们想知道序列何时进入循环。我们可以使用Set for stored values:

以命令式形式轻松完成此操作
// like in java
Set<Integer> values = new HashSet<>();
while (true) {
    int x = nextValueInSequence(x)
    if (values.contains(x)) {
        break;
    }
    values.add(x);
}

然而,对于Scala来说,提出&#34; functional&#34;解。但问题的作者似乎不知道如何在保持O(N)时间复杂性的情况下实现这一目标。我也是。唯一的评论似乎就是建议直截了当的O(N^2)解决方案。

5 个答案:

答案 0 :(得分:2)

我相信,有一种算法具有 O(N log N)时间复杂度(可能会将其提高到 O(N)),并且< strong> O(1)总内存消耗。也就是说,我们不必记住以前的大部分数字。不变因素虽然很高。

此内存消耗不是使用Stream计算的,而是使用由起始元素和递归公式定义的常规数字序列。例如Iterator.iterate(start)(a => a * a / 100 % 10000)。  Stream会记住以前的结果,并有效地使其成为 O(N)记忆。

让我们说在循环开始之前序列有 P ≥0个元素,并且循环中 L ≥1个元素。例如,序列[2, 10, 13, 9, 11, 17, 11, 17, ...] P = 4且 L = 2.我们需要找到 P +

在算法中我们必须遍历序列。我将当前位置称为&#34;指针&#34;。在数字序列中,指针仅表示数字。最初指针等于序列的起始元素,并且要将指针向前移动1步,我们必须将递归公式应用于它。

现在算法:

  1. 从两个指向序列开头的指针开始:&#34;慢&#34;和&#34;快速&#34;。慢指针一次移动1步,一次快速移动2步(即2次递归公式的应用)。
  2. 最初指针是相等的。开始向前移动它们直到它们再次相等,并跟踪慢速指针的步数。让我们命名指针再次相等的步数 K 0 。可以证明, P K 0 &lt; P + L K 0 = 0( mod L < /强>)。

    在这一步中我们还应该特别注意这种情况,当 P = 0时:当指针变得相等时,如果它们也等于起始元素,我们应该设置 K 0 = 0,以便以后能够区分这种情况。

    此步骤的时间复杂度为 O(N)

  3. 现在指针肯定在序列的循环中。再次开始向前移动它们并跟踪慢速指针的步数,直到它们再次相等。该步数是序列的循环 L 的长度。 (您也可以在此步骤中仅移动慢速指针,直到它返回到相同位置,但我会重复使用该功能移动两者,这不会增加时间复杂度)

    此步骤的时间复杂度为 O(N)

  4. 现在我们必须计算 P 。我们可以注意到,如果在算法的步骤2)中我们启动&#34; fast&#34;指针不是从一开始,但有一些移位 S :0≤ S &lt; L ,结果会有所不同: K S = K 0 - S ,如果 S K 0 - P ;或 K S = K 0 + L - S 否则。因此,我们可以使用二元搜索来找到最大偏移 S * :0≤ S * &lt; L K S * = K 0 - S * 。然后我们可以找到 P = K 0 - S * ,并返回< strong> P + L = K 0 - S * + L

    此步骤的时间复杂度为 O(N log N),因为二进制搜索中的每个步骤都需要 O(N)

  5. 所以我们有一个算法,在 O(N log N)中使用 O(1)内存。这是一个代码示例:

    case class Sequence[T](start: T)(f: T => T) {
      def next = Sequence(f(start))(f)
      def forward(steps: Int) =
        Sequence(Function.chain(List.fill(steps)(f))(start))(f)
    }
    
    object Sequence {
      def neum(a: Int) = Sequence(a)(a => a * a / 100 % 10000)
    
      def movesToEquality[T](
        slow: Sequence[T], fast: Sequence[T], count: Int = 1
      ): (Sequence[T], Int) = {
        val nextSlow = slow.next
        val nextFast = fast.forward(2)
        if (nextSlow == nextFast) (nextSlow, count)
        else movesToEquality(nextSlow, nextFast, count+1)
      }
    
      def findLoopStart[T](seq: Sequence[T]): Int = {
        val (inLoop, k0) = movesToEquality(seq, seq) match {
          case (c, k) if c == seq => (c, 0)
          case other => other
        }
        val (_, loopSize) = movesToEquality(inLoop, inLoop)
    
        def binarySearch(lo: Int, hi: Int): Int = {
          if (lo + 1 >= hi) lo
          else {
            val mid = (lo + hi) / 2
            if (movesToEquality(seq, seq.forward(mid))._2 == k0 - mid)
              binarySearch(mid, hi)
            else
              binarySearch(lo, mid)
          }
        }
    
        k0 - binarySearch(0, loopSize) + loopSize
      }
    }
    
    object Main extends App {
      println(Sequence.findLoopStart(Sequence.neum(1)))
      println(Sequence.findLoopStart(Sequence.neum(4100)))
      println(Sequence.findLoopStart(Sequence.neum(5761)))
    }
    

答案 1 :(得分:2)

以下是Kolmar在 O(N)时间和 O(1)空间中运行的答案的简化版本。 它基本上是这样做的:

  1. 使用快速\慢速策略查找任何重复的数字。它是可以被实际整数整除并且大于非循环前缀的最小整数。
  2. 寻找真实的时期。
  3. 在一段时间后重复找到第一个元素,即循环开始。
  4. 返回非循环前缀部分和循环部分。
  5. 代码:

    def cycleOf[T](seq: => Iterator[T]): (Iterator[T], Iterator[T]) = {
      def fast = seq.sliding(1, 2) map (_.head)
      val meet = seq zip fast drop 1 dropWhile { case (x, y) => x != y }
      val met = meet.next()
      val period = (meet indexOf met) + 1
      val start = seq drop period zip seq indexWhere { case (x, y) => x == y }
      (seq take start, seq.slice(start, start + period))
    }
    

    您可以通过

    尝试
    val (prefix, cycle) = cycleOf(neum(5761).iterator)
    

    然后prefix.toList

      

    列表(5761,1891,5758,1545,3870,9769,4333,7748,315,992,9840,   8256,1615,6082,9907,1486,2081,3305,9230,1929,7210,9841,   8452,4363,357,1274,6230,8129,806,6496,1980,9204,7136,9224,   821,6740,4276,2841,712,5069,6947,2608,8016,2562,5638,7870,   9369,7781,5439,5827,9539,9925,5056,5631,7081,1405,9740,   8676,2729,4474,166,275,756,5715,6612,7185,6242,9625,6406,   368,1354,8333,4388,2545,4770,7529,6858,321,1030,609,3708,   7492,1300,6900)

    cycle.toList

    List(6100, 2100, 4100, 8100)
    

    另请注意SpiderPig的建议:您只需将Stream替换为Iterator定义中的neum即可获得更高内存效率的版本。

答案 2 :(得分:1)

我确信有更好的方式来写这个,但这是我的第一次尝试:

def loop[A](xs: Stream[A]): Set[A] =
  xs.scanLeft(Set.empty[A])(_ + _).sliding(2)
    .find(_.map(_.size).toSet.size == 1).get.head

scala> neum(93).take(8).toList
res0: List[Int] = List(93, 86, 73, 53, 28, 7, 0, 0)

scala> loop(neum(93))
res1: Set[Int] = Set(0, 93, 28, 53, 73, 86, 7)

答案 3 :(得分:1)

我相信在尾递归函数的帮助下可以使用Set的解决方案:

@tailrec
def neumannCount(x: Int, m: Set[Int] = Set[Int]()): Int = {
    if (m.contains(x)) m.size else neumannCount(x * x / 100 % 10000, m + x)
}

该函数只获取当前值和先前元素的集合。它检查Set中是否存在值,如果没有 - 则生成下一个元素,另一个元素添加当前元素 - 并将它们传递给另一个调用同一函数。当值最后遇到时 - 我们只返回Set的大小作为结果(因此该函数是尾递归的)。

我相信这应该是时间上的O(1)和空间中的O(N),这要归功于相互建立的不可变集合(如果我理解它们的话)。

答案 4 :(得分:0)

它必须是流吗?迭代器更快。 这是两种不同的解决方案。两者都是功能性的,不会改变任何状态。

def neumann(seed: Int): Int = {
  Iterator.iterate(seed)(s => ((s * s)/100)%10000)
          .scanLeft(Set.empty[Int])((set, n) => if(set(n)) Set(-1) else set + n)
          .takeWhile(_ != Set(-1)).size - 1
}

def neumann(seed: Int): Int = {
  def search(s: Int, set: Set[Int], count: Int): Int = {
    if(set(s)) count
    else search(((s * s)/100)%10000, set + s, count + 1)
  }
  search(seed, Set.empty[Int], 0)
}