【譯】Swift算法俱樂(lè)部-選取樣本

本文是對(duì) Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Clubraywenderlich.com網(wǎng)站出品的用Swift實(shí)現(xiàn)算法和數(shù)據(jù)結(jié)構(gòu)的開(kāi)源項(xiàng)目,目前在GitHub上有18000+??,我初略統(tǒng)計(jì)了一下,大概有一百左右個(gè)的算法和數(shù)據(jù)結(jié)構(gòu),基本上常見(jiàn)的都包含了,是iOSer學(xué)習(xí)算法和數(shù)據(jù)結(jié)構(gòu)不錯(cuò)的資源。
??andyRon/swift-algorithm-club-cn是我對(duì)Swift Algorithm Club,邊學(xué)習(xí)邊翻譯的項(xiàng)目。由于能力有限,如發(fā)現(xiàn)錯(cuò)誤或翻譯不妥,請(qǐng)指正,歡迎pull request。也歡迎有興趣、有時(shí)間的小伙伴一起參與翻譯和學(xué)習(xí)??。當(dāng)然也歡迎加??,??????????。
本文的翻譯原文和代碼可以查看??swift-algorithm-club-cn/Selection Sampling


選取樣本(Selection Sampling)

目標(biāo):從n個(gè)項(xiàng)的集合中隨機(jī)選擇k個(gè)項(xiàng)。

假設(shè)你有一副52張牌,你需要隨機(jī)抽取10張牌。 這個(gè)算法可以讓你達(dá)成。

這是一個(gè)非??斓陌姹荆?/p>

func select<T>(from a: [T], count k: Int) -> [T] {
  var a = a
  for i in 0..<k {
    let r = random(min: i, max: a.count - 1)
    if i != r {
      swap(&a[i], &a[r])
    }
  }
  return Array(a[0..<k])
}

正如洗牌算法經(jīng)常發(fā)生的那樣,它將數(shù)組劃分為兩個(gè)區(qū)域。 第一個(gè)區(qū)域包含所選項(xiàng); 第二個(gè)區(qū)域是所有剩余的項(xiàng)。

一個(gè)例子。 假設(shè)一個(gè)數(shù)組是:

[ "a", "b", "c", "d", "e", "f", "g" ]

我們想選擇3個(gè)項(xiàng)目,所以k = 3。 在循環(huán)中,i最初為0,因此它指向"a"。

[ "a", "b", "c", "d", "e", "f", "g" ]
   i

我們計(jì)算i和數(shù)組的大小a.count之間的隨機(jī)數(shù)。 假設(shè)這個(gè)隨機(jī)數(shù)是4。 現(xiàn)在我們將"a"與索引為4的"e"交換,然后向前移動(dòng)i

[ "e" | "b", "c", "d", "a", "f", "g" ]
         i

|欄表示兩個(gè)區(qū)域之間的分割。 "e"是我們選擇的第一個(gè)元素。 我們繼續(xù)需要關(guān)注|欄右側(cè)的所有內(nèi)容。

再一次,我們要求ia.count之間的隨機(jī)數(shù),但因?yàn)?code>i已經(jīng)移位,隨機(jī)數(shù)永遠(yuǎn)不會(huì)小于1。所以我們?cè)僖膊粫?huì)交換"e"了。

假設(shè)隨機(jī)數(shù)為6,我們將"b""g"交換:

[ "e" , "g" | "c", "d", "a", "f", "b" ]
               i

還有一個(gè)隨機(jī)數(shù),假設(shè)它是4。 我們將"c""a"交換,最終左邊已經(jīng)選擇的項(xiàng)為:

[ "e", "g", "a" | "d", "c", "f", "b" ]

就是這樣。 十分簡(jiǎn)單。 這個(gè)函數(shù)的性能是O(k),因?yàn)橹灰覀冞x擇了k元素,就結(jié)束了。

下面是一種替代算法,稱為“水庫(kù)抽樣”(Reservoir Sampling):

