如何有效地随机选择数组项而不重复?

IT技术 javascript
2021-02-06 13:08:49

我知道这个问题有多种形式,但我无法找到与我的具体效率问题相关的答案。

我有以下代码可以正常工作。

我有一个 10 个项目的数组,我从中随机选择一个项目(按 Enter 键)。该代码保留了一个包含 5 个最新选项的数组,这些选项不能随机选择(以避免随着时间的推移重复太多)。

如果chooseName() 函数最初选择了最近5 次使用过的名称,它只会中断并再次调用自己,重复直到找到“唯一”名称。

我有两个问题:

  1. 说这是一个“递归函数”是否正确?

  2. 我担心理论上这可能会在找到唯一名称之前循环很长时间 - 有没有更有效的方法来做到这一点?

感谢您的任何帮助。

    var a = ["Roger", "Russell", "Clyde", "Egbert", "Clare", "Bobbie", "Simon", "Elizabeth", "Ted", "Caroline"];
    var b = [];

    var chooseName = function () {
    var unique = true;
    b.length = 5;
    num = Math.floor(Math.random() * a.length);
    name = a[num];    
        for (i = 0; i < a.length; i++) {
        if (b[i] == name) {
            chooseName();
            unique = false;
            break;
            }
        }
        if (unique == true) {
        alert(name);
        b.unshift(name);
        }
    }


    window.addEventListener("keypress", function (e) {
        var keycode = e.keyCode;
        if (keycode == 13) {
        chooseName();
        }
    }, false);
6个回答

我喜欢评论者@YuriyGalanter 的想法,即随机选择项目,直到所有项目都被采用,然后才重复,所以这是一个实现:

function randomNoRepeats(array) {
  var copy = array.slice(0);
  return function() {
    if (copy.length < 1) { copy = array.slice(0); }
    var index = Math.floor(Math.random() * copy.length);
    var item = copy[index];
    copy.splice(index, 1);
    return item;
  };
}

var chooser = randomNoRepeats(['Foo', 'Bar', 'Gah']);
chooser(); // => "Bar"
chooser(); // => "Foo"
chooser(); // => "Gah"
chooser(); // => "Foo" -- only repeats once all items are exhausted.

Whenever an item is selected, move it to the back of the array and randomly select from a slice of the original array array.slice(0, -5).

var a = ["Roger", "Russell", "Clyde", "Egbert", "Clare", "Bobbie", "Simon", "Elizabeth", "Ted", "Caroline"];

var chooseName = function () {
    var unique = true;
    num = Math.floor(Math.random() * a.length - 5);
    name = a.splice(num,1);
    a.push(name);
}


window.addEventListener("keypress", function (e) {
    var keycode = e.keyCode;
    if (keycode == 13) {
        chooseName();
    }
}, false);

编辑:这也有副作用,即不给任何发生在列表尾部的变量带来不公平的劣势,即在前 N 次调用中不会考虑它们。如果这对您来说是个问题,也许可以尝试在某处保存一个静态变量来跟踪要使用的切片的大小并在 B 处将其最大化(在本例中为 5)。例如

var a = ["Roger", "Russell", "Clyde", "Egbert", "Clare", "Bobbie", "Simon", "Elizabeth", "Ted", "Caroline"];
B = 5; //max size of 'cache'
N = 0;

var chooseName = function () {
    var unique = true;
    num = Math.floor(Math.random() * a.length - N);
    N = Math.min(N + 1, B);
    name = a.splice(num,1);
    a.push(name);
}

我推荐你使用underscore.js,它会很简单。

该函数shuffle以均匀分布的方式实现,因此如果数组a包含更多数据,则重复的概率将较低

var a = ["Roger", "Russell", "Clyde", "Egbert", "Clare", "Bobbie", "Simon", "Elizabeth", "Ted", "Caroline"];
b = _.shuffle(a).slice(0,5);
console.log(b);
谢谢。我目前正试图远离图书馆,因为我想学习纯 javascript 以确保我知道发生了什么。将来我会检查一下。
2021-03-13 13:08:49

当您实例化 Shuffler 时,将您的数组作为参数。它将创建数组的副本,每次调用 next() 时,它都会从副本中返回一个随机元素并将其从副本数组中删除,这样就不可能重复。

var Shuffler = function(a) {
    var aCopy = [],
        n     = 0;

    // Clone array
    for (n=0; n<a.length; n++) {
        aCopy.push(a[n]);
    }

    this.next = function() {
        if (aCopy.length == 0) { return null; }

        var nRandom  = Math.floor(Math.random() * (aCopy.length + 1)),
            mElement = aCopy[nRandom];

        delete aCopy[nRandom];
        return mElement;
    }
}

var oShuffler   = new Shuffler([/* names go here */]),
    sRandomName = null;

while (sRandomName = oShuffler.next()) {
    console.log(sRandomName);
}

是的,这是递归的,因为它不会减少状态,所以理论上它可以永远持续下去。

我假设不允许更改数组,否则您可以简单地从数组中删除最近的选择,并在最近的选择缓冲区溢出时将它们推回。

相反:从选择中排除数组末尾的 buffersize 项。(缓冲区大小从 0 开始,随着最近的选择被添加到缓冲区中,增长到你预设的缓冲区大小最大值。)当你做出选择时,你将它与缓冲区大小最近的选择进行比较。如果找到匹配项,则选择相应的排除项。

显然,这仍然具有必须检查缓冲区中每个最近选择以避免匹配的低效率。但它确实具有避免可能出现的递归的效率。