【问题标题】:How to choose a weighted random array element in Javascript?如何在 Javascript 中选择加权随机数组元素?
【发布时间】:2022-12-20 20:16:07
【问题描述】:

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

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

解释:

我制作了这张图表来展示它是如何工作的:

此图显示了在给出权重为[5, 2, 8, 3] 的输入时会发生什么。通过对权重进行部分求和,您只需找到第一个与随机数一样大的权重,即随机选择的项目。

如果在两个权重的边界上选择一个随机数,就像图中的 715 一样,我们选择较长的一个。这是因为0可以被Math.random选中,而1不能,所以我们得到了公平分配。如果我们选择较短的那个,A 可以在 18 次中选择 6 次(01234),赋予它比应有的更高的权重.

【讨论】:

  • 您可能想澄清什么是“答案#2”。答案按投票排序,因此它们的顺序可以更改。人们也可以对答案进行不同的排序(例如,最老的在前。)
  • 澄清一下:for 循环是最快的,但可读性较差。这个解决方案的速度为 200,000 次操作/秒,速度也不是特别慢。
  • @Envayo 不,没有区别。它基于权重的总和,因此您可以根据需要对其进行缩放。
  • @LovelyWeather89 您可以使用for 循环向后遍历数组,检查i(for 循环变量)是否等于items.indexOf(items[i])。如果不是,则意味着 i 中的项目是重复的。然后你只需要.push任何不重复的项目及其权重到空数组中。类似于this
  • @LovelyWeather89 哦,我错了,i++应该是i--&gt;应该是&gt;=。如果您只想删除普通数组中的重复项,而不是此答案中使用的 items/weights,您可以执行 Array.from(new Set(x)),其中 x 是要从中删除重复项的数组。
【解决方案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 common,25144 uncommon 等等。
【解决方案4】:

这是一个 O(1) (常数时间)算法来解决您的问题。

生成一个从 0 到 99 的随机数(总共 100 个)。如果在给定的子范围内有 40 个数字(0 到 39),那么随机选择的数字有 40% 的概率落在这个范围内。请参阅下面的代码。

const number = Math.floor(Math.random() * 99); // 0 to 99
let element;

if (number >= 0 && number <= 39) {
    // 40% chance that this code runs. Hence, it is a bike.
    element = "bike";
}

else if (number >= 40 && number <= 69) {
    // 30% chance that this code runs. Hence, it is a car.
    element = "car";
}

else if (number >= 70 && number <= 84) {
    // 15% chance that this code runs. Hence, it is a boat.
    element = "boat";
}

else if (number >= 85 && number <= 94) {
    // 10% chance that this code runs. Hence, it is a train.
    element = "train";
}

else if (number >= 95 && number <= 99) {
    // 5% chance that this code runs. Hence, it is a plane.
    element = "plane";
}

还记得这个小学的数学原理吗? “指定分布中的所有数字被随机选择的概率均等。”

这告诉我们每个随机数在特定范围内出现的概率均等,无论该范围有多大或多小。

而已。这应该工作!

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

      ES2015版本Radvylf Programs's answer

      function getWeightedRandomItem(items) {
          const weights = items.reduce((acc, item, i) => {
              acc.push(item.weight + (acc[i - 1] || 0));
              return acc;
          }, []);
          const random = Math.random() * weights[weights.length - 1];
          return items[weights.findIndex((weight) => weight > random)];
      }
      

      和 ES2022

      function getWeightedRandomItem(items) {
          const weights = items.reduce((acc, item, i) => {
              acc.push(item.weight + (acc[i - 1] ?? 0));
              return acc;
          }, []);
          const random = Math.random() * weights.at(-1);
          return items[weights.findIndex((weight) => weight > random)];
      }
      

      【讨论】:

        【解决方案7】:

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

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