如何在指定范围内生成随机数但排除某些数?

18

基本上我会在0-24之间随机选择一个数字:

Math.floor(Math.random() * myArray.length); // myArray contains 25 items
假设生成的数字为8,现在我想要在同样的0-24范围内获取另一个数字,但这一次,我不想要8。下一次,我可能会得到15。现在我想要再次随机,但我不想要8或15。目前我处理的方法是使用do while循环,如果生成的数字与之前相同,我就重新生成。这是我的一项作业的一小部分,实际上,我已经成功地满足了所有要求,所以我想正确地写出来,不会出错。

请参考以下链接:https://dev59.com/LUrSa4cB1Zd3GeqPYbCY#1858800 - paxdiablo
请参见以下链接:https://dev59.com/-nI_5IYBdhLWcg3wF_B3 - Jeffz
11个回答

24

将所有值都设置到一个数组中 (仅当您只使用小数字时,如示例中的25时,此选项才有效),像这样:

var array = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24];

接着,从0到数组长度之间选择一个随机数:

var num = Math.floor(Math.random() * array.length);

从数组中移除该索引号:

var roll = array.splice(num, 1);

Javascript的splice()方法可以从数组中移除索引项并将该项作为一个新数组返回。非常适合您的需求。

获取从roll中的第一个索引,因为我们只切掉了一个:

var yourNumber = roll[ 0 ];

你可以按照需要进行多次滚动。此外,您可能希望将原始数组作为副本存储,以便可以轻松“重置”数字。


谢谢。我认为你有一个错误,因为你使用了floor函数,所以不需要从长度中减去1。如果我错了,请告诉我,因为我有一些代码可以修复 =P - Pete
@Pete,你说得对。我对Math.random()的返回值有点模糊。我减去了1,因为可能会出现Math.random()返回“1”的情况。如果它从来没有返回过“1”,那么是的:减去一会引入一个错误。 - rockerest
@rockerest 是的,定义是它返回0-1之间的数字,所以我想这意味着它永远不可能完全是0或1。 - Pete
@Pete 看起来你是对的。Math.random() 的最大值小于1。我已经四处寻找,关于那个数字是多少的最佳答案是“小于1”。为什么这是一个如此重要的秘密呢?无论如何,我已经将答案回滚到我的初始响应。 - rockerest
1
@Pete:差不多,但它可以是零。具体来说,Math.random()返回一个范围在[0,1)之间的数字。没有确切的上限(可能是因为使用的确切算法取决于实现),但你基本上可以假设它是1以下的下一个浮点数。 - Matthew Crumley
显示剩余2条评论

6

这很简单。你不需要使用递归来解决这个问题。这些答案都很糟糕。理想情况下,你也不应该硬编码数组。

function getRandomWithOneExclusion(lengthOfArray,indexToExclude){

  var rand = null;  //an integer

    while(rand === null || rand === indexToExclude){
       rand = Math.round(Math.random() * (lengthOfArray - 1));
    }

  return rand;
}

现在使用上述函数返回的值来选择您想要的任何数组中的元素,就像这样:
var arr = [];
var random = getRandomWithOneExclusion(arr.length,5);  //array has length x, we want to exclude the 5th element
var elem = arr[random];

就是这样。如果您想要排除多个值,那么您需要使它更加复杂,但是对于排除一个值,这个方法很有效。递归解决方案过于复杂且不可取。

我没有测试过这个方法,但如果要排除多个元素,请尝试以下方法:

function getRandomWithManyExclusions(originalArray,arrayOfIndexesToExclude){

   var rand = null;

   while(rand === null || arrayOfIndexesToExclude.includes(rand)){
         rand = Math.round(Math.random() * (originalArray.length - 1));
    }
     return rand;
  }

上述方法与原作者的方法并没有太大区别。这种方法可以正常工作,因为它不会以偏见的方式从数组中采样。


一个 while 循环肯定是在这里前进的方式。 - Kody R.
1
你喜欢 while 循环、loopback 和 upvote 吗? - Alexander Mills
不确定这是否是最佳方法。如果您长时间获得与排除列表中相同的随机数,会怎么样? - everlasto
@everlasto 随机数生成器的分布是平滑/均匀的,因此除非您能获得具有非线性空洞的随机数字分布(例如我们手动创建的那个),否则可能无法避免该问题。 - Alexander Mills

