【问题标题】:Best way to randomize two identical arrays without overlap?随机化两个相同数组而不重叠的最佳方法?
【发布时间】:2015-12-29 01:11:15
【问题描述】:

假设我们有两个相同的数组{"A", "B", "C", "D", "E", "F"}。有没有一种快速的方法来随机化每个的顺序,以确保当两个排列在一起时,相同的字母永远不会位于相同的索引处? (显然,如果它会导致匹配,我们可以只生成一个新索引,但我想知道是否有一种方法可以减少重复)。

【问题讨论】:

  • 这是干什么用的?您能否更具体地说明您想要的结果?例如,哪些排列应该被将其与简单的“我如何打乱两个数组?”分开的规则拒绝。问题?

标签: c# arrays sorting random


【解决方案1】:

这行得通,而且我认为它很容易理解。

var source = new [] { "A", "B", "C", "D", "E", "F" };

var output1 = (string[])null;
var output2 = (string[])null;

var rnd = new Random();
Action shuffle = () =>
{
    output1 = source.OrderBy(x => rnd.Next()).ToArray();
    output2 = source.OrderBy(x => rnd.Next()).ToArray();
};

shuffle();
while (output1.Zip(output2, (o1, o2) => new { o1, o2 })
    .Where(x => x.o1 == x.o2)
    .Any())
{
    shuffle();
}

【讨论】:

  • 您确定按随机键排序是个好主意吗?我假设在一般情况下,根据特定的OrderBy 实现,它可能会导致分布有偏差甚至崩溃。
  • @AlexD - 是的,这是个好主意。它非常安全 - 没有崩溃。而且我过去做过分布分析,它产生了均匀分布。
  • 好吧,那么我们只是在检查一个具体的实现,对吧?它可能是稳定和均匀的。我担心的是,没有承诺会始终保持相同的实施。除此之外,它可能是O(N*log(N)) 而不是O(N)
  • @AlexD - 不,我检查了几个实现 - 包括 Fisher-Yates 和其他一些有偏见的排序 - 以进行比较。以这种方式排序是好的。还有一个相当铁定的承诺,.OrderBy 的这个实现不会改变(除非其中发现了一个错误)。微软不愿意做出改变,以至于在开发 Roslyn 时,他们重新实现了最初的编译器错误。
  • 最初的问题是问是否有“一种产生更少重复的方法”,我认为这意味着避免像你在这里做的那样重新滚动。不过有点模糊。
【解决方案2】:

您可以通过 O(n) 复杂性分两步完成。

[步骤 1] 以每个字母改变其原始位置的方式仅对一个数组进行随机播放,如下所示:

var rnd = new Random(0);
var x = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
for(int i = 0; i < x.Length; i++)
{
    var j0 = (i == x[i] - 'A')? i + 1: i;
    var j = rnd.Next(j0, x.Length);
    // x[i] ⟷ x[j]
    var t = x[i]; x[i] = x[j]; x[j] = t;
}

它保证了第一个和第二个数组在每个位置都是不同的。


[步骤 2]Fisher–Yates shuffle 用于两个数组同步

var y = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
for(int i = 0; i < x.Length; i++)
{
    var j = rnd.Next(i, x.Length);
    // x[i] ⟷ x[j]; y[i] ⟷ y[j]
    var
    t = x[i]; x[i] = x[j]; x[j] = t;
    t = y[i]; y[i] = y[j]; y[j] = t;
}

它确保两者的随机化,在每个位置保持差异。

【讨论】:

  • 第二种算法展示了@HEATH3N 试图避免的确切问题:两个数组中相同索引处的相同字母。从技术上讲,这是第一个解决方案也没有解决的要求,因为两个随机洗牌的数组仍然会定期在同一索引处以相同的字母结束。
  • @KirillShlenskiy "第二种算法展示了确切的问题" 你是指第 2 步吗?怎么会发生?在步骤 1 之后,数组在任何位置都是不同的。在第 2 步中,“对”x[i], y[i] 保持不变,只是改变了位置。
  • 抱歉,我没有仔细阅读并认为这是两个不同的解决方案,但实际上它们是同一解决方案的两个步骤。
  • @KirillShlenskiy 我认为这确实可能具有误导性。更新以使其更加突出。
  • 洗牌一个数组可能会更清晰、更有效,然后复制该数组并重新洗牌,不包括原始位置。
【解决方案3】:

我最好的建议是制作您自己的带有 2 个参数的随机化器方法:要打乱的数组和不允许匹配的数组。

这是一个类的快速示例,该类具有 2 个字符串数组,它将通过调用 (objectName).Shuffle(); 进行混洗;

public class ArrayShuffler {
    public String[] arr1;
    public String[] arr2;

    public ArrayShuffler() {
        arr1 = new String[] { "A", "B", "C", "D", "E", "F" };
        arr2 = new String[] { "A", "B", "C", "D", "E", "F" };
    }
    public void Shuffle() {
        shuffleArr(arr1);
        shuffleArr(arr2, arr1);
    }

    /// <summary>
    /// Can shuffle array, maching against a second array to prevent dublicates in same intex spot.
    /// </summary>
    /// <param name="arr">Array to be shuffled</param>
    /// <param name="validate">Array to mach against</param>
    private void shuffleArr(String[] arr, String[] validate = null) {
        Random r = new Random();
        int indx = 0;
        while(indx < arr.Length){
            int rIndx = r.Next(indx, arr.Length);
            string tmp = arr[indx];
            if(validate != null) { //Will only be performed if you specify an array to be matched against.
                if(arr[rIndx] != validate[indx]) {
                    arr[indx] = arr[rIndx];
                    arr[rIndx] = tmp;
                    indx++;
                }
                else if(indx == arr.Length - 1) {
                    shuffleArr(arr, validate);
                }
            }
            else { //Default operation
                arr[indx] = arr[rIndx];
                arr[rIndx] = tmp;
                indx++;
            }
        }
    }
}

【讨论】:

  • 假设rIndx 总是恰好等于indx。那么我们最终不是得到了两个不变的数组,它们在每个位置都匹配吗?
  • 如果rIndx 的值总是恰好等于indx,那么你的运气会有点差,但你是对的,我会编辑代码来解决这个问题
  • 现在假设洗牌后的arr1 仍然是A, B, C。假设当我们打乱arr2 的前两个元素时,我们得到B, A。不会导致死循环吗?我们无法以arr2 结束,因为它的最后一个元素总是与arr1 的最后一个元素发生冲突。
  • 添加了“else if”,如果在最后一个索引上验证失败,它将重试。不是特别有效,但它可以完成工作。 (注:尚未测试)
  • 请不要在方法中使用Random r = new Random(); - 如果方法被连续快速调用,它可能会出现重复值。最好创建一个字段级(甚至更好的线程静态)变量。
【解决方案4】:

假设您试图尽量减少不必要的重投次数,并且 两个结果不能相互匹配(允许输出字符位于一个特定的索引来匹配该索引处输入中的字符),我想我已经为你找到了解决方案。

它的要点是,我们即时构建生成的字符串,以跟踪每个列表中没有选择的字符,并暂时从我们选择的候选对象中删除我们首先为特定索引选择的字符第二个。我不认为这种方法有任何偏见,但我承认我不是这方面的专家。

public void Shuffle(int seed)
{
    char[] orig = { 'A', 'B', 'C', 'D', 'E', 'F' };
    List<char> buffer1 = new List<char>();
    List<char> buffer2 = new List<char>();

    // Keep track of which indexes haven't yet been used in each buffer.
    List<int> availableIndexes1 = new List<int>(orig.Length);
    List<int> availableIndexes2 = new List<int>(orig.Length);

    for (int i = 0; i < orig.Length; i++)
    {
        availableIndexes1.Add(i);
        availableIndexes2.Add(i);
    }

    Random rand = new Random(seed);

    // Treat the last 2 specially.  See after the loop for details.
    for (int i = 0; i < orig.Length - 2; i++)
    {
        // Choose an arbitrary available index for the first buffer.
        int rand1 = rand.Next(availableIndexes1.Count);

        int index1 = availableIndexes1[rand1];

        // Temporarily remove that index from the available indices for the second buffer.
        // We'll add it back in after if we removed it (note that it's not guaranteed to be there).
        bool removed = availableIndexes2.Remove(index1);
        int rand2 = rand.Next(availableIndexes2.Count);
        int index2 = availableIndexes2[rand2];
        if (removed)
        {
            availableIndexes2.Add(index1);
        }

        // Add the characters we selected at the corresponding indices to their respective buffers.
        buffer1.Add(orig[index1]);
        buffer2.Add(orig[index2]);

        // Remove the indices we used from the pool.
        availableIndexes1.RemoveAt(rand1);
        availableIndexes2.RemoveAt(rand2);
    }

    // At this point, we have 2 characters remaining to add to each buffer.  We have to be careful!
    // If we didn't do anything special, then we'd end up with the last characters matching.
    // So instead, we just flip up to a fixed number of coins to figure out the swaps that we need to do.
    int secondToLastIndex1Desired = rand.Next(2);
    int secondToLastIndex2Desired = rand.Next(2);

    // If the "desired" (i.e., randomly chosen) orders for the last two items in each buffer would clash...
    if (availableIndexes1[secondToLastIndex1Desired] == availableIndexes1[secondToLastIndex2Desired] ||
        availableIndexes1[(secondToLastIndex1Desired + 1) % 2] == availableIndexes2[(secondToLastIndex2Desired + 1) % 2])
    {
        // ...then swap the relative order of the last two elements in one of the two buffers.
        // The buffer whose elements we swap is also chosen at random.
        if (rand.Next(2) == 0)
        {
            secondToLastIndex1Desired = (secondToLastIndex1Desired + 1) % 2;
        }
        else
        {
            secondToLastIndex2Desired = (secondToLastIndex2Desired + 1) % 2;
        }
    }
    else if (rand.Next(2) == 0)
    {
        // Swap the last two elements in half of all cases where there's no clash to remove an affinity
        // that the last element has for the last index of the output, and an affinity that the first
        // element has for the second-to-last index of the output.
        int t = secondToLastIndex1Desired;
        secondToLastIndex1Desired = secondToLastIndex2Desired;
        secondToLastIndex2Desired = t;
    }

    buffer1.Add(orig[availableIndexes1[secondToLastIndex1Desired]]);
    buffer1.Add(orig[availableIndexes1[(secondToLastIndex1Desired + 1) % 2]]);

    buffer2.Add(orig[availableIndexes2[secondToLastIndex2Desired]]);
    buffer2.Add(orig[availableIndexes2[(secondToLastIndex2Desired + 1) % 2]]);

    Console.WriteLine(new string(buffer1.ToArray()));
    Console.WriteLine(new string(buffer2.ToArray()));
}

请注意,如果将其用于特别是长数组,则从 List&lt;T&gt;.Remove / List&lt;T&gt;.RemoveAt 移动的数据和前者完成的线性搜索可能无法很好地扩展。

【讨论】:

  • 我测试了你的代码,你对输出的最后两个元素有偏见。我从 10 个字符(aj)的序列中运行了 10,000,000 次迭代,然后对源中的字符进行了频率分析。 a 均匀分布在前 8 个插槽中,但在倒数第二个插槽中的可能性增加 4%,在最后一个插槽中的可能性降低 4%。 j 也均匀分布在前 8 个位置,但在倒数第二个位置的可能性降低 4%,在最后一个位置的可能性增加 4%。基本上是从a翻过来的。
  • @Enigmativity:感谢您的测试——我只是重复了您的测试并得到了相同的结果。诚然,我不确定究竟为什么存在这些相似性,但我确实确定(这次使用实际测试......喘气!)在“无冲突”案例中交换最后两个元素 50%时间完全消除了这种特定的偏见,并且对同一个 10,000,000 测试的结果进行了不科学的观察(我的大学统计课已经有一段时间了,所以我不想假装我做对了,即使感觉对我好)表明不再有字母/位置偏见。
  • 我保留了我的测试代码,所以我稍后会重新测试并让你知道我的结果。干杯。
猜你喜欢
  • 1970-01-01
  • 2021-05-18
  • 2020-12-10
  • 2021-04-26
  • 1970-01-01
  • 1970-01-01
  • 2012-02-25
相关资源
最近更新 更多