Here is a somewhat simple brute force algorithm.
First, generate all possible combinations of pairs between the two arrays. This so, with arrA
and arrB
both of length of four, the possible pairings are
------- 1 -------
(arrA[0], arrB[0])
(arrA[1], arrB[1])
(arrA[2], arrB[2])
(arrA[3], arrB[3])
------- 2 -------
(arrA[0], arrB[1])
(arrA[1], arrB[2])
(arrA[2], arrB[3])
(arrA[3], arrB[0])
------- 3 -------
(arrA[0], arrB[2])
(arrA[1], arrB[3])
(arrA[2], arrB[0])
(arrA[3], arrB[1])
------- 4 -------
(arrA[0], arrB[3])
(arrA[1], arrB[0])
(arrA[2], arrB[1])
(arrA[3], arrB[2])
These are the unique ones. A valid possible pairing is also
(arrA[3], arrB[3])
(arrA[0], arrB[0])
(arrA[2], arrB[2])
(arrA[1], arrB[1])
but it contains the same elements as the first combination we had, so we don't need it.
We can now eliminate any sequence where the members of the pair match. We'd be left with only the sequences that produce distinct pairs.
Finally, we just need to pick one of those and shuffle it - that will ensure that we could get one of the sequences we ignored first. That way, we only need to hold length
amount of sequences otherwise if we generate every possible sequence, we get up to length * (length!)
(!
is factorial).
As for how to generate all unique sequences, we can do a couple of tricks. First, we generate the sequence where the index of arrA
matches the index of arrB
arrA = [a, b, c, d] ---
| | | | |
| | | | |---> (a, w) (b, x) (c, y) (d, z)
v v v v |
arrB = [w, x, y, z] ---
Then we rotate arrB
by one element:
-------------
| |
> [w, x, y, z]<
| |
------------
and repeat:
arrA = [a, b, c, d] ---
| | | | |
| | | | |---> (a, x) (b, y) (c, z) (d, w)
v v v v |
arrB = [x, y, z, w] ---
We do that length
amount of times and we'd get the unique pairings.
As for the second trick, we don't need to actually change arrB
- instead we can just keep an offset that increments after each sequence is generated. The first time we generate things the offset is 0
since we produce (arrA[0], arrB[0]) (arrA[1], arrB[1])
- equal indexes. The next time we need to produce (arrA[0], arrB[1]) (arrA[1], arrB[2])
so take the next index in arrB
. We can clamp it within bounds using the remainder operator %
: x % number
is guaranteed to produce an integer in the range [0, number)
- between 0
(inclusive) and number
(exclusive). That gives us results similar to rotation but at the cost of a single numeric variable:
var offset = 2;
var length = 4;
for (var index = 0; index < length; index++) {
var fakeRotatedIndex = (index + offset) % length;
console.log(fakeRotatedIndex);
}
With all of this together we get a relatively cheap brute force algorithm that we can use.
The final step is shuffling and I'd use the solution from this answer with comments removed for brevity. It's a simple and effective Fisher-Yates shuffle implementation.
function crissCrossZip(arrA, arrB) {
var all = [];
//assuming equal length
var totalLength = arrA.length;
//produce all valid sequences
for (var offset = 0; offset < totalLength; offset++) {
var zip = arrA.map((x, index) => [x, arrB[(index + offset) % totalLength]]);
//ignore sequence if there are any matches
if (zip.every(([a, b]) => a !== b))
all.push(zip);
}
//pick random sequence
var chosenSequence = all[Math.floor(Math.random() * all.length)];
shuffle(chosenSequence);
return chosenSequence;
}
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
//// usage ////
var users = [1,2,3,4]
var data = [1,2,3,4]
var result = crissCrossZip(users, data);
console.log(result.map(x => JSON.stringify(x)))
Handling arrays with different amounts of elements
This can easily be modified to handle arrays of different length, if needed. This time we'd have to do the same but ensure that arrA
is the *shorterarray and
arrBis the longer one. We'd also have to use
maxLength` instead of the common length in order to exhaust all unique valid combinations:
arrA = [a, b, c, d] ------
| | | | |
| | | | |---> (a, v) (b, w) (c, x) (d, y)
v v v v |
arrB = [v, w, x, y, z] ---
// "rotate"
arrA = [a, b, c, d] ------
| | | | |
| | | | |---> (a, w) (b, x) (c, y) (d, z)
v v v v |
arrB = [w, x, y, z, v] ---
// etc
Also, if we swapped arrA
and arrB
at the start (because arrA
was longer) we need to swap the pairs at the end. Other than that, we have everything we need.
function crissCrossZip(arrA, arrB) {
var all = [];
var [shorter, longer] = [arrA, arrB].sort((a, b) => a.length - b.length);
var adjust = x => x;
if (shorter !== arrA)
adjust = x => x.map(y => y.reverse());
var maxLength = longer.length;
//produce all valid sequences
for (var offset = 0; offset < maxLength; offset++) {
var zip = shorter.map((x, index) => [x, longer[(index + offset) % maxLength]]);
//ignore sequence if there are any matches
if (zip.every(([a, b]) => a !== b))
all.push(zip);
}
//pick random sequence
var chosenSequence = all[Math.floor(Math.random() * all.length)];
shuffle(chosenSequence);
return adjust(chosenSequence);
}
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
document.getElementById("click_me").addEventListener("click", function() {
var users = extractValues("users");
var data = extractValues("data");
var result = crissCrossZip(users, data);
var div = document.createElement("div");
div.classList.add("result");
div.textContent = result.map(x => `(${x.join(", ")})`).join(", ");
document.getElementById("results").prepend(div);
})
function extractValues(id) {
return document.getElementById(id)
.value
.split(",")
.map(x => x.trim());
}
body {
background-color: white;
}
#results {
background-color: #ddd;
}
.result:nth-child(1) {
background-color: #ff0;
padding-left: 0;
}
<p>Enter comma separated values:</p>
<label for="users">users:</label> <input type="text" id="users" value="1, 2, 3, 4, 5"/>
<br/>
<label for="data">data:</label> <input type="text" id="data" value="1, 2, 3, 4"/>
<br/>
<button id="click_me">Produce pairs</button>
<div>Results (latest first):</div>
<div id="results"></div>
Optimisation paths
Only generate pairs without duplicates
The first optimisation is very cheap and relatively minor but still something that helps. When generating the zip
array we can completely discard it if any of the members are duplicated, we don't need to wait until after it is generated.
//produce all valid sequences
for (var offset = 0; offset < maxLength; offset++) {
var keep = true;
for (var index = 0; index < shorter.length; index++) {
var x = shorter[index];
var pair = [x, longer[(index + offset) % maxLength]];
if (pair[0] === pair[1]) {
keep = false;
break;
}
}
//ignore sequence if there are any matches
if (keep)
all.push(zip);
}
Only keep the array you want to return
instead of holding up to maxLength
amount of arrays and picking later, we can pick first and only generate up to the choice. We'd have to shift the choice as we go along, if we discard any sub-results. So, the sequence is this:
- Pick which sequence would be produced. We know what the possible amount of sequences is - it's
maxLength
, so we need a pick between 0
and maxLength
. Let's call that choiceIndex
.
- Start generating the arrays, keep the index of what the current array woud be. This happens to be
offset
in the loop already.
- only keep two arrays - the last good one and the current.
- if we get to
offset === choiceIndex
and the current array is not discarded - stop. No need to generate more. Just return the current array.
- if we get to
offset === choiceIndex
and the current array is discarded - return the last good one.
- if we get to
offset === choiceIndex
we neither have a last good array nor is the current array - continue until a good array is generated.
This way we only ever keep up to two arrays of pairs in memory, as opposed to all unique ones.
function crissCrossZip(arrA, arrB) {
var [shorter, longer] = [arrA, arrB].sort((a, b) => a.length - b.length);
var adjust = x => x;
if (shorter !== arrA)
adjust = x => x.map(y => y.reverse());
var maxLength = longer.length;
var chosenIndex = Math.floor(Math.random() * maxLength);
//produce valid sequences
var lastGoodArray, currentArray;
for (var offset = 0; offset < maxLength; offset++) {
var keep = true;
var currentArray = [];
for (var index = 0; index < shorter.length; index++) {
var x = shorter[index];
var pair = [x, longer[(index + offset) % maxLength]];
if (pair[0] === pair[1]) {
keep = false;
break;
}
currentArray.push(pair);
}
if (keep)
lastGoodArray = currentArray;
if (offset >= chosenIndex && lastGoodArray)
break;
}
if (!lastGoodArray)
return;
var chosenSequence = lastGoodArray;
shuffle(chosenSequence);
return adjust(chosenSequence);
}
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
document.getElementById("click_me").addEventListener("click", function() {
var users = extractValues("users");
var data = extractValues("data");
var result = crissCrossZip(users, data);
var div = document.createElement("div");
div.classList.add("result");
div.textContent = result.map(x => `(${x.join(", ")})`).join(", ");
document.getElementById("results").prepend(div);
})
function extractValues(id) {
return document.getElementById(id)
.value
.split(",")
.map(x => x.trim());
}
body {
background-color: white;
}
#results {
background-color: #ddd;
}
.result:nth-child(1) {
background-color: #ff0;
padding-left: 0;
}
<p>Enter comma separated values:</p>
<label for="users">users:</label> <input type="text" id="users" value="1, 2, 3, 4, 5"/>
<br/>
<label for="data">data:</label> <input type="text" id="data" value="1, 2, 3, 4"/>
<br/>
<button id="click_me">Produce pairs</button>
<div>Results (latest first):</div>
<div id="results"></div>