談談等概率不重復隨機數生成算法中的大學問


  等概率不重復的生成隨機數應該是在平時開發中常見的,也是面試中常問的基礎之一。有多種實現方式,有人人都可以想到的,也有不容易想到的巧妙算法,那么當有人問你哪個實現方式更好的時候你該怎么回答呢?回答巧妙的算法比普通算法好?答案顯而易見,首先要搞清楚應用場景和要解決的問題。這樣才能判斷一個算法或者方案的合適與否。

  接下來明確問題、提出多個解決方法,最后對比每個方法的優劣與使用場景。

  要求:

  可能有些具體的場景和問題需求都不一樣,可以統一:在一定范圍內等概率不重復的生成有限個隨機數。具體的可以定義為,在[m,n]之間等概率的生成k個不相同的隨機數。

   設計與實現:

  1.排重

  一個最簡單的想法就是先生成再排重,直到生成k個隨機數為止。把所有用到排重的算法都可以歸為一類,包括利用Map、Set、BitMap、數組下標去重的都算。因為本質上是一樣的,可能在排重的時候有些優化。

public List<Integer> random1(int m, int n, int k) {

        if (k < 1 || k > n-m+1) {
            System.out.println("Params is illegal.");
        }
        
        Random random = new Random();

        List<Integer> ret = new ArrayList<Integer>();

        while (ret.size() < k) {
            Integer rand = random.nextInt(n-m+1)+m; //生成[m,n]之間的隨機數
            if (!ret.contains(rand)) {
                ret.add(rand);
            }
        }

        return ret;
    }

 

  排重的一種改進算法,數組下標去重:

    //優化去重
    public List<Integer> random2(int m, int n, int k) {
        if (k < 1 || k > n-m+1) {
            System.out.println("Params is illegal.");
        }

        Random random = new Random();
        List<Integer> ret = new ArrayList<Integer>();
        int[] flag = new int[n-m+1];
        Arrays.fill(flag, 0);

        while (ret.size() < k) {
            Integer rand = random.nextInt(n-m+1)+m; //生成[m,n]之間的隨機數
            if (flag[rand-m] == 0) {
                ret.add(rand);
                flag[rand-m] = 1;
            }
        }

        return ret;
    }

  2.移動

  把[m,n]之間的數放到一個數組中,隨機生成一個范圍內的下標把選中的下標的值移動到最后一個,其余的向前移動。之后生成[m,n-1]范圍內的下標,依次類推,直到生成了k個隨機數。

 

    //移動
    public List<Integer> random3(int m, int n, int k) {
        if (k < 1 || k > n-m+1) {
            System.out.println("Params is illegal.");
        }

        Random random = new Random();
        List<Integer> ret = new ArrayList<Integer>();
        int[] arr = new int[n-m+1];
        int j = m;
        for (int i=0; i<n-m+1; i++) {
            arr[i] = j++;
        }

        int cur = n-m+1;

        while (cur > 0 && n-m+1-cur < k) {
            int randIndex = random.nextInt(cur);
            int randValue = arr[randIndex];
            ret.add(randValue);
            for (int i=randIndex+1; i<cur; i++) {
                arr[i-1] = arr[i];
            }
            arr[cur-1] = randValue;
            cur --;
        }

        return ret;
    }

 

  3.交換

 

  這種思路和上個移動的想法差不多,但不是再移動數組,而是交換。簡單來說就是選擇隨機生成數組下標之后和下標為i-1的交換。直到每個元素都被交換過一遍。然后可以截取這個數組的k個元素。

    //交換
    public List<Integer> random4(int m, int n, int k) {
        if (k < 1 || k > n-m+1) {
            System.out.println("Params is illegal.");
        }

        Random random = new Random();
        List<Integer> ret = new ArrayList<Integer>();
        int[] arr = new int[n-m+1];
        int j = m;
        for (int i=0; i<n-m+1; i++) {
            arr[i] = j++;
        }

        for (int i=n-m; i>=0; i--) {
            int randIndex = random.nextInt(n-m+1);
            int t = arr[randIndex];
            arr[randIndex] = arr[i];
            arr[i] = t;
        }

        for (int i=0; i<k; i++) { //截取前k個
            ret.add(arr[i]);
        }

        return ret;
    }

 

  熟悉Java的同學都知道JDK中集合工具里有個shuffle洗牌方法,其核心思想就是隨機交換。JDK-shuffle源碼:

public static void shuffle(List<?> list, Random rnd) {
        int size = list.size();
        if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=size; i>1; i--)
                swap(list, i-1, rnd.nextInt(i));
        } else {
            Object arr[] = list.toArray();

            // Shuffle array
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));

            // Dump array back into list
            // instead of using a raw type here, it's possible to capture
            // the wildcard but it will require a call to a supplementary
            // private method
            ListIterator it = list.listIterator();
            for (int i=0; i<arr.length; i++) {
                it.next();
                it.set(arr[i]);
            }
        }
    }

  比較:

  首先把問題分成幾種情況,規定以下指標:數據范圍大小amount=n-m+1,數據量大小k=[big, small],big代表接近amount,small代表更接近0。

  1.amount較小的情況下,其實差別不大,從時空復雜度上沒有區分度。

  2.amount較大的情況下,k=small。排重要好於交換和移動。因為要選擇出來的隨機數數量要比范圍小得多,這樣一來如果要交換整個范圍內的序列就會在效率上打折扣。因為在大范圍選取個別隨機數碰撞的概率較小所以排重工作就少了,這種情況下排重算法更好。

  3.amount較大的情況下,k=big。交換要好於移動和排重。因為在大范圍內生成大量的隨機數那么碰撞的幾率就會變大,而且越往后越大,試想一下,如果要在100W個數中隨機出99W個隨機數,到生成第99W個隨機數的時候碰撞率已經高達99%了。這是絕對忍受不了的。而反觀交換算法,因為k比較接近amount所以交換整個序列不會浪費太多交換次數。遍歷序列就能把整個序列等概率的shuffle一遍,然后截取k個即可。這種方法顯然要高效許多。

 

  更好的算法有待補充。。。

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM