11

首先,我知道Fisher-Yates shuffle。但是,为了争论,我想允许用户从下拉列表中选择一个排序选项。该列表将包括一个“随机”选项。根据他们的选择结果,我只想用 IComparer 实例替换我的排序。IComparer 会是什么样子?

谷歌提出了大量有缺陷的结果,它们都采用这种形式:

public class NaiveRandomizer<T> : IComparer<T>
{
    private static Random rand = new Random();

    public int Compare(T x, T y)
    {
        return (x.Equals(y))?0:rand.Next(-1, 2);
    }
}

但是,这种实现是有偏见的,在某些情况下甚至会抛出异常。偏差可以用下面的代码来证明:

void Test()
{
    Console.WriteLine("NaiveRandomizer Test:");
    var data = new List<int>() {1,2,3};
    var sortCounts = new Dictionary<string, int>(6);
    var randomly = new NaiveRandomizer<int>();

    for (int i=0;i<10000;i++)
    {   //always start with same list, in _the same order_.
        var dataCopy = new List<int>(data); 
        dataCopy.Sort(randomly);

        var key = WriteList(dataCopy);
        if (sortCounts.ContainsKey(key))
            sortCounts[key]++;
        else
            sortCounts.Add(key, 1);
    }

    foreach (KeyValuePair<string, int> item in sortCounts)
        Console.WriteLine(item.Key + "\t" + item.Value);
}

string WriteList<T>(List<T> list)
{
   string delim = "";
   string result = "";
   foreach(T item in list)
   {
       result += delim + item.ToString();
       delim = ", ";
   }
   return result;
}

那么如何实现一个随机IComparer<T>的来解决这些问题呢?允许要求每次调用都.Sort()使用单独的 IComparer 实例,因为我看不到任何其他方法可以做到这一点:必须使用其他一些真正随机的值来比较项目但该值也必须与项目一致在给定的排序操作中。

我在这里有一个开始,但它是匆忙发布的,非常慢,甚至没有返回所有可能的类型(测试表明它至少消除了偏见,如果你不计算缺失的选项)。我不希望像 Fisher-Yates 那样的 O(n) 性能,但我确实想要一些合理的东西(n log n 表示小的 n),我确实希望它能够显示所有可能的类型。不幸的是,该链接是该问题的当前公认答案,因此我希望能够用更好的东西替换它。

如果不出意外,我希望这能吸引所有那些寻找 IComparable 解决方案的谷歌查询——他们最终会在这里而不是在其他地方告诉他们使用不正确的版本。

4

7 回答 7

11

在这个帖子中,我有些惊讶地发布了多少错误答案。只是为了其他想出类似于 OP 发布的解决方案的人,以下代码看起来是正确的:

int[] nums = new int[1000];
for (int i = 0; i < nums.Length; i++)
{
    nums[i] = i;
}

Random r = new Random();
Array.Sort<int>(nums, (x, y) => r.Next(-1, 2));

foreach(var num in nums)
{
    Console.Write("{0} ", num);
}

但是,代码偶尔会抛出异常,但并非总是如此。这就是调试变得有趣的原因:) 如果您运行它足够多次,或者在循环中执行排序过程 50 次左右,您将收到一条错误消息:

IComparer (or the IComparable methods it relies upon) did not return zero when Array.Sort called x. CompareTo(x). x: '0' x's type: 'Int32' The IComparer: ''.

换句话说,快速排序将某个数字x与自身进行比较并得到非零结果。代码的明显解决方案是:

Array.Sort<int>(nums, (x, y) =>
    {
        if (x == y) return 0;
        else return r.NextDouble() < 0.5 ? 1 : -1;
    });

但即使这样也行不通,因为有时 .NET 将 3 个数字相互比较会返回不一致的结果,例如 A > B、B > C 和 C > A(哎呀!)。无论您使用 Guid、GetHashCode 还是任何其他随机生成的输入,如上所示的解决方案仍然是错误的。


话虽如此,Fisher-Yates 是洗牌数组的标准方法,因此首先没有真正的理由使用 IComparer。Fisher-Yates 是 O(n),而使用 IComparer 的任何实现都在后台使用快速排序,其时间复杂度为 O(n log n)。没有充分的理由不使用众所周知的、高效的标准算法来解决这类问题。

但是,如果您真的坚持使用 IComparer 和 rand,那么在排序之前应用您的随机数据。这需要将数据投影到另一个对象上,这样您就不会丢失随机数据:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Pair<T, U>
    {
        public T Item1 { get; private set; }
        public U Item2 { get; private set; }
        public Pair(T item1, U item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Pair<int, double>[] nums = new Pair<int, double>[1000];
            Random r = new Random();
            for (int i = 0; i < nums.Length; i++)
            {
                nums[i] = new Pair<int, double>(i, r.NextDouble());
            }

            Array.Sort<Pair<int, double>>(nums, (x, y) => x.Item2.CompareTo(y.Item2));

            foreach (var item in nums)
            {
                Console.Write("{0} ", item.Item1);
            }

            Console.ReadKey(true);
        }
    }
}

或者用你的坏自我获得 LINQy:

Random r = new Random();
var nums = from x in Enumerable.Range(0, 1000)
           orderby r.NextDouble()
           select x;
于 2009-02-17T18:38:35.710 回答
3