4
假设您需要从范围1...5中选择一个随机数,并排除值2, 4,则:
- 从范围1...3中选择一个随机数 - 对排除的数字列表进行排序 - 对于每个小于等于随机数的排除数字:将其加一后作为新的随机数

function getRandomExcept(min, max, except) {
  except.sort(function(a, b) {
    return a - b;
  });
  var random = Math.floor(Math.random() * (max - min + 1 - except.length)) + min;
  var i;
  for (i = 0; i < except.length; i++) {
    if (except[i] > random) {
      break;
    }
    random++;
  }
  return random;
}

/*
 * Test iterations. Make sure that:
 * excluded numbers are skipped 
 * numbers are equally distributed
 */
(function(min, max, except) {
  var iterations = 1000000;
  var i;
  var random;
  var results = {};
  for (i = 0; i < iterations; i++) {
    random = getRandomExcept(min, max, except);
    results[random] = (results[random] || 0) + 1;
  }
  for (random in results) {
    console.log("value: " + random + ", count: " + results[random] + ", percent: " + results[random] * 100 / iterations + "%");
  }
})(1, 5, [2, 4]);


3

这是一个没有递归和创建大型数组的示例:

const getRandomWithExclude = (min, max, excludeArray) => {
  const randomNumber = Math.floor(Math.random() * (max - min + 1 - excludeArray.length)) + min;
  return randomNumber + excludeArray.sort((a, b) => a - b).reduce((acc, element) => { return randomNumber >= element - acc ? acc + 1 : acc}, 0);
}

const min = 1;
const max = 10;
const excludeArray = [8,2,5];
const result = getRandomWithExclude(min, max, excludeArray);

2

Hmz :-? 从数组中随机获取项目并确保它们都是唯一的最快方法是:

var array = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24];

Array.prototype.shuffle = function shuffle(){
    var tempSlot;
    var randomNumber;
    for(var i =0; i != this.length; i++){
        randomNumber = Math.floor(Math.random() * this.length);
        tempSlot = this[i]; 
        this[i] = this[randomNumber]; 
        this[randomNumber] = tempSlot;
    }
}

while(array.length!=0){
    array.shuffle();
    alert(array.pop());    
}

原型函数,每次循环随机打乱整个数组...我没有证据,但我非常怀疑这是“最快的方法”。 - rockerest
如果你想要技术上准确的话,重新洗牌是不必要的。 "最快" 是指实现而不是处理速度 :-? - Khez
这是Jon Bentley的《编程珠玑》中提出的同一问题的解决方案。 - monsur
@Pete,我的脑袋刚刚爆炸了。 - rockerest
永远不可能有足够的随机数,那是肯定的 =p - Pete

1

我发现自己处于这样一种情况:需要为每个游戏坐标生成一个真正长的范围内的随机数,但要排除一些已经被占用的坐标。

可以想象,在帧之间进行重新计算(理想情况下在10-14ms内),因此递归、while循环或生成极长数组甚至都不是选项。

感谢Salman A和Sebastian Umiński展示了另一种更高效的解决问题的方法。

所以这里是我的修改过的ES6函数,我希望它能帮助到在我所处的情况下遇到问题的人 :)

const randNum = (min, max, exclude = []) => {
  let num = Math.floor(Math.random() * (max - min + 1 - exclude.length) + min);
  exclude
    .slice()
    .sort((a, b) => a - b)
    .every((exeption) => exeption <= num && (num++, true));
  return num;
};

console.log(randNum(0, 24, [8]));


0

步骤1> 创建一个名为CHECK_ARRAY的数组,将其填充为超出随机数范围的值[如果要生成0-25之间的数字,请将其填充为26]

步骤2-> 生成一个随机数并将其添加到RANDOM_ARRAY中,同时将其添加到CHECK_ARRAY中

i=0;
CHECK_ARRAY[i]=random;
i++;

步骤3 -> 生成一个新的随机数并通过CHECK_ARRAY,如果找到26则忽略,否则如果发现重复,则重新生成随机数并再次执行步骤3,直到找到唯一的随机数!

0

@Alex Chebotarsky的优秀回答基础上进行补充。

