6

在最近的一次采访中,我被问到以下问题:

getrnd50()使用生成 1-50 的随机数的给定方法打印 1-100的随机数。每个随机数应该只打印一次并且以随机顺序打印。不允许使用其他随机数生成器,并且不允许我更改 getrnd50().

我想出了以下代码,它给出了正确的输出。

import java.util.Random;

public class Test {

public static void main(String[] args) {
    int[] rs = new int[100];
    int count = 0;
    int k;
    while (count != 100) {

        // I decided to simply multiply the result of `getrnd50()` by 2. 
        // But since that would generate only even numbers,

        k = getrnd50() * 2;

        // I decided to randomly subtract 1 from the numbers. 
        // Which i accomlished as follows.          

        if (getrnd50() <= 25) // 25 is to half the possibilities.
            k--;

        // Every number is to be stored in its own unique location 
        // in the array `rs`, the location is `(number-1)`. 
        // Every time a number is generated it is checked whether it has 
        // already been generated. If not, it is saved in its position, printed and 
        // `count` is incremented.

        if (rs[k-1] == 0) {
            rs[k-1] = k;
            count++;
            System.out.print(k + " ");
        }
    }
}
// This is the method and i am not supposed to touch it.
static int getrnd50() {
    Random rand = new Random();
    return (1 + rand.nextInt(50));
}

}

虽然它在该轮中被接受,但在下一轮中,面试官告诉我这getrnd50()是一种昂贵的方法,即使在最好的情况下,我也必须为每个生成的数字调用两次。即 1-100 次 200 次。在最坏的情况下,它将是无穷大,平均情况下是数万。他要求我优化代码,以显着改善平均情况。

当我表示无法做到时,他给了我一个提示,他说:

考虑生成新数字时生成的数字数量。例如。如果count变成 99,我不必打电话getrnd50(),我可以简单地找到剩余的号码并打印出来。

虽然我理解他的漂移,但我不知道它会对我有什么帮助,所以很明显我被拒绝了。现在我很想知道答案。帮我!提前谢谢!

注意:如果有人懒得写冗长的代码,只需指出数字生成部分,其余的很容易。我们也不一定要遵循提示。

4

7 回答 7

6

关键是不要检查你之前是否生成过号码,如果只查找剩余的1个号码会非常昂贵,而是按顺序生成1-100的号码,然后洗牌。

在您的代码中,当您从 100 个数字中生成 99 个时,您将循环生成随机数,直到找到剩余的 1 个数字。这就是为什么您的版本中的平均情况如此糟糕的原因。

相反,如果您只是对数组进行洗牌,那么您只需要拥有与洗牌操作一样多的随机数,并且只需与您需要的数字输出一样多的洗牌操作。

(有关改组的完整详细信息,请查看Fisher-Yates改组,特别是可以在适当位置生成改组数组的由内而外的变体)

要生成随机数,您需要一个变量生成器,而不是一个固定的 1-50 生成器。您可以通过多种方式解决此问题,但如果您真的希望输出在可能的状态中具有良好的分布,请非常小心地将偏斜引入结果。

例如,我建议使用整数位,并进行移位,而不是尝试使用模数。如果值超出所需范围,这确实涉及一定量的循环,但是由于无法修改原始随机数生成,您的手有些束缚。

static int bits = 0;
static int r100 = 0;

static int randomInt(int range)
{
    int ret;

    int bitsneeded = 32 - Integer.numberOfLeadingZeros(range - 1);

    do {
            while(bits < bitsneeded)
            {
                    int r = (getrnd50()-1) * 50 + getrnd50()-1;
                    if(r < 2048)
                    {
                            r100 <<= 11;
                            r100 |= r;
                            bits += 11;
                    }
            }
            ret = r100 & ((1 << bitsneeded) - 1);
            bits -= bitsneeded;
            r100 >>=  bitsneeded;
    } while(ret >= range); 

        return ret + 1;
}

此实现将为您的 100 值混洗数组使用 150 个随机数区域中的某些内容。这比模数版本差,但优于输入范围的 2 倍,这是原始版本的最佳情况。如果随机生成是真正随机的,那么仍然是无穷大的最坏情况,但随机生成通常不会那样工作。如果确实如此,我不确定考虑到约束条件,未倾斜的结果是否现实。

为了说明,由于结果很微妙,这是我建议的随机例程与模数版本的图表:

随机发生器图

