如何在Javascript中选择加权随机数组元素?

IT技术 javascript select random
2021-02-19 02:57:01

例如:一个数组中有四个项目。我想随机得到一个,像这样:

array items = [
    "bike"    //40% chance to select
    "car"     //30% chance to select
    "boat"    //15% chance to select
    "train"   //10% chance to select
    "plane"   //5%  chance to select
]
5个回答

上面的两个答案都依赖于会很快变慢的方法,尤其是公认的方法。

function weighted_random(items, weights) {
    var i;

    for (i = 0; i < weights.length; i++)
        weights[i] += weights[i - 1] || 0;
    
    var random = Math.random() * weights[weights.length - 1];
    
    for (i = 0; i < weights.length; i++)
        if (weights[i] > random)
            break;
    
    return items[i];
}

到 2020 年 12 月,我用这个解决方案替换了旧的 ES6 解决方案,因为旧浏览器不支持 ES6,我个人认为这个解决方案更具可读性。

如果您更愿意使用具有属性item和的对象weight

function weighted_random(options) {
    var i;

    var weights = [];

    for (i = 0; i < options.length; i++)
        weights[i] = options[i].weight + (weights[i - 1] || 0);
    
    var random = Math.random() * weights[weights.length - 1];
    
    for (i = 0; i < weights.length; i++)
        if (weights[i] > random)
            break;
    
    return options[i].item;
}
sum无用,但acc具有相同的值
2021-04-17 02:57:01
最后一行可以简化: return items[chances.findIndex(el => el > rand)]
2021-04-27 02:57:01
您可能想澄清“答案 #2”是什么。答案按投票排序,因此它们的顺序可以更改。人们也可以对答案进行不同的排序(例如,最老的在前。)
2021-05-03 02:57:01
澄清一下:for 循环是最快的,但可读性较差。这个解决方案以 200,000 次操作/秒的速度运行,也不是特别慢。
2021-05-08 02:57:01
看起来棒极了!您是否有机会了解如何有效地将其应用于二维数组?:)
2021-05-08 02:57:01

一些 es6 方法,带有通配符处理:

const randomizer = (values) => {
    let i, pickedValue,
            randomNr = Math.random(),
            threshold = 0;

    for (i = 0; i < values.length; i++) {
        if (values[i].probability === '*') {
            continue;
        }

        threshold += values[i].probability;
        if (threshold > randomNr) {
                pickedValue = values[i].value;
                break;
        }

        if (!pickedValue) {
            //nothing found based on probability value, so pick element marked with wildcard
            pickedValue = values.filter((value) => value.probability === '*');
        }
    }

    return pickedValue;
}

用法示例:

let testValues = [{
    value : 'aaa',
    probability: 0.1
},
{
    value : 'bbb',
    probability: 0.3
},
{
    value : 'ccc',
    probability: '*'
}]

randomizer(testValues); // will return "aaa" in 10% calls, 
//"bbb" in 30% calls, and "ccc" in 60% calls;
确保您的值已经按概率排序
2021-04-15 02:57:01

这是一种更快的方法,然后其他答案建议......

您可以通过以下方式实现您想要的:

  1. 根据每个元素的概率将 0 到 1 段划分为多个部分(例如,概率为 60% 的元素将占据该段的 60%)。
  2. 生成一个随机数并检查它落在哪个段。

步骤1

为概率数组创建一个前缀和数组,其中的每个值将表示其对应部分的结束位置。

例如:如果我们有概率:60% (0.6)、30%、5%、3%、2%。前缀和数组将是:[0.6,0.9,0.95,0.98,1]

所以我们将有一个像这样划分的段(大约): [ | | ||]

第2步

生成一个 0 到 1 之间的随机数,并在前缀和数组中找到它的下界。您将找到的索引是随机数所在的段的索引

以下是您可以如何实现此方法:

let obj = {
    "Common": "60",
    "Uncommon": "25",
    "Rare": "10",
    "Legendary": "0.01",
    "Mythical": "0.001"
}
// turning object into array and creating the prefix sum array:
let sums = [0]; // prefix sums;
let keys = [];
for(let key in obj) {
    keys.push(key);
    sums.push(sums[sums.length-1] + parseFloat(obj[key])/100);
}
sums.push(1);
keys.push('NONE');
// Step 2:
function lowerBound(target, low = 0, high = sums.length - 1) {
    if (low == high) {
        return low;
    }
    const midPoint = Math.floor((low + high) / 2);
  
    if (target < sums[midPoint]) {
        return lowerBound(target, low, midPoint);
    } else if (target > sums[midPoint]) {
        return lowerBound(target, midPoint + 1, high);
    } else {
        return midPoint + 1;
    }
}

function getRandom() {
    return lowerBound(Math.random());
}

console.log(keys[getRandom()], 'was picked!');

希望你觉得这有帮助。 注意:( 在计算机科学中)列表/数组中值的下限是大于或等于它的最小元素。例如,数组:[1,10,24,99]和值 12。下限将是值为 24 的元素。当数组从最小到最大排序时(如我们的例子),通过二分搜索可以非常快速地找到每个值的下限(O(log(n)))。

代码不会根据定义的概率返回——我运行了 100,000 次,得到 0 次普通、5990 次不普通、25144 次罕见等。
2021-04-18 02:57:01
开头的 0 不是键的一部分,它是 59900 常见的,25144 不常见的等等。
2021-04-27 02:57:01
对不起,这很有帮助,但您能否提供一个使用它的示例?
2021-05-06 02:57:01

我添加了我的解决方案作为一种适用于较小数组(无缓存)的方法:

    static weight_random(arr, weight_field){
    
        if(arr == null || arr === undefined){
            return null;
        }
        const totals = [];
        let total = 0;
        for(let i=0;i<arr.length;i++){
            total += arr[i][weight_field];
            totals.push(total);
        }
        const rnd = Math.floor(Math.random() * total);
        let selected = arr[0];
        for(let i=0;i<totals.length;i++){
            if(totals[i] > rnd){
                selected = arr[i];
                break;
            }
        }
    return selected;

}

像这样运行它(提供数组和权重属性):

const wait_items =  [
    {"w" : 20, "min_ms" : "5000", "max_ms" : "10000"},
    {"w" : 20, "min_ms" : "10000", "max_ms" : "20000"},
    {"w" : 20, "min_ms" : "40000", "max_ms" : "80000"}
]   

const item = weight_random(wait_items, "w");
console.log(item);

你当然可以。这是一个简单的代码来做到这一点:

    // Object or Array. Which every you prefer.
var item = {
    bike:40, // Weighted Probability
    care:30, // Weighted Probability
    boat:15, // Weighted Probability
    train:10, // Weighted Probability
    plane:5 // Weighted Probability
    // The number is not really percentage. You could put whatever number you want.
    // Any number less than 1 will never occur
};

function get(input) {
    var array = []; // Just Checking...
    for(var item in input) {
        if ( input.hasOwnProperty(item) ) { // Safety
            for( var i=0; i<input[item]; i++ ) {
                array.push(item);
            }
        }
    }
    // Probability Fun
    return array[Math.floor(Math.random() * array.length)];
}

console.log(get(item)); // See Console.
@mattsoave 是的,这已经比第二个答案慢了几千倍,而且有五个选项。
2021-04-15 02:57:01
这对于小整数(这也是我的用例)相当有效,但由于它通过创建一个长度等于权重总和的新数组来工作,因此它可能会随着数字变大/变慢。它也不适用于非整数,因此您需要找到最小公分母才能得到整数(这对于非常精确的权重可能是不可能的)。
2021-05-08 02:57:01