LeetCode刷題筆記710黑名單中的隨機數------我終於弄懂了官方解題思路

地址: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時未傳參數。

官方三種解題方法解析

說實話我在拿到題目的時候只能想到第一種解題方法,不知道大家能想到那些呢?

解題方法1:維護白名單

若是咱們有了白名單(即黑名單以外的全部整數),那麼咱們就能夠在白名單中隨機選取整數並返回了。

咱們首先在集合中放入 [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()));
    }
}
複製代碼

解題方法2:二分查找法

官方題解:

二分法官方題解.png

問題:

  1. 爲何 mid = (lo + hi + 1) / 2 而不是 mid = (lo + hi ) / 2
  2. c = B[mid] - mid 表示 在總名單上 黑名單前面能夠插入的白名單數量(總名單能夠理解成[0,n)的列表)。爲何c是和k比較
  3. 爲何二分法查找結束以後會出現官方的現象

帶着上面的問題咱們來嘗試本身寫二分查找。

理解 c = B[mid] - mid 在總名單T 上 T[mid] = mid 就像官方圖上同樣 B[1] = 2; 2-1 = 1 即B[1] 前能夠插入一個白名單數

咱們知道在這個位置二分查找結束的標誌是lo==hi 即二分查找的高低位相等,那麼咱們怎麼肯定第K個白名單數是落在總名單上B[lo]的左側仍是右側呢?

二分查找k掉落位置問題.png

第k個白名單數據的計算方式

二分法計算.png

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的起算方式倒是不同的他們不能互換。

解題方法3:黑名單映射

黑名單映射相對二分查找理解起來很是簡單:總長度爲n 白名單長度爲wl 黑名單長度爲bl; wl + bl = n;咱們能夠有下面的結論:在總名單上前wl個數中有m個黑名單數(m<=wl)那麼在後[wl,n)必有m個白名單數

黑名單映射,如何映射到長度以外.png

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/…

相關文章
相關標籤/搜索