func reservoirSample<T>(from a: [T], count k: Int) -> [T] {
  precondition(a.count >= k)

  var result = [T]()      // 1
  for i in 0..<k {
    result.append(a[i])
  }

  for i in k..<a.count {  // 2
    let j = random(min: 0, max: i)
    if j < k {
      result[j] = a[i]
    }
  }
  return result
}

有兩個(gè)步驟:

  1. 使用原始數(shù)組中的第一個(gè)k元素填充result數(shù)組。 這被稱為“水庫(kù)”。
  2. 用剩余池中的元素隨機(jī)替換水庫(kù)中的元素。

該算法的性能為 O(n),因此它比第一算法慢一點(diǎn)。但是,它的最大優(yōu)點(diǎn)是它可以用于太大而無(wú)法容納在內(nèi)存中的數(shù)組,即使你不知道數(shù)組的大小是多少(在Swift中這可能類似于讀取文件元素的懶惰生成器)。

前兩種算法有一個(gè)缺點(diǎn):它們不保留原始順序的元素。在輸入數(shù)組中,"a"出現(xiàn)在"e"之前,但現(xiàn)在卻是另一種順序。如果要順序不變,則無(wú)法使用上面的方法。

下面這種替代方法,可以保持原始順序的完整性,但需要更多空間參與:

func select<T>(from a: [T], count requested: Int) -> [T] {
  var examined = 0
  var selected = 0
  var b = [T]()
  
  while selected < requested {                          // 1
    let r = Double(arc4random()) / 0x100000000          // 2
    
    let leftToExamine = a.count - examined              // 3
    let leftToAdd = requested - selected

    if Double(leftToExamine) * r < Double(leftToAdd) {  // 4
      selected += 1
      b.append(a[examined])
    }

    examined += 1
  }
  return b
}

該算法使用概率來(lái)決定是否在選擇中包括一個(gè)數(shù)字。

  1. 循環(huán)從頭到尾逐步完成數(shù)組。 它一直持續(xù)到我們從n的集合中選擇k個(gè)項(xiàng)。 這里,krequestedna.count。

  2. 計(jì)算0到1之間的隨機(jī)數(shù)。我們想要0.0 <= r < 1.0。 上限是排他性的; 我們從不希望它是1。這就是為什么我們將結(jié)果從arc4random()除以0x100000000而不是更常見(jiàn)的0xffffffff。

  3. leftToExamine是我們還沒(méi)有看過(guò)的項(xiàng)數(shù)目。 leftToAdd是我們?cè)谕瓿芍斑€需要選擇的項(xiàng)數(shù)。

  4. 這就是魔術(shù)發(fā)生的地方。 基本上,我們正在翻轉(zhuǎn)一枚硬幣。 如果是heads,我們將當(dāng)前數(shù)組元素添加到選擇中; 如果是tails,我們就跳過(guò)。

有趣的是,即使我們使用概率,這種方法總是保證我們最終得到輸出數(shù)組中的k項(xiàng)。

讓我們?cè)俅斡懻撓嗤睦印?輸入數(shù)組是:

[ "a", "b", "c", "d", "e", "f", "g" ]

循環(huán)依次查看每個(gè)元素,因此我們從"a"開(kāi)始。 我們得到一個(gè)介于0和1之間的隨機(jī)數(shù),假設(shè)它是0.841。 // 4處的公式將要檢查的項(xiàng)目數(shù)乘以此隨機(jī)數(shù)。 還有7個(gè)元素需要檢查,結(jié)果是:

7 * 0.841 = 5.887

我們將此與3進(jìn)行比較,因?yàn)槲覀兿胍x擇3個(gè)項(xiàng)目。 由于5.887大于3,我們跳過(guò)"a"并繼續(xù)移動(dòng)動(dòng)"b"。

再一次,我們得到一個(gè)隨機(jī)數(shù),比方說(shuō)0.212。 現(xiàn)在只剩下6個(gè)要檢查的元素,因此公式結(jié)果是:

6 * 0.212 = 1.272

小于3,我們?cè)谶x擇中添加"b"。 這是我們選擇的第一個(gè)項(xiàng),所以還剩下兩個(gè)。

