Заказать и взять в LINQ - PullRequest
1 голос
/ 02 апреля 2019

Я смотрел несколько видео на Youtube с интервью программистов. Один из вопросов - написать функцию, которая возвращает n-й наименьший элемент массива.

В видео, которое я видел, одна леди пыталась закодировать это в C ++ с некоторой периодичностью, но я подумал, что хорошо, в C # это одна строка:

var nth = vals.OrderBy(x => x).Take(i).Last();

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

Поэтому я решил проверить его и написал класс MyComparable : IComparable<MyComparable> с одним значением и статическим счетчиком в методе CompareTo:

 class MyComparable : IComparable<MyComparable>
    {
        public MyComparable(int val)
        {
            Val = val;
        }

        public static int CompareCount { get; set; }

        public int Val { get; set; }

        public int CompareTo(MyComparable other)
        {
            ++CompareCount;

            if (ReferenceEquals(this, other)) return 0;
            if (ReferenceEquals(null, other)) return 1;


            return Val.CompareTo(other.Val);
        }
    }

Затем я написал цикл, который находит n-й элемент в массиве:

 static void Main(string[] args)
        {
            var rand = new Random();

            var vals = Enumerable.Range(0, 10000)
//                .Reverse() // pesimistic scenario
//                .OrderBy(x => rand.Next()) // average scenario
                .Select(x => new MyComparable(x))
                .ToArray();


            for (int i = 1; i < 100; i++)
            {
                var nth = vals.OrderBy(x => x).Take(i).Last();
                Console.WriteLine($"i: {i,5}, OrderBy: {MyComparable.CompareCount,10}, value {nth.Val}");
                MyComparable.CompareCount = 0;


                var my_nth = vals.OrderByInsertion().Take(i).Last();
                Console.WriteLine($"i: {i,5}, Insert : {MyComparable.CompareCount,10}, value {my_nth.Val}");
                MyComparable.CompareCount = 0;

                my_nth = vals.OrderByInsertionWithIndex().Take(i).Last();
                Console.WriteLine($"i: {i,5}, Index  : {MyComparable.CompareCount,10}, value {my_nth.Val}");
                MyComparable.CompareCount = 0;

                Console.WriteLine();
                Console.WriteLine();
            }

        }

Я также написал 2 "разные" реализации для нахождения элемента min, его возврата и удаления из списка:

   public static IEnumerable<T> OrderByInsertion<T>(this IEnumerable<T> input) where T : IComparable<T>
        {
            var list = input.ToList();

            while (list.Any())
            {
                var min = list.Min();
                yield return min;
                list.Remove(min);
            }
        }

        public static IEnumerable<T> OrderByInsertionWithIndex<T>(this IEnumerable<T> input) where T : IComparable<T>
        {
            var list = input.ToList();

            while (list.Any())
            {
                var minIndex = 0;


                for (int i = 1; i < list.Count; i++)
                {
                    if (list[i].CompareTo(list[minIndex]) < 0)
                    {
                        minIndex = i;
                    }
                }

                yield return list[minIndex];
                list.RemoveAt(minIndex);
            }
        }

Результаты действительно были для меня неожиданностью:

i:     1, OrderBy:      19969, value 0
i:     1, Insert :       9999, value 0
i:     1, Index  :       9999, value 0


i:     2, OrderBy:      19969, value 1
i:     2, Insert :      19997, value 1
i:     2, Index  :      19997, value 1


i:     3, OrderBy:      19969, value 2
i:     3, Insert :      29994, value 2
i:     3, Index  :      29994, value 2


i:     4, OrderBy:      19969, value 3
i:     4, Insert :      39990, value 3
i:     4, Index  :      39990, value 3


i:     5, OrderBy:      19970, value 4
i:     5, Insert :      49985, value 4
i:     5, Index  :      49985, value 4

...

i:    71, OrderBy:      19973, value 70
i:    71, Insert :     707444, value 70
i:    71, Index  :     707444, value 70

...

i:    99, OrderBy:      19972, value 98
i:    99, Insert :     985050, value 98
i:    99, Index  :     985050, value 98

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

Итак, мой вопрос в основном к интервьюерам: как бы вы оценили такой ответ?

Код:

var nth = vals.OrderBy(x => x).Take(i).Last();

Сложность времени:

Я не знаю деталей, но, вероятно, OrderBy использует какую-то быструю сортировку, без n log(n), независимо от того, какой n-й элемент нам нужен.

Не могли бы вы попросить меня реализовать мои собственные методы, подобные этим, или этого будет достаточно для использования фреймворка?

EDIT:

Итак, как и в приведенном ниже ответе, OrderedEnumerable использует вариант QuickSelect для сортировки только тех элементов, которые вы запрашиваете. С положительной стороны, он кэширует порядок.

Хотя вы можете найти n-й элемент немного быстрее, он не класс быстрее, а на несколько процентов быстрее. Кроме того, каждый программист C # поймет ваш один вкладыш.

