除了排他性问题(有人选择自己的名字)之外,改组效果很好。
考虑到这一点,有很多方法可以实现这一目标。这种方法是“相对” O(n)(线性)时间复杂度,尽管它可能不是最有效的,因为它必须在池得到时执行IndexOf长度为 2。您不希望某人成为自己的秘密圣诞老人。另外,因为这个事实它不会是最均匀的概率分布¯\_(ツ)_/¯
简而言之,不要在生死攸关的秘密圣诞老人情况下使用它。
给定
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);
}
}
用法
var list = new[] {"Joe", "John", "Chris", "Henry", "Tom", "Patrick"};
foreach (var result in FunkyLotto(list))
Console.WriteLine(result);
输出
(Joe, Patrick)
(John, Chris)
(Chris, Joe)
(Henry, Tom)
(Tom, John)
(Patrick, Henry)
Full Demo Here
或者更有效的变体,它省略了Remove 的内存副本,并对池中倒数第二个和最后一个项目进行引用检查。
[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;
}
因为我可以,这里有一些基准
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 |
这里有完整的测试代码
[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);
}