地址:leetcode-cn.com/problems/ra…java
給定一個包含 [0,n) 中不重複整數的黑名單 blacklist ,寫一個函數從 [0, n) 中返回一個不在 blacklist 中的隨機整數。android
對它進行優化使其儘可能少調用系統方法 Math.random() 。git
提示:github
1 <= n <= 1000000000 0 <= blacklist.length < min(100000, N) [0, n) 不包含 n ,詳細參見 interval notation 。 示例 1:markdown
輸入: ["Solution","pick","pick","pick"] [[1,[]],[],[],[]] 輸出:[null,0,0,0] 示例 2:dom
輸入: ["Solution","pick","pick","pick"] [[2,[]],[],[],[]] 輸出:[null,1,1,1] 示例 3:函數
輸入: ["Solution","pick","pick","pick"] [[3,[1]],[],[],[]] 輸出:[null,0,0,2] 示例 4:oop
輸入: ["Solution","pick","pick","pick"] [[4,[2]],[],[],[]] 輸出:[null,1,3,1] 輸入語法說明:優化
輸入是兩個列表:調用成員函數名和調用的參數。Solution的構造函數有兩個參數,n 和黑名單 blacklist。pick 沒有參數,輸入參數是一個列表,即便參數爲空,也會輸入一個 [] 空列表spa
題目的意思仍是很是明確,可是在看到示例的時候倒是難以理解:這裏在簡單說明一下。
輸入: ["Solution","pick","pick","pick"] [[4,[2]],[],[],[]] 輸出:[null,1,3,1] 解釋:意思是會調用一次 Solution 構造函數 三次pick函數;調用 Solution 構造函數時長度的參數是 4,[2];調用pick時未傳參數。
說實話我在拿到題目的時候只能想到第一種解題方法,不知道大家能想到那些呢?
若是咱們有了白名單(即黑名單以外的全部整數),那麼咱們就能夠在白名單中隨機選取整數並返回了。
咱們首先在集合中放入 [0, N) 中的全部整數,隨後移除全部在黑名單中出現過的數,並把剩下的數放入列表中,就獲得了白名單。
leetcode官方代碼:
class Solution {
List<Integer> w;
Random r;
public Solution(int n, int[] b) {
w = new ArrayList<>();
r = new Random();
Set<Integer> W = new HashSet<>();
//現將全部數據加入到白名單中
for (int i = 0; i < n; i++) W.add(i);
//將黑名單中的數移除
for (int x : b) W.remove(x);
//構建真正的白名單
w.addAll(W);
}
public int pick() {
return w.get(r.nextInt(w.size()));
}
}
複製代碼
官方題解:
問題:
帶着上面的問題咱們來嘗試本身寫二分查找。
理解 c = B[mid] - mid 在總名單T 上 T[mid] = mid 就像官方圖上同樣 B[1] = 2; 2-1 = 1 即B[1] 前能夠插入一個白名單數
咱們知道在這個位置二分查找結束的標誌是lo==hi 即二分查找的高低位相等,那麼咱們怎麼肯定第K個白名單數是落在總名單上B[lo]的左側仍是右側呢?
1.在條件可能的狀況下 k落在黑名單左側,特殊狀況黑名單的最大數都比第k個白名單小
注意下下面的話可能不是太好理解: 第h黑名單前能個插入白名單總數 preCountW 個,在總名單上第h個黑名單數的左側第一個就是第 preCountW -1 個白名單數,那麼第k個白名單就是第h個黑名單的值 - (preCountW - k ) ;若是處於特殊狀況 那麼 第k個白名單 = 第h個黑名單的值 + k - preCountW
代碼以下:
/** * * k在總名單上落在 h的左側 * */
private int pick2fenLeft(int k) {
System.out.println("pick2fen start "+k);
if (blacklistLength == 0) {
return k;
}
int l = 0, h = blacklist.length - 1;
while (l != h) {
int mid = (l + h) / 2;
//blacklist[mid]-mid 的含義是 當前黑名單中的數據 - 黑名單順序數 = 當前位置以前能夠插入的白名單個數
int preCountW = blacklist[mid] - mid;
if (preCountW >= k+1) {//第k個實際有k+1個數 mid 是包含的
h = mid;//由於 此時blacklist[mid]前確定包含 k+1個白名單,可是不能肯定 blacklist[mid-1]的狀況,所以h不能減小
} else {
l = mid+1;//向上擡,放在左側
}
}
int preCountW = blacklist[h] - h;
System.out.println("pick2fen end l = " + l + " h= " + h+" preCountW ="+preCountW +" k = "+k);
if (preCountW >= k+1) {//落在左側
//在h以前共有白名單的個數
// preCountW -(k+1) 爲當前位置距離第k個空白元素的間距 那麼第k個的位置在哪裏呢?k的座標從0開始
// 第k個到 第preCountW-1個的間距是 preCountW-1 -k ;總間距是 preCountW -k;
// blacklist[h] - preCountW +k =h + preCountW - preCountW + k
return h+k;
} else {
//h + preCountW + (k - preCountW)
return h+k+1;
}
}
複製代碼
2.條件可能的狀況下k落在黑名單右側,特殊狀況黑名單的最小數都比第k個白名單大
/** * * k在總名單上落在 h的右側 * */
private int pick2fen(int k) {
System.out.println("pick2fen start "+k);
if (blacklistLength == 0) {
return k;
}
int l = 0, h = blacklist.length - 1;
while (l != h) {
int mid = (l + h+1) / 2;
//blacklist[mid]-mid 的含義是 當前黑名單中的數據 - 黑名單順序數 = 當前位置以前能夠插入的白名單個數
try {
int preCountW = blacklist[mid] - mid;
if (preCountW >= k+1) {//第k個實際有k+1個數 mid 是包含的 此時k在黑名單mid的左側
h = mid-1;//向下減放在右側
} else {
l = mid;
}
}catch (Exception r){
System.out.println("mid is "+mid);
throw r;
}
}
int preCountW = blacklist[h] - h;
System.out.println("pick2fen end l = " + l + " h= " + h+" preCountW ="+preCountW +" k = "+k);
if (preCountW >= k+1) {//preCountW >= k+1 保證落在第h個黑名單數的左側
// 黑名單左側第一個是第 preCountW -1 個白名單 其值 = blacklist[h]-1
// 第k個白名單 = blacklist[h]-1 - (preCountW -1 - k) = h + preCountW - 1 - preCountW + 1 + k = h + k;
return k+h;//此時 h = 0
} else {//此時最大的黑名單數也比白名單小。那麼第k個白名單 = 黑名單數的總個數+k +1 (由於k和黑名單總數h都是從0開始,因此要加1)
return h+k+1;
}
}
複製代碼
能夠看到不論咱們默認選取k落在黑名單的左側仍是右側,他們的最終返回值都是同樣的。
**特別注意:**l=mid+1的時候 默認k落在黑名單左側;h = mid - 1的時候k落在黑名單右側。可是這兩種狀況下mid的起算方式倒是不同的他們不能互換。
黑名單映射相對二分查找理解起來很是簡單:總長度爲n 白名單長度爲wl 黑名單長度爲bl; wl + bl = n;咱們能夠有下面的結論:在總名單上前wl個數中有m個黑名單數(m<=wl)那麼在後[wl,n)必有m個白名單數
leetcode官方代碼:
class Solution {
Map<Integer, Integer> m;
Random r;
int wlen;
public Solution(int n, int[] b) {
m = new HashMap<>();
r = new Random();
wlen = n - b.length;
Set<Integer> w = new HashSet<>();
//注意這裏是從wlen開始的
for (int i = wlen; i < n; i++) w.add(i);
for (int x : b) w.remove(x);
Iterator<Integer> wi = w.iterator();
for (int x : b)
if (x < wlen)
m.put(x, wi.next());
}
public int pick() {
int k = r.nextInt(wlen);
return m.getOrDefault(k, k);
}
}
複製代碼
代碼參考地址:github.com/xiaolutang/…