Почему неспособность распознать равенство портит сортировку списка C#?

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

я написал заказ на struct, и допустил одну ошибку:

  • моя структура имеет особое состояние, назовем это "мин".
  • если структура находится в состоянии min, то она меньше любой другой структуры.
  • мой CompareTo метод сделал одну ошибку:a.CompareTo(b) вернутся -1, когда a был "мин", но, конечно, если b также " min " он должен возвращать 0.

теперь эта ошибка полностью испортили List<MyStruct> Sort() метод: весь список будет (иногда) выходить в случайном порядке.

  • мой список содержал ровно один объект в состоянии "min".
  • кажется, моя ошибка может повлиять только на вещи, если один" минимальный " объект сравнивался с самим собой.
  • почему это вообще происходит при сортировке?
  • и даже если это так,как это может привести к неправильному порядку двух "не-мин" объектов?

использование LINQ OrderBy метод может вызвать бесконечный цикл...

маленький, полный, тест, пример:

struct MyStruct : IComparable<MyStruct>
{
    public int State;
    public MyStruct(int s) { State = s; }
    public int CompareTo(MyStruct rhs)
    {
        // 10 is the "min" state.  Otherwise order as usual
        if (State == 10) { return -1; } // Incorrect
        /*if (State == 10) // Correct version
        {
            if (rhs.State == 10) { return 0; }
            return -1;
        }*/
        if (rhs.State == 10) { return 1; }
        return this.State - rhs.State;
    }
    public override string ToString()
    {
        return String.Format("MyStruct({0})", State);
    }
}

class Program
{
    static int Main()
    {
        var list = new List<MyStruct>();
        var rnd = new Random();
        for (int i = 0; i < 20; ++i)
        {
            int x = rnd.Next(15);
            if (x >= 10) { ++x;  }
            list.Add(new MyStruct(x));
        }
        list.Add(new MyStruct(10));
        list.Sort();
        // Never returns...
        //list = list.OrderBy(item => item).ToList();

        Console.WriteLine("list:");
        foreach (var x in list) { Console.WriteLine(x); }

        for (int i = 1; i < list.Count(); ++i)
        {
            Console.Write("{0} ", list[i].CompareTo(list[i - 1]));
        }

        return 0;
    }
}

1 ответов


кажется, моя ошибка может повлиять только на вещи, если один" минимальный " объект сравнивался с самим собой.

не совсем. Это может и быть вызвано, если было два разных объекта "min". В случае списка, отсортированного в это конкретное время, это может произойти только в том случае, если элемент сравнивается с самим собой. Но другой случай стоит рассмотреть в целом с точки зрения того, почему предоставление нетранзитивного компаратора методу, который ожидает транзитивного компаратора это очень плохо.

почему это вообще происходит при сортировке?

почему бы и нет?

List<T>.Sort() С помощью Array.Sort<T> на его элементы. Array.Sort<T> в свою очередь использует смесь сортировки вставки, Heapsort и Quicksort, но для упрощения рассмотрим общий quicksort. Для простоты мы будем использовать IComparable<T> напрямую, а не через System.Collections.Generic.Comparer<T>.Default:

public static void Quicksort<T>(IList<T> list) where T : IComparable<T>
{
  Quicksort<T>(list, 0, list.Count - 1);
}
public static void Quicksort<T>(IList<T> list, int left, int right) where T : IComparable<T>
{
  int i = left;
  int j = right;
  T pivot = list[(left + right) / 2];

  while(i <= j)
  {
    while(list[i].CompareTo(pivot) < 0)
      i++;

    while(list[j].CompareTo(pivot) > 0)
      j--;

    if(i <= j)
    {
      T tmp = list[i];
      list[i] = list[j];
      list[j] = tmp;
      i++;
      j--;
    }
  }

  if(left < j)
    Quicksort(list, left, j);

  if(i < right)
    Quicksort(list, i, right);
}

это работает следующим образом:

  1. выбрать элемент, называется pivot, из списка (мы используем середину).
  2. переупорядочить список так, чтобы все элементы со значениями, меньшими, чем pivot, находились перед pivot, а все элементы со значениями, большими, чем pivot, - после него.
  3. pivot теперь находится в своем конечном положении, с несортированным под-списком до и после него. Рекурсивно примените те же шаги к этим двум под-спискам.

теперь, есть две вещи, чтобы отметить о примере кода выше.

во-первых, мы не мешаем pivot сравнивается с самим собой. Мы могли бы это сделать, но зачем? Во-первых, нам нужен какой-то код сравнения для этого, который именно то, что вы уже предоставили в своем CompareTo() метод. Чтобы избежать впустую CompareTo нужно позвонить CompareTo()* дополнительное время для каждого сравнения (!) или же отслеживать позицию pivot что добавило бы больше отходов, чем удалило.

и даже если это так, как это может привести к тому, что относительный порядок двух "не-мин" объектов будет неправильным?

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

использование метода LINQ OrderBy может вызвать бесконечный цикл

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

*если бы это была ссылка, а не тип значения, мы могли бы сделать ReferenceEquals быстро, но помимо того, что это не будет хорошо с структур, и тот факт, что если это действительно была экономия времени для данного типа надо if(ReferenceEquals(this, other)) return 0; на CompareTo в любом случае, он все равно не исправит ошибку, когда в списке будет более одного "минимального" элемента.