11

まず、フィッシャー・イェーツ・シャッフルについて知っています。しかし、引数のために、ユーザーがドロップダウン リストから並べ替えオプションを選択できるようにしたいとしましょう。このリストには、「ランダム」オプションが含まれます。彼らの選択の結果に基づいて、ソートを IComparer インスタンスに置き換えたいだけです。IComparer はどのように見えるでしょうか?

Google は多くの欠陥のある結果を表示しますが、これらはすべて次の形式を取ります。

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 に対して n log n) が必要であり、考えられるすべての並べ替えが表示されることを期待しています。残念ながら、そのリンクはその質問に対する現在受け入れられている回答であるため、もう少し良いものに置き換えることができることを望んでいます。

他に何もないとしても、これが IComparable ソリューションを探しているすべての Google クエリの磁石になることを望んでいます。つまり、間違ったバージョンを使用するように他の場所に指示するのではなく、ここに行き着くということです。

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

私が別の場所で得た 1 つの提案は、コレクションをアレンジするための単一の操作を記述する個別の IArranger インターフェイスを作成することでした。これは、個々のアイテムではなくコレクション全体を操作するため、IComparer/IComparable が機能しない場合に機能します。次のようになります。

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

次にShuffle、適切な Fisher-Yates アルゴリズムを使用して IArranger インターフェースから を実装し、関心のある追加の各バリエーションをラップする実装も用意します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 にこれのサポートを追加します。その後も、単純なランタイム アルゴリズムのスワッピングが得られ、シャッフル アルゴリズムの実装が改善され、それを使用するコードが自然に感じられます。

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

ある時点で (T のインスタンスが等しい場合) ゼロ リターンを要求するIComparerは、フィッシャー イェーツ シャッフルを統計的に模倣する汎用 IComparer を作成することを数学的に不可能にします。必ず偏りがあります。実際のシャッフルでは、特定の値を強制的に返すことは決してありません。

于 2009-02-17T18:44:08.833 に答える
0

James Curran のアイデアをフォローアップするには、IComparer に「並べ替えられた」値をリストとして保持させます。新しい値が発生した場合、それをリストのランダムな位置に挿入します。リスト インデックスで比較します。リストをバランスの取れたツリーまたは何かとして維持することで最適化します。このような IComparer のすべてのインスタンスは、一貫性のあるランダムな並べ替え順序を維持するため、ランダムな並べ替えを一貫して同じランダムな順序にするか、毎回異なる順序にするかを選択できます。「ランダム」と読みたい場合は、マイナーな変更により、等しい要素を異なる順序位置に「ソート」することもできます。

于 2009-02-18T00:51:13.863 に答える
0

やらないでください。

これまでに提案されたすべてのアルゴリズムは、出力に何らかのバイアスを導入します (一部は他よりも大きくなります)。

@Princess と @Luke は、データとともに乱数を保存することを提案しています。ただし、これらの乱数のいずれか 2 つが別の乱数と同じ値を持つ可能性があるため、これら 2 つの項目間のソート順は決定論的にバイアスされます。

これの最悪のケースは、並べ替えルーチンが「安定」している場合です (つまり、等しいと見なされるオブジェクトは、常に入力されたのと同じ順序で出力されます)。Array.Sort はたまたま安定していません (内部的に QuickSort を使用しています) が、2 つの項目が入力内のどこにあるか (具体的には QuickSort の相対的な場所) に応じて同じ値を持つ場合に常に発生する偏りがあります。ピボット)。

この乱数のキースペースが増加するにつれて、衝突の確率は低下します (ランダム性の良いソースを使用)。ただし、ソートする値の数が増えるにつれて、誕生日のパラドックスにより、それらの中で衝突する少なくとも 1 つのペアは非常に急速に上昇します。

整数キーの場合、キーには 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 に答える
0

興味深い取り組みです。ほとんどの場合、IComparer の誤用/乱用です。

その目的のために構築されていないメカニズムを使用して、ランダムな加重ソートを実行しようとしています。

独自の並べ替えルーチンと独自の比較機能を実装してみませんか? それでも足りない気がします。

于 2009-02-18T14:20:25.230 に答える
0

ランダムな値が事前に割り当てられている非表示フィールドに基づいてソートするのはどうですか?

于 2009-02-17T18:00:12.620 に答える