Почему неспособность распознать равенство портит сортировку списка 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);
}
это работает следующим образом:
- выбрать элемент, называется pivot, из списка (мы используем середину).
- переупорядочить список так, чтобы все элементы со значениями, меньшими, чем pivot, находились перед pivot, а все элементы со значениями, большими, чем pivot, - после него.
- pivot теперь находится в своем конечном положении, с несортированным под-списком до и после него. Рекурсивно примените те же шаги к этим двум под-спискам.
теперь, есть две вещи, чтобы отметить о примере кода выше.
во-первых, мы не мешаем pivot
сравнивается с самим собой. Мы могли бы это сделать, но зачем? Во-первых, нам нужен какой-то код сравнения для этого, который именно то, что вы уже предоставили в своем CompareTo()
метод. Чтобы избежать впустую CompareTo
нужно позвонить CompareTo()
* дополнительное время для каждого сравнения (!) или же отслеживать позицию pivot
что добавило бы больше отходов, чем удалило.
и даже если это так, как это может привести к тому, что относительный порядок двух "не-мин" объектов будет неправильным?
потому что разделы quicksort, он не делает один массивный вид, но серию мини-сортов. Поэтому неправильное сравнение получает ряд возможностей испортить части этих видов, каждый раз приводя к Под-списку неправильно отсортированных значений, которые алгоритм считает "обработанными". Поэтому в тех случаях, когда ошибка в компараторе попадает, ее повреждение может быть распространено по большей части списка. Так же, как он делает свою сортировку серией мини-сортов, так он будет делать сортировку багги серией мини-сортов багги.
использование метода LINQ OrderBy может вызвать бесконечный цикл
он использует вариант Quicksort, который гарантирует стабильность; два эквивалентных элемента по-прежнему будут иметь тот же относительный порядок после поиска, что и раньше. Дополнительная сложность, по-видимому, приводит к тому, что он не только сравнивает элемент с самим собой, но затем он продолжает делать это вечно, так как пытается убедиться, что он находится как перед собой, так и в том же порядке, что и раньше. (Да, последнее предложение не имеет смысла, и именно поэтому оно никогда не возвращается).
*если бы это была ссылка, а не тип значения, мы могли бы сделать ReferenceEquals
быстро, но помимо того, что это не будет хорошо с структур, и тот факт, что если это действительно была экономия времени для данного типа надо if(ReferenceEquals(this, other)) return 0;
на CompareTo
в любом случае, он все равно не исправит ошибку, когда в списке будет более одного "минимального" элемента.