【问题标题】:OrderBy with a non-transitive IComparer带有非传递性 IComparer 的 OrderBy
【发布时间】:2013-12-03 23:25:52
【问题描述】:

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

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

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

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

【问题讨论】:

  • 用物品试试吧{ 0.3, 1.5 }, { 0.6, 4.5 }, { 0.9, 3 } 看看你会得到什么
  • @kevingessner 我看到了这个问题,我不认为这是重复的。我特意问的是使用非传递性 IComparer 的后果是什么。
  • @ohmusama 输出是(0.3, 1.5) (0.6, 4.5) (0.9, 3),这是我所期望的,但这并不能说明一般情况。 (代码可在线测试here
  • 结果是可能相同数据的非确定性排序顺序。此外,在某些用途中,非传递比较器可能会导致无限循环。

标签: c# linq sorting undefined-behavior internals


【解决方案1】:

问题是第一个排序级别(X)可能已经导致不同的订单。想象一下,所有项目都在彼此的一个 epsilon 范围内。然后所有排序顺序都与您的比较器一致,因为它将始终返回 0。排序算法可以翻转硬币并仍然提供“正确”答案。没用。

如果第一级是任意排序的,你不能指望第二级排序。

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

您如何仍能实现您的目标?您的问题只是定义不明确,因为许多解决方案都是可能的。我得到了您想要实现的目标,但是您当前对问题的定义是不可能的。

我建议:按X(不带epsilon)对所有项目进行排序。然后,从左到右遍历已排序的项目,并将这些项目合并到最多一个 epsilon 宽的组中。这会为您提供 X-value 最多相差 epsilon 的项目组。

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

【讨论】:

  • 我不认为你的遍历是正确的。用 epsilon 0.4 说“1, 1.4, 1.6”。根据您的算法,结果是“(1,1.4), 1.6”,表示两个数字顺序:“1, 1.4, 1.6”和“1.4, 1, 1.6”。明确的第二个是错误的。唯一的顺序是“1, 1.4, 1.6”。
  • @Vince 为什么"1.4, 1, 1.6" 根据这个算法可能是一个结果顺序?您可以按 X 值对单个组中的项目进行排序,以使顺序明确且没有歧义。
  • 组是一个抽象概念。例如,通常排序数字“2,1”,那么它具有唯一的顺序“1,2”。现在这个问题有一个特殊的要求(epsilon)。使用 epsilon 3 对数字“1,2,3”进行排序,然后任何顺序(1,2,3 或 3,2,1)都可以。没有概念组。
  • @Vince 我介绍了这个概念,现在它已经存在了。首先你说这个算法是错误的,因为它产生了错误排序的输出。然后我修好了,你说任何顺序都是正确的。我不明白这个问题。你可以随心所欲地拥有它,这是一个不重要的细节。你能告诉我这个算法没有正确排序的具体输入吗?我已经解决了您已经提供的所有意见。
  • "first(1),second(1)" 和 epsilon(0),它有两个顺序 "first(1),second(1)" 和 "second(1),first(1) )”。删除前缀(第一个和第二个)后,它具有唯一的顺序“1,1”(实际上是两个顺序)。现在,带有 epsilon(1) 的“1,2,3”。数字没有高低,排序只是一个方向(从小到大,或从大到小)。根据您的算法,它将是“(1,2),3”,并且看起来 2 比 3 更像 1。太不公平了。
【解决方案2】:

查看我的代码 sn-p 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表示a

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-27
    • 2010-10-30
    • 1970-01-01
    相关资源
    最近更新 更多