OrderBy与非传递IComparer

时间:2013-12-03 23:25:53

标签: c# linq sorting undefined-behavior internals

采用自定义IComparer,如果它们的差异小于给定的epsilon,则将两个双精度视为相等。

如果在OrderBy()中使用此IComparer会发生什么。ThenBy()子句?

具体来说,我正在考虑以下实施:

public class EpsilonComparer : IComparer<double>
{
    private readonly double epsilon;

    public EpsilonComparer(double epsilon)
    {
        this.epsilon = epsilon;
    }

    public int Compare(double d1, double d2)
    {
        if (Math.Abs(d1-d2)<=epsilon) return 0;

        return d1.CompareTo(d2);
    }
}

现在这种IComparer关系显然不具有传递性。 (if a ~ b and b ~ c then a ~ c

使用epsilon == 0.6:

  • 比较(1,1.5)== 0
  • 比较(1.5,2)== 0
  • 比较(1,2)== -1

如果在OrderBy查询中使用此IComparer会发生什么,例如:

List<Item> itemlist;
itemList = itemlist.OrderBy(item=>item.X, new EpsilonComparer(0.352))
                   .ThenBy (item=>item.Y, new EpsilonComparer(1.743)).ToList();

排序的行为是否符合预期,首先按X排序列表,然后按Y排序,同时将大致相等的值视为完全相等? 在某些情况下它会爆炸吗? 或者整个这种定义不明确?

使用没有传递性的IComparer会产生什么后果?

(我知道这很可能是c#语言的未定义行为。我仍然对答案非常感兴趣。)

还有另一种方法可以获得这种排序行为吗? (除了舍入值之外,当两个接近的双打时会引入伪像,一个被向上舍入而另一个被向上舍入)

此问题中代码的在线版权可用here

2 个答案:

答案 0 :(得分:0)

查看我的代码段here。它只是用于第一级排序而不是优化。

OrderBy和ThenBy正在使用通用算法。你需要用像我这样的特殊算法重新实现OrderBy和ThenBy。然后它可以作为OrderBy().ThenBy()

算法详情:

在EpsilonComparer下的排序顺序(x1 x2 x3 ...),如果x4> x1,则x5> x1。如果x4 = x1,则x3 = x1且x5> x1或x5 = x1。

用epsilon(0.4),输入以下数字:0.1,0.6,1,1.1,1.6,2,2,2.6,3,3.1,3.6,4,4.1,4.6,5,5.1,5.6,6,6.1 ,6.6

结果:0.1 0.6 1 1.1(1.6 2 2)2.6 3 3.1 3.6 4 4.1 4.6(5 5.1)5.6(6 6.1)6.6

(a,b,c)表示这些数字相等且数字的顺序不固定。它们可以是(a,b,c),(c,a,b)和任何其他顺序。

a b 表示&lt; b,订单是固定的。

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

namespace Rextester
{
    class Program
    {
        public static void Main(string[] args)
        {
            new EpsilonSort(new EpsilonComparer(0.4), 0.1, 0.6, 1, 1.1, 1.6, 2, 2, 2.6, 3, 3.1, 3.6, 4, 4.1, 4.6, 5, 5.1, 5.6, 6, 6.1, 6.6).Sort();
        }
    }

    public class EpsilonSort
    {
        private readonly IComparer<double> m_comparer;
        private readonly double[] m_nums;
        public EpsilonSort(IComparer<double> comparer, params double[] nums)
        {
            this.m_comparer = comparer;
            this.m_nums = nums;
        }

        public void Sort()
        {
            Node root = new Node();
            root.Datas = new List<double>(this.m_nums);

            foreach (double i in (double[])this.m_nums.Clone())
            {
                this.ProcessNode(i, root);
            }

            this.OutputNodes(root);
        }

        private void OutputNodes(Node root)
        {
            if (root.Datas == null)
            {
                foreach (var i in root.Nodes)
                {
                    this.OutputNodes(i);
                }
            }
            else
            {
                if (root.Datas.Count == 1)
                {
                    Console.WriteLine(root.Datas[0]);
                }
                else
                {
                    Console.Write('(');
                    foreach (var i in root.Datas)
                    {
                        Console.Write(i);
                        Console.Write(' ');
                    }
                    Console.WriteLine(')');
                }
            }
        }

        private void ProcessNode(double value, Node one)
        {
            if (one.Datas == null)
            {
                foreach (var i in one.Nodes)
                {
                    this.ProcessNode(value, i);
                }
            }
            else
            {
                Node[] childrennodes = new Node[3];
                foreach (var i in one.Datas)
                {
                    int direction = this.m_comparer.Compare(i, value);
                    if (direction == 0)
                    {
                        this.AddData(ref childrennodes[1], i);
                    }
                    else
                    {
                        if (direction < 0)
                        {
                            this.AddData(ref childrennodes[0], i);
                        }
                        else
                        {
                            this.AddData(ref childrennodes[2], i);
                        }
                    }
                }
                childrennodes = childrennodes.Where(x => x != null).ToArray();
                if (childrennodes.Length >= 2)
                {
                    one.Datas = null;
                    one.Nodes = childrennodes;
                }
            }
        }

        private void AddData(ref Node node, double value)
        {
            node = node ?? new Node();
            node.Datas = node.Datas ?? new List<double>();
            node.Datas.Add(value);
        }

        private class Node
        {
            public Node[] Nodes;
            public List<double> Datas;
        }
    }

    public class EpsilonComparer : IComparer<double>
    {
        private readonly double epsilon;

        public EpsilonComparer(double epsilon)
        {
            this.epsilon = epsilon;
        }

        public int Compare(double d1, double d2)
        {
            if (Math.Abs(d1 - d2) <= epsilon) return 0;

            return d1.CompareTo(d2);
        }
    }
}

答案 1 :(得分:0)

问题是第一个排序级别(在X上)可能导致不同的订单。想象一下,所有物品都在彼此的一个ε之内。然后所有排序顺序都与您的比较器一致,因为它总是返回0.排序算法可以翻转硬币并仍然提供“正确”的答案。没用。

如果第一级是任意排序的,则不能指望第二级排序级别能够正常工作。

当然,所有这些讨论都没有实际意义,因为您违反了排序API的前提条件。即使它碰巧工作了,你也无法确定它是否适用于a)所有未来.NET版本的所有数据b)。

你怎么能实现目标?您的问题不明确,因为许多解决方案都是可能的。我得到你想要达到的目标,但目前对问题的定义是不可能的。

我建议:按X排序所有项目(不用epsilon)。然后,从左到右遍历已排序的项目,并将项目合并为最多一个epsilon的组。这为您提供了X - 值最多为epsilon的项目组。

然后,您可以使用组号作为第一个排序级别。它只是一个简单的整数,所以排序就没问题了。对于Y字段,您可以使用不带epsilon的普通比较器(或重复相同的技巧)。