经过一些单元测试,我发现有些额外的检查是明智的:

/**
 * Generates a random int within the max and min range.
 * Maximum is exclusive and minimum is inclusive.
 * @param min
 * @param max
 */
export const randomInt = (
  min: number,
  max: number,
): number => (Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min)) + Math.ceil(min)));

/**
 * Generates a random int within the max and min range with an array of excludes.
 * Maximum is exclusive and minimum is inclusive.
 * @param min
 * @param max
 * @param excludes
 */
export const randomIntWithExclude = (
  min: number,
  max: number,
  excludes: number[] = [],
): number => {
  if (min === max && excludes.includes(min)) throw new RangeError('All values are excluded');
  if (min === max) return min;
  if (max < min) [max, min] = [min, max];

  let num = randomInt(min, max);
  if (!excludes || !excludes.length) return num;

  excludes
    .sort((a, b) => a - b)
    .every((except) => except <= num && (num >= max ? num -= 1 : num += 1, true));
  if (excludes.includes(num)) throw new RangeError('All values are excluded');
  return num;
};

如果您感兴趣,这里是单元测试:

import {
  genRndNumUniqArray,
  randomIntWithExclude,
  randomInt,
} from './mathFuncs';

describe('[NumberFuncs]', () => {
  test.repeats(
    { times: 1000 },
    '[randomIntWithExclude] Should generate a random number excluding values in an array',
    () => {
      const excludesLength = randomInt(0, 10);
      const excludes = excludesLength
        ? genRndNumUniqArray(0, 100, excludesLength)
        : [];

      const [min, max] = excludes.length
        ? [Math.min(...excludes), Math.max(...excludes)]
        : [randomInt(0, 10), randomInt(10, 100)];

      try {
        const num = randomIntWithExclude(min, max, excludes);
        expect(num).not.toBeIncludedIn(excludes);
        expect(num).toBeGreaterThanOrEqual(min);
        expect(num).toBeLessThan(max);
      } catch (error) {
        if (min === max && excludes.includes(min)) {
          expect(error).toBeInstanceOf(RangeError);
        }
      }
    },
  );

  test.repeats(
    { times: 100 },
    '[randomIntWithExclude] Should throw a `RangeError` if all possible values are in the excludes array',
    () => {
      const excludes = [...Array(randomInt(2, 10)).keys()];
      const [min, max] = [Math.min(...excludes), Math.max(...excludes)];

      try {
        randomIntWithExclude(min, max, excludes);
        expect(true).toBe(false); // This is not supposed to be reached since the code above throws an error
      } catch (error) {
        if (min === max && excludes.includes(min)) {
          expect(error).toBeInstanceOf(RangeError);
        }
      }
    },
  );
});

这个函数是单元测试的依赖项:

/**
 * Generates an array of unique numbers
 * @param min
 * @param max
 * @param size
 */
export function genRndNumUniqArray(min: number, max: number, size: number): number[] {
  const rng = Math.min(max - min, size);
  if (rng < 1) return [];
  const nums = new Set<number>();
  while (nums.size !== rng) {
    const n = randomInt(min, max);
    nums.add(n);
  }
  return Array.from(nums);
}

如果您对test.repeats更感兴趣,它是一个自定义的jest扩展:

./jest.extends.ts

const handleError = ({
  name,
  errors,
  failPct,
  canFailPct,
  passIfOnePasses,
  debug,
  times,
  passes,
}: {
  name: string,
  times: number,
  canFailPct: number,
  passIfOnePasses?: boolean,
  passes: number[]
  errors: [number, any][],
  failPct: number,
  debug?: boolean,
}) => {
  if (passIfOnePasses && passes.length) return;

  if (errors.length && failPct > (canFailPct ?? 0)) {
    if (debug) {
      throw new Error(`
Test: ${name}
Ran: ${times} times
Failures: \x1b[31m${errors.length}\x1b[0m
Passes: \x1b[32m${passes.length}\x1b[0m
Fail rate: \x1b[31m${failPct * 100}%\x1b[0m
${canFailPct ? `Failed more than the ${canFailPct * 100}% limit` : ''}\n
Errors:
${errors.map((e) => `RUN: ${e[0]}\n${e[1].message}`).join('\n\n')}
`);
    } else {
      throw new Error(`
Test: ${name}
Ran: ${times} times
Failures: \x1b[31m${errors.length}\x1b[0m
Passes: \x1b[32m${passes.length}\x1b[0m
Fail rate: \x1b[31m${failPct * 100}%\x1b[0m
${canFailPct ? `Failed more than the ${canFailPct * 100}% limit` : ''}\n
Last error:
${errors[errors.length - 1][1]}\n
You can pass the \x1b[1;33m\`debug: true\`\x1b[0m option to see all errors.
`);
    }
  }
};

