QuickSort и Hoare Partition - PullRequest
       25

QuickSort и Hoare Partition

13 голосов
/ 26 августа 2011

Мне сложно перевести QuickSort с разделением Hoare на 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. </p>

Вот псевдокод дляРазделение 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 

Спасибо!

РЕДАКТИРОВАТЬ:

Правильный код 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;
    }
}

Ответы [ 7 ]

7 голосов
/ 26 июля 2014

Чтобы ответить на вопрос «Почему работает разбиение Hoare?»:

Давайте упростим значения в массиве до трех видов: L значения (те, которые меньше, чем значение поворота), E значения (те, которые равны значению поворота), и Значение G (больше, чем значение разворота).

Мы также дадим специальное имя одному месту в массиве; мы назовем это местоположение s , и это место, где указатель j находится после завершения процедуры. Знаем ли мы заранее, где находится s ? Нет, но мы знаем, что некоторое местоположение будет соответствовать этому описанию.

Используя эти термины, мы можем выразить цель процедуры разбиения в несколько разных терминах: она состоит в том, чтобы разбить один массив на два меньших подмассива, которые не будут неправильно отсортированы относительно каждого Другой. Это «не отсортированное» требование удовлетворяется, если выполняются следующие условия:

  1. Подмассив "low", который идет от левого конца массива до и включает s , не содержит G значений.
  2. Массив "high", который начинается сразу после s и продолжается до правого конца, не содержит L значений.

Это действительно все, что нам нужно сделать. Нам даже не нужно беспокоиться о том, где значения E оказываются на каждом проходе. До тех пор, пока каждый проход корректирует подмассивы по отношению друг к другу, более поздние проходы будут заботиться о любом беспорядке, который существует внутри любого подмассива.

Итак, давайте теперь рассмотрим вопрос с другой стороны: как процедура разбиения гарантирует, что в s или слева от него нет значений G и нет L значения справа от s ?

Ну, «набор значений справа от s » совпадает с «набором ячеек, на которые указатель j перемещается, прежде чем достигнет s ». И «набор значений слева от включительно s » совпадает с «набором значений, по которым указатель i перемещается до того, как j достигает s ».

Это означает, что любые значения, которые являются неуместными, на некоторой итерации цикла будут находиться под одним из двух наших указателей. (Для удобства, скажем, это указатель j , указывающий на значение L , хотя он работает точно так же для указателя i , указывающего на G value.) Где будет указатель i , когда указатель j находится на неуместном значении? Мы знаем, что это будет:

  1. в расположении в "низком" подмассиве, где значение L может быть без проблем;
  2. указывает на значение, которое является либо E , либо значением G , которое может легко заменить значение L при j указатель. (Если бы оно не было на E или G , оно бы не остановилось.)

Обратите внимание, что иногда указатели i и j фактически останавливаются на значениях E . Когда это произойдет, значения будут переключены, даже если в этом нет необходимости. Это не приносит никакого вреда, хотя; мы говорили ранее, что размещение значений E не может привести к неправильной сортировке между подмассивами.

Итак, подведем итог: разделение Hoare работает, потому что:

  1. Он разделяет массив на меньшие подмассивы, которые не отсортированы по отношению друг к другу;
  2. Если вы продолжите делать это и рекурсивно сортировать подмассивы, в конце концов от массива, который не будет отсортирован, не останется ничего.
5 голосов
/ 26 августа 2011

Я считаю, что с этим кодом есть две проблемы. Для начала, в вашей функции быстрой сортировки, я думаю, вы хотите изменить порядок строк

 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 это нигде не упоминается; Мне нужно было зайти на страницу с ошибками в книге , чтобы найти это. Это почти наверняка является причиной проблемы, с которой вы столкнулись при ошибке «access out of range», так как с этим нарушенным инвариантом вы можете запустить сразу из массива!

Что касается анализа разбиения Hoare, я бы предложил начать с простого прохождения его вручную. Там также более подробный анализ здесь . Интуитивно понятно, что он работает путем увеличения двух диапазонов от концов диапазона друг к другу - один с левой стороны, содержащий элементы, меньшие, чем стержень, и один с правой стороны, содержащий элементы, большие, чем стержень. Это можно немного изменить, чтобы получить алгоритм разделения Bentley-McIlroy (на который есть ссылка в ссылке), который хорошо масштабируется для обработки одинаковых ключей.

Надеюсь, это поможет!

3 голосов
/ 23 мая 2016

Ваш окончательный код неверен, поскольку начальное значение 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;
    }
}

Дополнительные пояснения:

  1. часть раздела - это просто перевод псевдокода.(Обратите внимание, что возвращаемое значение равно j)

  2. для рекурсивной части, обратите внимание, что проверка базового случая (end <= start вместо end <= start + 1 в противном случае вы пропустите [2 1]subarray)

0 голосов
/ 09 апреля 2017

Простая реализация в 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;
    }
}
0 голосов
/ 20 февраля 2017

Прежде всего, вы неправильно поняли алгоритм разбиения Хоара, который можно увидеть из переведенного кода в c, Поскольку вы рассматривали пивот как самый левый элемент подмассива.

Я объясню, что вы рассматриваете крайний левый элемент как ось.

int HoarePartition (int a[],int p, int r) 

Здесь p и r представляют нижнюю и верхнюю границу массива, который также может быть частью большего массива (подмассива), подлежащего разбиению.

, поэтому мы начинаем с указателей (маркеров), изначально указывающих на конечные точки массива до и после (просто bcoz с использованием цикла do while). Поэтому

i=p-1,

j=r+1;    //here u made mistake

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

Таким образом, мы будем перемещать маркер «i», пока не получим элемент, который больше или равен pivot. И точно так же маркер j, пока мы не найдем элемент, меньший или равный точке поворота.

Теперь, если i

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 до 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);
}
0 голосов
/ 14 января 2017
  1. переместить пивот к первому. (Например, используйте медиану трех. Переключитесь на сортировку вставки для небольшого размера ввода.)
  2. перегородка,
    • периодически поменять местами самый левый в настоящий момент 1 с самым правым в настоящий момент 0.
      0 - cmp (val, pivot) == true, 1 - cmp (val, pivot) == false.
      остановка, если не слева <право. </li>
    • после этого поменяйте шарнир с крайним правым 0.
0 голосов
/ 18 октября 2013

Ваш последний 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[Я] могу сделать это работает.Зачем?Потому что A [i] теперь больше, чем A [r] ИЛИ i == r.Поэтому мы должны обменяться, чтобы гарантировать функцию раздела.

...