JavaRush /Java блогы /Random-KK /Теорияда және практикада сұрыптау алгоритмдері
Viacheslav
Деңгей

Теорияда және практикада сұрыптау алгоритмдері

Топта жарияланған
Сұрыптау - an objectілерде орындалатын әрекеттердің немесе әрекеттердің негізгі түрлерінің бірі. Бала кезінің өзінде-ақ балалардың ойлауын дамыта отырып, сұрыптауға үйретеді. Компьютерлер мен бағдарламалар да ерекшелік емес. Алгоритмдердің алуан түрлілігі бар. Мен сізге олардың не екенін және қалай жұмыс істейтінін қарауды ұсынамын. Сонымен қатар, бір күні сізден сұхбатта осылардың бірі туралы сұралса ше?
Теорияда және практикада сұрыптау алгоритмдері – 1

Кіріспе

Элементтерді сұрыптау - әзірлеуші ​​​​үйренуі керек алгоритм категорияларының бірі. Бір кездері мен оқып жүргенде информатикаға онша мән бермейтін болса, қазір мектепте сұрыптау алгоритмдерін жүзеге асырып, оны түсіне білуі керек. Негізгі алгоритмдер, ең қарапайымдары цикл арқылы жүзеге асырылады 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) деп аталады. Бұл белгіні «үлкен О» немесе «ассимптотикалық мінез-құлық» деп те атайды. Бірақ сіз «алгоритмнің күрделілігін» жай ғана есте сақтай аласыз.
Теорияда және практикада сұрыптау алгоритмдері – 2

Ең қарапайым сұрыптау (көпіршікті сұрыптау)

Сонымен, бізде массив бар және оны қайталай аламыз. Тамаша. Енді оны өсу ретімен сұрыптап көрейік. Бұл біз үшін нені білдіреді? Бұл екі элемент берілген (мысалы, 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 өзгерген кезде жұмыс уақыты қаншаға артады. Көпіршікті сұрыптау - ең қарапайым және тиімсіз түрлердің бірі. Оны кейде «ақымақ сұрыптау» деп те атайды. Қатысты материал:
Теорияда және практикада сұрыптау алгоритмдері – 3

Таңдау сұрыптау

Басқа сұрыптау – таңдау сұрыптау. Оның квадраттық күрделілігі де бар, бірақ бұл туралы кейінірек. Сондықтан идея қарапайым. Әрбір өту ең кіші элементті таңдап, оны басына жылжытады. Бұл жағдайда әрбір жаңа өтуді оңға қарай жылжыту арқылы бастаңыз, яғни бірінші өту - бірінші элементтен, екінші өту - екіншісінен. Ол келесідей болады:
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));
Бұл сұрыптау тұрақсыз, себебі бірдей элементтер (біз элементтерді сұрыптайтын сипаттама тұрғысынан) өз орнын өзгерте алады. Жақсы мысал Уикипедия мақаласында келтірілген: Таңдау_бойынша сұрыптау . Қатысты материал:
Теорияда және практикада сұрыптау алгоритмдері – 4

Кірістіру сұрыптауы

Кірістіру сұрыптауы да квадраттық күрделілікке ие, өйткені бізде қайтадан цикл ішінде цикл бар. Оның таңдау сұрыптауынан айырмашылығы неде? Бұл сұрыптау «тұрақты». Бұл бірдей элементтер олардың ретін өзгертпейтінін білдіреді. Біз сұрыптайтын сипаттамалар бойынша бірдей.
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));
Қатысты материал:
Теорияда және практикада сұрыптау алгоритмдері – 5

Шаттл сұрыптау

Қарапайым сорттардың ішінде тағы бір түрі бар - шаттл сұрыптау. Бірақ мен шаттл түрін ұнатамын. Меніңше, біз ғарыш кемелері туралы сирек сөйлесетін сияқтымыз, ал шаттл - жүгіру. Сондықтан, шаттлдардың ғарышқа қалай ұшырылатынын елестету оңайырақ. Міне, осы алгоритммен байланыс. Алгоритмнің мәні неде? Алгоритмнің мәні мынада: біз солдан оңға қарай қайталаймыз және элементтерді ауыстырған кезде біз айырбастың қайталану қажеттігін білу үшін артта қалған барлық басқа элементтерді тексереміз.
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));
Қатысты материал:
Теорияда және практикада сұрыптау алгоритмдері – 6

Қабық сұрыптау

Басқа қарапайым сұрыптау - 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));
Қатысты материал:
Теорияда және практикада сұрыптау алгоритмдері – 7

Біріктіру сұрыптауы

Көрсетілген қарапайым сорттардан басқа, күрделірек сорттар бар. Мысалы, біріктіру сұрыптауы. Біріншіден, рекурсия көмекке келеді. Екіншіден, біздің күрделілігіміз енді үйреніп қалғандай квадраттық болмайды. Бұл алгоритмнің күрделілігі логарифмдік. 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 бөлікке бөлінсе, онда бөлгіш массивді бөлген бөлімдер үшін рекурсивті сұрыптау деп атаймыз. Біз сұрыпталған бөлімді таңдайтын қосымша буфер массивін дайындаймыз. Содан кейін курсорды сұрыпталатын аймақтың басына орналастырамыз және біз дайындаған бос массивтің әрбір элементін аралап, оны ең кішкентай элементтермен толтырамыз. Егер курсор көрсететін элемент бөлгіш көрсететін элементтен кіші болса, онда бұл элементті буфер массивіне орналастырып, курсорды жылжытамыз. Әйтпесе, бөлгіш көрсететін элементті буфер массивіне орналастырамыз және бөлгішті жылжытамыз. Бөлгіш сұрыпталған аумақтың шекарасынан шыққанда немесе біз бүкіл массивді толтырған кезде, көрсетілген ауқым сұрыпталған болып саналады. Қатысты материал:
Теорияда және практикада сұрыптау алгоритмдері – 8

Санау сұрыптау және радиуспен сұрыптау

Тағы бір қызықты сұрыптау алгоритмі - Санау сұрыптауы. Бұл жағдайда алгоритмдік күрделілік 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. Мен мұнда алгоритмді тек көрнекі түрде ұсынамын. Іске асыру үшін материалдарды қараңыз:
Теорияда және практикада сұрыптау алгоритмдері – 9
Материалдар:
Теорияда және практикада сұрыптау алгоритмдері – 10

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. Егер кішірек мән табылмаса, онда L совпадёт с 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 при помощи обмена. Материал:

Итог

Выше мы рассмотрели "джентельменский" набор алгоритмов сортировки, реализованных на Java. Алгоритмы вообще штука полезная, How с прикладной точки зрения, так и с точки зрения развития мышления. Некоторые из них попроще, некоторые посложнее. По Howим-то умные люди защищали различные работы на степени, а по другим писали толстые-толстые книги. Надеюсь, приложенный к статье материал позволит вам узнать ещё больше, так How это обзорная статья, которая и так получилась увесистой. И цель её — дать небольшое вступление. Про введение в алгоритмы можно так же прочитать ознакомиться с книгой " Грокаем Алгоримы". Также мне нравится плэйлист от Jack Brown — AQA Decision 1 1.01 Tracing an Algorithm. Ради интереса можно посмотреть на визуализацию алгоритмов на sorting.at и visualgo.net. Ну и весь Youtube к вашим услугам.
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION