[面經]一道關於隨機算法的面試題


今天碰到了一道面試題:原題大致是,每首歌曲都是一個評分,現在有2000首歌曲,要求實現一個隨機播放器,每首歌曲播放的概率應該正比於它的評分,例如評分9.1的歌曲,和評分7.9的歌曲,播放的次數應該是91:79。

面試官給的答案是大致如此:

先把評分從小到大排序,之后把根據每首歌的評分,生成一個半閉開區間,然后生成一個隨機數,看隨機數落在哪個區間,就是選擇的那首歌。例如,有三首歌,評分是[1,2,3] 那么應該是生成三個區間 [0-1,1-3,3-6],之后生成一個0-6之間的隨機數,隨機數落在哪個區間,就選擇對應的歌曲。考慮排序的效率,這是一個nLogn的算法。

但是,這個算法是有紕漏的,沒有考慮到評分重復的情況,如果三首歌的評分是[1,2,2],那么應該是生成兩個區間[0-1,1-5], 如果落在第二個區間,還需要從兩首評分為2的歌曲里面隨機選出一首來。這樣的話,實現起來就相當復雜了。

最后,如果照上面那樣考慮,就完整了,但是實現起來的話,會發現,沒有很好的數據結構能判斷哪個隨機數是落在哪個區間的,除非遍歷所有的區間。

那么,優雅又高效的解法是什么樣的,假定每個評分只到小數點后一位,那么其實,利用空間換取時間的思路,只需要把每首歌按照他的評分多少相應的復制多少重復的歌曲,並且把所有的歌曲都扔到一個池子里面,之后從池子里面等概率的選取一首歌就行了。在最壞的情況下,2000首歌曲的評分都是9.9,那么每首歌需要復制99首,空間效率是On,時間復雜度為O1

算法的scala實現如下:

class RandomSong(val rate: Array[Double]) {
  val rateWithIndex = rate.map(x => (x * 10).toInt).zipWithIndex
  val songPool = rateWithIndex.flatMap { case (rate, index) => Array(index).padTo(rate, index)}

  def pickSong:Int = songPool(Random.nextInt(songPool.size))

}

測試

object main {
  def main(args: Array[String]) {
    val r = new RandomSong(Array(0.9,0.9,0.1,0.2))
    var count: Map[Int, Int] = Map()
    1 to 10000 foreach { x =>
      val song = r.pickSong
      count.get(song) match {
        case None => count += (song -> 1)
        case Some(n) => count += (song -> (1 + n))
      }
    }
    println("count = " + count)
  }
}

結果

count = Map(2 -> 477, 1 -> 4312, 3 -> 970, 0 -> 4241)

ps:我是回家路上才想起這種解法的,我和我老婆說,化學系畢業的她直接就給出了正確的解法,哎,被數學學霸碾壓的滋味就是這么銷魂。

 

更新:早上和V站的V友討論以后,發現面試官說的那種映射是可以實現的,例如有三首歌,評分是[1,2,3]那么區間段是[0-1,2-4,4-6]這個時候,只需要存一個數組[1,4,6],之后用2分查找就能得出正確的結論了,當然還需要考慮評分重復的情況。

rangeMap guava中有現成的實現,我還是太年輕啊。此外,這種加權隨機的算法,早有研究

http://www.electricmonk.nl/Writings/HomePage?action=download&upname=weighted_random_dist.pdf

http://www.electricmonk.nl/log/2009/12/23/weighted-random-distribution/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM