這道題應該算是我原創的的一道題,來源於我遇到的一個具體需求。大體需求是已知一批數和每一個數出現的次數,而後寫個接口,每次調用都能返回已知數據中的某個數,且返回的機率和原始數據中每一個數出現的機率一致,題目描述起來有些繞口,咱們來舉個實際的例子。
以上面的輸入爲例,要求實現的接口必須以11.96%的機率返回五、18.10%的機率返回91……16.55%的機率返回98,固然個人要求不單單是這幾個數,而是可能有10^5個數。 先別急着往下看,給你幾分鐘先思考下。java
各類語言其實都內置了random函數,能夠隨機返回int或者long型的隨機數,這裏咱們先不考慮溢出的問題。爲了方便講解,假設咱們已有n個數存在在num[n]中,其出現的頻次存放在fre[n]中。 藉助已有的random(),咱們很簡單就能夠生成0-n之間的一個隨機數i,可是若是直接返回num[i]的話,每一個數返回的機率是一致的,明顯不知足咱們的需求。dom
其實解決方案也很簡單,咱們按照每一個數出現的頻次大小,將其映射成不一樣的區間大小,出現的機率越大,區間越大。想象下,這些數據按不一樣的區間大小把一個飛鏢盤分紅不一樣的部分,咱們生成數的時候就是拿個飛鏢隨機扎,扎到哪一個算哪一個。
固然咱們能夠直接用一位直線區間描述上面的二維飛鏢盤模型。只須要隨機生成0-100%之間的數便可,假設某次隨機生成的數是0.65(65%),咱們算一下 正好對應在數字58對應的區間上,因此此次直接返回58就是了,咱們能夠開始寫代碼了。
函數
int[] num; // 數字 int[] fre; // 出現的頻次 double[] pro; // 出現的機率 int n; // 數據量 void init() { int sum = 0; for (int i = 0; i < n; i++) { sum += fre[i]; } for (int i = 0; i < n; i++) { pro[i] = fre[i]/sum; // 計算出每一個數出現的機率 } } int getRandom() { double rp = random.getNextDouble(); double sum = 0; for (int i = 0; i < n; i++) { if (sum >= r && sum + pro[i] > rp) { //找到命中的區間 return num[i]; } sum += pro[i]; } return num[n-1]; }
彷佛一切都很完美,但每次getRandom()的時間複雜度是O(n),大量的使用性能也抗不太住。有沒有更好的實現方式?既然寫到這裏了,必然是有的。性能
上面代碼循環中有個sum += pro[i]; 每次計算都要累加,咱們是否是能夠提早在init()中累加好?而後你會發現由於每次累加的數都只正數,因此pro是個遞增序列,對於有序序列的查找 二分必然是首選。這時候咱們能夠用二分重寫上面代碼。.net
int[] num; // 數字 int[] fre; // 出現的頻次 double[] pro; // 出現的機率 int n; // 數據量 void init() { int sum = 0; for (int i = 0; i < n; i++) { sum += fre[i]; } for (int i = 0; i < n; i++) { pro[i] = fre[i]/sum; // 計算出每一個數出現的機率 if (i != 0) { pro[i] += pro[i-1]; } } } int getRandom() { double rp = random.getNextDouble(); int l = 0; int r = n-1; while (l != r) { // 二分查找肯定區間位置 int mid = (l + r) >> 1; if (pro[mid] < rp) { l = mid + 1; } else { r = mid; } } return num[n-1]; }
到這裏問題就完全解決了,可是最後給你們留下一個思考題。code
上述代碼中pro[]的計算有必要嗎? 可否直接用fre[]替代其功能?
本文來自https://blog.csdn.net/xindooblog