最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Shuffle Array So No Two Keys Are in the Same Location - Stack Overflow

programmeradmin6浏览0评论

I'm working on a Secret Santa app, and I'm matching up people by shuffling up an array of users, and then iterating through it using a for loop, then invoking the function if a key in the first array is identical to a key in the second array. However, this is causing some problems when being executed within the application, and results in it hanging at times. In addition, the theoretical maximum execution time for this function is infinite, and something I want to avoid.

Is there a way to shuffle an array so that no two keys are in the same location without reiterating the function if it fails?

The code I'm using (not implemented into the application itself) is as follows:

//underscore utility library
var _ = require('underscore');
//pretty console colors
var colors = require('colors');
//fs module to write to file
var fs = require('fs');
//path utilities
var path = require('path');
//arr will contain the UIDs of the participants.
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50];
//sArr will be the pairings.
var sArr = [];
//plain-text data
var writeMe = '';
//i for an i
var i = 0;
//var declaration to shut up the linter
var justDoIt;
var writeIt;
var shuffleIt;
var checkIt;

var shuffleIt = function () {
  //use Underscore.js' shuffle function on the array.
  sArr = _.shuffle(arr);
};

var checkIt = function () {
  for (i = 0; i < arr.length; i++) {
    //check to make sure that there are no duplicates. 
    //otherwise, politely plain
    if (arr[i] === sArr[i]) {
      //well fuck. both are the same. Let's do it again
      console.log(arr[i] + "=>" + sArr[i]);
      console.log("ERROR".red + "\n==================\n");
      justDoIt();
      return;
    }
    //otherwise, print current pairing to console
    writeMe += arr[i] + '=>' + sArr[i] + "\n";
    console.log(arr[i] + '=>' + sArr[i]);
  }
  //we're done!
  console.log("All good!".green);
  //let's write it out
  writeIt();
  return;
};

var justDoIt = function () {
  //roll it
  shuffleIt();
  //check it
  checkIt();
};

var writeIt = function () {
  //write it to a file
  fs.writeFileSync(path.join(__dirname, 'pairings.txt'), writeMe);
};
//init
justDoIt();

I'm working on a Secret Santa app, and I'm matching up people by shuffling up an array of users, and then iterating through it using a for loop, then invoking the function if a key in the first array is identical to a key in the second array. However, this is causing some problems when being executed within the application, and results in it hanging at times. In addition, the theoretical maximum execution time for this function is infinite, and something I want to avoid.

Is there a way to shuffle an array so that no two keys are in the same location without reiterating the function if it fails?

The code I'm using (not implemented into the application itself) is as follows:

//underscore utility library
var _ = require('underscore');
//pretty console colors
var colors = require('colors');
//fs module to write to file
var fs = require('fs');
//path utilities
var path = require('path');
//arr will contain the UIDs of the participants.
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50];
//sArr will be the pairings.
var sArr = [];
//plain-text data
var writeMe = '';
//i for an i
var i = 0;
//var declaration to shut up the linter
var justDoIt;
var writeIt;
var shuffleIt;
var checkIt;

var shuffleIt = function () {
  //use Underscore.js' shuffle function on the array.
  sArr = _.shuffle(arr);
};

var checkIt = function () {
  for (i = 0; i < arr.length; i++) {
    //check to make sure that there are no duplicates. 
    //otherwise, politely plain
    if (arr[i] === sArr[i]) {
      //well fuck. both are the same. Let's do it again
      console.log(arr[i] + "=>" + sArr[i]);
      console.log("ERROR".red + "\n==================\n");
      justDoIt();
      return;
    }
    //otherwise, print current pairing to console
    writeMe += arr[i] + '=>' + sArr[i] + "\n";
    console.log(arr[i] + '=>' + sArr[i]);
  }
  //we're done!
  console.log("All good!".green);
  //let's write it out
  writeIt();
  return;
};

var justDoIt = function () {
  //roll it
  shuffleIt();
  //check it
  checkIt();
};

var writeIt = function () {
  //write it to a file
  fs.writeFileSync(path.join(__dirname, 'pairings.txt'), writeMe);
};
//init
justDoIt();
Share Improve this question asked Dec 24, 2014 at 2:49 Brandon AnzaldiBrandon Anzaldi 7,2803 gold badges38 silver badges56 bronze badges 17
  • 3 Way too plicated. Shuffle the array once, make a copy of it, transpose the elements in that copy by one. Voila, guaranteed unique match-ups. – deceze Commented Dec 24, 2014 at 2:51
  • 3 @deceze What if the shuffled array was one away from having an exact match? – Vitruvie Commented Dec 24, 2014 at 2:52
  • 1 @Saposhiente After the copy, all of them will be exact matches, but in a random order. Shifting them by one will make a set of random matches that are guaranteed to have no collisions. – Brandon Anzaldi Commented Dec 24, 2014 at 2:54
  • 4 Anyone else notice this is for a secret Santa app... with only 1 day left before Christmas? that's a seriously tight deadline! ;-) – scunliffe Commented Dec 24, 2014 at 2:59
  • 4 @deceze I see more clearly what exactly you meant by this algorithm. It's simple because it is not truly random: It is guaranteed to produce a gifting assignment such that if you follow a chain of one gift to the next, you always go through every person in the group before returning to your start; in a truly random shuffle, there will be many separate loops of gifts. – Vitruvie Commented Dec 24, 2014 at 3:08
 |  Show 12 more ments

