【问题标题】:How can I prevent repeating numbers in a random range operator Kotlin?如何防止在随机范围运算符 Kotlin 中重复数字?
【发布时间】:2021-11-03 02:09:18
【问题描述】:

我有一个应用程序,它根据通过获取的整数为您提供特定照片

class RandomImageLogic(){
    fun retrive(): Int{
        return (1..9).random()
    }
}

但是,重复的结果并不专业,因为我希望每次调用函数时都获取一个随机整数,以便每次按下按钮时图像都不同。每当按钮调用函数时,如何获取新的随机整数?

【问题讨论】:

标签: android kotlin random


【解决方案1】:

简单的方法是传入您收到的最后一个随机数并将其过滤掉。

fun retrive(except: Int): Int{
    return ((1..9).filter {it != except}).random();
}

【讨论】:

  • 应该是!=
【解决方案2】:

在您的情况下,此方法可能不会被非常频繁地调用(仅当用户单击按钮时)。

如果要更频繁地调用此方法,则应谨慎使用IntRange 上的filter(如@avalerio 的回答中所建议的那样)。 这会迭代整个范围(不必要地花费时间),并且会在每次调用时创建一个临时的ArrayList(创建不必要的垃圾并比需要更频繁地触发垃圾收集器)。

这是一个示例对象NonRepeatingRandom(如果你愿意,你也可以将它实现为一个类)。 retrieve(已通过 max 参数和基本健全性检查进行了扩展)如果连续两次生成相同的数字,则再次递归调用自身:

object NonRepeatingRandom {
    private var previous = -1

    fun retrieve(max : Int = 9): Int {
        if(max < 0) {
            error("Only positive numbers")
        }
        if(max <= 1) {
            // There is nothing random about 0 or 1, do not check against previous, just return
            previous = max
            return max
        }

        val rand = (1..9).random()
        return if(rand == previous) {
            retrieve(max) // recursive call if two subsequent retrieve() calls would return the same number
        } else {
            previous = rand // remember last random number
            rand
        }
    }
}

fun main(args: Array<String>) {
    repeat(1000) {
        println(NonRepeatingRandom.retrieve())
    }
}

我做了一个简单粗暴的性能测试,调用我的“递归”方法 1000 万次,调用“filter”方法 1000 万次。

递归:125 毫秒(10 次 mio 调用)

过滤器:864 毫秒(10 次 mio 调用)

