6 алгоритмов сортировки на Java для новичков |
||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2019-04-27 17:50 Изучение алгоритмов сортировки на языке Java поможет не изобретать велосипеды и быстро выскочить на лесенку карьерного роста. Задействование алгоритмов сортировки поможет нам упорядочить массивы Java. Для понимания: сортировка чисел от наименьшего к большему или наоборот, а также лексикографический порядок – это примеры алгоритмов сортировки, способные упорядочить Java строки и числа Сортировка пузырьком Слышали о сортировке пузырьком? Его популярность обусловлена простотой, наглядностью и, конечно, названием. Алгоритм просматривает массив и сравнивает каждую пару соседних элементов. Когда он встречает пару элементов, расположенных не по порядку, происходит замена двух элементов местами. Остается вопрос: как узнать, что все элементы упорядочены? В этом случае очередная итерация пройдет без замены соседних элементов. Вот шаги для сортировки массива чисел от наименьшего к большему:
Для полной сортировки нужен еще один шаг. Третья итерация пройдет уже без замены. Так вы поймете, что массив отсортирован. Но причём тут пузырьки? Посмотрите снова на пример, и вы увидите, что алгоритм как бы смещается вправо. По этому поведению элементов в массиве и возникла аналогия с «пузырьками», всплывающими на «поверхность». Реализация Функция входит в цикл Массив в алгоритме считается отсортированным. При первой замене доказывается обратное и запускается еще одна итерация. Цикл останавливается, когда все пары элементов в массиве пропускаются без замен: public static void bubbleSort(int[] array) { boolean sorted = false; int temp; while(!sorted) { sorted = true; for (int i = 0; i < array.length - 1; i++) { if (a[i] > a[i+1]) { temp = a[i]; a[i] = a[i+1]; a[i+1] = temp; sorted = false; } } } } Будьте осторожны с формулировкой условия замены! Например, при условии Временная сложность Рассмотрим наихудший сценарий. Вот в чем вопрос: сколько итераций нужно для сортировки всего массива? Пример: 5 4 3 2 1 При первой итерации Расширьте это утверждение для массива из Каждая Сортировка вставками Этот алгоритм разделяет оригинальный массив на сортированный и несортированный подмассивы. Длина сортированной части равна 1 в начале и соответствует первому (левому) элементу в массиве. После этого остается итерировать массив и расширять отсортированную часть массива одним элементом с каждой новой итерацией. После расширения новый элемент помещается на свое место в отсортированном подмассиве. Это происходит путём сдвига всех элементов вправо, пока не встретится элемент, который не нужно двигать. В приведенном ниже массиве жирная часть отсортирована в порядке возрастания. Посмотрите что произойдет в этом случае:
Теперь вы видите, что отсортированная часть дополнилась элементом. Каждая следующая итерация делает то же самое, и к концу вы получите отсортированный массив! Реализация public static void insertionSort(int[] array) { for (int i = 1; i < array.length; i++) { int current = array[i]; int j = i - 1; while(j >= 0 && current < array[j]) { array[j+1] = array[j]; j--; } // в этой точке мы вышли, так что j так же -1 // или в первом элементе, где текущий >= a[j] array[j+1] = current; } } Временная сложность Вернемся к худшему сценарию – к массиву, отсортированному в убывающем порядке. В этом случае каждая итерация сдвигает отсортированный массив на единицу O(n). Придется делать это для каждого элемента в каждом массиве, что приведет к сложности равной O(n ^ 2). Сортировка выбором Сортировка выбором тоже разделяет массив на сортированный и несортированный подмассивы. Но на этот раз сортированный подмассив формируется вставкой минимального элемента не отсортированного подмассива в конец сортированного, заменой:
Реализация В каждой итерации вы предполагаете, что первый неотсортированный элемент минимален и итерируете по всем оставшимся элементам в поисках меньшего. После нахождения текущего минимума неотсортированной части массива вы меняете его местами с первым элементом, и он уже часть отсортированного массива: public static void selectionSort(int[] array) { for (int i = 0; i < array.length; i++) { int min = array[i]; int minId = i; for (int j = i+1; j < array.length; j++) { if (array[j] < min) { min = array[j]; minId = j; } } // замена int temp = array[i]; array[i] = min; array[minId] = temp; } } Временная сложность При поиске минимума для длины массива проверяются все элементы, поэтому сложность равна O(n). Поиск минимума для каждого элемента массива равен O(n^2). Сортировка слиянием Сортировка слиянием эффективнее, чем примеры алгоритмов сортировки, представленные выше, благодаря использованию рекурсии и подходу «разделяй и властвуй». Массив делится на два подмассива, а затем происходит:
На схеме показана работа рекурсивных вызовов. Для массивов, отмеченных стрелкой вниз, вызывается функция. Во время слияния возвращаются массивы со стрелкой вверх. Всё просто: мы следуем за стрелкой вниз к нижней части дерева, а затем возвращаемся и объединяем. В примере массив Реализация В главную функцию передаются Основа нашей рекурсии гарантирует, что мы выйдем, когда закончим, или когда Возможно, вы вспомните дерево и спросите: почему мы не передаем два меньших массива? Ответ прост: это не нужно и вызовет огромное потребление памяти для очень длинных массивов. Достаточно следовать индексам не нарушая логики дерева рекурсии: public static void mergeSort(int[] array, int left, int right) { if (right <= left) return; int mid = (left+right)/2; mergeSort(array, left, mid); mergeSort(array, mid+1, right); merge(array, left, mid, right); } Для сортировки двух подмассивов в один нужно вычислить их длину и создать временные массивы, в которые будем копировать. Так можно свободно изменять главный массив. После копирования мы проходим по результирующему массиву и назначаем текущий минимум. Помните, что наши подмассивы отсортированы? Теперь нужно просто выбрать наименьший из двух элементов, которые еще не были выбраны, и двигать итератор для этого массива вперед: void merge(int[] array, int left, int mid, int right) { // вычисляем длину int lengthLeft = mid - left + 1; int lengthRight = right - mid; // создаем временные подмассивы int leftArray[] = new int [lengthLeft]; int rightArray[] = new int [lengthRight]; // копируем отсортированные массивы во временные for (int i = 0; i < lengthLeft; i++) leftArray[i] = array[left+i]; for (int i = 0; i < lengthRight; i++) rightArray[i] = array[mid+i+1]; // итераторы содержат текущий индекс временного подмассива int leftIndex = 0; int rightIndex = 0; // копируем из leftArray и rightArray обратно в массив for (int i = left; i < right + 1; i++) { // если остаются нескопированные элементы в R и L, копируем минимальный if (leftIndex < lengthLeft && rightIndex < lengthRight) { if (leftArray[leftIndex] < rightArray[rightIndex]) { array[i] = leftArray[leftIndex]; leftIndex++; } else { array[i] = rightArray[rightIndex]; rightIndex++; } } // если все элементы были скопированы из rightArray, скопировать остальные из leftArray else if (leftIndex < lengthLeft) { array[i] = leftArray[leftIndex]; leftIndex++; } // если все элементы были скопированы из leftArray, скопировать остальные из rightArray else if (rightIndex < lengthRight) { array[i] = rightArray[rightIndex]; rightIndex++; } } } Временная сложность Хотите легко рассчитывать рекурсивные реализации алгоритмов сортировки? Приготовьтесь к математике Для вычисления временной сложности нам понадобится основная теорема о рекуррентных соотношениях. Временную сложность рекурсивных алгоритмов сортировки можно описать следующим уравнением: Здесь Остальная часть уравнения – это сложность слияния всех решений в одно конечное. Упомянутая теорема решит все за вас: Если Так, если Примените теорему, и вы увидите, что в нашем случае Пирамидальная сортировка Для понимания работы пирамидального алгоритма сортировки нужно понять структуру, на которой он основан – пирамиду. Пирамида или двоичная куча – это дерево, в котором каждый узел состоит в отношениях с дочерними узлами. Добавление нового узла начинается с левой позиции нижнего неполного уровня. По мере движения вниз по дереву значения уменьшаются (min-heap) или увеличиваются (max-heap). Смотрите пример max-heap: А теперь представим пирамиду в виде массива: 8 5 6 3 1 2 4 Чтение графа сверху вниз здесь представлено слева направо. Мы добились того, что позиция дочернего элемента по отношению к И наоборот, для С этими знаниями вы сделаете max-heap из любого массива! Для этого проверьте каждый элемент на условие, что каждый из его дочерних элементов имеет меньшее значение. Условие верно? Тогда меняйте местами один из дочерних элементов с родительским и повторяйте рекурсию с новым родительским элементом (он может всё ещё быть больше другого дочернего).
Вы научились строить пирамиду из массива, все остальное гораздо проще! Поменяйте местами корень пирамиды с концом массива, и сократите массив на единицу. Постройте кучу из сокращенного массива и повторяйте процесс:
И так далее. Видите закономерность? Реализация static void heapify(int[] array, int length, int i) { int leftChild = 2*i+1; int rightChild = 2*i+2; int largest = i; // если левый дочерний больше родительского if (leftChild < length && array[leftChild] > array[largest]) { largest = leftChild; } // если правый дочерний больше родительского if (rightChild < length && array[rightChild] > array[largest]) { largest = rightChild; } // если должна произойти замена if (largest != i) { int temp = array[i]; array[i] = array[largest]; array[largest] = temp; heapify(array, length, largest); } } public static void heapSort(int[] array) { if (array.length == 0) return; // Строим кучу int length = array.length; // проходим от первого без ответвлений к корню for (int i = length / 2-1; i >= 0; i--) heapify(array, length, i); for (int i = length-1; i >= 0; i--) { int temp = array[0]; array[0] = array[i]; array[i] = temp; heapify(array, i, 0); } } Временная сложность Посмотрите на функцию Готовы посчитать, сколько вызовов нужно в наихудшем сценарии? В худшем случае рекурсивный вызов дойдет до самой вершины пирамиды прыжками к родителям каждого узла в отношении Это ещё не всё! В связи с циклами Быстрая сортировка На этом участнике нашего топа мы закончим разбирать примеры алгоритмов сортировки. Перед вами очередной алгоритм техники «разделяй и властвуй». Он выбирает один элемент массива в качестве стержня и сортирует остальные элементы вокруг (меньшие элементы налево, большие направо). Так соблюдается правильная позиция самого «стержня». Затем алгоритм рекурсивно повторяет сортировку для правой и левой частей. Реализация static int partition(int[] array, int begin, int end) { int pivot = end; int counter = begin; for (int i = begin; i < end; i++) { if (array[i] < array[pivot]) { int temp = array[counter]; array[counter] = array[i]; array[i] = temp; counter++; } } int temp = array[pivot]; array[pivot] = array[counter]; array[counter] = temp; return counter; } public static void quickSort(int[] array, int begin, int end) { if (end <= begin) return; int pivot = partition(array, begin, end); quickSort(array, begin, pivot-1); quickSort(array, pivot+1, end); } Временная сложность Временную сложность алгоритма быстрой сортировки можно описать следующим уравнением: В наихудшем сценарии наибольший и наименьший элементы всегда выбираются в качестве стержня. Тогда уравнение приобретает вид: Получается O(n^2). На фоне алгоритмов сортировки со сложностью O(nlog n), выглядит не очень На практике быстрая сортировка применяется широко. Судите сами: у алгоритма очень хорошее среднее время запуска, также равное O(nlog n), он эффективен для больших потоков ввода. И на этом преимущества не заканчиваются! Алгоритм не занимает дополнительного пространства, вся сортировка происходит «на месте», отсутствуют затратные вызовы распределения, из-за чего его часто предпочитают сортировке слиянием. Источник: stackabuse.com Комментарии: |
|