【问题标题】:How to choose a weighted random array element in Javascript?如何在Javascript中选择加权随机数组元素?
【发布时间】:2017-09-19 19:58:25
【问题描述】:

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

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
]

【问题讨论】:

标签: javascript select random


【解决方案1】:

以上两个答案都依赖于会很快变慢的方法,尤其是被接受的方法。

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,我个人认为这个解决方案更具可读性。

如果您更愿意使用具有itemweight 属性的对象:

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;
}

【讨论】:

  • 您可能想澄清“答案#2”是什么。答案是按投票排序的,因此它们的顺序可以改变。人们还可以对答案进行不同的排序(例如,按最老的在前)。
  • 澄清一下:for 循环是最快的,但可读性较差。这个解决方案,每秒 200,000 次操作,也不是很慢。
  • sum 没有用,而acc 具有相同的值
  • 最后一行可以简化:return items[chances.findIndex(el =&gt; el &gt; rand)]
  • @Envayo 不,没有区别。它基于权重的总和,因此您可以随意缩放。
【解决方案2】:

一些 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;

【讨论】:

  • 确保你的值已经按概率排序
【解决方案3】:

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

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

  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!');

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

【讨论】:

  • 很抱歉,这很有帮助,但您能否提供一个使用示例?
  • 代码不会根据定义的概率返回 - 我运行了 100,000 次,得到 0 个常见、5990 个不常见、25144 个稀有等。
  • 开头的 0 不是键的一部分,它是 59900 常见的,25144 不常见的等等。
【解决方案4】:

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

    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);

【讨论】:

    【解决方案5】:

    当然可以。这是一个简单的代码:

        // 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 是的,这已经比第二个答案慢了几千倍,而且有五个选项。
    猜你喜欢
    • 2022-12-20
    • 2020-06-09
    • 1970-01-01
    • 1970-01-01
    • 2011-05-26
    • 1970-01-01
    相关资源
    最近更新 更多