1. 程式人生 > >你真的懂隨機嗎——shuffle一個簡單實用的隨機演算法

你真的懂隨機嗎——shuffle一個簡單實用的隨機演算法

正經學徒,佛系記錄,不搞事情

隨機數的定義:

其實隨機數是分為隨機數和隨機數的

所謂真隨機數,根據百科的解釋,真隨機數必須依賴於物理現象,例如大樂透,拋硬幣,公路上第五輛過來的車是單號還是雙號

而偽隨機數又區分為強偽隨機和弱偽隨機

弱隨機數:首先要滿足是隨機的,但容易找出隨機的規律性

強隨機數:首先要滿足是隨機的,且無法預測規律

綜上所述,平時專案中用純演算法實現的隨機其實都屬於偽隨機數,跟是否使用種子等因素無關。而真隨機數的生成,聽聞有人使用滑鼠移動的軌跡,排風扇的噪音大小等物理現象來控制隨機數的生成。

實戰:
這裡以qq音樂為例(誰讓網易雲下架了杰倫的歌),分析qq音樂的隨機播放功能
假設有ABCDEF六首歌,隨機播放功能有以下規律:

  1. 從播放第一首音樂開始,點選下一首,接下來的五首歌都不會重複,即隨機播放在一輪播放內音樂是不會重複的
  2. 從播放第一首音樂開始,點選上一首,接下來的五首歌都不會重複,即隨機播放在一輪播放內音樂是不會重複的
  3. 通過上下切換音樂,在六首歌的範圍內,已經播放過的音樂,順序是固定的,當再進行一次切換時,歌曲是隨機的,且回到規律1、2

通過上述規律,可以推測出一種隨機的過程:
當播放第一首音樂開始,後臺將會把所有歌曲的順序打亂,並儲存歌曲的順序,如當前歌單順序是ABCDEF,此時播放音樂C,後臺會打亂音樂的順序:CDABFE,
且C和E收尾相連,形成一個環狀,此時點選下一首的順序將會是DABFE,點選上一首

的順序是EFBAD,上下切換的順序將會是DABADCEFEFB。
當播放的音樂最後一首是B時,此時點選下一首將不會是C,而是重新排序的一個數組,同理當播放的音樂最後一首是D時,此時點選上一首將不會是C,而是重新排序的一個數組。
知道隨機的過程後,問題就落在了怎麼去隨機歌單,這裡就涉及到了shuffle洗牌演算法
java的Collections類已經內建提供了shuffle演算法,程式碼如下

    private static Random r;    
    public static void shuffle(List<?> list) {
        Random rnd = r;
        if (rnd == null)
            r = rnd = new Random(); // harmless race.
        shuffle(list, rnd);
    }
    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();
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));
            ListIterator it = list.listIterator();
            for (int i=0; i<arr.length; i++) {
                it.next();
                it.set(arr[i]);
            }
        }
    }
    public static void swap(List<?> list, int i, int j) {
        final List l = list;
        l.set(i, l.set(j, l.get(i)));
    }
    private static void swap(Object[] arr, int i, int j) {
        Object tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

可見這裡過載了兩個shuffle方法,如果不傳遞Random引數,則預設使用當前時間作為種子
接下來對集合進行替換,這裡的洗牌演算法是直接對集合本身進行替換,如ABCDEF,首先從F開始,隨機與六位中的某一位替換位置,之後是E,與五位中的某一位替換位置,具體與哪一位通過Random的隨機數來決定。

最後就是實現播放器隨機播放功能的java程式:

建立一個實體類 MusicOrder,用於模擬儲存歌單順序等資訊,實際專案應持久化到資料庫中,如redis等。

    //歌單
    List<String> order = new ArrayList<>(Arrays.asList("NO1.安靜", "NO2.手寫的從前", "NO3.以父之名", "NO4.一路向北", "NO5.你好嗎"));
    //當前播放下標
    private int currentPlayIndex = 0;
    //最上一首的下標
    private int prevIndex = 0;
    //最下一首的下標
    private int nextIndex = 0;

建立一個工具類MusicShuffleUtil,用於打亂歌單順序,切歌,播放。

    public static MusicOrder musicOrder = new MusicOrder();
    public static void startMusic(String name){
        //獲取歌單
        List<String> order = musicOrder.getOrder();
        if(name != null && name.length()>0){
            int index = order.indexOf(name);
            order.remove(index);
            //洗牌演算法打亂除當前音樂的其它音樂順序
            Collections.shuffle(order);
            //將當前播放的音樂置為第一位
            order.add(0, name);
        }else{
            //洗牌演算法打亂全部順序
            Collections.shuffle(order);
            name = order.get(0);
        }
        //清零
        musicOrder.setCurrentPlayIndex(0);
        musicOrder.setNextIndex(0);
        musicOrder.setPrevIndex(0);
        System.out.println("洗牌後的歌曲順序:"+musicOrder.getOrder());
        //播放
        playMusic(name);
    }

    public static void nextMusic(){
        int currentPlayIndex = musicOrder.getCurrentPlayIndex()+1;
        if(currentPlayIndex==musicOrder.getOrder().size()){
            currentPlayIndex -= musicOrder.getOrder().size();
        }
        //當前下標大於下一首的最大下標則修改最大下一首下標
        if(currentPlayIndex>musicOrder.getNextIndex()){
            musicOrder.setNextIndex(currentPlayIndex);
        }
        //歌單已播完,從新重新整理歌單順序
        if(currentPlayIndex == musicOrder.getPrevIndex()){
            startMusic("");
        }else{
            musicOrder.setCurrentPlayIndex(currentPlayIndex);
            //播放
            playMusic(musicOrder.getOrder().get(currentPlayIndex));
        }
    }

    public static void prevMusic(){
        int currentPlayIndex = musicOrder.getCurrentPlayIndex()-1;
        if(currentPlayIndex<0){
            currentPlayIndex += musicOrder.getOrder().size();
        }
        //當前下標小於上一首的最小下標則修改最小上一首下標
        if(currentPlayIndex<musicOrder.getPrevIndex()){
            musicOrder.setPrevIndex(currentPlayIndex);
        }
        //歌單已播完,從新重新整理歌單順序
        if(currentPlayIndex == musicOrder.getNextIndex()){
            startMusic("");
        }else{
            musicOrder.setCurrentPlayIndex(currentPlayIndex);
            //播放
            playMusic(musicOrder.getOrder().get(currentPlayIndex));
        }
    }

    public static void playMusic(String name){
        //播放音樂操作
        System.out.println("播放:"+name);
    }

測試:以NO3音樂開始隨機播放,控制輸入1切換上一首,其它切換下一首

    public static void main(String[] args) {
        //開始播放
        MusicShuffleUtil.startMusic("NO3.以父之名");
        Scanner sc=new Scanner(System.in);
        while(sc.hasNext())
        {
            if("1".equals(sc.next())){
                MusicShuffleUtil.prevMusic();
            }else{
                MusicShuffleUtil.nextMusic();
            }
        }
    }

結果:

專案地址:

https://pan.baidu.com/s/1cFrSBPZv4eh6NV8LpV1ZPg 提取碼: xbaq