Оглавление
1.1 Основная идея биржевой сортировки
2. Реализация метода быстрой сортировки
3.3 Почему шаги 2 и 3 идеи реализации нельзя поменять местами
3.4 реализация кода версии hoare
4.3 Кодовая реализация метода копания
4.4 Проверка кода метода копания
5. Версии с передним и задним указателем
5.3 Кодовая реализация метода переднего и заднего указателя
5.4 Проверка кода до и после метода указателя
7. Оптимизированная быстрая сортировка
1. Общие алгоритмы сортировки
1.1 Основная идея биржевой сортировки
Пузырьковая сортировка — это одна из обменных сортировок, давайте сначала разберемся со следующими идеями пузырьковой сортировки.
Основная идея: Так называемый обмен заключается в том, чтобы поменять местами две записи в последовательности в соответствии с результатом сравнения ключевых значений двух записей в последовательности.Записи с меньшими значениями ключа перемещаются в впереди последовательности.
2. Реализация метода быстрой сортировки
Рекурсивная реализация очень похожа на обход бинарного дерева в прямом порядке.Существуют три распространенных способа разделить интервал на левую и правую половины: 1. вариант Хоара, 2. метод копания, 3. метод левого и правого указателя.
2.1 Основная идея
Быстрая сортировка — это метод обменной сортировки по структуре бинарного дерева, предложенный Хоаром в 1962 году. Его основная идея заключается в следующем: любой элемент в последовательности сортируемых элементов принимается
за эталонное значение, а сортируемое множество делится на две подпоследовательности в соответствии с к коду сортировки все элементы в левой подпоследовательности меньше опорного значения,
все элементы в правой подпоследовательности больше опорного значения, а затем самая левая подпоследовательность повторяет процесс до тех пор, пока все элементы не будут расположены в соответствующей позиции .Поэтому очень важный момент сзади — идти налево, чтобы найти маленькое, и направо, чтобы найти большое.
3 хора (Холл) версия
3.1 Идеи реализации
Указываем сортировать в порядке возрастания , имя сортируемого массива — a, ключ опорного значения, нижний индекс опорного значения keyi, левое левое, правое право.
1. Выберите ключ.Ключом может быть любой элемент массива, который нужно отсортировать.В этой статье мы выбираем ключ как a[left] ;
2. Идти влево от правого конца (хвоста) массива, когда a[right] < key(a[keyi]), right останавливается;
3. Затем идите вправо от левого конца (головы) массива, когда a[left] > key(a[keyi]), left останавливается;
4. Поменять местами [левый], [правый];
5. Повторите шаги 2, 3 и 4. Когда левый и правый перейдут в одно и то же положение, поменяйте местами элемент и ключ в этом положении, и положение ключа будет определено.
6. В это время ключ делит массив на левый и правый интервалы.Используем первые 5 шагов для левого и правого интервалов, а затем снова определяем ключи левого и правого интервалов, рекурсируем левый интервал, а затем повторите правильный интервал для достижения сортировки.
Примечание. Порядок шагов 2 и 3 изменить нельзя. А почему, мы поговорим об этом после построения диаграммы.
3.2 Схема идей
Наши схемы построены в соответствии с идеями реализации.
3.3 Почему шаги 2 и 3 идеи реализации нельзя поменять местами
Мы можем видеть, что на мысленной диаграмме, когда наконец будет найдено 3, R пойдет первым, а R первым встретится с 3. Если L пойдет первым, ищите большую левую, и L не остановится, когда встретит 3, и перейдет непосредственно к позиции R, позиция R равна 9, когда они встретятся в это время, поменяйте их местами. После обмена не все левые интервалы меньше, чем ключ (6), и произойдет ошибка. Следовательно, если клавиша выбрана слева, сначала пойдет правая.
Q: Мы понимаем версию hoare.Если клавиша выбрана справа, левая идет первой?
Ответ : Да. На самом деле это вопрос о том, встречается ли L с R или R с L. Мы все еще можем использовать приведенное выше решение, чтобы подумать об этой проблеме. Когда L встречается с R, это означает, что R остановился, а R останавливается на [правой] <клавише. В это время, когда L встречается с R, позиция встречи меньше чем key , обмен (key, a[left]), в это время последний элемент, меньший, чем key, помещается в крайнее левое положение, а key находится в определенной позиции, нет необходимости снова настраивать, левый интервал с ключом в качестве средняя точка все меньше ключа, правый интервал больше ключа;
Ключ находится справа. Когда R встречается с L, это означает, что L остановился. Когда L останавливается, это означает, что [левый]> ключ. В это время, когда R встречается с L, позиция встречи больше, чем ключ. Обмен (a[left],key), то последний элемент больше ключа помещается в крайнее правое положение, а ключ находится в определенной позиции, поэтому нет необходимости настраивать его снова.Левый интервал с ключом как средняя точка все меньше ключа, а правый интервал больше ключа.
Таким образом, если клавиша выбрана слева, сначала идите направо; если клавиша выбрана справа, сначала идите налево.
3.4 реализация кода версии hoare
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
Тест кода версии 3.5 hoare
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Print(int* a, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//快速排序递归实现
//快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
return left;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
void test()
{
int a[] = { 6,3,2,1,5,7,9 };
QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
test();
return 0;
}
4. Метод копания
4.1 Идеи реализации
Указываем сортировать по возрастанию , имя сортируемого массива a, ключ значения ссылки, лево лево, право право.
1. Выберите ключ. Ключом может быть любой элемент в массиве, который нужно отсортировать. Мы по-прежнему выбираем ключ как a[left] и присваиваем ключу a[left] для сохранения. Позиция выбранного элемента в качестве ключа является положение ямы ;
2. Идти влево от правого конца (хвоста) массива, когда a[right] < клавиша, right останавливается, ставим a[right] в ямку, а pit меняется на позицию с индексом right, при этом правильное время Не двигаться;
3. Затем идем вправо от левого конца (головы) массива, когда клавиша a[left] >, левая останавливается, ставим a[left] в ямку, а ямку меняем на позицию с нижним индексом left , в это время левая не двигается;
4. Повторите шаги 2 и 3. Когда левый и правый переходят в одно и то же положение, это положение является последним положением ямы.Поместите ключ в это положение ямы, и окончательное положение ключа будет определено, и нет необходимости чтобы отрегулировать его позже.
5. В это время ключ делит массив на левый и правый интервалы.Используем первые 4 шага для левого и правого интервалов, а затем снова определяем ключи левого и правого интервалов, рекурсируем левый интервал, а затем повторите правильный интервал для достижения сортировки.
Отличие копательного метода от хорового варианта:
1. Метод копания ямы не должен думать, что элементы в конечном положении встречи L и R меньше ключа, потому что конечное положение встречи - это положение ямы, просто поместите ключ непосредственно в положение ямы;
2. Не нужно думать о том, почему вы должны сначала идти направо, если вы выбираете левый ключ в качестве ключа, и сначала идти влево, если вы выбираете правый ключ в качестве ключа. потому что вам нужно заполнить отверстие слева, вы должны сначала пойти направо. Более уместно понимать это предложение, ища маленькое слева и большое справа.
4.2 Схема мышления
4.3 Кодовая реализация метода копания
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
4.4 Проверка кода метода копания
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Print(int* a, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
//右边找小
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
void test()
{
int a[] = { 6,3,2,1,5,7,9 };
QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
test();
return 0;
}
5. Версии с передним и задним указателем
5.1 Идеи реализации
Мы указываем сортировку в порядке возрастания , имя отсортированного массива — a, а ссылочное значение — ключ .
1. Выберите ключ, ключ может быть любым элементом в сортируемом массиве, мы по-прежнему выбираем ключ как [левый];
2. Определите указатель prev и указатель cur, инициализируйте prev так, чтобы он указывал на начальную позицию массива, а cur указывает на следующую позицию prev. Cur идет первым, cur находит элементы меньше ключа, останавливается после их нахождения, пропускает prev++, а затем меняет местами (a[cur], a[prev]). После обмена продолжаем возвращаться, значение которое находит cur не меньше ключа, cur продолжает возвращаться, найдя его, пускаем prev++, обмениваем (a[cur], a[prev]), и повторяем это шаг;
3. Когда cur пройдёт весь массив, меняем местами (a[left], a[prev]), после чего определяется конечная позиция ключа. key делит массив на левый и правый подинтервалы, левый подинтервал меньше ключа, а правый подинтервал больше ключа;
4. Левый и правый подинтервалы продолжают повторять первые 3 шага, а сортировка массива осуществляется рекурсивным спуском вниз.
5.2 Схема мышления
Непрерывный обмен здесь фактически бросает значение, меньшее, чем ключ вперед, и бросает значение, большее, чем ключ, назад.Значение между cur и prev на самом деле является всеми значениями, большими, чем ключ.Непрерывный обмен реализует окончательное деление по ключу Левый и правый интервалы, левый интервал меньше ключа, а правый интервал больше ключа.
5.3 Кодовая реализация метода переднего и заднего указателя
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
5.4 Проверка кода до и после метода указателя
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
void test()
{
int a[] = { 6,3,2,1,5,7,9 };
QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
test();
return 0;
}
6. Анализ временной сложности
6.1 В лучшем случае
В приведенных выше трех случаях наилучшая временная сложность составляет O(N*logN) .
Каждый раз, когда ключ помещается в середину интервала, он должен рекурсировать logN раз, как двоичное дерево, а временная сложность сортировки каждого подинтервала составляет O (N), поэтому в лучшем случае это O (N * logN).
6.2 Наихудший случай
Когда массив отсортирован, временная сложность составляет O(N^2) независимо от того, является ли ключ крайним слева или самым правым .
7. Оптимизированная быстрая сортировка
Есть две идеи для быстрой оптимизации сортировки:
1. Мы можем оптимизировать метод выбора ключа;
2. Чтобы вернуться к небольшому подинтервалу, мы можем рассмотреть возможность использования сортировки вставками, также известной как межмалая оптимизация.
7.1 Оптимизация выбора ключей
Оптимизация выбора ключей в основном предназначена для упорядоченных или близких к упорядоченным массивов.
У нас есть две идеи по оптимизации выбора ключей:
1. Случайным образом выберите ключ;
2. Возьмите выбранный ключ на три цифры. (Возьмите левое, среднее, правое и выберите промежуточное значение среди чисел, нижние индексы которых являются этими тремя позициями в качестве ключа).
Первый способ мышления неуправляем, поэтому второй способ подбора ключей является наиболее подходящим.
Ниже приведен оптимизированный код для получения трех чисел:
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[left] < a[right])
return right;
else
return left;
}
else //a[left] > a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[left] > a[right])
return right;
else
return left;
}
}
int PartSort3(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
После того, как мы его получим, мы меняем число на a[left], и нет проблем с тем, чтобы по-прежнему использовать предыдущий метод с прямыми и обратными указателями. Версия Холла — это тот же метод оптимизации, что и метод копания.
Если мы не оптимизируем выбор трех чисел, когда массив упорядочен или близок к упорядочению, временная сложность будет в худшем случае, O (N ^ 2). После взятия центра трех чисел, если массив упорядочен, временная сложность по-прежнему O (N * logN).
7.2 Межсотовая оптимизация
При рекурсии нетрудно увидеть на картинке, которую мы нарисовали раньше.Когда мы постоянно делим, мы будем делить все больше и больше позже.Когда объем данных особенно велик, потребление стека будет очень большим, что вызовет риск переполнения стека. Поэтому, когда деление достигает определенного уровня, мы больше не делим и напрямую выбираем сортировку вставками. При нормальных обстоятельствах, когда количество данных подинтервала равно 10, мы больше не используем рекурсию и используем сортировку вставками напрямую.
Код реализации:
// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[i + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[left] < a[right])
return right;
else
return left;
}
else //a[left] > a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[left] > a[right])
return right;
else
return left;
}
}
// 快速排序前后指针法
//[left, right]
int PartSort3(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
//子区间只有一个值,或者子区间不存在的时候递归结束
if (left >= right)
return;
//小区间优化
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
Эти два метода оптимизации имеют определенную степень улучшения с точки зрения времени и пространства , но суть быстрой сортировки не изменилась, а оптимизация — просто вишенка на торте исходной идеи.