所以总而言之,我认为虽然你的随机生成效率有点低,并且可以改进,但面试官正在寻找的真正巨大的胜利在于,首先不需要这么多随机数,通过洗牌而不是以不断降低的概率重复搜索。

于 2013-01-02T14:54:26.303 回答
3

因为 100 / 50 是一个整数,所以这很容易。由于 50 / (100 / 50) 是一个整数,所以它更容易。

如果你不太明白,这里有一些示例代码:

int rnd1 = getrnd50();
int rnd2 = getrnd50();
if (rnd1 % 2 == 0)
{
    rnd2 += 50;
}
return rnd2;

这是一个大纲:

  • 两个数字,在 1 到 50 之间随机选择,称为ab
  • 如果a是偶数,则将 50 添加到b
  • 返回b

如果您愿意,可以将其设为单行:

return getrnd50() + getrnd50() % 2 * 50;

不过这有点太糊涂了。

编辑:我看到这个问题真的是要求一个随机列表,而不是一个随机整数序列。

这可以通过创建一个从 1 到 100 的列表并进行 100 次随机交换来完成,例如 Fisher-Yates shuffle。我想,使用 Fisher-Yates shuffle 时,绝对最小调用次数为 93(由公式 给出ceil(log50(100!))),但使用更简单的算法,您可以使用 200。

简单的算法将涉及将 100 个元素中的每一个与 100 个中的一个随机元素进行交换。要选择的数字将使用上述生成器从 1-100 生成。

例如:

for (int i = 0; i < 100; i++)
{
    swap(i, getrnd100() - 1); // - 1 for zero base!
}

这是一些完整的代码:

int[] result = new int[100];
for (int i = 0; i < 100; i++)
{
    result[i] = i + 1;
}
for (int i = 0; i < 100; i++)
{
    int j = (getrnd50() + getrnd50() % 2 * 50) - 1;
    int tmp = result[i];
    result[i] = result[j];
    result[j] = tmp;
}
return result;

(免责声明:我不懂 Java,也没有测试过。)

最佳情况 200,最坏情况 200,平均情况 200。

于 2013-01-02T14:54:24.987 回答
3

这是您如何回答它的方法。它利用了这样一个事实,

  • 假设您使用 shuffle 来交换“卡片”的 O(n),则模数会在 shuffle 中减小。即从int[]每个值中的一个开始,然后像 Collections.shuffle() 一样对其进行洗牌。
  • 如果你调用 getrnd50() 两次,你有比你需要的更多的随机性,特别是当你有少于 50 个值可以交换时。

编辑:对于那些不熟悉 shuffle 如何工作的人,我添加了 shuffle 的代码

import java.util.*;
import java.lang.*;

class Main {
    public static void main(String... args) {
        int samples = 100;

        // all the numbers [1, 100]
        int[] nums = new int[samples];
        for (int i = 0; i < samples; i++) nums[i] = i + 1;

        for (int i = samples - 1; i > 0; i--) {
            int swapWith = nextInt(i + 1);

            // swap nums[i] and nums[swapWith]
            if (swapWith == i) continue;
            int tmp = nums[swapWith];
            nums[swapWith] = nums[i];
            nums[i] = tmp;
        }
        System.out.println("calls/sample " + (double) calls / samples);
        System.out.println(Arrays.toString(nums));

        int[] count49 = new int[49];
        for (int i = 0; i < 49 * 10000; i++)
            count49[nextInt(49) - 1]++;
        int[] count54 = new int[54];
        for (int i = 0; i < 54 * 10000; i++)
            count54[nextInt(54) - 1]++;
        System.out.println("Histogram check (49): " + Arrays.toString(count49));
        System.out.println("Histogram check (54): " + Arrays.toString(count54));

    }

    // keep track of the range of values.
    static int maxRandom = 1;
    // some random value [0, maxRandom)
    static int rand100 = 0;

    static int nextInt(int n) {
        while (maxRandom < 10 * n * n) {
            maxRandom *= 50;
            rand100 = rand100 * 50 + getrnd50() - 1;
        }
        int ret = rand100 % n;
        maxRandom = (maxRandom + n - 1) / n;
        rand100 /= n;
        return ret + 1;
    }

    static final Random rand = new Random();
    static int calls = 0;

    static int getrnd50() {
        calls++;
        return (1 + rand.nextInt(50));
    }
}

印刷

通话/样本 0.94

