订购并领取LINQ

时间:2019-04-02 20:59:22

标签: c# sorting linq

我在YouTube上观看了一些采访程序员的视频。问题之一是编写一个返回数组的第n个最小元素的函数。

在视频中,我看到一位女士尝试使用C ++进行一些重复编码,但是我认为很好,在C#中,这只是一个衬里:

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

然后我意识到我不知道实际上这是否是一个好的解决方案,因为下一个问题是时间复杂度。我去看了文档,发现所有由OrderBy返回的对象具有枚举时执行完全延迟排序所需的所有信息。

所以我决定对其进行测试,并在CompareTo方法中编写了一个具有单个值和静态计数器的类MyComparable : IComparable<MyComparable>

 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();

时间复杂度:

我不知道细节,但是不管我们想要哪个第n个元素,OrderBy都可能使用某种快速排序,而不是n log(n)

您会要求我实现那些我自己的方法还是使用框架就足够了?

编辑:

因此,事实证明,像下面的建议答案一样,OrderedEnumerable使用QuickSelect的变体仅将元素排序到您要的位置。从好的方面来说,它会缓存订单。

虽然您可以更快地找到第n个元素,但它的分类速度并不快,但速度要快一些。而且,每个C#程序员都会理解您的要求。

我认为在面试中我的答案将最终指向“我将使用OrderBy,因为它足够快,编写过程只需10秒钟。如果事实证明它很慢,我们可以使用QucikSelect获得一些时间,但是很好地实现它需要很多时间”

感谢每个决定参加此活动的人,并对所有浪费时间认为这是另外的事情的人表示歉意:)

1 个答案:

答案 0 :(得分:1)

好吧,让我们从低挂的水果开始:

您的实现是错误的。您需要从序列中提取index + 1个元素。要了解这一点,请考虑index = 0并重新阅读Take的文档。

您的“基准比较”仅工作,因为在IEnumerable上调用OrderBy()不会修改基础集合。对于我们要做的事情,只允许对基础数组进行修改就容易了。因此,我可以自由更改代码,以在每次迭代中从头开始生成值。

另外,Take(i + 1).Last()等效于ElementAt(i)。您确实应该使用它。

哦,您的基准测试确实没有用,因为您需要使用Take消耗范围内的元素越多,这些算法之间应越接近。据我所知,您对O(n log n)的运行时分析是正确的。

有一种解决方案的时间复杂度为O(n)(不是我先前错误地声称的O(log n))。这是面试官期望的解决方案。
无论花多少钱,您编写的代码都无法移至该解决方案,因为您无法控制排序过程。

如果您可以实施快速选择(此处是目标),则会对您在此处提出的LINQ查询进行理论上的改进(尤其是对于高索引) )。以下是根据您的代码来自wikipedia article on quickselect的伪代码的翻译

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实现使用随机枢轴元素,因此其输出有相当大的变化(例如参见第二轮)。它也比标准库中实现的高度优化的排序算法差很多。
即使在这种情况下,即使我没有花很多力气,SelectK也会直接胜过标准库。

现在用3 [1] (这是一个非常糟糕的枢轴选择器)的中值替换随机枢轴,我们可以获得一个稍有不同的SelectK并与OrderBy和SelectK竞争。 >

我一直在使用数组中具有1m个元素的这三匹马进行比赛,使用您已经拥有的随机排序,在数组的最后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

即使对于10万个元素,并且不将索引限制为数组的末尾,这种模式似乎仍然存在,尽管并不那么明显:

--- 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

通常来说,草率地实现的quickselect在平均情况下要比您的建议要好三分之二...我想说的是一个非常有力的指标,如果您想了解更多细节,它是更好的算法详细信息。

当然,您的实现显然更容易理解:)

[1]-取自this SO answer的实现,每个递归深度步骤进行3次比较