對於算法書買了一本又一本卻沒一本讀完超過 10%,Leetcode 刷題歷來沒堅持超過 3 天的我來講,算法能力真的是渣渣。可是,今天決定寫一篇跟算法有關的文章。原由是讀了吳師兄的文章《掃雷與算法:如何隨機化的佈雷(二)之洗牌算法》。由於掃雷這個遊戲我是寫過的,具體見:《Python:遊戲:掃雷》。python
遊戲開始的時候須要隨機佈雷。掃雷的高級是 16 × 30 的網格,一共有 99 個雷。若是從 0 開始給全部網格作標記,那麼佈雷的問題就成了從 480 個數中隨機選取 99 個數。 第一反應天然是記錄已選項:算法
import random
mines = set()
for i in range(99):
j = random.randint(0, 480)
while j in mines:
j = random.randint(0, 480)
mines.add(j)
print(mines)
複製代碼
不過這算法看着彷佛有點 low 啊。dom
其實從 480 個數中隨機抽取 99 個數,那麼只要將這 480 個數打亂,取前 99 個數就行了。這就引出了:高納德置亂算法(洗牌算法)。spa
這個算法很牛逼卻很好理解,通俗的解釋就是:將最後一個數和前面任意 n-1 個數中的一個數進行交換,而後倒數第二個數和前面任意 n-2 個數中的一個數進行交換......以此類推。.net
這個原理很好理解,通俗得不能再通俗,稍微想一下就會明白,確實如此。code
洗牌算法的 Python 實現以下:cdn
import random
lst = list(range(10))
for i in reversed(range(len(lst))):
j = random.randint(0, i)
lst[i], lst[j] = lst[j], lst[i]
print(lst)
複製代碼
看了吳師兄的文章,我立馬去翻了個人掃雷代碼,我以爲,我必定是用的那個很 「low」 的算法。翻出代碼一看,我用的是 Python 提供了隨機取樣算法:random.sample
,感嘆 python 的強大,這都有。而後我就想到了,隨機打亂一個序列,random.shuffle
不就是幹這事的嗎?那麼 random.shuffle
會是用的洗牌算法嗎?blog
翻看 random.shuffle
的源碼,發現正是洗牌算法。遊戲
def shuffle(self, x, random=None):
if random is None:
randbelow = self._randbelow
for i in reversed(range(1, len(x))):
j = randbelow(i + 1)
x[i], x[j] = x[j], x[i]
else:
_int = int
for i in reversed(range(1, len(x))):
j = _int(random() * (i + 1))
x[i], x[j] = x[j], x[i]
複製代碼
一切都是如此的天然而美好,而後我又去瞄了一眼 random.sample
的源碼,而後就一頭霧水了。我截了部分源碼:內存
n = len(population)
result = [None] * k
setsize = 21 # size of a small set minus size of an empty list
if k > 5:
setsize += 4 ** _ceil(_log(k * 3, 4)) # table size for big sets
if n <= setsize:
# An n-length list is smaller than a k-length set
pool = list(population)
for i in range(k): # invariant: non-selected at [0,n-i)
j = randbelow(n-i)
result[i] = pool[j]
pool[j] = pool[n-i-1] # move non-selected item into vacancy
else:
selected = set()
selected_add = selected.add
for i in range(k):
j = randbelow(n)
while j in selected:
j = randbelow(n)
selected_add(j)
result[i] = population[j]
return result
複製代碼
setsize
變量雖然看得一頭霧水,可是下面的 if
和 else
部分仍是能看懂的。if
裏是洗牌算法,而 else
裏是那個倒是我看着很 「low」 記錄已選項算法。
這是怎麼回事?爲了弄明白其中的道理,我去搜了不少文章查看,最有價值的是下面這篇:blog.csdn.net/harry_128/a…
隨機取樣有兩種實現方式,一是隨機抽取且不放回,就是洗牌算法;二是隨機抽取且放回,就是我想到的記錄已選項算法。random.sample
根據條件選擇其中之一執行。那麼就是說,洗牌算法和記錄已選項算法之間是各有優劣的。這讓我有點驚訝,不明擺着洗牌算法更優嗎?
首先,這個抽樣算法確定不能改變原序列的順序,而洗牌算法是會改變序列順序的,因此只能使用序列的副本,代碼中也是這麼作的 pool = list(population)
建立副本,而記錄已選項算法是不會改變原序列順序的,因此無需建立副本。建立副本也須要消耗時間和空間,算法天然也是要把這考慮進去的。當須要取的樣本數量 K 相較於樣本整體數量 N 較小時,隨機取到重複值的機率也就相對較小。
那 sample
是依據什麼來判斷應該用哪一個算法的呢?源碼中的判斷基於 setsize
變量,其中還有一段讓人看不懂的公式。其實這是在計算 set
所需的內存開銷,算法的實現主要考慮的是額外使用的內存,若是 list 拷貝原序列內存佔用少,那麼用洗牌算法;若是 set 佔用內存少,那麼使用記錄已選項算法。
What?竟然是根據額外佔用內存多少來判斷?這有點太難以想象了。Why?
咱們來看一下算法的時間複雜度。對於算法很渣渣的小夥伴(例如我)來講,計算算法的時間複雜度也是件挺困難的事,爲了簡單起見,我用一種簡單的方式來講明。
先說洗牌算法,時間複雜度是 O(K),這個比較好理解。那麼,對於記錄已選項算法,時間複雜度是 O(NlogN)。這個別問我是怎麼算出來的,我沒算,抄的。有興趣的小夥伴能夠自行去計算一下。
咱們來想一個簡單的,對於記錄已選項算法,若是每次選取的值剛好都沒有重複,那麼時間複雜度是多少呢?很顯然是 O(K)。那麼當 K 遠小於 N 的時候,咱們能夠認爲時間複雜度就是 O(K)。
而 sample
算法的思想就是,當 K 較 N 相對較小時,兩種算法的時間複雜度都是 O(K),則選用佔用內存較小的;當 K 較 N 相對較接近時,記錄已選項算法的時間複雜度就會高於 O(K),這時就選用洗牌算法。
只得感嘆算法真的博大精深。