[1, 37, 4, 98, 76, 53, 26, 55, 9, 78, 57, 58, 47, 12, 44, 25, 82, 2, 42, 30, 88, 81, 64, 99, 16 , 28, 34, 29, 51, 36, 13, 94, 80, 66, 19, 38, 20, 8, 40, 89, 72, 56, 75, 96, 35, 100, 95, 17, 74, 69 , 11, 31, 86, 92, 6, 27, 22, 70, 63, 32, 93, 84, 71, 15, 23, 5, 14, 62, 49, 43, 87, 65, 83, 33, 45 , 52, 39, 91, 60, 73, 68, 24, 97, 46, 50, 18, 79, 48, 77, 67, 59, 10, 7, 54, 90, 85, 21, 61, 41, 3 ]

直方图检查(49):[10117,10158,10059,10188,10188,10338,9959,10313,10278,10166,9828,10159,10159,10250,10155 ,10029,10052,9996,10057,9849,9990,9914,9914,9835,10029,9738,9953,9828,9896,9931,9995,9995,9995,10034,10034,10067,10067 , 10007, 9765]

直方图检查(54):[10124,10251,10071,10071,1002,10196,10170,10123,10096,9966,9966,10225,10262,10036,10029,10029,9862,9862,9994,9994,9960 , 10118, 10163, 9986, 9988, 10008, 9965, 9967, 9950, 9965, 9870, 10172, 9952, 9972, 9828, 9754, 10152, 9943, 9996, 9779, 10014, 9937, 9931, 9794, 9708, 9978 , 9894, 9803, 9904, 9915, 9927, 10000, 9838]

在这种情况下,100 个号码需要少于 100 次调用 getrnd50

如果你有 1000 个值要洗牌

calls/sample 1.509
于 2013-01-02T15:15:21.293 回答
0

您的代码的性能损失在该行

if (getrnd50() <= 25)

您需要找到一种方法从生成的单个随机数中获取更多信息,否则您正在浪费那些昂贵的生成资源。这是我的建议:

首先假设我们将有一个随机数生成器用于数字 0-15。每个数字都可以表示为二叉树中的一条路径,其中叶子代表数字。所以我们可以说,我们true每次从根开始向左走时都会评估 if 条件。

问题是随机数生成器在不以 2 的幂次方结束的区间内生成数字。所以我们需要扩展那棵树。这样做是这样
的:如果随机数在 0-31 范围内,我们可以为这些数字创建一棵树。如果它在 32-47 范围内,我们使用 0-15 的树来表示这些,在 48-49 的情况下,我们使用树来表示数字 0-1。

因此,在最坏的情况下,我们不会使用来自该随机数的更多信息,但在大多数情况下,我们会使用。所以这应该会显着改善平均情况。

于 2013-01-02T14:54:52.243 回答
0

(1) 创建一个用 {1,...,100} 初始化的数组 A。保留此数组的可变“长度”。

(2)创建随机方法,随机生成一个从1到length的数。每次调用该方法都会调用 getrnd50() 不超过 2 次。将返回的值称为 'index'。

(3) 输出 A[index],将 A[length] 交换为 A[index] 和 length--。

(4) 重复(1)-(3),直到数组为空。

于 2013-01-02T15:19:46.193 回答
0

好的,所以您可以打印最后一个丢失的 n 组 anny 数,而不是由随机数生成器生成?

如果是这样,您可以使用递归并在每次调用时减小集合的大小,直到您只有 n=2,然后调用 getrnd50() 一次。当您递归返回时,只需在每组上打印缺失的数字。

于 2013-01-02T15:01:39.127 回答
0
 List<Integer> lint ; 

  public void init(){
      random = new Random();
      lint = new LinkedList<>();
      for(int i = 1 ; i < 101; ++i) {
          lint.add(i); // for truly random results, this needs to be randomized.
      }
  }

  Random random ; 
  public int getRnd50() {
      return random.nextInt(50) + 1;
  }

  public int getRnd100(){

      int value = 0;
      if (lint.size() > 1) {
          int index = getRnd50()%lint.size();
          value = lint.remove(index); 
      } else if (lint.size() == 1 ) {
          value = lint.remove(0);
      }      

      return value;      
  }

准确地调用 getRnd50() 99 次。它不是真正随机的,因为存储在 100 个整数列表中的数字是按顺序排列的。

于 2013-01-02T15:09:26.043 回答