LeetCode398-隨機數索引

doublexi 發表於 2021-12-09
LeetCode

原題連結:【398. 隨機數索引】:https://leetcode-cn.com/problems/random-pick-index/

題目描述:

給定一個可能含有重複元素的整數陣列,要求隨機輸出給定的數字的索引。 您可以假設給定的數字一定存在於陣列中。

注意:

陣列大小可能非常大。 使用太多額外空間的解決方案將不會通過測試。

示例:

int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);

// pick(3) 應該返回索引 2,3 或者 4。每個索引的返回概率應該相等。
solution.pick(3);

// pick(1) 應該返回 0。因為只有nums[0]等於1。
solution.pick(1);

解題思路:

解法1:雜湊表。

暴力解法。使用雜湊表儲存陣列中每個元素值到其所有的下標索引的對映。

即key為元素值,value為元素值對應的索引下標列表。

再用random函式,從下標列表中隨機選擇一個索引返回即可。

這個解法非常佔用額外空間O(N),如果陣列很大,很容易oom。

解法2:動態陣列儲存索引列表。

定義一個儲存索引下標的索引列表indexList,用動態陣列,方便擴充套件。

遍歷陣列,如果元素跟target目標值相等,則把索引值新增進indexList列表。

取隨機數,rand範圍為【0,indexList.length ),然後將對應位置的索引值返回即可。

概率為:1 / indexList.length

這裡相比上一種解法,已經降了一個數量級,但是如果N很大,或者數字分佈不均勻,indexList也可能非常大,也非常佔用額外空間,空間消耗也可能會達到O(N),可能會oom。

解法3:蓄水池抽樣演算法。

演算法是:要從超大陣列N中取k個元素,這裡是取一個元素,即K=1

那麼每次只取一個樣本值,那麼逐個遍歷陣列,當遇到第i個數時,就以 1 / i 的概率抽取它做樣本值,以(i - 1)/ i 的概率保留原來的樣本值,不替換。

我們不需要儲存索引下標列表,只需要一個變數cnt計數,統計匹配到了多少個相同的數字,方便後去取隨機數,算概率。

遍歷陣列,如果元素跟target目標值相等,則cnt++,計數器+1。

然後取隨機數,rand範圍為【1,cnt】。

因為這裡只取一個數,就是K=1的案例。

所以如果rand == 1,則取當前元素的下標,替換為結果值。

如果rand > 1,則保留之前的結果,繼續往下走。知道結束返回結果值。

特點:這裡只需要遍歷一遍陣列,而且沒有額外的空間消耗,只有一個計數器變數cnt。空間複雜度為O(1)。(非常推薦)

題解程式碼:

程式碼如下:(java版)

//給定一個可能含有重複元素的整數陣列,要求隨機輸出給定的數字的索引。 您可以假設給定的數字一定存在於陣列中。 
//
// 注意: 
//陣列大小可能非常大。 使用太多額外空間的解決方案將不會通過測試。 
//
// 示例: 
//
// 
//int[] nums = new int[] {1,2,3,3,3};
//Solution solution = new Solution(nums);
//
//// pick(3) 應該返回索引 2,3 或者 4。每個索引的返回概率應該相等。
//solution.pick(3);
//
//// pick(1) 應該返回 0。因為只有nums[0]等於1。
//solution.pick(1);
// 
// Related Topics 水塘抽樣 雜湊表 數學 隨機化 
//  125  0


import java.util.Random;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {

    // 需要把nums傳遞給pick方法,所以我們需要定義一個類變數
    int[] nums;
    public Solution(int[] nums) {
        this.nums = nums;
    }
    
    public int pick(int target) {
        // 定義一個變數,儲存結果值
        int result = 0;
        // 定義一個變數cnt,計數器,標記匹配到了幾個相同的數字,方便後面取隨機數範圍
        int cnt = 0;
        // 迴圈遍歷連結串列
        for (int i=0; i<nums.length; i++) {
            // 匹配跟target元素是否相等
            if (nums[i] == target) {
                // 匹配到了,則計數器+1
                cnt++;    
                // 取隨機數rand範圍為[1, cnt],注意這裡邊界是cnt
                int rand = new Random().nextInt(cnt) + 1;
                // 取每個數的概率都是 1 / cnt,也就是說有1 / cnt的概率,更新結果下標值
                if (rand == 1) {
                    // rand等於1,則取當前元素的下標,更新結果值
                    result = i;
                }
            }

        }

        // 返回結果下標
        return result;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int param_1 = obj.pick(target);
 */
//leetcode submit region end(Prohibit modification and deletion)