【讨论】:

    【解决方案3】:

    预填充和随机随机播放方法:

    class RandomIntIterator(
        private val range: IntRange
    ) : Iterator<Int> {
    
        private lateinit var iterator: Iterator<Int>
    
        init { randomize() }
    
        fun randomize() {
            iterator = range.shuffled().iterator()
        }
    
        override fun hasNext() = iterator.hasNext()
        override fun next() = iterator.next()
    }
    

    ...

    val rnd = RandomIntIterator(1..9)
    ...
    // on button click
    if (rnd.hasNext()) {
        val num = rnd.next()
        // use num
    } else {
        // renew (if needed)
        rnd.randomize()
    }
    

    【讨论】:

      【解决方案4】:

      我喜欢使用Sequences 来生成无休止的值流。

      在这种情况下,我们必须编写自定义代码,因为检查重复值是一个有状态的操作,虽然 Sequences 有一个 distinct() 状态过滤器,但它适用于所有生成的值 - 我们只希望它适用于一个有限的窗口。

      TL;DR:

      class RandomImageLogic(
        private val random: Random,
        /** The number of sequential values that must be distinct */
        noRepeatsLimit: Int = 2
      ) {
      
        private val sourceValues: List<Int> = (0..9).toList()
      
        private fun nextValue(vararg exclusions: Int): Int =
          (sourceValues - exclusions.asList()).random(random)
      
        private val randomInts: Iterator<Int> =
          generateSequence({
            // the initial value just has one random int
            val next = nextValue()
            ArrayDeque(listOf(next))
          }) { previousValues ->
            // generate the next value, excluding previous values
            val nextValue = nextValue(*previousValues.toIntArray())
      
            // limit the size of previousValues, if necessary
            if (previousValues.size >= noRepeatsLimit)
              previousValues.removeLastOrNull()
            // add the generated value to the beginning of the deque
            previousValues.addFirst(nextValue)
      
            previousValues
          }
            .map {
              // convert the Sequence to a list of ints,
              // each element is the first item in the deque
              it.first()
            }
            .iterator()
      
        fun retrieve(): Int {
          return randomInts.next()
        }
      }
      

      测试

      让我们先编写一个测试,以确保我们的解决方案有效。 Kotest 有一个特定的property based testing 子项目,这将使我们能够非常快速地涵盖广泛的测试用例。

      于是,我跑遍了the setup,开始设置测试用例。

      播种RandomImageLogic

      首先我修改了RandomImageLogic 类,以便可以使用提供的Random 为随机选择播种。

      import kotlin.random.Random
      
      class RandomImageLogic(private val random: Random) {
        fun retrieve(): Int {
          return (1..9).random(random = random)
        }
      }
      

      这将帮助我们为RandomImageLogic 创建一个Generator

      测试所有值

      现在我们可以使用 Kotest 编写一个基于属性的测试,该测试将断言“对于所有顺序值,它们都是不同的”

      import io.kotest.core.spec.style.FunSpec
      import io.kotest.property.arbitrary.arbitrary
      import io.kotest.property.forAll
      
      class RandomImageLogicTest: FunSpec({
      
        // This generator will create a new instance of `RandomImageLogic` 
        // and generate two sequential values.
        val sequentialValuesArb = arbitrary { rs ->
          val randomImageLogic = RandomImageLogic(rs.random)
      
          val firstValue = randomImageLogic.retrieve()
          val secondValue = randomImageLogic.retrieve()
      
          firstValue to secondValue
        }
      
        test("expect sequential values are different") {
          forAll(sequentialValuesArb) { (firstValue, secondValue) ->
            firstValue != secondValue
          }
        }
      
      })
      

      当然,测试失败了。

      Property failed after 4 attempts
      
          Arg 0: (1, 1)
      
      Repeat this test by using seed 1210584330919845105
      
      Caused by org.opentest4j.AssertionFailedError: expected:<true> but was:<false>
      

      所以让我们修复它吧!

      生成序列

      正如我之前所说,我真的很喜欢 Sequences。它们非常适合这个用例,因为我们有无限的价值来源。

      为了演示如何制作序列,让我们转换现有代码,并使用Iterator 来获取值。

      class RandomImageLogic(private val random: Random) {
      
        private val randomInts =
          // generate a sequence using values from this lambda
          generateSequence { (1..9).random(random = random) }
            // use an iterator to fetch values
            .iterator()
      
        fun retrieve(): Int {
          return randomInts.next()
        }
      }
      

      这还没有解决问题 - 序列一次只生成并提供一个值,因此无法进行任何过滤。幸运的是generateSequence() 有一个带有nextFunction: (T) -&gt; T? 的变体,我们可以根据之前的值确定下一个值。

      如果我们使用此构造函数,并进行一些重构以共享源值,并使用 util 方法生成下一个值同时过滤掉以前的值...

        private val sourceValues: List<Int> = (0..9).toList()
      
        private fun nextValue(vararg exclusions: Int): Int =
          (sourceValues - exclusions.asList()).random(random)
      
        private val randomInts: Iterator<Int> =
          generateSequence({ nextValue() }) { previousValue ->
            nextValue(previousValue)
          }
            .iterator()
      

      现在如果我们运行测试,它就通过了!

      Test                                    Duration  Result
      expect sequential values are different  0.077s    passed
      

      改进:两个以上不同的顺序值

      如果您不只是希望两个连续的值是不同的,而是 3,会发生什么?甚至更多?让我们将“无重复值”限制配置为可配置,我认为这将说明为什么序列是一个很好的解决方案。

      class RandomImageLogic(
        private val random: Random,
        /** The number of sequential values that must be distinct */
        noRepeatsLimit: Int = 2
      ) {
        // ...
      }
      

      测试

      再一次,让我们编写一个测试以确保一切按预期工作。

      import io.kotest.core.spec.style.FunSpec
      import io.kotest.matchers.collections.shouldHaveSize
      import io.kotest.matchers.collections.shouldNotContainDuplicates
      import io.kotest.property.Arb
      import io.kotest.property.arbitrary.int
      import io.kotest.property.checkAll
      import kotlin.random.Random
      
      class RandomImageLogicTest : FunSpec({
      
        test("expect arbitrary sequential values are different") {
          checkAll(Arb.int(), Arb.int(1..10)) { seed, noRepeatsLimit->
      
            val randomImageLogic = RandomImageLogic(Random(seed), noRepeatsLimit)
      
            val result = List(noRepeatsLimit) { randomImageLogic.retrieve() }
      
            withClue("Result: $result") {
              result shouldHaveSize noRepeatsLimit
              result.shouldNotContainDuplicates()
            }
          }
        }
      
      })
      

      当然测试失败了。

      Property test failed for inputs
      
      0) -459964888
      1) 5
      
      Caused by java.lang.AssertionError: Result: [3, 8, 0, 2, 8]
      Collection should not contain duplicates
      

      有很多选项可以使序列有状态 - 让我们再看一个。

      值序列

      我们可以有一个序列,而不是单个值的序列,其中每个元素不仅是当前值的列表,而且是以前看到的值的列表。

      让我们使用ArrayDeque 来存储这些值,因为从开头和结尾添加和删除值很容易。

      同样,我们使用相同的 generateSequence 构造函数和 seedFunctionnextFunction - 除了这次每个元素都是存储所有值的双端队列,并且在 nextFunction 中,我们将新值添加到双端队列的开头, 如果大于窗口大小则修剪它noRepeatsLimit

        private val randomInts: Iterator<Int> =
          generateSequence({
            // the initial value just has one random int
            val next = nextValue()
            ArrayDeque(listOf(next))
          }) { previousValues ->
            // generate the next value, excluding previous values
            val nextValue = nextValue(*previousValues.toIntArray())
      
            // limit the size of previousValues, if necessary
            if (previousValues.size >= noRepeatsLimit)
              previousValues.removeLastOrNull()
            // add the generated value to the beginning of the deque
            previousValues.addFirst(nextValue)
      
            previousValues
          }
            .map {
              // convert the Sequence to a list of ints, 
              // each element is the first item in the deque
              it.first()
            }
            .iterator()
      

      是的,测试通过了!

      Test                                              Duration  Result
      expect arbitrary sequential values are different  0.210s    passed
      

      存储状态

      考虑如何存储状态很重要。 RandomImageLogic 需要“状态”来了解生成的值是否不同。

      在序列实现中,它存储在内部,因此专门与RandomImageLogic 的实例相关联。也许您的应用程序在任何时候都只有一个 RandomImageLogic 实例,在这种情况下,状态将始终是最新的,并且将在所有调用之间共享。

      但是如果RandomImageLogic 的实例不止一个,会发生什么?或者如果有多线程?或者如果RandomImageLogic 实例被重新创建?

      这些问题的答案取决于实施和情况。根据您的问题,我怀疑图像从不重复并不重要,但我提出这一点是因为考虑这一点很重要。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-01
        • 1970-01-01
        • 2017-05-20
        • 2017-12-25
        • 2011-02-27
        • 2022-01-07
        相关资源
        最近更新 更多