30 从n个数中随机获取m个数字

前言

本博文部分图片, 思路来自于剑指offer 或者编程珠玑

问题描述

从给定的n个数中随机抽取m个数字
不知道 这道题目属不属于”编程珠玑”中的, 我找了一下 似乎没有找到

思路

思路一 : 总共需要找到m个数字, 找第一个数字 : 生成一个[0, n)的随机数k 获取该位置的数据; 找第二个数字 : 生成一个[0, n-1)的随机数k2, 那么 这时候就应该排除第一次找到的k对不对, so 如果k2>=k, 则令k2++, 这样就保证了在[0, k-1], [k+1, n)两个区间之间找到另外的一个随机数值
其他的数字 依次类推

思路二 : 从第1个元素 遍历到第n个元素, 找到当前元素的概率为 : (还剩下需要找的元素的个数 / 剩下的元素总数)
获取到第一个元素的概率为 : m / n
获取到第二个元素的概率为 : ( (m-1)/(n-1) * (m/n) ) + (m/(n-1) * (n-m/n) ) = (m^2-m + mn-m^2) / (n * (n-1) ) = m / n
其他的元素 依次类推

思路三 : 思路三可以说是从实现上来说 是最简单的了, shuffle一下给定的序列, 然后 获取前m位[规则 可自定义, 因为现在随机化了嘛]即可

参考代码

/**
 * file name : Test10SelectMFromN.java
 * created at : 11:11:40 AM Jun 22, 2015
 * created by 970655147
 */

package com.hx.test06;

public class Test10SelectMFromN {

    // 随机的从n个数中选择m个数
    public static void main(String []args) {

        int range = 200;
        int n = 10, m = 4;
//      int[] arr = Tools.newIntArray(range, n);
        int[] arr = new int[]{0,  1,  2,  3,  4,  5,  6,  7,  8, 9 };
        Log.log(arr);
        long start = System.currentTimeMillis();

        selectMFromN01(arr, n, m);
        selectMFromN02(arr, n, m);
        selectMFromN03(arr, n, m);

        long spent = System.currentTimeMillis() - start;
        Log.log("spent : " + spent + " ms ...");
    }

    // random
    static Random ran = new Random();

    // 随机的从n个数中选择m个数
    // sleectedIdx 中包含的是已经选择了的数据的索引
    // selected 中包含的是选取的数据的索引
    // 思路 : 循环m次, 每次ran生成一个arr.length - i, 的数据的随机索引
        // 然后, 开始和已经选择了的数的索引进行比较, 如果这个随机数大于等于val 则selected++ [表示该位置的数据已经被选择了, 类似于第一次是从n个元素中找出一个k, 第二次是从n-1个元素中找出一个k2, 如果k2大于等于k, 则将k2的索引向后移动一位, 确保能够将k2移动到正确的位置]
            // 直到selectedIdx中某一个数据小于selected
        // 最后的 select即为当前循环所选取的索引
    // selecteIdx是用TreeSet维护的一个有序的集合[必需确保其有序]
    // 此方法 适合于m比较小的情况
    // 如果m太大的情况下, 可以转换一种思路, 选取(n-m)个数  作为排除的数
    public static void selectMFromN01(int[] arr, int n, int m) {
        Set<Integer> selectedIdx = new TreeSet<>();
        for(int i=0; i<m; i++) {
            int select = ran.nextInt(n - i);

            Iterator<Integer> it = selectedIdx.iterator();
            while(it.hasNext() ) {
                int val = it.next();
                if(select >= val) {
                    select ++;
                } else {
                    break ;
                }
            }

            selectedIdx.add(select);
        }

        for(Integer idx : selectedIdx) {
            Log.logWithoutLn(arr[idx] + " ");
        }
        Log.enter();
    }

    // 思路 : 从n-i 中获取一个随机值, 如果该值小于m, 则当前索引对应的数据加入到selected中, 然后  更新m
    // m减到了0, 则说明获取到的数据  已经够了, 后面则获取不到数据了, 因为(0, n-i)中没有比0小的
    // 获取第一个数据的概率为 m / n
        // 如果获取到第一个数据   则获取到第二个数据的概率为 m / (n-1)
            // 否则   获取第二个数据的概率为 m / (n-1)
        // 其他数据 一次类推..
    public static void selectMFromN02(int[] arr, int n, int m) {
        List<Integer> selected = new ArrayList<>(m);
        for(int i=0; i<n; i++) {
            int select = ran.nextInt(n - i);
            if(select < m) {
                selected.add(arr[i]);
                m --;
            }
        }

        Log.log(selected );
    }

    // 思路 : shuffle 前m个元素   然后 前m个元素即为所求
    // 此方法效率比较高
    public static void selectMFromN03(int[] arr, int n, int m) {
        List<Integer> selected = new ArrayList<>(m);
        for(int i=0; i<m; i++) {
            Tools.swap(arr, i, ran.nextInt(arr.length));
        }

        for(int i=0; i<m; i++) {
            selected.add(arr[i]);
        }

        Log.log(selected);
    }

}

效果截图

这里写图片描述

总结

没想到思路二居然是正确的吧 ?
确实 这种看起来有时候感觉挺玄乎的东西, 确实是可以通过证明来证实其是正确的

思路三, 相比于思路一来说 则是大道至简的,
思路一 千幸万苦向随机找到剩余的备选数据中的一个, 然而 思路三, 却不用理会, 因为shuffle之后, 你根本不知道 “谁是谁”

注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!

以下是基于 C++ 的猜数字游戏代码: ``` #include <iostream> #include <cstdlib> #include <ctime> #include <cstring> using namespace std; int main() { const int MAX_N = 10; // N 的最大值 const int MAX_M = 10; // M 的最大值 int n, m, a[MAX_N], b[MAX_N]; char s[MAX_N]; // 随机生成 N 位数字 srand((unsigned)time(NULL)); n = rand() % MAX_N + 1; for (int i = 0; i < n; i++) a[i] = rand() % 10; cout << "猜数字游戏开始!" << endl; cout << "请输入一个 " << n << " 位数字(每位数字之间用空格隔开):" << endl; // 获取用户输入的 N 位数字 for (int i = 0; i < n; i++) cin >> b[i]; // 开始游戏 for (int i = 1; i <= MAX_M; i++) { int A = 0, B = 0; // 获取用户猜测的 N 位数字 cout << "请输入你猜测的 " << n << " 位数字(每位数字之间用空格隔开):" << endl; for (int j = 0; j < n; j++) cin >> b[j]; // 判断猜测结果 for (int j = 0; j < n; j++) if (b[j] == a[j]) A++; else for (int k = 0; k < n; k++) if (b[j] == a[k]) B++; cout << "第 " << i << " 次猜测结果为 " << A << "A" << B << "B" << endl; // 猜结束游戏 if (A == n) { cout << "恭喜你,猜了!" << endl; return 0; } } // 次用完结束游戏 cout << "很遗憾,次用完了,游戏结束!" << endl; return 0; } ``` 运行程序后,首先会要求用户输入一个 N 位数字,然后程序会随机生成一个 N 位数字作为待猜测数字。接着,程序会要求用户输入猜测的 N 位数字,然后根据用户猜测的数字与待猜测数字的匹配情况输出本次猜测结果。一共有 M 次机会猜测,若猜则游戏结束,若 M 次机会用完则游戏也结束。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值