Раздел QuickSort и Hoare
Мне сложно перевести QuickSort с разделением Хоар на C-код и не могу понять, почему. Код, который я использую, показан ниже:
void QuickSort(int a[],int start,int end) {
int q=HoarePartition(a,start,end);
if (end<=start) return;
QuickSort(a,q+1,end);
QuickSort(a,start,q);
}
int HoarePartition (int a[],int p, int r) {
int x=a[p],i=p-1,j=r;
while (1) {
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j)
swap(&a[i],&a[j]);
else
return j;
}
}
Кроме того, я действительно не понимаю, почему работает HoarePartition
. Может кто-нибудь объяснить, почему это работает, или, по крайней мере, связать меня со статьей, которая делает?
Я видел пошаговую работу алгоритма разбиения, но у меня нет интуитивного чувства к нему. В моем коде это даже не работает. Например, учитывая массив
13 19 9 5 12 8 7 4 11 2 6 21
Он будет использовать точку поворота 13, но в конечном итоге с массивом
6 2 9 5 12 8 7 4 11 19 13 21
И вернет j
, который равен a[j] = 11
. Я думал, что должно быть правдой, что массив, начинающийся с этой точки и идущий вперед, должен иметь значения, которые больше, чем точка поворота, но это не так, потому что 11 < 13.
Здесь псевдокод для разбиения Hoare (из CLRS, второе издание), если это полезно:
Hoare-Partition (A, p, r)
x ← A[p]
i ← p − 1
j ← r + 1
while TRUE
repeat j ← j − 1
until A[j] ≤ x
repeat i ← i + 1
until A[i] ≥ x
if i < j
exchange A[i] ↔ A[j]
else return j
Спасибо!
EDIT:
Правильный код C для этой проблемы в конечном итоге будет следующим:
void QuickSort(int a[],int start,int end) {
int q;
if (end-start<2) return;
q=HoarePartition(a,start,end);
QuickSort(a,start,q);
QuickSort(a,q,end);
}
int HoarePartition (int a[],int p, int r) {
int x=a[p],i=p-1,j=r;
while (1) {
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j)
swap(&a[i],&a[j]);
else
return j+1;
}
}
Ответы
Ответ 1
Чтобы ответить на вопрос "Почему работает разделение Хоар?":
Давайте упростить значения в массиве только три вида: значения L (те, меньше, чем значение поворота), значения E (те, равно значению поворота), и значение G (те, больше, чем значение поворота).
Мы также укажем специальное имя в одном месте в массиве; мы назовем это местоположение s, и это место, где находится указатель j, когда процедура заканчивается. Знаем ли мы заранее, какое место находится? Нет, но мы знаем, что какое-то место встретит это описание.
С помощью этих терминов мы можем выразить цель процедуры разбиения в несколько разных терминах: она состоит в том, чтобы разбить один массив на два меньших подматрицы, которые не сортируются по-разному друг относительно друга. Это требование не "неправильно отсортировано" выполняется, если выполняются следующие условия:
- Под-массив "низкий", который идет от левого конца массива до и включает s, не содержит значений G.
- Под-массив "высокий", который начинается сразу после s и продолжается до правого конца, не содержит значений L.
Это действительно все, что нам нужно сделать. Нам даже не нужно беспокоиться, где значения E заканчиваются на любом проходе. До тех пор, пока каждый проход получает субмассивы по отношению друг к другу, последующие проходы будут заботиться о любом беспорядке, который существует внутри любого подматрица.
Итак, теперь давайте обратимся к вопросу с другой стороны: как процедура секционирования гарантирует отсутствие значений G в s или слева от нее, а L значений справа от s?
Ну, "набор значений справа от s" совпадает с "набором ячеек, который j-указатель перемещается, прежде чем он достигнет s". И "набор значений слева и в том числе s" совпадает с "набором значений, которые i-указатель перемещает, прежде чем j достигает s".
Это означает, что любые значения, которые неуместны, будут на некоторой итерации цикла быть под одним из двух наших указателей. (Для удобства, скажем, это j-указатель, указывающий на значение L, хотя он работает точно так же для i-го указателя, указывающего на значение G.) Где будет указатель i, когда j-указатель находится на неуместном значении? Мы знаем, что это будет:
- в местоположении в "низком" подмассиве, где значение L может идти без проблем;
- указывая на значение, которое либо E, либо значение G, которое может легко заменить значение L под j-указателем. (Если бы это не было на значении E или G, оно не остановилось бы там.)
Обратите внимание, что иногда указатели я и j фактически останавливаются на значениях E. Когда это произойдет, значения будут переключаться, даже если в этом нет необходимости. Это не наносит вреда; мы уже говорили, что размещение значений E не может вызвать неправильную сортировку между субмассивами.
Итак, суммируя, разбиение Хоар работает, потому что:
- Он разделяет массив на более мелкие под-массивы, которые не сортируются некорректно относительно друг друга;
- Если вы продолжаете делать это и рекурсивно сортируете суб-массивы, в конечном итоге не останется ничего от несортированного массива.
Ответ 2
Я считаю, что с этим кодом есть две проблемы. Для начала, в вашей функции Quicksort, я думаю, вы хотите изменить порядок строк
int q=HoarePartition(a,start,end);
if (end<=start) return;
чтобы они были такими:
if (end<=start) return;
int q=HoarePartition(a,start,end);
Однако вы должны сделать еще больше, чем это; в частности, это должно читать
if (end - start < 2) return;
int q=HoarePartition(a,start,end);
Причиной этого является то, что раздел Hoare не работает корректно, если диапазон, который вы пытаетесь разбить, имеет нулевой размер или один. В моем издании CLRS это нигде не упоминается; Мне нужно было перейти на страницу ошибок страницы, чтобы найти это. Это почти наверняка является причиной проблемы, с которой вы столкнулись с ошибкой "получить доступ за пределы диапазона", поскольку с этим инвариантом вы можете запустить сразу же массив!
Что касается анализа разбиения Хоара, я бы предложил начать, просто проследив его вручную. Здесь также более подробный анализ здесь. Интуитивно он работает, увеличивая два диапазона от концов диапазона друг к другу - один с левой стороны, содержащий элементы, меньшие, чем ось поворота, и один с правой стороны, содержащий элементы, большие, чем точка поворота. Это может быть слегка изменено для создания алгоритма разбиения Bentley-McIlroy (ссылка на ссылку), который отлично масштабируется для обработки одинаковых ключей.
Надеюсь, это поможет!
Ответ 3
Ваш последний код неверен, поскольку начальное значение j
должно быть r + 1
вместо r
. В противном случае ваша функция раздела всегда игнорирует последнее значение.
На самом деле, HoarePartition работает, потому что для любого массива A[p...r]
, который содержит не менее 2 элементов (т.е. p < r
), каждый элемент A[p...j]
является <=
каждым элементом A[j+1...r]
, когда он завершается.
Таким образом, следующие два сегмента, которые повторяют главный алгоритм, это [start...q]
и [q+1...end]
Итак, правильный код C выглядит следующим образом:
void QuickSort(int a[],int start,int end) {
if (end <= start) return;
int q=HoarePartition(a,start,end);
QuickSort(a,start,q);
QuickSort(a,q + 1,end);
}
int HoarePartition (int a[],int p, int r) {
int x=a[p],i=p-1,j=r+1;
while (1) {
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j)
swap(&a[i],&a[j]);
else
return j;
}
}
Дополнительные пояснения:
-
Часть раздела - это просто перевод псевдокода. (Обратите внимание, что возвращаемое значение j
)
-
для рекурсивной части, обратите внимание, что проверка базового регистра (end <= start
вместо end <= start + 1
в противном случае вы пропустите поднабор [2 1]
)
Ответ 4
Последний код C работает. Но это не интуитивно.
И теперь я, к счастью, изучаю CLRS.
По-моему, псевдокод CLRS ошибочен (в 2e)
Наконец, я считаю, что было бы правильно изменить место.
Hoare-Partition (A, p, r)
x ← A[p]
i ← p − 1
j ← r + 1
while TRUE
repeat j ← j − 1
until A[j] ≤ x
repeat i ← i + 1
until A[i] ≥ x
if i < j
exchange A[i] ↔ A[j]
else
exchnage A[r] ↔ A[i]
return i
Да, добавьте обмен A [r] ↔ A [i] может заставить его работать.
Зачем?
Поскольку A [i] теперь больше A [r] OR я == r.
Поэтому мы должны обменять, чтобы гарантировать функцию раздела.
Ответ 5
- перемещение по центру. (например, используйте медиану из трех. переключитесь на сортировку вставки для небольшого размера ввода.)
- раздел,
- повторная замена в настоящий момент самая левая 1 с самым правым в настоящее время 0.
0 - cmp (val, pivot) == true, 1 - cmp (val, pivot) == false.
остановить, если не осталось < право.
- после этого, своп-поворот с самым правым 0.
Ответ 6
Прежде всего, вы неправильно поняли алгоритм разделения Hoare, который можно увидеть из переведенного кода в c,
Так как u считается поворотным как самый левый элемент подмассива.
Поясняю, что u рассматривает самый левый элемент как ось вращения.
int HoarePartition (int a[],int p, int r)
Здесь p и r представляют собой нижнюю и верхнюю границы массива, которые также могут быть частью большего массива (subarray).
поэтому мы начинаем с указателей (маркеров), первоначально указывающих до и после конечных точек массива (просто bcoz с использованием do while loop). Поэтому
i=p-1,
j=r+1; //here u made mistake
Теперь в соответствии с разбиением на разделы мы хотим, чтобы каждый элемент слева от поворота был меньше или равным оси вращения и больше, чем на правой стороне оси.
Итак, мы будем перемещать маркер "i", пока не получим элемент, который больше или равен оси поворота. И аналогично "j" до тех пор, пока мы не найдем элемент, который меньше или равен оси вращения.
Теперь, если я < j мы заменяем элементы bcoz, оба элемента находятся в неправильной части массива. Таким образом, код будет
do j--; while (a[j] <= x); //look at inequality sign
do i++; while (a[i] >= x);
if (i < j)
swap(&a[i],&a[j]);
Теперь, если "i" не меньше, чем "j" , это означает, что теперь нет элемента между ними для замены, поэтому мы возвращаем позицию "j" .
Итак, теперь массив после секционированной нижней половины находится от 'start to j'
верхняя половина от "j + 1 до конца"
поэтому код будет выглядеть как
void QuickSort(int a[],int start,int end) {
int q=HoarePartition(a,start,end);
if (end<=start) return;
QuickSort(a,start,q);
QuickSort(a,q+1,end);
}
Ответ 7
Простая реализация в java.
public class QuickSortWithHoarePartition {
public static void sort(int[] array) {
sortHelper(array, 0, array.length - 1);
}
private static void sortHelper(int[] array, int p, int r) {
if (p < r) {
int q = doHoarePartitioning(array, p, r);
sortHelper(array, p, q);
sortHelper(array, q + 1, r);
}
}
private static int doHoarePartitioning(int[] array, int p, int r) {
int pivot = array[p];
int i = p - 1;
int j = r + 1;
while (true) {
do {
i++;
}
while (array[i] < pivot);
do {
j--;
}
while (array[j] > pivot);
if (i < j) {
swap(array, i, j);
} else {
return j;
}
}
}
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}