Перемешать с помощью IComparer - PullRequest
11 голосов
/ 17 февраля 2009

Прежде всего, я знаю о перетасовке Фишера-Йейтса. Но давайте скажем ради аргументов, что я хочу позволить пользователю выбрать опцию сортировки из выпадающего списка. Этот список будет включать «Случайный» вариант. Основываясь на результате их выбора, я просто хочу заменить в своем экземпляре 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, поскольку я не вижу другого способа сделать это: элементы должны сравниваться с использованием другого, действительно случайного значения, но это значение должно также быть согласованным для элемента в данной операции сортировки.

У меня есть начало здесь , но оно было опубликовано на скорую руку, чрезвычайно медленно и даже не возвращает все возможные сортировки (тестирование показывает, что оно по крайней мере устраняет предвзятость, если не считать недостающие варианты). Я не ожидаю O (n) производительности, как у Фишера-Йейтса, но я хочу что-то разумное (n log n для небольшого числа n), и я ожидаю, что оно покажет все возможные сортировки. К сожалению, эта ссылка является текущим принятым ответом на этот вопрос, и поэтому я надеюсь, что смогу заменить ее на что-нибудь получше.

Если ничего другого, я хочу, чтобы это было магнитом для всех тех запросов Google, которые ищут IComparable-решение - что они окажутся здесь, а не где-то еще, говоря им использовать неправильную версию.

Ответы [ 7 ]

11 голосов
/ 17 февраля 2009

Я был несколько удивлен в этой теме , как много неправильных ответов было опубликовано. Просто ради других, которые придумали решение, аналогичное тому, которое опубликовал ОП, следующий код выглядит правильным:

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 или какой-либо другой случайно сгенерированный ввод, решение, подобное показанному выше, по-прежнему неверно.


С учетом вышесказанного, Фишер-Йейтс является стандартным способом перетасовки массивов, поэтому нет никакой реальной причины использовать IComparer в первую очередь. Fisher-Yates - это O (n), тогда как любая реализация, использующая IComparer, использует быструю сортировку за кулисами, которая имеет временную сложность O (n log n). Просто нет веской причины не использовать хорошо известный эффективный стандартный алгоритм для решения такого рода проблем.

Однако, если вы действительно настаиваете на использовании IComparer и ранда, примените ваши случайные данные перед тем, как вы отсортируете. Для этого требуется проекция данных на другой объект, чтобы вы не потеряли случайные данные:

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;
3 голосов
/ 18 февраля 2009

Одно предложение, которое я получил в другом месте, заключалось в создании отдельного интерфейса IArranger, который описывает одну операцию для Упорядочить коллекцию. Это может работать там, где IComparer / IComparable не может, потому что он работает со всей коллекцией, а не с отдельными элементами. Это может выглядеть примерно так:

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

Тогда я мог бы реализовать Shuffle из интерфейса 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);
}
1 голос
/ 17 февраля 2009

IComparer , требующий нулевого возврата в некоторой точке (для равных экземпляров T), делает математически невозможным создание универсального IComparer, который будет статистически имитировать перемешивание Фишера-Йейтса. Всегда будет предвзятость. Для настоящей случайности вы никогда не захотите заставить ее возвращать какое-либо конкретное значение.

0 голосов
/ 18 февраля 2009

Не делай этого.

Все алгоритмы, предложенные к настоящему времени, вносят какой-то уклон в вывод (некоторые больше, чем другие).

@ Princess и @Luke предлагают хранить случайное число рядом с данными. Однако, поскольку существует вероятность того, что любые два из этих случайных чисел могут иметь то же значение, что и другое, порядок сортировки между этими двумя элементами будет детерминированно смещен

Наихудший случай для этого был бы, если процедура сортировки "стабильна" (то есть объекты, которые считаются равными, всегда выводятся в том же порядке, в котором они были введены). Array.Sort не является стабильным (он использует QuickSort для внутреннего использования), но все же существует смещение, возникающее всякий раз, когда два элемента имеют одинаковое значение, которое зависит от того, где они находятся на входе (и, в частности, от того, где они находятся относительно QuickSort. поворот).

По мере того, как увеличивается пространство клавиш для этого случайного числа, вероятность столкновения уменьшается (с хорошим источником случайности), но имейте в виду, что по мере увеличения количества сортируемых значений парадокс дня рождения диктует, что вероятность столкновения хотя бы одной пары среди них возрастает очень быстро.

Для целочисленного ключа существует 2 ^ 32 уникальных значения для ключа, и даже при условии, что существует абсолютно равномерное распределение случайных значений, с 75 000 строк, существует 50% вероятность того, что произойдет столкновение. Wikipedia .

Подход криптографического хэша, который вы предложили, потенциально имеет достаточно большое битовое пространство ключей (160), чтобы вероятность возникновения коллизии была незначительной, но ваш алгоритм разлагает всю эту случайность обратно до единственного целого, прежде чем фактически выполнить сравнение, которое сводит на нет преимущество этого большего пространства клавиш.

Ваш лучший подход заключается в том, чтобы связать отдельное значение "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);
0 голосов
/ 18 февраля 2009

Интересное начинание. Скорее всего, злоупотребление / злоупотребление IComparer.

Вы пытаетесь выполнить случайную взвешенную сортировку, используя механизм, который не был создан для этой цели.

Почему бы не реализовать собственную процедуру сортировки и свой собственный компаратор? У меня такое ощущение, что даже этого будет недостаточно.

0 голосов
/ 18 февраля 2009

Чтобы продолжить идею Джеймса Керрана: пусть IComparer поддерживает «отсортированные» значения в виде списка; если возникает новое значение, вставьте его в список в произвольной позиции; сравнить по индексу списка. Оптимизируйте, поддерживая список в виде сбалансированного дерева или чего-то еще. Каждый экземпляр такого IComparer будет поддерживать последовательный и случайный порядок сортировки, поэтому у вас есть выбор, чтобы ваша случайная сортировка всегда была одинаковой или случайной. Незначительная модификация даже позволит «отсортировать» одинаковые элементы в разных позициях, если вы предпочитаете читать «случайный» таким образом.

0 голосов
/ 17 февраля 2009

Как насчет сортировки на основе скрытого поля, которому заранее присвоено случайное значение?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...