Я думаю, что мой ответ во время собеседования закончится где-то рядом: «Я буду использовать OrderBy, потому что он достаточно быстрый, а запись занимает 10 секунд. Если окажется, что он медленный, мы можем выиграть некоторое время с QucikSelect, но на его реализацию уходит много времени "

Спасибо всем, кто решил поучаствовать в этом, и извините всех, кто потратил свое время, думая, что это что-то другое

1 Ответ

1 голос
/ 10 апреля 2019

Хорошо, давайте начнем с низко висящих фруктов:

Ваша реализация неверна.Вам нужно взять index + 1 элементов из последовательности.Чтобы понять это, рассмотрите index = 0 и перечитайте документацию для Take.

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

Кроме того, Take(i + 1).Last() эквивалентно ElementAt(i).Вы действительно должны использовать это.

Да, и ваш эталонный тест действительно бесполезен, потому что чем больше элементов вашего диапазона вам нужно использовать с Take, тем больше эти алгоритмы должны приближаться друг к другу.Насколько я могу судить, вы правы в своем анализе времени выполнения O (n log n).

Существует решение, которое имеет временную сложность O (n), хотя (не O (log)о) как я неправильно утверждал ранее).Это то решение, которого ожидал интервьюер.
Во что бы то ни стало, написанный вами код не может быть перенесен в это решение, поскольку вы не можете контролировать процесс сортировки.

Если бы у вас была возможность реализовать Quickselect (что и является целью), в результате вы получите теоретическое улучшение по сравнению с предложенным здесь LINQ-запросом (особенно для высоких показателей).Ниже приведен перевод этого псевдокода из статьи в Википедии о быстром отборе , основанной на вашем коде

static T SelectK<T>(T[] values, int left, int right, int index) 
   where T : IComparable<T>
{
    if (left == right) { return values[left]; }
    // could select pivot deterministically through median of 3 or something
    var pivotIndex = rand.Next(left, right + 1);
    pivotIndex = Partition(values, left, right, pivotIndex);
    if (index == pivotIndex) {
        return values[index];
    } else if (index < pivotIndex) {
        return SelectK(values, left, pivotIndex - 1, index);
    } else {
        return SelectK(values, pivotIndex + 1, right, index);
    }
}

static int Partition<T>(T[] values, int left, int right, int pivot) 
    where T : IComparable<T>
{
    var pivotValue = values[pivot];
    Swap(values, pivot, right);
    var storeIndex = left;
    for (var i = left; i < right; i++) {
        if (values[i].CompareTo(pivotValue) < 0)
        {
            Swap(values, storeIndex, i);
            storeIndex++;
        }
    }
    Swap(values, right, storeIndex);
    return storeIndex;
}

Нерепрезентативная выборка теста, который я запускал, дает вывод:

i:  6724, OrderBy:      52365, value 6723
i:  6724, SelectK:      40014, value 6724


i:   395, OrderBy:      14436, value 394
i:   395, SelectK:      26106, value 395


i:  7933, OrderBy:      32523, value 7932
i:  7933, SelectK:      17712, value 7933


i:  6730, OrderBy:      46076, value 6729
i:  6730, SelectK:      34367, value 6730


i:  6536, OrderBy:      53458, value 6535
i:  6536, SelectK:      18341, value 6536

Поскольку моя реализация SelectK использует случайный элемент поворота, его выходные данные довольно сильно различаются (см., Например, второй запуск).Это также значительно хуже, чем высоко оптимизированный алгоритм сортировки, реализованный в стандартной библиотеке.
Даже в некоторых случаях DirectK ​​превосходит стандартную библиотеку, хотя я не прилагал особых усилий.

Сейчасзаменив случайный шарнир на медиану 3 [1] (что является довольно плохим селектором пивота), мы можем получить немного другой SelectK и расу, которая отличается от OrderBy и SelectK.

IВы участвовали в гонках этих трех лошадей с 1-метровыми элементами в массиве, используя случайную сортировку, которую вы уже имели, запрашивая индекс в последних 20% массива и получили результаты, подобные следующим:

Winning counts: OrderBy 32, SelectK 32, MedianOf3 35
Winning counts: OrderBy 26, SelectK 35, MedianOf3 38 
Winning counts: OrderBy 25, SelectK 41, MedianOf3 33

Evenдля 100k элементов и без ограничения индекса до конца массива этот шаблон, кажется, сохраняется, хотя и не так ярко выраженный:

--- 100k elements
Winning counts: OrderBy 24, SelectK 34, MedianOf3 41
Winning counts: OrderBy 33, SelectK 33, MedianOf3 33
Winning counts: OrderBy 32, SelectK 38, MedianOf3 29
--- 1m elements
Winning counts: OrderBy 27, SelectK 32, MedianOf3 40
Winning counts: OrderBy 32, SelectK 38, MedianOf3 29
Winning counts: OrderBy 35, SelectK 31, MedianOf3 33

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

Конечно, ваша реализация значительно проще для понимания:)

[1] - Реализация взята из этот SO-ответ , с 3 сравнениями на шаг глубины рекурсии

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