Метод быстрой сортировки. Метод Хоара - Быстрая сортировка(Quick-sort)

Было подсчитано, что до четверти времени централизованных компьютеров уделяется сортировке данных. Это потому, что намного легче найти значение в массиве, который был заранее отсортирован. В противном случае поиск немного походит на поиск иголки в стоге сена.

Есть программисты, которые всё рабочее время проводят в изучении и внедрении алгоритмов сортировки. Это потому, что подавляющее большинство программ в бизнесе включает в себя управление базами данных. Люди ищут информацию в базах данных всё время. Это означает, что поисковые алгоритмы очень востребованы.

Но есть одно "но". Поисковые алгоритмы работают намного быстрее с базами данных, которые уже отсортированы. В этом случае требуется только линейный поиск.

В то время как компьютеры находятся без пользователей в некоторые моменты времени, алгоритмы сортировки продолжают работать с базами данных. Снова приходят пользователи, осуществляющие поиск, а база данных уже отсортирована, исходя из той или иной цели поиска.

В этой статье приведены примеры реализации стандартных алгоритмов сортировки.

Сортировка выбором (Selection sort)

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

Код C++

void SortAlgo::selectionSort(int data, int lenD) { int j = 0; int tmp = 0; for (int i=0; idata[k]){ j = k; } } tmp = data[i]; data[i] = data[j]; data[j] = tmp; } }

Пузырьковая сортировка (Bubble sort)

При пузырьковой сортировке сравниваются соседние элементы и меняются местами, если следующий элемент меньше предыдущего. Требуется несколько проходов по данным. Во время первого прохода сраваются первые два элемента в массиве. Если они не в порядке, они меняются местами и затем сравнивается элементы в следующей паре. При том же условии они так же меняются местами. Таким образом сортировка происходит в каждом цикле пока не будет достигнут конец массива.

Код C++

