地址: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/…