const repeatTest = async (
  options: jest.RepeatWithCanFail | jest.RepeatWithPass | jest.RepeatWithDefaults,
  name: string,
  fn?: jest.ProvidesCallback,
  timeout?: number,
) => {
  if (options.canFailPct && (options.canFailPct < 0 || options.canFailPct > 1)) {
    throw new Error('`canFailPct` must be between 0 and 1');
  }

  const passes: number[] = [];
  const errors: [number, any][] = [];

  return test(name, async () => {
    for await (const i of [...Array(options.times).keys()]) {
      try {
        if (fn) {
          // @ts-ignore
          await fn();
          passes.push(i);
        }
      } catch (error) {
        errors.push([i, error.stack ?? error.toString()]);
      }
    }
    const failPct = errors.length / options.times;

    handleError({
      name,
      errors,
      failPct,
      canFailPct: options.canFailPct ?? 0,
      passIfOnePasses: options.passIfOnePasses,
      debug: options.debug,
      times: options.times,
      passes,
    });
  }, timeout);
};

test.repeats = repeatTest;
it.repeats = repeatTest;

当测试失败时,它会打印出这个:

  [NumberFuncs]
    ✕ [getRandomIntWithExclude] (216 ms)

  ● [NumberFuncs] › [randomIntWithExclude]


    Test: [randomIntWithExclude]
    Ran: 1000 times
    Failures: 95
    Passes: 905
    Fail rate: 9.5%


    Last error:
    Error: expect(received).toBeGreaterThanOrEqual(expected)

    Expected: >= 67
    Received:    66

./jest.config.js

在运行测试之前,请确保先运行扩展文件,并在使用typescript时,在jest.d.tstsconfig.json中包括jest自定义类型。

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
  ...
  setupFilesAfterEnv: ['./jest/extends.ts'],
  ...
};

jest.d.ts

export {}
declare global {
  namespace jest {  
    type RepeatWithCanFail = {
      times: number,
      canFailPct: number,
      passIfOnePasses?: undefined,
      debug?: boolean,
    }
    
    type RepeatWithPass = {
      times: number,
      canFailPct?: undefined,
      passIfOnePasses: boolean,
      debug?: boolean,
    }
    
    type RepeatWithDefaults = {
      times: number,
      canFailPct?: undefined,
      passIfOnePasses?: undefined,
      debug?: boolean,
    }

    type RepeatOpts<O = any> =
    O extends RepeatWithCanFail
    ? RepeatWithCanFail
    : O extends RepeatWithPass
    ? RepeatWithPass
    : RepeatWithDefaults;

    interface It {
      repeats: <O extends RepeatOpts>(
        options: RepeatOpts<O>,
        name: string,
        fn?: jest.ProvidesCallback,
        timeout?: number,
      ) => void;
    }
  }
}

0

这里有一个经过测试的简单解决方案:

var array= [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24];
var random_value; 
var index;
var shuffled_array = new Array(24);

for (var i = 0; i < 24; i++) { 
random_value = array[Math.floor(Math.random()*array.length)]; //Returns a value between 1 and 24
index = array.indexOf(random_card); //Gets the index of the choosen random value
array.splice(index, 1); //Go to index of that array and remove it
shuffled_array [i] = random_value; //Put that value in a new array

window.alert("array: "+array+"\n"+"random_value: "+random_value+"\n"+"shuffled_array: "+shuffled_array);
}

在其他的解决方案中,我认为他们忘了搜索索引。

0
我确定有几种方法可以做到这一点,但是你可以把所有数字放入类似于堆栈的东西中,混合它们然后弹出来以获得随机数。或者,每次随机查找并从堆栈中删除它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接