我在别处得到的一个建议是创建一个单独的 IAranger 接口,该接口描述了一个排列集合的单个操作。这可以在 IComparer/IComparable 不能工作的地方工作,因为它对整个集合而不是单个项目进行操作。它可能看起来像这样:

public interface IArranger<T>
{
    IEnumerable<T> Arrange(IEnumerable<T> items);
}

然后我可以Shuffle使用适当的 Fisher-Yates 算法从 IArranger 接口实现 a ,并且还可以实现包装IEnumerable.Sort()/IComparable/IComparer我关心的每个其他品种。这可能看起来像这样:

public class ComparerArranger<T> : IArranger<T>
{
    private IComparer<T> comparer;

    public ComparableArranger(IComparer<T> comparer)
    {
        this.comparer = comparer;
    }

    public IEnumerable<T> Arrange(IEnumerable<T> items)
    {
       return items.OrderBy(i => i, comparer);
    }
}

或者

//uses the default Comparer for the type (Comparer<T>.Default)
public class TypeArranger<T> : IArranger<T> 
{
    public IEnumerable<T> Arrange(IEnumerable<T> items)
    {
       return items.OrderBy(i => i);
    }
}

或者

public class ShuffleArranger<T> : IArranger<T>
{
    //naive implementation for demonstration
    // if I ever develop this more completely I would try to
    // avoid needing to call .ToArray() in here
    // and use a better prng
    private Random r = new Random();

    public IEnumerable<T> Arrange(IEnumerable<T> items)
    {
        var values = items.ToArray();

        //valid Fisher-Yates shuffle on the values array
        for (int i = values.Length; i > 1; i--)
        {
            int j = r.Next(i);
            T tmp = values[j];
            values[j] = values[i - 1];
            values[i - 1] = tmp;
        }
        foreach (var item in values) yield return item;
    }
}

最后一步,我通过扩展方法将对此的支持添加到任何 IEnumerable 中。然后你仍然得到简单的运行时算法交换,你有一个更好的 shuffle 算法实现,使用它的代码感觉很自然:

public static IEnumerable<T> Arrange(this IEnumerable<T> items, IArranger<T> arranger)
{
    return arranger.Arrange(items);
}
于 2009-02-18T14:31:07.340 回答
1

IComparer在某个点需要零回报(对于 T 的相等实例),使得从数学上创建一个通用的 IComparer 来模拟 Fisher-Yates Shuffle 是不可能的。总会有偏见。对于真正的洗牌,你永远不想强迫它返回任何特定的值。

于 2009-02-17T18:44:08.833 回答
0

如何基于预先分配随机值的隐藏字段进行排序?

于 2009-02-17T18:00:12.620 回答
0

跟进 James Curran 的想法:让 IComparer 将“排序”的值保持为一个列表;如果出现新值,则将其插入到列表中的随机位置;按列表索引进行比较。通过将列表维护为平衡树或其他东西进行优化。这种 IComparer 的每个实例都将保持一致且随机的排序顺序,因此您可以选择让您的 Random 排序始终保持相同的随机顺序或每次都不同。如果您更喜欢以这种方式阅读“随机”,那么微小的修改甚至允许将相同的元素“排序”到不同的排序位置。

于 2009-02-18T00:51:13.863 回答
0

一个有趣的尝试。很可能是对 IComparer 的误用/滥用。

您正在尝试通过使用不是为此目的而构建的机制来进行随机加权排序。

为什么不实现自己的排序例程和自己的比较器?我有一种感觉,即使那样也不够。

于 2009-02-18T14:20:25.230 回答
0

不要这样做。

迄今为止提出的所有算法都在输出中引入了某种偏差(一些比其他更大)。

@Princess 和@Luke 建议在数据旁边存储一个随机数。但是,由于这些随机数中的任何两个可能具有与另一个相同的值,因此这两个项目之间的排序顺序将具有确定性偏差

最坏的情况是如果排序例程是“稳定的”(即被认为相等的对象总是按照它们输入的顺序输出)。Array.Sort 并不稳定(它在内部使用 QuickSort),但是当两个项目具有相同的值时仍然会出现偏差,这取决于它们在输入中的位置(特别是它们相对于 QuickSort 的位置)枢)。

随着这个随机数的键空间增加,发生冲突的概率会下降(随机性的来源很好),但请记住,随着您要排序的值数量的增加,生日悖论表明其中至少一对碰撞的速度很快。

对于整数键,该键有 2^32 个唯一值,即使假设随机值完全均匀分布,有 75,000 行,也有 50% 的概率会发生冲突。 维基百科

您提出的加密哈希方法可能具有足够大的密钥空间 (160) 位,以使发生冲突的机会可以忽略不计,但是您的算法在实际进行比较之前将所有随机性分解回单个 int,这否定了那个更大的键空间。

您最好的方法是将不同的“sortOrder”值与每个数据项相关联,使用经过验证的算法对这些值进行洗牌,然后按该值对结果进行排序。

如果您使用的是 Array.Sort,则会出现一个重载,它需要一个“键”数组和一个“值”数组。键数组正常排序,但是每当键数组中的值被移动时,值数组中的相应条目也被移动。

就像是:


Something[] data;//populated somewhere
int[] keys = new int[data.Length];//or long if you might have lots of data
for(int i=0;i<keys.Length;++i) {
 keys[i] = i;
}

Shuffle(keys);

Array.Sort(keys, data);
于 2009-02-18T15:46:43.587 回答