void SortAlgo::bubbleSort(int data, int lenD) { int tmp = 0; for (int i = 0;i=(i+1);j--){ if (data[j]

Сортировка вставками (Insertion sort)

При сортировке вставками массив разбивается на две области: упорядоченную и и неупорядоченную. Изначально весь массив является неупорядоченной областью. При первом проходе первый элемент из неупорядоченной области изымается и помещается в правильном положении в упорядоченной области.

На каждом проходе размер упорядоченной области возрастает на 1, а размер неупорядоченной области сокращается на 1.

Основной цикл работает в интервале от 1 до N-1. На j-й итерации элемент [i] вставлен в правильное положение в упорядоченной области. Это сделано путем сдвига всех элементов упорядоченной области, которые больше, чем [i], на одну позицию вправо. [i] вставляется в интервал между теми элементами, которые меньше [i], и теми, которые больше [i].

Код C++

void SortAlgo::insertionSort(int data, int lenD) { int key = 0; int i = 0; for (int j = 1;j=0 && data[i]>key){ data = data[i]; i = i-1; data=key; } } }

Сортировка слиянием (Merge sort)

Код C++

void SortAlgo::mergeSort(int data, int lenD) { if (lenD>1){ int middle = lenD/2; int rem = lenD-middle; int * L = new int ; int * R = new int ; for (int i=0;i

Быстрая сортировка (Quick sort)

Быстрая сортировка использует алгоритм "разделяй и властвуй". Она начинается с разбиения исходного массива на две области. Эти части находятся слева и справа от отмеченного элемента, называемого опорным. В конце процесса одна часть будет содержать элементы меньшие, чем опорный, а другая часть будет содержать элементы больше опорного.

Код C++

void SortAlgo::quickSort(int * data, int const len) { int const lenD = len; int pivot = 0; int ind = lenD/2; int i,j = 0,k = 0; if (lenD>1){ int * L = new int ; int * R = new int ; pivot = data; for (i=0;i

Цель: изучение алгоритма быстрой сортировки и ее модификаций.

На этом занятии мы изучим алгоритм быстрой сортировки, который, пожалуй, используется более часто, чем любой другой. Основа алгоритма была разработана в 1960 году (C.A.R.Hoare) и с тех пор внимательно изучалась многими людьми. Быстрая сортировка особенно популярна ввиду легкости ее реализации; это довольно хороший алгоритм общего назначения, который хорошо работает во многих ситуациях, и использует при этом меньше ресурсов, чем другие алгоритмы.

Основные достоинства этого алгоритма состоят в том, что он точечный (использует лишь небольшой дополнительный стек), в среднем требует только около N log N операций для того, чтобы отсортировать N элементов, и имеет экстремально короткий внутренний цикл. Недостатки алгоритма состоят в том, что он рекурсивен (реализация очень затруднена когда рекурсия недоступна), в худшем случае он требует N2 операций, кроме того он очень "хрупок": небольшая ошибка в реализации, которая легко может пройти незамеченной, может привести к тому, что алгоритм будет работать очень плохо на некоторых файлах.

Производительность быстрой сортировки хорошо изучена. Алгоритм подвергался математическому анализу, поэтому существуют точные математические формулы касающиеся вопросов его производительности. Результаты анализа были неоднократно проверены эмпирическим путем, и алгоритм был отработан до такого состояния, что стал наиболее предпочтительным для широкого спектра задач сортировки. Все это делает алгоритм стоящим более детального изучения наиболее эффективных путей его реализации. Похожие способы реализации подходят также и для других алгоритмов, но в алгоритме быстрой сортировки мы можем использовать их с уверенностью, поскольку его производительность хорошо изучена.

Улучшить алгоритм быстрой сортировки является большим искушением: более быстрый алгоритм сортировки - это своеобразная "мышеловка" для программистов. Почти с того момента, как Oia?a впервые опубликовал свой алгоритм, в литературе стали появляться "улучшенные" версии этого алгоритма. Было опробовано и проанализировано множество идей, но все равно очень просто обмануться, поскольку алгоритм настолько хорошо сбалансирован, что результатом улучшения в одной его части может стать более сильное ухудшение в другой его части. Мы изучим в некоторых деталях три модификации этого алгоритма, которые дают ему существенное улучшение.

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

Суть алгоритма: число операций перемены местоположений элементов внутри массива значительно сократится, если менять местами далеко стоящие друг от друга элементы. Для этого выбирается для сравнения один элемент х, отыскивается слева первый элемент, который не меньше х, а справа первый элемент, который не больше х. Найденные элементы меняются местами. После первого же прохода все элементы, которые меньше х, будут стоять слева от х, а все элементы, которые больше х, - справа от х. С двумя половинами массива поступают точно также. Продолжая деление этих половин до тех пор пока не останется в них по 1 элементу.

Program Quitsort; uses crt; Const N=10; Type Mas=array of integer; var a: mas; k: integer; function Part(l, r: integer):integer; var v, i, j, b: integer; begin V:=a[r]; I:=l-1; j:=r; repeat repeat dec(j) until (a[j]<=v) or (j=i+1); repeat inc(i) until (a[i]>=v) or (i=j-1); b:=a[i]; a[i]:=a[j]; a[j]:=b; until i>=j; a[j]:=a[i]; a[i]:= a[r]; a[r]:=b; part:=i; end; procedure QuickSort(l, t: integer); var i: integer; begin if l

60,79, 82, 58, 39, 9, 54, 92, 44, 32 60,79, 82, 58, 39, 9, 54, 92, 44, 32 9,79, 82, 58, 39, 60, 54, 92, 44, 32 9,79, 82, 58, 39, 60, 54, 92, 44, 32 9, 32, 82, 58, 39, 60, 54, 92, 44, 79 9, 32, 44, 58, 39, 60, 54, 92, 82, 79 9, 32, 44, 58, 39, 54, 60, 92, 82, 79 9, 32, 44, 58, 39, 92, 60, 54, 82, 79 9, 32, 44, 58, 39, 54, 60, 79, 82, 92 9, 32, 44, 58, 54, 39, 60, 79, 82, 92 9, 32, 44, 58, 60, 39, 54, 79, 82, 92 9, 32, 44, 58, 54, 39, 60, 79, 82, 92 9, 32, 44, 58, 54, 39, 60, 79, 82, 92 9, 32, 44, 58, 54, 39, 60, 79, 82, 92 9, 32, 39, 58, 54, 44, 60, 79, 82, 92 9, 32, 39, 58, 54, 44, 60, 79, 82, 92 9, 32, 39, 44, 54, 58, 60, 79, 82, 92 9, 32, 39, 44, 58, 54, 60, 79, 82, 92 9, 32, 39, 44, 54, 58, 60, 79, 82, 92 9, 32, 39, 44, 54, 58, 60, 79, 92, 82 9, 32, 39, 44, 54, 58, 60, 79, 82, 92

"Внутренний цикл" быстрой сортировки состоит только из увеличения указателя и сравнения элементов массива с фиксированным числом. Это как раз и делает быструю сортировку быстрой. Сложно придумать более простой внутренний цикл. Положительные эффекты сторожевых ключей также оказывают здесь свое влияние, поскольку добавление еще одной проверки к внутреннему циклу оказало бы отрицательное влияние на производительность алгоритма.

Самая сомнительная черта вышеприведенной программы состоит в том, что она очень мало эффективна на простых подфайлах. Например, если файл уже сортирован, то разделы будут вырожденными, и программа просто вызовет сама себя N раз, каждый раз с меньшим на один элемент подфайлом. Это означает, что не только производительность программы упадет примерно до N2/2, но и пространство необходимое для ее работы будет около N (смотри ниже), что неприемлемо. К счастью, есть довольно простые способы сделать так, чтобы такой "худший" случай не произошел при практическом использовании программы.

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

Характеристики Производительности Быстрой Сортировки

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

CN = 2CN/2+N - наилучший случай.

(2CN/2 покрывает расходы по сортировке двух полученных подфайлов; N - это стоимость обработки каждого элемента, используя один или другой указатель.) Нам известно также, что примерное значение этого выражения равно CN = N lg N.

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

Свойство 1 Быстрая сортировка в среднем использует 2N ln N сравнений.

Методы улучшения быстрой сортировки.

1. Небольшие Подфайлы.

Первое улучшение в алгоритме быстрой сортировки возникает из наблюдения, что программа гарантировано вызывает себя для огромного количества небольших подфайлов, поэтому следует использовать самый лучший метод сортировки когда мы встречаем небольшой подфайл. Очевидный способ добиться этого, это изменить проверку в начале рекурсивной функции из "if r>l then" на вызов сортировки вставкой (соответственно измененной для восприятия границ сортируемого подфайла): "if r-l<=M then insertion(l, r)." Значение для M не обязано быть "самым-самым" лучшим: алгоритм работает примерно одинаково для M от 5 до 25. Время работы программы при этом снижается примерно на 20% для большинства программ.

При небольших подфайлах (5- 25 элементов) быстрая сортировка очень много раз вызывает сама себя (в наше примере для 10 элементов она вызвала сама себя 15 раз), поэтому следует применять не быструю сортировку, а сортировку вставкой.

Procedure QuickSort (l,t:integer); var i:integer; begin if t-l>m then begin i:=part(l,t); QuickSort (l,i-1); QuickSort (i+1,t); end Else Insert(l,t); end;

2. Деление по Медиане из Трех

Второе улучшение в алгоритме быстрой сортировки состоит в попытке использования лучшего делящего элемента. У нас есть несколько возможностей. Наиболее безопасная из них будет попытка избежать худшего случая посредством выбора произвольного элемента массива в качестве делящего элемента. Тогда вероятность худшего случая становится пренебрежимо мала. Это простой пример "вероятностного" алгоритма, который почти всегда работает вне зависимости от входных данных. Произвольность может быть хорошим инструментом при разработке алгоритмов, особенно если возможны подозрительные входные данные.

Более полезное улучшение состоит в том, чтобы взять из файла три элемента, и затем использовать среднее из них в качестве делящего элемента. Если элементы взяты из начала, середины, и конца файла, то можно избежать использования сторожевых элементов: сортируем взятые три элемента, затем обмениваем центральный элемент с a, и затем используем алгоритм деления на массиве a. Это улучшение называется делением по медиане из трех.

Метод деления по медиане из трех полезен по трем причинам. Во-первых, он делает вероятность худшего случая гораздо более низкой. Чтобы этот алгоритм использовал время пропорциональной N2, два из трех взятых элементов должны быть либо самыми меньшими, либо самыми большими, и это должно повторяться из раздела в раздел. Во-вторых, этот метод уничтожает необходимость в сторожевых элементах, поскольку эту роль играет один из трех взятых нами перед делением элементов. В третьих, он на самом деле снижает время работы алгоритма приблизительно на 5%.

Procedure exchange(i,j:integer); var k:integer; begin k:=a[i]; a[i]:=a[j]; a[j]:=k; end; procedure Mediana; var i:integer; begin i:=n div 4;{Рис.} if a[i]>a then if a[i]>a then exchange(i,n) else exchange(i*3,n) else if a>a then exchange(i*2,n); quicksort(1,n); end;

3. Нерекурсивная реализация.

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

Комбинация нерекурсивной реализации деления по медиане из трех с отсечением на небольшие файлы может улучшить время работы алгоритма от 25% до 30%.

Итак, на сегодняшнем занятии мы рассмотрели алгоритм быстрой сортировки.

Слияние

На сегодняшнем занятии мы начнем рассмотрении темы внешняя сортировка.

Внешняя сортировка сортирует файлы, которые не помещаются целиком в оперативную память.

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

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

Под слиянием понимается объединение двух (или более) упорядоченных последовательностей в одну упорядоченную последовательность при помощи циклического выбора элементов, доступных в данный момент.

Слияние - намного более простая операция, чем сортировка.

Мы рассмотрим 2 алгоритма слияния:

Прямое слияние. Алгоритм Боуза - Нельсона

Последовательность а разбивается на две половины b и с.

Последовательности b и с сливаются при помощи объединения отдельных элементов в упорядоченные пары.

Полученной последовательности присваивается имя а, после чего повторяются шаги 1 и 2; при этом упорядоченные пары сливаются в упорядоченные четверки.

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

Пример

Исходная последовательность

А = 44 55 12 42 94 18 06 67 1 b = 44 55 12 42 с = 94 18 06 67 а = 44 94" 18 55" 06 12" 42 67 2 b = 44 94" 18 55" с =06 12" 42 67 а = 06 12 44 94" 18 42 55 67" 3 b = 06 12 44 94" с = 18 42 55 67" а = 06 12 18 42 44 55 67 94

Операция, которая однократно обрабатывает всё множество данных, называется фазой.

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

В нашем примере сортировка производится за три прохода. Каждый проход состоит из фазы разбиения и фазы слияния.

Главным минусом сортировки слиянием является удвоение размера памяти, первоначально занятой сортируемыми данными. Рассмотрим алгоритм с рекурсивным актом слияния, предложенный Боузом и Нельсоном и не требующий резерва памяти.

Он основан на очевидной идее: слить две равные упорядоченные части можно слиянием их начальных половин, слиянием конечных и слиянием 2-й половины 1-го результата с 1-й половиной 2-го результата, например:

Если части не равны или не делятся точно пополам, процедуру уточняют надлежащим образом. Аналогично слияние "половинок" можно свести к слиянию "четвертушек", "восьмушек" и т. д.; имеет место рекурсия.

Const n=200; Type tipkl=word; tip = Record kl: tipkl; z:Array of real End; Var A: Array of tip; j:word; Procedure Bose (Var AA; voz:Boolean); Var m,j:word; x:tip; {tip - тип сортируемых записей} A: Array of tip Absolute AA; Procedure Sli(j,r,m: word); { r - расстояние между началами сливаемых частей, а m - их размер, j - наименьший номер записи} Begin if j+r<=n Then If m=1 Then Begin If voz Xor (A[j].kl < A.kl) Then Begin x:=A[j]; A[j]:= A; A:=x End End Else Begin m:=m div 2; Sli(j,r,m); {Слияние "начал"} If j+r+m<=n Then Sli(j+m,r,m); {Слияние "концов"} Sli(j+m,r-m,m) End {Слияние в центральной части} End{блока Sli}; Begin m:=1; Repeat j:=1; {Цикл слияния списков равного размера: } While j+m<=n do Begin Sli(j,m,m); j:=j+m+m End; m:=m+m {Удвоение размера списка перед началом нового прохода} Until m >= n {Конец цикла, реализующего все дерево слияний} End{блока Bose}; BEGIN Randomize; For j:=1 to n do begin A[j].kl:= Random(65535); Write(A[j].kl:8); end; Readln; Bose(A,true); For j:=1 to n do Write(A[j].kl:8); Readln END.

Естественное (Неймановское) слияние.

Она объединяются упорядоченные части, спонтанно возникшие в исходном массиве; они могут быть также следствием предыдущей обработки данных. Рассчитывать на одинаковый размер сливаемых частей не приходится.

Записи, идущие в порядке неубывания ключей, сцепляются, образуя подсписок. Минимальный подсписок одна запись.

Пример:

Пусть даны ключи записей

5 7 8 3 9 4 1 7 6

Ищем подсписки

В один общий список соединяются 1-й, 3-й, 5-й и т. д. подсписки, в другой - 2-й, 4-й и т. д. подсписки.

Произведем слияние 1 подсписка 1 списка и 1 подсписка 2 списка, 2 подсписка 1 списка и 2 подсписка 2 списка и т.д.

Будут получены следующие цепи

3 --> 5 --> 7 --> 8 --> 9 и 1 --> 4 --> 7

Подсписок, состоящий из записи "6", пары не имеет и "принудительно" объединяется с последней цепью, принимающей вид 1 --> 4--> 6 --> 7.

При нашем небольшом числе записей 2-й этап, на котором сливаются две цепи, окажется последним.

В общем случае на каждом этапе подсписок - результат слияния начальных подсписков 1 и 2 списка становится началом нового 1-го списка, а результат слияния следующих двух подсписков - началом 2-го списка. Следующие образуемые подсписки поочередно включаются в 1-й и во 2-й список.

Для программной реализации заводят массив sp: элемент sp[i] - это номер записи, которая следует за i-й.

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

Repeat {Повторение актов слияний подсписков} If A[j].kl < A[i].kl Then {Выбирается меньшая запись} Begin sp[k]:=j; k:=j; j:=sp[j]; If j<=0 Then {Сцепление с остатком "i"-подсписка} Begin sp[k]:=i; Repeat m:=i; i:=sp[i] Until i<=0 End End Else Begin sp[k]:=i; k:=i; i:=sp[i]; If i<=0 Then {Сцепление с остатком "j"-подсписка} Begin sp[k]:=j; Repeat m:=j; j:=sp[j] Until j<=0 End End; If j<=0 Then Begin sp[m]:= 0; sp[p]:=-sp[p]; i:=-i; j:=-j; If j<>0 Then p:=r; k:=r; r:=m End Until j=0;

{В конец сформированного подсписка всегда заносится нулевая ссылка (sp[m]:= 0), ибо он может оказаться последним.

Действие sp[p]:= -sp[p] обозначает минусом конец ранее построенного подсписка.

Итак, на сегодняшнем занятии мы рассмотрели алгоритмы слияния.

Подробности Категория: Сортировка и поиск

Быстрая сортировка (англ. quicksort ), часто называемая qsort (по имени в стандартной библиотеке языка Си) - широко известный алгоритм сортировки, разработанный английским информатиком Чарльзом Хоаром во время его работы в МГУ в 1960 году.

Один из самых быстрых известных универсальных алгоритмов сортировки массивов: в среднем O(n log n) обменов при упорядочении n элементов; из-за наличия ряда недостатков на практике обычно используется с некоторыми доработками.

Отличительной особенностью быстрой сортировки является операция разбиения массива на две части относительно опорного элемента. Например, если последовательность требуется упорядочить по возрастанию, то в левую часть будут помещены все элементы, значения которых меньше значения опорного элемента, а в правую элементы, чьи значения больше или равны опорному. Вне зависимости от того, какой элемент выбран в качестве опорного, массив будет отсортирован, но все же наиболее удачным считается ситуация, когда по обеим сторонам от опорного элемента оказывается примерно равное количество элементов. Если длина какой-то из получившихся в результате разбиения частей превышает один элемент, то для нее нужно рекурсивно выполнить упорядочивание, т. е. повторно запустить алгоритм на каждом из отрезков.

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

  • разбиение массива относительно опорного элемента;
  • рекурсивная сортировка каждой части массива.

Реализация алгоритма на различных языках программирования:

C

Работает для произвольного массива из n целых чисел.

Int n, a[n]; //n - количество элементов void qs(int* s_arr, int first, int last) { int i = first, j = last, x = s_arr[(first + last) / 2]; do { while (s_arr[i] < x) i++; while (s_arr[j] > x) j--; if(i <= j) { if (s_arr[i] > s_arr[j]) swap(&s_arr[i], &s_arr[j]); i++; j--; } } while (i <= j); if (i < last) qs(s_arr, i, last); if (first < j) qs(s_arr, first, j); }

Исходный вызов функции qs для массива из n элементов будет иметь следующий вид.

Java/C#

Int partition (int array, int start, int end) { int marker = start; for (int i = start; i <= end; i++) { if (array[i] <= array) { int temp = array; // swap array = array[i]; array[i] = temp; marker += 1; } } return marker - 1; } void quicksort (int array, int start, int end) { if (start >= end) { return; } int pivot = partition (array, start, end); quicksort (array, start, pivot-1); quicksort (array, pivot+1, end); }

C# с обобщенными типами, тип Т должен реализовывать интерфейс IComparable

Int partition(T m, int a, int b) where T:IComparable { int i = a; for (int j = a; j <= b; j++) // просматриваем с a по b { if (m[j].CompareTo(m[b]) <= 0) // если элемент m[j] не превосходит m[b], { T t = m[i]; // меняем местами m[j] и m[a], m, m и так далее... m[i] = m[j]; // то есть переносим элементы меньшие m[b] в начало, m[j] = t; // а затем и сам m[b] «сверху» i++; // таким образом последний обмен: m[b] и m[i], после чего i++ } } return i - 1; // в индексе i хранится <новая позиция элемента m[b]> + 1 } void quicksort(T m, int a, int b) where T: IComparable// a - начало подмножества, b - конец { // для первого вызова: a = 0, b = <элементов в массиве> - 1 if (a >= b) return; int c = partition(m, a, b); quicksort(m, a, c - 1); quicksort(m, c + 1, b); } //Пример вызова //double arr = {9,1.5,34.4,234,1,56.5}; //quicksort(arr,0,arr.Length-1); //

C# с использованием лямбда-выражений

Using System; using System.Collections.Generic; using System.Linq; static public class Qsort { public static IEnumerable QuickSort(this IEnumerable list) where T: IComparable { if (!list.Any()) { return Enumerable.Empty(); } var pivot = list.First(); var smaller = list.Skip(1).Where(item => item.CompareTo(pivot) <= 0).QuickSort(); var larger = list.Skip(1).Where(item => item.CompareTo(pivot) > 0).QuickSort(); return smaller.Concat(new { pivot }).Concat(larger); } //(тоже самое, но записанное в одну строку, без объявления переменных) public static IEnumerable shortQuickSort(this IEnumerable list) where T: IComparable { return !list.Any() ? Enumerable.Empty() : list.Skip(1).Where(item => item.CompareTo(list.First()) <= 0).shortQuickSort().Concat(new { list.First() }) .Concat(list.Skip(1).Where(item => item.CompareTo(list.First()) > 0).shortQuickSort()); } }

C++

Быстрая сортировка на основе библиотеки STL.

#include #include #include template< typename BidirectionalIterator, typename Compare > void quick_sort(BidirectionalIterator first, BidirectionalIterator last, Compare cmp) { if(first != last) { BidirectionalIterator left = first; BidirectionalIterator right = last; BidirectionalIterator pivot = left++; while(left != right) { if(cmp(*left, *pivot)) { ++left; } else { while((left != --right) && cmp(*pivot, *right)) ; std::iter_swap(left, right); } } --left; std::iter_swap(first, left); quick_sort(first, left, cmp); quick_sort(right, last, cmp); } } // для вещественных int partition (double *a, int p, int r) { double x = *(a+r); int i = p - 1; int j; double tmp; for (j = p; j < r; j++) { if (*(a+j) <= x) { i++; tmp = *(a+i); *(a+i) = *(a+j); *(a+j) = tmp; } } tmp = *(a+r); *(a+r) = *(a+i+1); *(a+i+1) = tmp; return i + 1; } void quicksort (double *a, int p, int r) { int q; if (p < r) { q = partition (a, p, r); quicksort (a, p, q-1); quicksort (a, q+1, r); } } template< typename BidirectionalIterator > inline void quick_sort(BidirectionalIterator first, BidirectionalIterator last) { quick_sort(first, last, std::less_equal< typename std::iterator_traits< BidirectionalIterator >::value_type >());

Java

import java.util.Comparator; import java.util.Random; public class Quicksort { public static final Random RND = new Random(); private void swap(Object array, int i, int j) { Object tmp = array[i]; array[i] = array[j]; array[j] = tmp; } private int partition(Object array, int begin, int end, Comparator cmp) { int index = begin + RND.nextInt(end - begin + 1); Object pivot = array; swap(array, index, end); for (int i = index = begin; i < end; ++ i) { if (cmp.compare(array[i], pivot) <= 0) { swap(array, index++, i); } } swap(array, index, end); return (index); } private void qsort(Object array, int begin, int end, Comparator cmp) { if (end > begin) { int index = partition(array, begin, end, cmp); qsort(array, begin, index - 1, cmp); qsort(array, index + 1, end, cmp); } } public void sort(Object array, Comparator cmp) { qsort(array, 0, array.length - 1, cmp); }

Java, с инициализацией и перемешиванием массива и с измерением времени сортировки массива нанотаймером (работает только если нет совпадающих элементов массива)

<=N;i=i+1) { A[i]=N-i; System.out.print(A[i]+" "); } System.out.println("\nBefore qSort\n"); // перемешивание массива Random r = new Random(); //инициализация от таймера int yd,xs; for (int i=0;i<=N;i=i+1) { yd=A[i]; xs=r.nextInt(N+1); A[i]=A; A=yd; } for (int i=0;i<=N;i=i+1) System.out.print(A[i]+" "); System.out.println("\nAfter randomization\n"); long start, end; int low=0; int high=N; start=System.nanoTime(); // получить начальное время qSort(A,low,high); end=System.nanoTime(); // получить конечное время for (int i=0;i<=N;i++) System.out.print(A[i]+" "); System.out.println("\nAfter qSort"); System.out.println("\nTime of running: "+(end-start)+"nanosec"); } //описание функции qSort public static void qSort(int A, int low, int high) { int i = low; int j = high; int x = A[(low+high)/2]; do { while(A[i] < x) ++i; while(A[j] > x) --j; if(i <= j){ int temp = A[i]; A[i] = A[j]; A[j] = temp; i ++ ; j --; } } while(i <= j); //рекурсивные вызовы функции qSort if(low < j) qSort(A, low, j); if(i < high) qSort(A, i, high); } }

JavaScript

Import java.util.Random; public class QuickSort { public static void main(String args) { int N=10; int A; A = new int; // заполнение массива for (int i=0;i<=N;i=i+1) { A[i]=N-i; System.out.print(A[i]+" "); } System.out.println("\nBefore qSort\n"); // перемешивание массива Random r = new Random(); //инициализация от таймера int yd,xs; for (int i=0;i<=N;i=i+1) { yd=A[i]; xs=r.nextInt(N+1); A[i]=A; A=yd; } for (int i=0;i<=N;i=i+1) System.out.print(A[i]+" "); System.out.println("\nAfter randomization\n"); long start, end; int low=0; int high=N; start=System.nanoTime(); // получить начальное время qSort(A,low,high); end=System.nanoTime(); // получить конечное время for (int i=0;i<=N;i++) System.out.print(A[i]+" "); System.out.println("\nAfter qSort"); System.out.println("\nTime of running: "+(end-start)+"nanosec"); } //описание функции qSort public static void qSort(int A, int low, int high) { int i = low; int j = high; int x = A[(low+high)/2]; do { while(A[i] < x) ++i; while(A[j] > x) --j; if(i <= j){ int temp = A[i]; A[i] = A[j]; A[j] = temp; i ++ ; j --; } } while(i <= j); //рекурсивные вызовы функции qSort if(low < j) qSort(A, low, j); if(i < high) qSort(A, i, high); } }

Python

С использованием генераторов:

Def qsort(L): if L: return qsort( if x=L]) return

Математическая версия:

Def qsort(L): if L: return qsort(filter(lambda x: x < L, L)) + L + qsort(filter(lambda x: x >= L, L)) return

Joy

DEFINE sort == split] [ dip cons concat] binrec .

PHP

function qsort($s) { for($i=0, $x=$y=array(); $iHaskell

Qsort = qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)

Математическая версия - с использованием генераторов:

Qsort = qsort (x:xs) = qsort ++ [x] ++ qsort

Common Lisp

В отличие от других вариантов реализации на функциональных языках, представленных здесь, приводимая реализация алгоритма на Лиспе является "честной" - она не порождает новый отсортированный массив, а сортирует тот, который поступил ей на вход, "на том же месте". При первом вызове функции в параметры l и r необходимо передать нижний и верхний индексы массива (или той его части, которую требуется отсортировать). Код использует "императивные" макросы Common Lisp"а.

(defun quickSort (array l r) (let ((i l) (j r) (p (svref array (round (+ l r) 2)))) (while (<= i j) (while (< (svref array i) p) (incf i)) (while (> (svref array j) p) (decf j)) (when (<= i j) (rotatef (svref array i) (svref array j)) (incf i) (decf j))) (if (>= (- j l) 1) (quickSort array l j)) (if (>= (- r i) 1) (quickSort array i r))) array)

Pascal

В данном примере показан наиболее полный вид алгоритма, очищенный от особенностей, обусловленных применяемым языком. В комментариях показано несколько вариантов. Представленный вариант алгоритма выбирает опорный элемент псевдослучайным образом, что, теоретически, сводит вероятность возникновения самого худшего или приближающегося к нему случая к минимуму. Недостаток его - зависимость скорости алгоритма от реализации генератора псевдослучайных чисел. Если генератор работает медленно или выдаёт плохие последовательности ПСЧ, возможно замедление работы. В комментарии приведён вариант выбора среднего значения в массиве - он проще и быстрее, хотя, теоретически, может быть хуже.

Внутреннее условие, помеченное комментарием «это условие можно убрать» - необязательно. Его наличие влияет на действия в ситуации, когда поиск находит два равных ключа: при наличии проверки они останутся на местах, а при отсутствии - будут обменены местами. Что займёт больше времени - проверки или лишние перестановки, - зависит как от архитектуры, так и от содержимого массива (очевидно, что при наличии большого количества равных элементов лишних перестановок станет больше). Следует особо отметить, что наличие условия не делает данный метод сортировки устойчивым.

Const max=20; { можно и больше... } type list = array of integer; procedure quicksort(var a: list; Lo,Hi: integer); procedure sort(l,r: integer); var i,j,x,y: integer; begin i:=l; j:=r; x:=a; { x:= a[(r+l) div 2]; - для выбора среднего элемента } repeat while a[i] x - сортировка по убыванию} while x a[j] - сортировка по убыванию} if i<=j then begin if a[i] > a[j] then {это условие можно убрать} {a[i] < a[j] при сортировке по убыванию} begin y:=a[i]; a[i]:=a[j]; a[j]:=y; end; i:=i+1; j:=j-1; end; until i>=j; if l

Устойчивый вариант (требует дополнительно O(n)памяти)

Const max=20; { можно и больше… } type list = array of integer; procedure quicksort(var a: list; Lo,Hi: integer); procedure sort(l,r: integer); var i,j,x,xval,y: integer; begin i:=l; j:=r; x:=random(r-l+1)+l; xval:=a[x]; xvaln:=num[x]{ x:=(r+l) div 2; - для выбора среднего элемента } repeat while (a[i] - сортировка по убыванию} while (xval - сортировка по убыванию} if i<=j then begin y:=a[i]; a[i]:=a[j]; a[j]:=y; y:=num[i]; num[i]:=num[j]; num[j]:=y; i:=i+1; j:=j-1 end; until i>j; if l

Быстрая сортировка, нерекурсивный вариант

Нерекурсивная реализация быстрой сортировки через стек. Функции compare и change реализуются в зависимости от типа данных.

Procedure quickSort(var X: itemArray; n: integer); type p_node = ^node; node = record node: integer; next: p_node end; var l,r,i,j: integer; stack: p_node; temp: item; procedure push(i: integer); var temp: p_node; begin new(temp); temp^.node:=i; temp^.next:=stack; stack:=temp end; function pop: integer; var temp: p_node; begin if stack=nil then pop:=0 else begin temp:=stack; pop:=stack^.node; stack:=stack^.next; dispose(temp) end end; begin stack:=nil; push(n-1); push(0); repeat l:=pop; r:=pop; if r-l=1 then begin if compare(X[l],X[r]) then change(X[l],X[r]) end else begin temp:=x[(l+r) div 2]; {random(r-l+1)+l} i:=l; j:=r; repeat while compare(temp,X[i]) do i:=i+1; while compare(X[j],temp) do j:=j-1; if i<=j then begin change(X[i],X[j]); i:=i+1; j:=j-1 end; until i>j; if l

Prolog

split(H, , , Z) :- order(A, H), split(H, X, Y, Z). split(H, , Y, ) :- not(order(A, H)), split(H, X, Y, Z). split(_, , , ). quicksort(, X, X). quicksort(, S, X) :- split(H, T, A, B), quicksort(A, S, ), quicksort(B, Y, X).

Ruby

def sort(array) return if array.empty? left, right = array.partition { |y| y <= array.first } sort(left) + [ array.first ] + sort(right) end

SML

This example demonstrates the use of an arbitrary predicate in a functional language.

Fun quicksort lt lst = let val rec sort = fn => | (x::xs) => let val (left,right) = List.partition (fn y => lt (y, x)) xs in sort left @ x:: sort right end in sort lst end

JavaScript

function QuickSort(A, p, r) { if(pTCL # Функция выбирает подсписок из списка используя условие condition proc lfind {data arg condition} { set foo foreach item $data { set $arg $item if {} {lappend foo $item} } return $foo } # Сама сотрировка proc QSort data { set result {} if { != 0} { set check set result [ concat \ ] \ \ ]] } return $result }

Perl

@out=(5,2,7,9,2,5,67,1,5,7,-8,5,0); sub sortQ{ my($s, $e) = @_; my $m = $s - 1; for($s..$e - 1){ if($out[$_] lt $out[$e]){ ++$m; ($out[$m], $out[$_]) = ($out[$_], $out[$m]); } } ++$m; ($out[$m], $out[$e]) = ($out[$e], $out[$m]); sortQ($s, $m-1) if $s < $m-1; sortQ($m+1, $e) if $m+1 < $e; } sortQ(0, $#out);

F#

Let rec quicksort = function -> | h::t -> quicksort ([ for x in t when x<=h -> x]) @ [h] @ quicksort ([ for x in t when x>h -> x]);;

OCaml

Let rec qsort l=match l with -> |a::b-> (qsort (List.filter ((>=) a) b) lt) @ [a] @ (qsort (List.filter ((<) a) b));;

Erlang

Qsort() -> ; qsort() -> qsort() ++ [H] ++ qsort().

D

Array qsort(array)(array _a) { alias typeof(array.init) _type; array filter(bool delegate(_type) dg, array a){ array buffer = null; foreach(value; a) { if(dg(value)){ buffer ~= value; } } return buffer; } if(_a.length <= 1) { return _a; } else { return qsort(filter((_type e){ return _a >= e; }, _a)) ~ _a ~ qsort(filter((_type e){ return _a < e; }, _a)); } }

Более короткий вариант с использованием стандартной библиотеки Phobos:

Import std.algorithm; T _qsort3(T)(T a) { if(a.length <= 1) return a; else return _qsort3(a.filter!(e => a >= e).array) ~ a ~ _qsort3(a.filter!(e => a < e).array); }

Scala

Def qsort](list: List[A]): List[A] = list match { case head::tail => { qsort(tail filter(head>=)) ::: head:: qsort(tail filter(head<)) } case _ => list; }

Еще вариант:

Def sort(xs: Array): Array = { if (xs.length <= 1) xs else { val pivot = xs(xs.length / 2) Array.concat(sort(xs filter (pivot >)), xs filter (pivot ==), sort(xs filter (pivot <))) } }

Clojure

Ленивая реализация:

(defn qsort [] (letfn [(f [g] (qsort (filter #(g % x) xs)))] (when x (lazy-cat (f <) [x] (f >=)))))

Shen/Qi II

(define filter {(A --> boolean) --> (list A) --> (list A)} _ -> T? -> (append [A] (filter T? B)) where (T? A) T? [_|B] -> (filter T? B)) (define q-sort {(list number) --> (list number)} -> -> (append (q-sort (filter (> A) )) [A] (q-sort (filter (< A) ))))

VB.NET

Судя по тестам, сортировка пузырьком 5000 занимает в 8 с половиной раз больше времени, чем qSort"ом

Sub Swap(ByRef Val1, ByRef Val2) Dim Proc Proc = Val1 Val1 = Val2 Val2 = Proc End Sub Function partition(ByRef a() As Integer, ByVal left As Integer, ByVal right As Integer, ByRef pivot As Integer) Dim i Dim piv Dim store piv = a(pivot) Swap(a(right - 1), a(pivot)) store = left For i = left To right - 2 If a(i) <= piv Then Swap(a(store), a(i)) store = store + 1 End If Next Swap(a(right - 1), a(store)) Return store End Function Function getpivot(ByRef a() As Integer, ByVal left As Integer, ByVal right As Integer) Return New System.Random().Next(left, right - 1) End Function Sub quicksort(ByRef a() As Integer, ByVal left As Integer, ByVal right As Integer) Dim pivot As Integer If right - left > 1 Then pivot = getpivot(a, left, right) pivot = partition(a, left, right, pivot) quicksort(a, left, pivot) quicksort(a, pivot + 1, right) End If End Sub Sub qSort(ByVal a() As Integer) Dim i Dim ii For i = 0 To a.Length() - 1 ii = New System.Random().Next(0, a.Length() - 1) If i <> ii Then Swap(a(i), a(ii)) End If Next quicksort(a, 0, a.Length()) End Sub

Вызов функции:

QSort(имя сортируемого массива)

PHP

Function quicksort (& $array , $l = 0 , $r = 0 ) { if($r === 0) $r = count($array)-1; $i = $l; $j = $r; $x = $array[($l + $r) / 2]; do { while ($array[$i] < $x) $i++; while ($array[$j] > $x) $j--; if ($i <= $j) { if ($array[$i] > $array[$j]) list($array[$i], $array[$j]) = array($array[$j], $array[$i]); $i++; $j--; } } while ($i <= $j); if ($i < $r) quicksort ($array, $i, $r); if ($j > $l) quicksort ($array, $l, $j); }

Встроенный язык 1С 8.*

Здесь приведен алгоритм сортировки на примере объекта типа «СписокЗначений», но его можно модифицировать для работы с любым объектом, для этого нужно изменить соответствующим образом код функций «СравнитьЗначения», «ПолучитьЗначение», «УстановитьЗначение».

Функция СравнитьЗначения(Знач1, Знач2) Если Знач1>Знач2 Тогда Возврат 1; КонецЕсли; Если Знач1<Знач2 Тогда Возврат -1; КонецЕсли; Возврат 0; КонецФункции Функция ПолучитьЗначение(Список, Номер) Возврат Список.Получить(Номер-1).Значение; КонецФункции Процедура УстановитьЗначение(Список, Номер, Значение) Список[Номер-1].Значение = Значение; КонецПроцедуры Процедура qs_0(s_arr, first, last) i = first; j = last; x = ПолучитьЗначение(s_arr, Окр((first + last) / 2, 0)); Пока i <= j Цикл Пока СравнитьЗначения(ПолучитьЗначение(s_arr, i), x)=-1 Цикл i=i+1; КонецЦикла; Пока СравнитьЗначения(ПолучитьЗначение(s_arr, j), x)=1 Цикл j=j-1; КонецЦикла; Если i <= j Тогда Если i < j Тогда к=ПолучитьЗначение(s_arr, i); УстановитьЗначение(s_arr, i, ПолучитьЗначение(s_arr, j)); УстановитьЗначение(s_arr, j, к); КонецЕсли; i=i+1; j=j-1; КонецЕсли; КонецЦикла; Если i < last Тогда qs_0(s_arr, i, last); КонецЕсли; Если first < j Тогда qs_0(s_arr, first,j); КонецЕсли; КонецПроцедуры Процедура Сортировать(Список, Размер="", Первый="", Последний="") Если Не ЗначениеЗаполнено(Первый) Тогда Первый=1; КонецЕсли; Если НЕ ЗначениеЗаполнено(Последний) Тогда Последний=Размер; КонецЕсли; qs_0(Список, Первый, Последний); КонецПроцедуры

Turbo Basic 1.1

DEF FN QSORT(LOW,HIGH) LOCAL I,J,X,TEMP J=HIGH X=Y[(LOW+HIGH)/2] DO WHILE Y[I]X:J=J-1:WEND IF I<=J THEN TEMP=Y[I] Y[I]=Y[J] Y[J]=TEMP I=I+1 J=J-1 END IF LOOP WHILE I<=J IF LOW

Пример вызова функции FN QSORT(LOW,HIGH), входные и выходные данные в массиве DIM Y

LOW=N1 HIGH=N2 F=FN QSORT(LOW,HIGH)

Стоит отметить, что быстрая сортировка может оказаться малоэффективной на массивах, состоящих из небольшого числа элементов, поэтому при работе с ними разумнее отказаться от данного метода. В целом алгоритм неустойчив, а также использование рекурсии в неверно составленном коде может привести к переполнению стека. Но, несмотря на эти и некоторые другие минусы, быстрая сортировка все же является одним из самых эффективных и часто используемых методов.

При написании статьи были использованы открытые источники сети интернет:

незнакомец 30 мая 2011 в 15:24

Быстрая сортировка и с чем её едят

  • Чулан *

Всем привет! Я расскажу об алгоритме быстрой сортировки и покажу, как его можно реализовать программно.

Итак, быстрая сортировка, или, по названию функции в Си, Qsort - это алгоритм сортировки, сложность которого в среднем составляет O(n log(n)). Суть его предельно проста: выбирается так называемый опорный элемент, и массив делится на 3 подмассива: меньших опорного, равных опорному и больших опорного. Потом этот алгоритм применяется рекурсивно к подмассивам.

Алгоритм

  1. Выбираем опорный элемент
  2. Разбиваем массив на 3 части
    • Создаём переменные l и r - индексы соответственно начала и конца рассматриваемого подмассива
    • Увеличиваем l, пока l-й элемент меньше опорного
    • Уменьшаем r, пока r-й элемент больше опорного
    • Если l всё ещё меньше r, то меняем l-й и r-й элементы местами, инкрементируем l и декрементируем r
    • Если l вдруг становится больше r, то прерываем цикл
  3. Повторяем рекурсивно, пока не дойдём до массива из 1 элемента
Что ж, выглядит не так уж сложно. Реализуем на Си? Нет проблем!
void qsort (int b, int e)
{
int l = b, r = e;
int piv = arr[(l + r) / 2]; // Опорным элементом для примера возьмём средний
while (l <= r)
{
while (arr[l] < piv)
l++;
while (arr[r] > piv)
r--;
if (l <= r)
swap (arr, arr);
}
if (b < r)
qsort (b, r);
if (e > l)
qsort (l, e);
} /* ----- end of function qsort ----- */

// qsort (0, n-1);


* This source code was highlighted with Source Code Highlighter .

Эта реализация имеет ряд недостатков, таких как возможное переполнение стека из-за большого количества вложенной рекурсии и то, что опорным элементом всегда берётся средний. Для примера это, может, и нормально, но при решении, например, олимпиадных задач, хитрое жюри может специально подобрать такие тесты, чтобы на них это решение работало слишком долго и не проходило в лимит. В принципе, в качестве опорного элемента можно брать любой, но лучше, чтобы он был максимально приближен к медиане, поэтому можно выбрать его случайно или взять средний по значению из первого, среднего и последнего. Зависимость быстродействия от опорного элемента - один из недостатков алгоритма, ничего с этим не поделать, но сильная деградация производительности происходит редко, обычно если сортируется специально подобранный набор чисел. Если всё-таки нужна сортировка, работающая гарантированно быстро, можно использовать, например, пирамидальную сортировку, всегда работающую строго за O(n log n). Обычно Qsort всё же выигрывает в производительности перед другими сортировками, не требует много дополнительной памяти и достаточно прост в реализации, поэтому пользуется заслуженной популярностью.

Писáл сам, изредка поглядывая на Википедию . Пользуясь случаем, передаю спасибо замечательным преподавателям и студентам ПетрГУ, научившим меня множеству полезных вещей, в том числе и этому алгоритму!

Теги: Qsort, быстрая сортировка, алгоритмы сортировки, алгоритмы, C

Алгоритмы и структуры данных для начинающих: сортировка

Никита Прияцелюк

В этой части мы посмотрим на пять основных алгоритмов сортировки данных в массиве. Начнем с самого простого - сортировки пузырьком - и закончим «быстрой сортировкой» (quicksort) .

Для каждого алгоритма, кроме объяснения его работы, мы также укажем его сложность по памяти и времени в наихудшем, наилучшем и среднем случае.

Также смотрите другие материалы этой серии: , и .

Метод Swap

Для упрощения кода и улучшения читаемости мы введем метод Swap , который будет менять местами значения в массиве по индексу.

Void Swap(T items, int left, int right) { if (left != right) { T temp = items; items = items; items = temp; } }

Пузырьковая сортировка

Сортировка пузырьком - это самый простой алгоритм сортировки. Он проходит по массиву несколько раз, на каждом этапе перемещая самое большое значение из неотсортированных в конец массива.

Например, у нас есть массив целых чисел:

При первом проходе по массиву мы сравниваем значения 3 и 7. Поскольку 7 больше 3, мы оставляем их как есть. После чего сравниваем 7 и 4. 4 меньше 7, поэтому мы меняем их местами, перемещая семерку на одну позицию ближе к концу массива. Теперь он выглядит так:

Этот процесс повторяется до тех пор, пока семерка не дойдет почти до конца массива. В конце она сравнивается с элементом 8, которое больше, а значит, обмена не происходит. После того, как мы обошли массив один раз, он выглядит так:

Поскольку был совершен по крайней мере один обмен значений, нам нужно пройти по массиву еще раз. В результате этого прохода мы перемещаем на место число 6.

И снова был произведен как минимум один обмен, а значит, проходим по массиву еще раз.

При следующем проходе обмена не производится, что означает, что наш массив отсортирован, и алгоритм закончил свою работу.

Public void Sort(T items) { bool swapped; do { swapped = false; for (int i = 1; i < items.Length; i++) { if (items.CompareTo(items[i]) > 0) { Swap(items, i - 1, i); swapped = true; } } } while (swapped != false); }

Сортировка вставками

Сортировка вставками работает, проходя по массиву и перемещая нужное значение в начало массива. После того, как обработана очередная позиция, мы знаем, что все позиции до нее отсортированы, а после нее - нет.

Важный момент: сортировка вставками обрабатывает элементы массива по порядку. Поскольку алгоритм проходит по элементам слева направо, мы знаем, что все, что слева от текущего индекса - уже отсортировано. На этом рисунке показано, как увеличивается отсортированная часть массива с каждым проходом:

Постепенно отсортированная часть массива растет, и, в конце концов, массив окажется упорядоченным.

Давайте взглянем на конкретный пример. Вот наш неотсортированный массив, который мы будем использовать:

Алгоритм начинает работу с индекса 0 и значения 3. Поскольку это первый индекс, массив до него включительно считается отсортированным.

На этом этапе элементы с индексами 0..1 отсортированы, а про элементы с индексами 2..n ничего не известно.

Следующим проверяется значение 4. Так как оно меньше семи, мы должны перенести его на правильную позицию в отсортированную часть массива. Остается вопрос: как ее определить? Это осуществляется методом FindInsertionIndex . Он сравнивает переданное ему значение (4) с каждым значением в отсортированной части, пока не найдет место для вставки.

Итак, мы нашли индекс 1 (между значениями 3 и 7). Метод Insert осуществляет вставку, удаляя вставляемое значение из массива и сдвигая все значения, начиная с индекса для вставки, вправо. Теперь массив выглядит так:

Теперь часть массива, начиная от нулевого элемента и заканчивая элементом с индексом 2, отсортирована. Следующий проход начинается с индекса 3 и значения 4. По мере работы алгоритма мы продолжаем делать такие вставки.

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

Public void Sort(T items) { int sortedRangeEndIndex = 1; while (sortedRangeEndIndex < items.Length) { if (items.CompareTo(items) < 0) { int insertIndex = FindInsertionIndex(items, items); Insert(items, insertIndex, sortedRangeEndIndex); } sortedRangeEndIndex++; } } private int FindInsertionIndex(T items, T valueToInsert) { for (int index = 0; index < items.Length; index++) { if (items.CompareTo(valueToInsert) > 0) { return index; } } throw new InvalidOperationException("The insertion index was not found"); } private void Insert(T itemArray, int indexInsertingAt, int indexInsertingFrom) { // itemArray = 0 1 2 4 5 6 3 7 // insertingAt = 3 // insertingFrom = 6 // // Действия: // 1: Сохранить текущий индекс в temp // 2: Заменить indexInsertingAt на indexInsertingFrom // 3: Заменить indexInsertingAt на indexInsertingFrom в позиции +1 // Сдвинуть элементы влево на один. // 4: Записать temp на позицию в массиве + 1. // Шаг 1. T temp = itemArray; // Шаг 2. itemArray = itemArray; // Шаг 3. for (int current = indexInsertingFrom; current > indexInsertingAt; current--) { itemArray = itemArray; } // Шаг 4. itemArray = temp; }

Сортировка выбором

Сортировка выбором - это некий гибрид между пузырьковой и сортировкой вставками. Как и сортировка пузырьком, этот алгоритм проходит по массиву раз за разом, перемещая одно значение на правильную позицию. Однако, в отличие от пузырьковой сортировки, он выбирает наименьшее неотсортированное значение вместо наибольшего. Как и при сортировке вставками, упорядоченная часть массива расположена в начале, в то время как в пузырьковой сортировке она находится в конце.

Давайте посмотрим на работу сортировки выбором на нашем неотсортированном массиве.

При первом проходе алгоритм с помощью метода FindIndexOfSmallestFromIndex пытается найти наименьшее значение в массиве и переместить его в начало.

Имея такой маленький массив, мы сразу можем сказать, что наименьшее значение - 3, и оно уже находится на правильной позиции. На этом этапе мы знаем, что на первой позиции в массиве (индекс 0) находится самое маленькое значение, следовательно, начало массива уже отсортировано. Поэтому мы начинаем второй проход - на этот раз по индексам от 1 до n – 1.

На втором проходе мы определяем, что наименьшее значение - 4. Мы меняем его местами со вторым элементом, семеркой, после чего 4 встает на свою правильную позицию.

Теперь неотсортированная часть массива начинается с индекса 2. Она растет на один элемент при каждом проходе алгоритма. Если на каком-либо проходе мы не сделали ни одного обмена, это означает, что массив отсортирован.

После еще двух проходов алгоритм завершает свою работу:

Public void Sort(T items) { int sortedRangeEnd = 0; while (sortedRangeEnd < items.Length) { int nextIndex = FindIndexOfSmallestFromIndex(items, sortedRangeEnd); Swap(items, sortedRangeEnd, nextIndex); sortedRangeEnd++; } } private int FindIndexOfSmallestFromIndex(T items, int sortedRangeEnd) { T currentSmallest = items; int currentSmallestIndex = sortedRangeEnd; for (int i = sortedRangeEnd + 1; i < items.Length; i++) { if (currentSmallest.CompareTo(items[i]) > 0) { currentSmallest = items[i]; currentSmallestIndex = i; } } return currentSmallestIndex; }

Сортировка слиянием

Разделяй и властвуй

До сих пор мы рассматривали линейные алгоритмы. Они используют мало дополнительной памяти, но имеют квадратичную сложность. На примере сортировки слиянием мы посмотрим на алгоритм типа «разделяй и властвуй» (divide and conquer) .

Алгоритмы этого типа работают, разделяя крупную задачу на более мелкие, решаемые проще. Мы пользуемся ими каждый день. К примеру, поиск в телефонной книге - один из примеров такого алгоритма.

Если вы хотите найти человека по фамилии Петров, вы не станете искать, начиная с буквы А и переворачивая по одной странице. Вы, скорее всего, откроете книгу где-то посередине. Если попадете на букву Т, перелистнете несколько страниц назад, возможно, слишком много - до буквы О. Тогда вы пойдете вперед. Таким образом, перелистывая туда и обратно все меньшее количество страниц, вы, в конце концов, найдете нужную.

Насколько эффективны эти алгоритмы?

Предположим, что в телефонной книге 1000 страниц. Если вы открываете ее на середине, вы отбрасываете 500 страниц, в которых нет искомого человека. Если вы не попали на нужную страницу, вы выбираете правую или левую сторону и снова оставляете половину доступных вариантов. Теперь вам надо просмотреть 250 страниц. Таким образом мы делим нашу задачу пополам снова и снова и можем найти человека в телефонной книге всего за 10 просмотров. Это составляет 1% от всего количества страниц, которые нам пришлось бы просмотреть при линейном поиске.

Сортировка слиянием

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

Давайте посмотрим на такой массив:

Разделим его пополам:

И будем делить каждую часть пополам, пока не останутся части с одним элементом:

Теперь, когда мы разделили массив на максимально короткие участки, мы сливаем их в правильном порядке.

Сначала мы получаем группы по два отсортированных элемента, потом «собираем» их в группы по четыре элемента и в конце собираем все вместе в отсортированный массив.

Для работы алгоритма мы должны реализовать следующие операции:

  1. Операцию для рекурсивного разделения массива на группы (метод Sort).
  2. Слияние в правильном порядке (метод Merge).

Стоит отметить, что в отличие от линейных алгоритмов сортировки, сортировка слиянием будет делить и склеивать массив вне зависимости от того, был он отсортирован изначально или нет. Поэтому, несмотря на то, что в худшем случае он отработает быстрее, чем линейный, в лучшем случае его производительность будет ниже, чем у линейного. Поэтому сортировка слиянием - не самое лучшее решение, когда надо отсортировать частично упорядченный массив.

Public void Sort(T items) { if (items.Length <= 1) { return; } int leftSize = items.Length / 2; int rightSize = items.Length - leftSize; T left = new T; T right = new T; Array.Copy(items, 0, left, 0, leftSize); Array.Copy(items, leftSize, right, 0, rightSize); Sort(left); Sort(right); Merge(items, left, right); } private void Merge(T items, T left, T right) { int leftIndex = 0; int rightIndex = 0; int targetIndex = 0; int remaining = left.Length + right.Length; while(remaining > 0) { if (leftIndex >= left.Length) { items = right; } else if (rightIndex >= right.Length) { items = left; } else if (left.CompareTo(right) < 0) { items = left; } else { items = right; } targetIndex++; remaining--; } }

Быстрая сортировка

Быстрая сортировка - это еще один алгоритм типа «разделяй и властвуй». Он работает, рекурсивно повторяя следующие шаги:

  1. Выбрать ключевой индекс и разделить по нему массив на две части. Это можно делать разными способами, но в данной статье мы используем случайное число.
  2. Переместить все элементы больше ключевого в правую часть массива, а все элементы меньше ключевого - в левую. Теперь ключевой элемент находится в правильной позиции - он больше любого элемента слева и меньше любого элемента справа.
  3. Повторяем первые два шага, пока массив не будет полностью отсортирован.

Давайте посмотрим на работу алгоритма на следующем массиве:

Сначала мы случайным образом выбираем ключевой элемент:

Int pivotIndex = _pivotRng.Next(left, right);

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

Перемещение значений осуществляется методом partition .

На этом этапе мы знаем, что значение 6 находится на правильной позиции. Теперь мы повторяем этот процесс для правой и левой частей массива.

Мы рекурсивно вызываем метод quicksort на каждой из частей. Ключевым элементом в левой части становится пятерка. При перемещении значений она изменит свой индекс. Главное - помнить, что нам важно именно ключевое значение, а не его индекс.

Снова применяем быструю сортировку:

И еще раз:

У нас осталось одно неотсортированное значение, а, поскольку мы знаем, что все остальное уже отсортировано, алгоритм завершает работу.

Random _pivotRng = new Random(); public void Sort(T items) { quicksort(items, 0, items.Length - 1); } private void quicksort(T items, int left, int right) { if (left < right) { int pivotIndex = _pivotRng.Next(left, right); int newPivot = partition(items, left, right, pivotIndex); quicksort(items, left, newPivot - 1); quicksort(items, newPivot + 1, right); } } private int partition(T items, int left, int right, int pivotIndex) { T pivotValue = items; Swap(items, pivotIndex, right); int storeIndex = left; for (int i = left; i < right; i++) { if (items[i].CompareTo(pivotValue) < 0) { Swap(items, i, storeIndex); storeIndex += 1; } } Swap(items, storeIndex, right); return storeIndex; }

Заключение

На этом мы заканчиваем наш цикл статей по алгоритмам и структурам данных для начинающих. За это время мы рассмотрели связные списки, динамические массивы, двоичное дерево поиска и множества с примерами кода на C#.