Каков наилучший алгоритм сортировки на месте для сортировки односвязного списка
Я читал на месте алгоритм сортировки для сортировки связанных списков. Согласно Википедии
Merge sort часто является лучшим выбором для сортировки связанного списка: в этой ситуации относительно легко реализовать сортировку слиянием таким образом, что требуется только
Θ(1)
дополнительное пространство и медленная производительность случайного доступа связанного списка делают некоторые другие алгоритмы (такие как quicksort) работать плохо, а другие (такие как heapsort) полностью невозможно.
насколько мне известно, алгоритм сортировки слиянием составляет не алгоритм сортировки на месте и имеет наихудшую пространственную сложность O(n)
вспомогательный. Теперь, с учетом этого, я не могу решить, существует ли подходящий алгоритм для сортировки одиночного связанного списка с O(1)
вспомогательное пространство.
2 ответов
как указал Фабио А. в комментарии, алгоритм сортировки, подразумеваемый следующими реализациями слияния и разделения, фактически требует o(log n) дополнительного пространства в виде кадров стека для управления рекурсией (или их явным эквивалентом). Алгоритм O (1)-пространства возможен с использованием совершенно другого подхода снизу вверх.
вот алгоритм слияния O(1)-space, который просто создает новый список, перемещая нижний элемент из верхней части каждого списка в конец нового списка:
struct node {
WHATEVER_TYPE val;
struct node* next;
};
node* merge(node* a, node* b) {
node* out;
node** p = &out; // Address of the next pointer that needs to be changed
while (a && b) {
if (a->val < b->val) {
*p = a;
a = a->next;
} else {
*p = b;
b = b->next;
}
// Next loop iter should write to final "next" pointer
p = &(*p)->next;
}
// At least one of the input lists has run out.
if (a) {
*p = a;
} else {
*p = b; // Works even if b is NULL
}
return out;
}
можно избежать указателя на указатель p
специальным корпусом первый элемент, который будет добавлен в список вывода, но я думаю, что способ, которым я это сделал, яснее.
вот алгоритм разделения O (1)-space, который просто разбивает список на 2 равные части:
node* split(node* in) {
if (!in) return NULL; // Have to special-case a zero-length list
node* half = in; // Invariant: half != NULL
while (in) {
in = in->next;
if (!in) break;
half = half->next;
in = in->next;
}
node* rest = half->next;
half->next = NULL;
return rest;
}
обратите внимание, что half
перемещается вперед только в два раза меньше, чем in
есть. После возвращения этой функции список первоначально передавался как in
будет изменен таким образом, что он содержит только первые элементы ceil(n/2), а возвращаемое значение-это список, содержащий оставшиеся элементы пола(n/2).
это как-то напоминает мне о моем ответе на Голландский Национальный флаг проблема вопрос.
подумав, что это то, к чему я пришел, давайте посмотрим, сработает ли это. Я полагаю, что основная проблема-это шаг слияния mergesort в O(1)
дополнительное пространство.
наше представление связанного списка:
[ 1 ] => [ 3 ] => [ 2 ] => [ 4 ]
^head ^tail
вы в конечном итоге с этим слияние шаг:
[ 1 ] => [ 3 ] => [ 2 ] => [ 4 ]
^p ^q ^tail
будучи p
и q
в указатели для сегментов, которые мы хотим объединить.
просто добавьте свои узлы после tail
указатель. Если *p <= *q
добавить p
в хвосте.
[ 1 ] => [ 3 ] => [ 2 ] => [ 4 ] => [ 1 ]
^p ^pp ^q/qq ^tail ^tt
в противном случае, добавьте q
.
[ 1 ] => [ 3 ] => [ 2 ] => [ 4 ] => [ 1 ] => [ 2 ]
^p ^pp ^q ^qq/tail ^tt
(отслеживание окончания нашего списка q
становится сложно)
теперь, если вы переместите их, вы быстро потеряете след, где вы находитесь. Вы можете победить это, имея умный способ переместить указатели или добавить длины в уравнение. Я определенно предпочитаю последнее. Подход становится:
[ 1 ] => [ 3 ] => [ 2 ] => [ 4 ]
^p(2) ^q(2) ^tail
[ 3 ] => [ 2 ] => [ 4 ] => [ 1 ]
^p(1) ^q(2) ^tail
[ 3 ] => [ 4 ] => [ 1 ] => [ 2 ]
^p(1) ^q(1) ^tail
[ 4 ] => [ 1 ] => [ 2 ] => [ 3 ]
^p(0)/q(1) ^tail
[ 1 ] => [ 2 ] => [ 3 ] => [ 4 ]
^q(0) ^tail
теперь, вы используете O(1)
дополнительное пространство, чтобы иметь возможность перемещать элементы.