Shuffling would work well apart from the exclusivity issue (someone picking their own name).
Taking that into consideration, there are many ways to achieve this. This approach is "relatively" O(n) (linear) time complexity, though it's probably not the most efficient as it has to do an IndexOf
when the pool gets to the length of 2. You don't want someone being their own secret Santa. Additionally, because that fact it wouldn't be the most even probability distribution ¯\_(ツ)_/¯
In short, don't use this in life or death Secret Santa situations.
Given
private static IEnumerable<(T, T)> FunkyLotto<T>(IEnumerable<T> source)
{
var pool = source.ToList();
var iteration = pool.ToList();
foreach (var item in iteration)
{
int choice;
if (pool.Count == 2 && pool.IndexOf(item) >= 0)
// special case when there is only 2 left, and there is a case for a duplicate
choice = (pool.IndexOf(item) + 1) % pool.Count;
else
choice = _r.Next(0, pool.Count);
choice = Equals(item, pool[choice]) ? (choice + 1) % pool.Count : choice;
yield return (item, pool[choice]);
pool.RemoveAt(choice);
}
}
Usage
var list = new[] {"Joe", "John", "Chris", "Henry", "Tom", "Patrick"};
foreach (var result in FunkyLotto(list))
Console.WriteLine(result);
Output
(Joe, Patrick)
(John, Chris)
(Chris, Joe)
(Henry, Tom)
(Tom, John)
(Patrick, Henry)
Full Demo Here
Or a more efficient variation which omits the memory copy of Remove
, and does a reference check on the second last and last item in the pool.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Swap<T>(T[] array, int indexA, int indexB)
{
var tmp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = tmp;
}
private static IEnumerable<(T, T)> FunkyLotto2<T>(IEnumerable<T> source)
{
var pool = source.ToArray();
var iteration = pool.ToArray();
var poolLength = pool.Length;
foreach (var item in iteration)
{
int choice;
if (poolLength == 2)
choice = Equals(item, pool[0]) ? 1 : 0;
else
{
choice = _r.Next(0, poolLength);
choice = Equals(item, pool[choice]) ? (choice + 1) % poolLength : choice;
}
yield return (item, pool[choice]);
Swap(pool, choice, poolLength - 1);
poolLength--;
}
}
And a slightly more efficient variation that uses an array parameter/result, and a pointer based stack allocated index array.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void Swap(int* array, int indexA, int indexB)
{
var tmp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = tmp;
}
public static unsafe (T, T)[] FunkyLotto3<T>(T[] source)
{
var pool = stackalloc int[source.Length];
for (var i = 0; i < source.Length; i++)
pool[i] = i;
var result = new (T, T)[source.Length];
var poolLength = source.Length;
for (var index = 0; index < source.Length; index++)
{
int choice;
if (poolLength == 2)
choice = index == pool[0] ? 1 : 0;
else
{
choice = _r.Next(0, poolLength);
choice = index == pool[choice] ? (choice + 1) % poolLength : choice;
}
result[index] = (source[index], source[pool[choice]]);
Swap(pool, choice, poolLength - 1);
poolLength--;
}
return result;
}
And just because I can, Here some benchmarks
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1256 (1909/November2018Update/19H2)
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET Core SDK=5.0.101
[Host] : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
.NET Core 5.0 : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
Job=.NET Core 5.0 Runtime=.NET Core 5.0
Method |
N |
Mean |
Error |
StdDev |
Gen 0 |
Gen 1 |
Gen 2 |
Allocated |
Original |
100 |
6.209 us |
0.0478 us |
0.0448 us |
0.7019 |
- |
- |
5896 B |
Modified |
100 |
3.575 us |
0.0301 us |
0.0282 us |
0.6943 |
- |
- |
5824 B |
Modified2 |
100 |
1.312 us |
0.0041 us |
0.0036 us |
0.0267 |
- |
- |
224 B |
Original |
1000 |
67.507 us |
0.2349 us |
0.2197 us |
6.4697 |
0.1221 |
- |
54640 B |
Modified |
1000 |
29.546 us |
0.4567 us |
0.4272 us |
6.5002 |
0.1221 |
- |
54568 B |
Modified2 |
1000 |
13.413 us |
0.0230 us |
0.0215 us |
0.2289 |
- |
- |
2024 B |
Original |
10000 |
1,005.864 us |
2.7138 us |
2.5385 us |
64.4531 |
7.8125 |
- |
553608 B |
Modified |
10000 |
289.806 us |
1.7409 us |
1.5432 us |
65.9180 |
10.2539 |
- |
553536 B |
Modified2 |
10000 |
130.208 us |
0.4183 us |
0.3493 us |
2.1973 |
- |
- |
20024 B |
Full test code here
[SimpleJob(RuntimeMoniker.NetCoreApp50)]
[MemoryDiagnoser]
public class Test
{
private byte[] data;
[Params(100, 1000, 10000)] public int N;
[GlobalSetup]
public void Setup()
{
data = new byte[N];
new Random(42).NextBytes(data);
}
private static readonly Random _r = new Random();
private static IEnumerable<(T, T)> FunkyLotto<T>(IEnumerable<T> source)
{
var pool = source.ToList();
var iteration = pool.ToList();
foreach (var item in iteration)
{
int choice;
if (pool.Count == 2 && pool.IndexOf(item) >= 0) // special case when there is only 2 left, and there is a case for a duplicate
choice = (pool.IndexOf(item) + 1) % pool.Count;
else
choice = _r.Next(0, pool.Count);
choice = Equals(item, pool[choice]) ? (choice + 1) % pool.Count : choice;
yield return (item, pool[choice]);
pool.RemoveAt(choice);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Swap<T>(T[] array, int indexA, int indexB)
{
var tmp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = tmp;
}
private static IEnumerable<(T, T)> FunkyLotto2<T>(IEnumerable<T> source)
{
var pool = source.ToArray();
var iteration = pool.ToArray();
var poolLength = pool.Length;
foreach (var item in iteration)
{
int choice;
if (poolLength == 2)
choice = Equals(item, pool[0]) ? 1 : 0;
else
{
choice = _r.Next(0, poolLength);
choice = Equals(item, pool[choice]) ? (choice + 1) % poolLength : choice;
}
yield return (item, pool[choice]);
Swap(pool, choice, poolLength - 1);
poolLength--;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe void Swap(int* array, int indexA, int indexB)
{
var tmp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = tmp;
}
public static unsafe (T, T)[] FunkyLotto3<T>(T[] source)
{
var pool = stackalloc int[source.Length];
for (var i = 0; i < source.Length; i++)
pool[i] = i;
var result = new (T, T)[source.Length];
var poolLength = source.Length;
for (var index = 0; index < source.Length; index++)
{
int choice;
if (poolLength == 2)
choice = index == pool[0] ? 1 : 0;
else
{
choice = _r.Next(0, poolLength);
choice = index == pool[choice] ? (choice + 1) % poolLength : choice;
}
result[index] = (source[index], source[pool[choice]]);
Swap(pool, choice, poolLength - 1);
poolLength--;
}
return result;
}
[Benchmark]
public (byte, byte)[] Original() => FunkyLotto(data).ToArray();
[Benchmark]
public (byte, byte)[] Modified() => FunkyLotto2(data).ToArray();
[Benchmark]
public (byte, byte)[] Modified2() => FunkyLotto3(data);
}