Сұрыптау - an objectілерде орындалатын әрекеттердің немесе әрекеттердің негізгі түрлерінің бірі. Бала кезінің өзінде-ақ балалардың ойлауын дамыта отырып, сұрыптауға үйретеді. Компьютерлер мен бағдарламалар да ерекшелік емес. Алгоритмдердің алуан түрлілігі бар. Мен сізге олардың не екенін және қалай жұмыс істейтінін қарауды ұсынамын. Сонымен қатар, бір күні сізден сұхбатта осылардың бірі туралы сұралса ше?
Материалдар:
L совпадёт с
Кіріспе
Элементтерді сұрыптау - әзірлеуші үйренуі керек алгоритм категорияларының бірі. Бір кездері мен оқып жүргенде информатикаға онша мән бермейтін болса, қазір мектепте сұрыптау алгоритмдерін жүзеге асырып, оны түсіне білуі керек. Негізгі алгоритмдер, ең қарапайымдары цикл арқылы жүзеге асырыладыfor
. Әрине, элементтер жинағын, мысалы, массивті сұрыптау үшін бұл жинақты қандай да бір жолмен өту керек. Мысалы:
int[] array = {10, 2, 10, 3, 1, 2, 5};
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
Бұл code бөлігі туралы не айта аласыз? int i
Бізде индекс мәнін ( ) 0-ден массивтің соңғы элементіне өзгертетін цикл бар . Негізінде, біз жай ғана массивтің әрбір элементін алып, оның мазмұнын басып шығарамыз. Жиымдағы элементтер неғұрлым көп болса, codeты орындау үшін соғұрлым ұзағырақ болады. Яғни, егер n элементтер саны болса, n=10 болғанда бағдарлама n=5-ке қарағанда 2 есе көп орындалады. Бағдарламамызда бір цикл болған кезде орындалу уақыты сызықты түрде артады: элементтер неғұрлым көп болса, соғұрлым орындалу ұзағырақ болады. Жоғарыдағы codeтың алгоритмі сызықтық уақытта (n) жұмыс істейді екен. Мұндай жағдайларда «алгоритм күрделілігі» O(n) деп аталады. Бұл белгіні «үлкен О» немесе «ассимптотикалық мінез-құлық» деп те атайды. Бірақ сіз «алгоритмнің күрделілігін» жай ғана есте сақтай аласыз.
Ең қарапайым сұрыптау (көпіршікті сұрыптау)
Сонымен, бізде массив бар және оны қайталай аламыз. Тамаша. Енді оны өсу ретімен сұрыптап көрейік. Бұл біз үшін нені білдіреді? Бұл екі элемент берілген (мысалы, a=6, b=5), егер а b-дан үлкен болса (егер a > b) болса, a және b мәндерін ауыстыру керек дегенді білдіреді. Бұл индекс бойынша жинақпен жұмыс істегенде (массив сияқты) біз үшін нені білдіреді? Бұл a индексі бар элемент b индексі бар элементтен үлкен болса, (массив[a] > массив[b]), мұндай элементтерді ауыстыру керек дегенді білдіреді. Орындарды ауыстыру көбінесе своп деп аталады. Орындарды ауыстырудың әртүрлі жолдары бар. Бірақ біз қарапайым, түсінікті және есте сақтау оңай codeты қолданамыз:private void swap(int[] array, int ind1, int ind2) {
int tmp = array[ind1];
array[ind1] = array[ind2];
array[ind2] = tmp;
}
Енді біз мынаны жаза аламыз:
int[] array = {10, 2, 10, 3, 1, 2, 5};
System.out.println(Arrays.toString(array));
for (int i = 1; i < array.length; i++) {
if (array[i] < array[i - 1]) {
swap(array, i, i-1);
}
}
System.out.println(Arrays.toString(array));
Көріп отырғанымыздай, элементтер шынымен орындарын ауыстырды. Біз бір элементтен бастадық, себебі... егер массив тек бір элементтен тұрса, 1 < 1 өрнегі ақиқат мәнін қайтармайды және осылайша біз бір элементі бар немесе мүлде жоқ массив жағдайларынан өзімізді қорғаймыз және code жақсырақ көрінеді. Бірақ біздің соңғы массив бәрібір сұрыпталмаған, себебі... Бір жолда барлығын сұрыптау мүмкін емес. Біз сұрыпталған массив алғанша бір-бірден өтуді орындайтын тағы бір циклды қосуымыз керек:
int[] array = {10, 2, 10, 3, 1, 2, 5};
System.out.println(Arrays.toString(array));
boolean needIteration = true;
while (needIteration) {
needIteration = false;
for (int i = 1; i < array.length; i++) {
if (array[i] < array[i - 1]) {
swap(array, i, i-1);
needIteration = true;
}
}
}
System.out.println(Arrays.toString(array));
Сонымен, бірінші сұрыптауымыз нәтиже берді. while
Басқа итерациялар қажет емес деп шешкенше, сыртқы циклде ( ) қайталаймыз . Әдепкі бойынша, әрбір жаңа итерация алдында біз массив сұрыпталған деп есептейміз және бұдан әрі қайталанғымыз келмейді. Сондықтан біз элементтерді дәйекті түрде өтіп, бұл болжамды тексереміз. Бірақ егер элементтер ретсіз болса, біз элементтерді ауыстырамыз және элементтердің қазір дұрыс тәртіпте екеніне сенімді емес екенімізді түсінеміз. Сондықтан біз тағы бір итерацияны орындағымыз келеді. Мысалы, [3, 5, 2]. 5 - үштен көп, бәрі жақсы. Бірақ 2 саны 5-тен аз. Алайда, [3, 2, 5] тағы бір өтуді қажет етеді, өйткені 3 > 2 және оларды ауыстыру керек. Біз цикл ішінде циклді қолданатындықтан, біздің алгоритміміздің күрделілігі артады. n элементпен ол n * n болады, яғни O(n^2). Бұл күрделілік квадраттық деп аталады. Біз түсінгеніміздей, қанша итерация қажет болатынын нақты біле алмаймыз. Алгоритмнің күрделілік көрсеткіші күрделіліктің өсу тенденциясын, ең нашар жағдайды көрсету мақсатына қызмет етеді. Элементтер саны n өзгерген кезде жұмыс уақыты қаншаға артады. Көпіршікті сұрыптау - ең қарапайым және тиімсіз түрлердің бірі. Оны кейде «ақымақ сұрыптау» деп те атайды. Қатысты материал:
Таңдау сұрыптау
Басқа сұрыптау – таңдау сұрыптау. Оның квадраттық күрделілігі де бар, бірақ бұл туралы кейінірек. Сондықтан идея қарапайым. Әрбір өту ең кіші элементті таңдап, оны басына жылжытады. Бұл жағдайда әрбір жаңа өтуді оңға қарай жылжыту арқылы бастаңыз, яғни бірінші өту - бірінші элементтен, екінші өту - екіншісінен. Ол келесідей болады:int[] array = {10, 2, 10, 3, 1, 2, 5};
System.out.println(Arrays.toString(array));
for (int left = 0; left < array.length; left++) {
int minInd = left;
for (int i = left; i < array.length; i++) {
if (array[i] < array[minInd]) {
minInd = i;
}
}
swap(array, left, minInd);
}
System.out.println(Arrays.toString(array));
Бұл сұрыптау тұрақсыз, себебі бірдей элементтер (біз элементтерді сұрыптайтын сипаттама тұрғысынан) өз орнын өзгерте алады. Жақсы мысал Уикипедия мақаласында келтірілген: Таңдау_бойынша сұрыптау . Қатысты материал:
Кірістіру сұрыптауы
Кірістіру сұрыптауы да квадраттық күрделілікке ие, өйткені бізде қайтадан цикл ішінде цикл бар. Оның таңдау сұрыптауынан айырмашылығы неде? Бұл сұрыптау «тұрақты». Бұл бірдей элементтер олардың ретін өзгертпейтінін білдіреді. Біз сұрыптайтын сипаттамалар бойынша бірдей.int[] array = {10, 2, 10, 3, 1, 2, 5};
System.out.println(Arrays.toString(array));
for (int left = 0; left < array.length; left++) {
// Retrieve the value of the element
int value = array[left];
// Move through the elements that are before the pulled element
int i = left - 1;
for (; i >= 0; i--) {
// If a smaller value is pulled out, move the larger element further
if (value < array[i]) {
array[i + 1] = array[i];
} else {
// If the pulled element is larger, stop
break;
}
}
// Insert the extracted value into the freed space
array[i + 1] = value;
}
System.out.println(Arrays.toString(array));
Қатысты материал:
- Java кірістіру сұрыптауы түсіндірілді
- CS50: Кірістіру сұрыптауы
- JavaRush жүйесіндегі CS50: кірістіру сұрыптауы
Шаттл сұрыптау
Қарапайым сорттардың ішінде тағы бір түрі бар - шаттл сұрыптау. Бірақ мен шаттл түрін ұнатамын. Меніңше, біз ғарыш кемелері туралы сирек сөйлесетін сияқтымыз, ал шаттл - жүгіру. Сондықтан, шаттлдардың ғарышқа қалай ұшырылатынын елестету оңайырақ. Міне, осы алгоритммен байланыс. Алгоритмнің мәні неде? Алгоритмнің мәні мынада: біз солдан оңға қарай қайталаймыз және элементтерді ауыстырған кезде біз айырбастың қайталану қажеттігін білу үшін артта қалған барлық басқа элементтерді тексереміз.int[] array = {10, 2, 10, 3, 1, 2, 5};
System.out.println(Arrays.toString(array));
for (int i = 1; i < array.length; i++) {
if (array[i] < array[i - 1]) {
swap(array, i, i - 1);
for (int z = i - 1; (z - 1) >= 0; z--) {
if (array[z] < array[z - 1]) {
swap(array, z, z - 1);
} else {
break;
}
}
}
}
System.out.println(Arrays.toString(array));
Қатысты материал:
Қабық сұрыптау
Басқа қарапайым сұрыптау - Shell сұрыптауы. Оның мәні көпіршікті сұрыптауға ұқсас, бірақ әрбір итерацияда бізде салыстырылған элементтер арасында әртүрлі алшақтық болады. Әрбір итерация ол екі есе азаяды. Міне, іске асыру мысалы:int[] array = {10, 2, 10, 3, 1, 2, 5};
System.out.println(Arrays.toString(array));
// Calculate the gap between the checked elements
int gap = array.length / 2;
// As long as there is a difference between the elements
while (gap >= 1) {
for (int right = 0; right < array.length; right++) {
// Shift the right pointer until we can find one that
// there won't be enough space between it and the element before it
for (int c = right - gap; c >= 0; c -= gap) {
if (array[c] > array[c + gap]) {
swap(array, c, c + gap);
}
}
}
// Recalculate the gap
gap = gap / 2;
}
System.out.println(Arrays.toString(array));
Қатысты материал:
Біріктіру сұрыптауы
Көрсетілген қарапайым сорттардан басқа, күрделірек сорттар бар. Мысалы, біріктіру сұрыптауы. Біріншіден, рекурсия көмекке келеді. Екіншіден, біздің күрделілігіміз енді үйреніп қалғандай квадраттық болмайды. Бұл алгоритмнің күрделілігі логарифмдік. O(n log n) түрінде жазылған. Ендеше мұны істейік. Алдымен сұрыптау әдісіне рекурсивті шақыруды жазайық:public static void mergeSort(int[] source, int left, int right) {
// Choose a separator, i.e. split the input array in half
int delimiter = left + ((right - left) / 2) + 1;
// Execute this function recursively for the two halves (if we can split(
if (delimiter > 0 && right > (left + 1)) {
mergeSort(source, left, delimiter - 1);
mergeSort(source, delimiter, right);
}
}
Енді оған негізгі әрекетті қосамыз. Міне, біздің суперәдісіміздің мысалы:
public static void mergeSort(int[] source, int left, int right) {
// Choose a separator, i.e. split the input array in half
int delimiter = left + ((right - left) / 2) + 1;
// Execute this function recursively for the two halves (if we can split(
if (delimiter > 0 && right > (left + 1)) {
mergeSort(source, left, delimiter - 1);
mergeSort(source, delimiter, right);
}
// Create a temporary array with the desired size
int[] buffer = new int[right - left + 1];
// Starting from the specified left border, go through each element
int cursor = left;
for (int i = 0; i < buffer.length; i++) {
// We use the delimeter to point to the element from the right side
// If delimeter > right, then there are no unadded elements left on the right side
if (delimiter > right || source[cursor] > source[delimiter]) {
buffer[i] = source[cursor];
cursor++;
} else {
buffer[i] = source[delimiter];
delimiter++;
}
}
System.arraycopy(buffer, 0, source, left, buffer.length);
}
Мысалды шақыру арқылы іске қосайық mergeSort(array, 0, array.length-1)
. Көріп отырғаныңыздай, мәні сұрыпталатын бөлімнің басы мен соңын көрсететін массивді енгізу ретінде қабылдауымызға келіп тіреледі. Сұрыптау басталған кезде бұл массивтің басы мен соңы. Әрі қарай бөлгішті есептейміз - бөлгіш орны. Егер бөлгіш 2 бөлікке бөлінсе, онда бөлгіш массивді бөлген бөлімдер үшін рекурсивті сұрыптау деп атаймыз. Біз сұрыпталған бөлімді таңдайтын қосымша буфер массивін дайындаймыз. Содан кейін курсорды сұрыпталатын аймақтың басына орналастырамыз және біз дайындаған бос массивтің әрбір элементін аралап, оны ең кішкентай элементтермен толтырамыз. Егер курсор көрсететін элемент бөлгіш көрсететін элементтен кіші болса, онда бұл элементті буфер массивіне орналастырып, курсорды жылжытамыз. Әйтпесе, бөлгіш көрсететін элементті буфер массивіне орналастырамыз және бөлгішті жылжытамыз. Бөлгіш сұрыпталған аумақтың шекарасынан шыққанда немесе біз бүкіл массивді толтырған кезде, көрсетілген ауқым сұрыпталған болып саналады. Қатысты материал:
Санау сұрыптау және радиуспен сұрыптау
Тағы бір қызықты сұрыптау алгоритмі - Санау сұрыптауы. Бұл жағдайда алгоритмдік күрделілік O(n+k) болады, мұндағы n – элементтердің саны, ал k – элементтің ең үлкен мәні. Алгоритмде бір мәселе бар: біз массивтегі минималды және максималды мәндерді білуіміз керек. Міне, санау сұрыптауын іске асырудың мысалы:public static int[] countingSort(int[] theArray, int maxValue) {
// Array with "counters" ranging from 0 to the maximum value
int numCounts[] = new int[maxValue + 1];
// In the corresponding cell (index = value) we increase the counter
for (int num : theArray) {
numCounts[num]++;
}
// Prepare array for sorted result
int[] sortedArray = new int[theArray.length];
int currentSortedIndex = 0;
// go through the array with "counters"
for (int n = 0; n < numCounts.length; n++) {
int count = numCounts[n];
// go by the number of values
for (int k = 0; k < count; k++) {
sortedArray[currentSortedIndex] = n;
currentSortedIndex++;
}
}
return sortedArray;
}
Біз түсінгеніміздей, ең төменгі және максималды мәндерді алдын ала білу өте ыңғайсыз. Содан кейін тағы бір алгоритм бар - Radix Sort. Мен мұнда алгоритмді тек көрнекі түрде ұсынамын. Іске асыру үшін материалдарды қараңыз:
Java жылдам сұрыптау
Ал, десерт үшін - ең танымал алгоритмдердің бірі: жылдам сұрыптау. Оның алгоритмдік күрделілігі бар, яғни бізде O(n log n) бар. Бұл сұрыпты Хоар сорты деп те атайды. Бір қызығы, алгоритмді Хоар Кеңес Одағында болған кезінде ойлап тапқан, ол Мәскеу университетінде компьютерлік аударманы оқып, орысша-ағылшынша тілашар құрастырып жатқан. Бұл алгоритм Java тіліндегі Arrays.sort бағдарламасында күрделірек іске асыруда да қолданылады. Collections.sort туралы не деуге болады? Мен олардың «қапшық астында» қалай сұрыпталғанын өзіңіз көруді ұсынамын. Сонымен, code:public static void quickSort(int[] source, int leftBorder, int rightBorder) {
int leftMarker = leftBorder;
int rightMarker = rightBorder;
int pivot = source[(leftMarker + rightMarker) / 2];
do {
// Move the left marker from left to right while element is less than pivot
while (source[leftMarker] < pivot) {
leftMarker++;
}
// Move the right marker until element is greater than pivot
while (source[rightMarker] > pivot) {
rightMarker--;
}
// Check if you don't need to swap elements pointed to by markers
if (leftMarker <= rightMarker) {
// The left marker will only be less than the right marker if we have to swap
if (leftMarker < rightMarker) {
int tmp = source[leftMarker];
source[leftMarker] = source[rightMarker];
source[rightMarker] = tmp;
}
// Move markers to get new borders
leftMarker++;
rightMarker--;
}
} while (leftMarker <= rightMarker);
// Execute recursively for parts
if (leftMarker < rightBorder) {
quickSort(source, leftMarker, rightBorder);
}
if (leftBorder < rightMarker) {
quickSort(source, leftBorder, rightMarker);
}
}
Мұнда бәрі өте қорқынышты, сондықтан біз оны анықтаймыз. Енгізу массивінің int[]
көзі үшін біз солға (L) және оңға (R) екі маркер орнатамыз. Бірінші рет шақырылғанда, олар массивтің басы мен аяғына сәйкес келеді. Әрі қарай, тірек элемент анықталады, aka pivot
. pivot
Осыдан кейін біздің міндетіміз - кіші мәндерді солға pivot
, ал үлкендерін оңға жылжыту . Мұны істеу үшін алдымен көрсеткішті L
мәнінен үлкен мәнді тапқанша жылжытыңыз pivot
. Егер кішірек мән табылмаса, ондаpivot
. Потом двигаем указатель
R
пока не найдём меньшее, чем
pivot
meaning. Если меньшее meaning не нашли, то
R
совпадёт с
pivot
. Далее, если указатель
L
находится до указателя
R
or совпадает с ним, то пытаемся выполнить обмен элементов, если элемент
L
меньше, чем
R
. Далее
L
сдвигаем вправо на 1 позицию,
R
сдвигаем влево на одну позицию. Когда левый маркер
L
окажется за правым маркером
R
это будет означать, что обмен закончен, слева от
pivot
меньшие значения, справа от
pivot
— большие значения. После этого рекурсивно вызываем такую же сортировку для участков массива от начала сортируемого участка до правого маркера и от левого маркера до конца сортируемого участка. Почему от начала до правого? Потому что в конце итерации так и получится, что правый маркер сдвинется настолько, что станет границей части слева. Этот алгоритм более сложный, чем простая sorting, поэтому его лучше зарисовать. Возьмём белый лист бумаги, запишем: 4 2 6 7 3 , а
Pivot
по центру, т.е. число 6. Обведём его в круг. Под 4 напишем
L
, под 3 напишем
R
. 4 меньше чем 6, 2 меньше чем 6. Total,
L
переместился на положение
pivot
, т.к. по условию
L
не может уйти дальше, чем
pivot
. Напишем снова 4 2 6 7 3 , обведём 6 вкруг (
pivot
) и поставим под ним
L
. Теперь двигаем указатель
R
. 3 меньше чем 6, поэтому ставим маркер
R
на цифру 3. Так How 3 меньше, чем
pivot 6
выполняем
swap
, т.е. обмен. Запишем результат: 4 2 3 7 6 , обводим 6 вкруг, т.к. он по прежнему
pivot
. Указатель
L
на цифре 3, указатель
R
на цифре 6. Мы помним, что двигаем указатели до тех пор, пока
L
не зайдём за
R
.
L
двигаем на следующую цифру. Тут хочется разобрать два варианта: если бы предпоследняя цифра была 7 и если бы она была не 7, а 1.
Предпоследня цифра 1: Сдвинули указатель
L
на цифру 1, т.к. мы можем двигать
L
до тех пор, пока указатель
L
указывает на цифру, меньшую чем
pivot
. А вот
R
мы не можем сдвинуть с 6, т.к. R не мы можем двигать только если указатель
R
указывает на цифру, которая больше чем
pivot
.
swap
не делаем, т.к. 1 меньше 6. Записываем положение: 4 2 3 1 6, обводим
pivot 6
.
L
сдвигается на
pivot
и больше не двигается.
R
тоже не двигается. Обмен не производим. Сдвигаем
L
и
R
на одну позицию и подписываем цифру 1 маркером
R
, а
L
получается вне числа. Т.к.
L
вне числа — ничего не делаем, а вот часть 4 2 3 1 выписываем снова, т.к. это наша левая часть, меньшая, чем
pivot 6
. Выделяем новый
pivot
и начинаем всё снова )
Предпоследняя цифра 7: Сдвинули указать
L
на цифру 7, правый указатель не можем двигать, т.к. он уже указывает на pivot. т.к. 7 больше, чем
pivot
, то делаем
swap
. Запишем результат: 4 2 3 6 7, обводим 6 кружком, т.к. он
pivot
. Указатель
L
теперь сдвигается на цифру 7, а указатель
R
сдвигается на цифру 3. Часть от
L
до конца нет смысла сортировать, т.к. там всего 1 элемент, а вот часть от 4 до указателя
R
отправляем на сортировку. Выбираем
pivot
и начинаем всё снова ) Может на первый взгляд показаться, что если расставить много одинаковых с
pivot
значений, это сломает алгоритм, но это не так. Можно напридумывать каверзных вариантов и на бумажке убедиться, что всё правильно и подивиться, How такие простые действия предоставляют такой надёжный механизм. Единственный минус — такая sorting не является стабильной. Т.к. при выполнении обмена одинаковые элементы могут поменять свой порядок, если один из них встретился до
pivot
до того, How другой элемент попал в часть до
pivot
при помощи обмена. Материал:
GO TO FULL VERSION