今天在工做中遇到這樣一個問題:給定1個矩形,左下角的點point(x, y),長w,高h,要在這個矩形裏隨機出n個不一樣的點用來種怪。這個算法該怎麼寫呢?這對於我來講確實成爲了一個問題。圖示以下:算法
因爲任務時間緊,作的又是Demo的緣由,我不假思索的寫出了下面這個算法:數組
//從[from, to]區間中隨機一個整數 function randomInt (from, to) { //方法實現就不寫了 }
function randomCoords (point, w, h, n) { //目的數組 var coords = []; var map = {}; var count = n; if ((w + 1) * (h + 1) < n) { count = (w + 1) * (h + 1); } while (count > 0) { var x = randomInt(point.x, point.x + w); var y = randomInt(point.y, point.y + h); //已經隨機出該點,就丟掉 if (map[x] && map[x][y]) continue; if (!map[x]) map[x] = {}; map[x][y] = true; coords.push({x:x, y:y}); count --; }
return coords;
}
這個算法着實太笨了,當時也沒有細想。等把任務完成了,我發現離下班的時間還有很長一段時間。由於制度上要求要加班,我只能忍了。我想,若是被別人看到我寫的這麼一個垃圾算法,那他確定認爲我實在太Low了。因此,我開始認真思索這個問題。服務器
先不考慮優化的算法該如何實現,我想先了解一下這個算法到底有多Low。我假設了一種狀況,長和高都是5,而後把全部點都隨機出來,須要的平均次數是多少呢?答案是:5/5 * 5/4 * 5/3 * 5/2 * 5/1 = 26.04166667。還好,不是不少次,還能承受。而後我把5改爲n作成一個圖表,圖表以下:dom
當n=10的時候,就須要循環2000屢次;而當n=12的時候,就得須要循環18000屢次了。圖表是否是很嚇人,我反正是驚呆了。這絕對不行,若是是作項目的時候要這樣寫,我確定已經被老闆罵死了。優化
那該怎麼改呢?我立刻就想到了一個改進的方法。若是要隨機的點數超出一半點數的話,我就隨機出不用的點來,那麼剩下的點就是我要的點了。這個想法其實很好,可是要結合上面的算法用的話,其實還不是很好。spa
那麼到底該怎麼實現呢?我開始從問題本質考慮。通常狀況下,隨機分爲兩種,放回抽樣隨機和不放回抽樣隨機,它們隨機所須要的次數都是n次,只不過不放回抽樣須要把已經隨機出來的元素從樣本庫裏拿出來。code
很顯然,這個問題是不放回抽樣隨機。那麼又該怎麼把已經隨機出來的元素從樣本庫裏拿出來呢?有一種代碼裏經常使用的方法,就是構建一個樣本庫的數組,而後再從數組裏把那個元素拿出來。這樣的確能夠避免隨機次數成近似指數級增加。它構建樣本庫數組的增加曲線是平方級,通常狀況下是服務器能夠接受的。可是若是要在手機等移動設備上執行的話,咱們就要精益求精了。那麼有沒有更好的算法呢?固然有。blog
我就不廢話了,說一下我最終是如何實現的:io
一、求出矩形中所擁有的點數totalNum;function
二、在[0, totalNum-1]區間中隨機出一個數字來,而後把該數字按必定規則對應到矩形中的某個點上,而後totalNum減1;
三、隨機n次,獲得全部隨機點。
這裏最關鍵的是第二步,要實現它須要作到以下兩點:
一、指定數字到(x,y)座標的映射規則。我採用的是x=point.x+rand%(w+1); y=point.y+Math.floor(rand/(w+1));
二、設置map,其key值是已經隨機出的數字,其value值是最近一次隨機出該數字時隨機區間的末尾數字,即totalNum-1。我管這種方法叫尾數置換。其圖示以下:
廢話很少說了,代碼呈上:
function randomCoords (point, w, h, n) { var coords = []; var map = {}; var totalNum = (w + 1) * (h + 1); var num = n; if (totalNum < n) { num = totalNum; } while (coords.length < num) { //在區間[0, totalNum-1]區間裏隨機 var randRaw = utils.random_int(0, totalNum - 1); //求出有效值並計算出座標值 var rand = map[randRaw] === undefined ? randRaw : map[randRaw]; var x = point.x + rand % (w + 1); var y = point.y + Math.floor(rand / (w + 1)); coords.push({x:x, y:y}); //更新置換尾數和區間 totalNum --; map[randRaw] = totalNum; } return coords; }
這個算法的時間複雜度是O(n)級的,遠小於前面兩種算法。並且它還能夠優化,思路前面已經給出來了,就是當要隨機的點數超出總點數一半的時候,就隨機出不須要的點數,而後剩下的點就是所要求的點了。思路很簡單,代碼我就不在這裏寫了。