3 Answers 3

Reset to default 8

The general term for a rearrangement where no element is in its original position is a derangement, and it's a fascinating problem.

You are looking for an unbiased (uniformly random), non-rejection (not "repeat until it works") algorithm for a random derangement.

It's not trivial.

This is one such algorithm (EDIT: it does not appear to be unbiased):

function derangementNumber(n) {
    if(n == 0) {
        return 1;
    }
    var factorial = 1;
    while(n) {
        factorial *= n--;
    }
    return Math.floor(factorial / Math.E);
}

function derange(array) {
    array = array.slice();
    var mark = array.map(function() { return false; });
    for(var i = array.length - 1, u = array.length - 1; u > 0; i--) {
        if(!mark[i]) {
            var unmarked = mark.map(function(_, i) { return i; })
                .filter(function(j) { return !mark[j] && j < i; });
            var j = unmarked[Math.floor(Math.random() * unmarked.length)];

            var tmp = array[j];
            array[j] = array[i];
            array[i] = tmp;

            // this introduces the unbiased random characteristic
            if(Math.random() < u * derangementNumber(u - 1) /  derangementNumber(u + 1)) {
                mark[j] = true;
                u--;
            }
            u--;
        }
    }
    return array;
}

var a = [1, 2, 3, 4, 5, 6, 7];
derange(a);

You can look at this paper, this slide deck, or this Mathematics question too.

As was pointed out in the ments, by shuffling the initial array, then shifting it by 1 (or any number less than the length of the array) will yield a random sort.

Sample code:

// Underscore Utility Library, since I was using this anyway.
var _ = require('underscore');
// Example shuffle: [4,2,1,9,5,8,3,7,6]
var arr = _.shuffle([1,2,3,4,5,6,7,8,9]);
var sArr = _.union(_.rest(arr), [_.first(arr)]);

// Log out pairs.
for (i = 0; i < arr.length; i++) {
    console.log(arr[i] + ' --> ' + sArr[i]);
}

An example output for this is:

6 --> 3
3 --> 1
1 --> 2
2 --> 4
4 --> 7
7 --> 5
5 --> 8
8 --> 9
9 --> 6

The caveat of this solution is that it's not truly random. It will create certain impossible situations. For example, with this solution, it would be impossible for two users to get each other. 3 cannot have 6 if 6 already has 3. It creates a chain-effect that will ensure a random ordering, and that each recipient pairing will be unpredictable without knowledge of the previous pairings, but it will not be truly random.

The following seems reasonably random and guarantees that a member will not be returned to its original position:

// Return a new array that is a deranged version of arr
// Guarantee that no member retains its original position
function derange(arr) {

  // Make a copy of arr
  var c = arr.slice();

  // If arr.length is < 2, return copy
  if (c.length < 2) {
    return c;
  }

  var result = [];
  var idx, i, iLen;

  // Keep track of whether last member has been moved
  var lastMoved = false;

  // Randomly remove a member of c, with conditions...
  for (i=0, iLen=c.length - 1; i<iLen; i++) {

    // If get down to final two and last hasn't been moved,
    // swap last two and append to result
    if (c.length == 2 && !lastMoved) {
      result = result.concat(c.reverse().splice(0,2))
      break;
    }

    // Otherwise, select a remaining member of c
    do {
      idx = Math.random() * c.length | 0;

    // But make sure it's not going back in the same place
    } while (arr.indexOf(c[idx]) == result.length)

    // Add member to result
    result.push(c.splice(idx, 1)[0]);

    // Remember if last was just moved
    lastMoved = lastMoved || idx == c.length;

  }

  // Add the last member, saves a do..while iteration about half the time
  if (c.length) result.push(c[0]);
  return result;
}

Some results:

console.log(derange([1,2,3,4,5,6]));

 [6, 4, 2, 5, 3, 1]
 [4, 1, 2, 5, 6, 3]
 [5, 4, 2, 6, 1, 3]
 [4, 5, 2, 3, 6, 1]
 [4, 6, 1, 5, 2, 3]
 [5, 6, 1, 3, 4, 2]
 [2, 4, 6, 1, 3, 5]
 [2, 1, 4, 5, 6, 3]

It's impossible to preclude certain values from Math.random, so if you get one you don't like, there's no choice but to get another. So a do..while loop is used to ensure that a member originally at index i isn't moved to new index i. However, such collisions should be minimal. It would also be good to optimise the lookup of the original value other than using indexOf. Perhaps that only matters for very large arrays.

If it gets to the last two members and the last member of the original array hasn't been moved, it will simply reverse then append the last two members, which is the only possible oute at that point.

发布评论

评论列表(0)

  1. 暂无评论