到下一個(gè)元素,"c"。 隨機(jī)數(shù)為0.264,得出結(jié)果:

5 * 0.264 = 1.32

只要再選擇2個(gè)項(xiàng),因此這個(gè)數(shù)字必須小于2。它是,我們還在選擇中加入"c"。 總選擇是["b","c"]

只要再選擇1個(gè)項(xiàng),但仍有4個(gè)候選項(xiàng)要查看。 假設(shè)下一個(gè)隨機(jī)數(shù)是0.718。 該公式現(xiàn)在給出:

4 * 0.718 = 2.872

要選擇此元素,數(shù)字必須小于1,因?yàn)橹皇O?個(gè)項(xiàng)要選擇。 2.872不是,所以我們跳過(guò)"d"。 只剩下三種可能性 - 我們會(huì)在耗盡元素之前選到它嗎?

隨機(jī)數(shù)為0.346。 該公式給出:

3 * 0.346 = 1.038

有點(diǎn)太高了。 我們跳過(guò)"e"。 只有兩名候選項(xiàng)了......

請(qǐng)注意,現(xiàn)在字面上我們正在處理拋硬幣:如果隨機(jī)數(shù)小于0.5,我們選擇"f",我們就完成了。 如果它大于0.5,我們繼續(xù)最后的元素。 假設(shè)我們得到0.583:

2 * 0.583 = 1.166

我們跳過(guò)"f"并查看最后一個(gè)元素。 無(wú)論我們?cè)谶@里得到什么隨機(jī)數(shù),它應(yīng)該總是選擇"g"或者我們不會(huì)選擇足夠的元素而算法不起作用!

假設(shè)我們的最終隨機(jī)數(shù)是0.999(記住,它永遠(yuǎn)不會(huì)是1.0或更高)。 實(shí)際上,無(wú)論我們?cè)谶@里選擇什么,公式總是會(huì)給出小于1的值:

1 * 0.999 = 0.999

因此,如果我們還沒(méi)有足夠多的選擇,那么總是會(huì)選擇最后一個(gè)元素。最后的選擇是[ "b", "c", "g" ]。請(qǐng)注意,元素仍處于原始順序,因?yàn)槲覀兪菑淖蟮接也樵償?shù)組。

也許你還不相信......如果我們總是將0.999作為隨機(jī)值(最大可能值),那還能選擇3項(xiàng)嗎? 好吧,讓我們做數(shù)學(xué):

7 * 0.999 = 6.993     小于3嗎? no
6 * 0.999 = 5.994     小于3嗎? no
5 * 0.999 = 4.995     小于3嗎? no
4 * 0.999 = 3.996     小于3嗎? no
3 * 0.999 = 2.997     小于3嗎? YES
2 * 0.999 = 1.998     小于2嗎? YES
1 * 0.999 = 0.999     小于1嗎? YES

它總是有效! 但這是否意味著靠近數(shù)組末尾的元素比一開(kāi)始的元素更有可能被選中? 不,所有元素同樣可能被選中。 (如果不相信我的話:在playground 看一下快速測(cè)試,在實(shí)踐中證明了這一點(diǎn)。)

以下是如何測(cè)試此算法的示例:

let input = [
  "there", "once", "was", "a", "man", "from", "nantucket",
  "who", "kept", "all", "of", "his", "cash", "in", "a", "bucket",
  "his", "daughter", "named", "nan",
  "ran", "off", "with", "a", "man",
  "and", "as", "for", "the", "bucket", "nan", "took", "it",
]

let output = select(from: input, count: 10)
print(output)
print(output.count)

第二種算法的性能是O(n),因?yàn)樗赡苄枰闅v整個(gè)輸入數(shù)組。

注意: 如果k > n / 2,那么以相反的方式執(zhí)行它并選擇要?jiǎng)h除的a.count - k項(xiàng)更有效。

代碼基于發(fā)表于1993年10月Dobb博士的雜志的Algorithm Alley。


作者:Matthijs Hollemans
翻譯:Andy Ron
校對(duì):Andy Ron

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容