Полный гайд по алгоритмам сортировки на Java для новичков ч. 2 | robot_dreams
Для отслеживания статуса заказа — авторизируйтесь
Введите код, который был выслан на почту Введите код с SMS, который был выслан на номер
 
Код действителен в течение 5 минут Код с sms действителен в течение 5 минут
Вы уверены, что хотите выйти?
Сеанс завершен
На главную
Полный гайд по алгоритмам сортировки на Java для новичков ч. 2

Полный гайд по алгоритмам сортировки на Java для новичков ч. 2

Часть 2. Сортируем методом Шелла, подсчетом и быстрым способом

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

Эффективность алгоритмов сортировки

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

O(n log n), где n — количество входных данных:

  • быстрая сортировка Java;
  • сортировка Шелла на Java.

O(n + K), где n — количество элементов во входном массиве, а K — диапазон входных данных:

  • сортировка подсчетом Java.

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

Быстрая сортировка — это алгоритм сортировки, основанный на стратегии «разделяй и властвуй».

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

Алгоритм быстрой сортировки Java

Основной процесс в быстрой сортировке — разбиение (partition). Он помещает опорный элемент (pivot) в соответствующую позицию в отсортированном массиве, перемещая все элементы, которые меньше него, влево, а которые больше него — вправо.

Функция partition() вызывается рекурсивно с каждой стороны от опорного элемента после того, как он помещается в надлежащее положение. В результате получаем отсортированный массив.

Есть несколько вариантов выбора опорного элемента:

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

Логика проста: начинаем с крайнего левого элемента и храним индекс меньшего или равного элемента в переменной i. Если при проходе находится меньший элемент, мы меняем местами текущий элемент с элементом array[i]. В противном случае — игнорируем текущий элемент.

Пример: быстрая сортировка Java

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

package sorting;


public class QuickSort implements SortingMethod {


    // Swaps two elements
    void swap(int[] array, int i, int j)
    {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }


    /*  Takes last element as pivot,
        moves the pivot element to the correct position,
        and places all smaller to the left
        all greater elements to the right
    */
    int partition(int[] array, int left, int right)
    {
        // Choose the pivot element
        int pivot = array[right];


        // The correct position of the pivot found so far
        int i = (left - 1);


        for (int j = left; j <= right - 1; j++) {


            // If current element is smaller than the pivot
            if (array[j] < pivot) {


                // Swap two elements
                i++;
                swap(array, i, j);
            }
        }
        swap(array, i + 1, right);
        return (i + 1);
    }


    // Partitions the array and sorts its parts
    void quickSort(int[] array, int left, int right)
    {
        if (left < right) {


            // Move the pivot to the correct position
            int partitioningIndex = partition(array, left, right);


            // Sort elements before
            quickSort(array, left, partitioningIndex - 1);
            // Sort elements after
            quickSort(array, partitioningIndex + 1, right);
        }
    }


    public void sort(int[] array){
        quickSort(array, 0, array.length - 1);
    }
}

Характеристики быстрой сортировки

Временная сложность в наилучшем случае: O (n log n), в среднем: O (n log n)

В наихудшем случае: O(n2)

Дополнительное пространство: O(1). Но этот показатель актуален только если не учитывать пространство для стека рекурсии. Если же учитывать, то в наихудшем случае сортировка выполнит O(n) вложенных рекурсивных вызовов и для нее потребуется O(n) дополнительного места.

Среди преимуществ быстрой сортировки:

  • благодаря стратегии «разделяй и властвуй» этот метод упрощает решение проблем;
  • способ эффективен для больших наборов данных;
  • требует немного памяти для работы.

Недостатки быстрой сортировки:

  • В худшем случае временная сложность составляет O(n2). Это происходит, когда выбран неудачный опорный элемент.
  • Этот алгоритм малоэффективен для небольших наборов данных.
  • Это нестабильный метод сортировки. То есть, если есть два элемента с одним и тем же ключом, их порядок относительно друг друга не будет соблюден. Так происходит потому, что мы меняем местами элементы относительно позиции опорного элемента и не учитываем их исходные позиции.

Сортировка подсчетом Java

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

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

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

Алгоритм сортировки подсчетом Java

Для простоты рассмотрим массив с семью значениями в диапазоне от 0 до 5. Входной массив: [1, 4, 1, 2, 3, 5, 2]

Найдем максимальное значение в исходном массиве (max). В нашем случае оно равно 5.

Создадим вспомогательный массив count длиной max+1 для хранения количества вхождений уникальных элементов и инициализируем его нулями.

Количество: 0, 0, 0, 0, 0, 0

(значения): 0, 1, 2, 3, 4, 5

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

Количество: 0, 2, 2, 1, 1, 1

(значения): 0, 1, 2, 3, 4, 5

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

Количество: 0, 2, 4, 5, 6, 7

(значения): 0, 1, 2, 3, 4, 5

Создадим целевой массив и инициализируем его нулями.

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

Пример: сортировка подсчетом Java

package sorting;


// Java class implementing Counting Sort
class CountingSort implements SortingMethod {


  public void sort(int array[]) {
    int size = array.length;
    int[] output = new int[size + 1];


    // Find the maximum value in the array
    int max = array[0];
    for (int i = 1; i < size; i++) {
      if (array[i] > max)
        max = array[i];
    }


    // Create count array and fill it with zeros
    int[] count = new int[max + 1];
    for (int i = 0; i < max; ++i) {
      count[i] = 0;
    }


    // Find and store the count of each element
    for (int i = 0; i < size; i++) {
      count[array[i]]++;
    }


    // Calculate and store cummulative sum of elements of count array
    for (int i = 1; i <= max; i++) {
      count[i] += count[i - 1];
    }


    // Traverse the initial array from the end and place the elements
    // in output array into their respective positions
    for (int i = size - 1; i >= 0; i--) {
      output[count[array[i]] - 1] = array[i];
      count[array[i]]--;
    }


    // Copy the sorted elements into the initial array
    for (int i = 0; i < size; i++) {
      array[i] = output[i];
    }
  }
}

Характеристики сортировки подсчетом

Временная сложность: O(n + K)

Дополнительное пространство: O(n + K)

Также у сортировки подсчетом есть ряд особенностей:

  • В этом методе используются некоторые допущения насчет данных. Например, предполагается, что данные будут находиться в диапазоне от 0 до 5 или от 10 до 99 и так далее. Также предполагается, что входные данные будут положительными целыми числами. При этом алгоритм можно расширить для обработки отрицательных чисел.
  • Алгоритм основан не на сравнении значений. Он хэширует значения во вспомогательном массиве и использует их для сортировки.
  • Метод использует временный массив, потому сортировка производится не на месте.
  • Сортировка подсчетом эффективна, если диапазон входных данных ненамного превышает количество элементов, которые требуется отсортировать. А если задан диапазон от 1 до 10 тыс., а данные — 7, 5010, 258, 10 000 и 3 000, то он будет крайне неэффективным.
  • Часто этот метод сортировки используется как подпрограмма для других алгоритмов, например, для сортировки по разрядам (radix sort).

Сортировка подсчетом обычно работает быстрее, чем все алгоритмы, основанные на сравнении (например, сортировка слиянием и быстрая сортировка), если диапазон входных значений того же порядка, что и их количество. Этот метод стабильный и его легко программировать. Но недостатки тоже есть:

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

Сортировка Шелла на Java

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

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

Алгоритм сортировки Шелла

  • Инициализируем значение размера части (h).
  • Делим массив на меньшие части с одинаковым расстоянием до h.
  • Сортируем эти части с помощью сортировки вставкой.
  • Повторяем шаг 1, пока список не будет отсортирован.

Пример: сортировка Шелла Java

package sorting;


class ShellSort implements SortingMethod {
    public void sort(int array[]) 
    { 
        int n = array.length; 


        // Start with a maximum gap, then reduce it
        for (int gap = n/2; gap > 0; gap /= 2)
        { 
            // Apply insertion sort to the current gap size. 
            for (int i = gap; i < n; i += 1) 
            {
                int temp = array[i];


                // shift earlier sorted elements until 
                // the correct position for a[i] is found 
                int j; 
                for (j = i; j >= gap && array[j - gap] > temp; j -= gap)
                {
                    array[j] = array[j - gap]; 
                }


                // put the original a[i] stored in temp in its correct position
                array[j] = temp; 
            } 
        } 
    }
}

Характеристики сортировки Шелла

Временная сложность приведенной выше реализации сортировки Шелла: O(n2). В этом алгоритме используется исходная последовательность Шелла, в которой на каждой итерации интервал уменьшается наполовину. Существует много других способов уменьшения интервала, которые позволяют повысить производительность алгоритма.

ССложность сортировки Шелла в худшем случае составляет O(n2). Если исходный массив уже отсортирован, то общее число сравнений равно размеру данного массива. Поэтому сложность в лучшем случае: O(n log n). Сложность сортировки Шелла в среднем случае: O(n log n).

Независимо от последовательности интервалов, пространственная сложность сортировки Шелла составляет О(1).

Код для демонстрации времени сортировки

Создайте папку sorting и переместите туда файлы с кодом всех рассмотренных выше классов (например, код класса ShellSort — в файл ShellSort.java и так далее).

Создайте в этой же папке файл SortingMethod.java с таким кодом:

package sorting;
// Java class declaring SortingMethod interface
interface SortingMethod {
        // Sorts an array
        public void sort(int array[]);
}

В той же папке создайте файл SortingDemo.java с таким кодом:

package sorting;
// Java class demonstrating different sorting methods
public class SortingDemo {
        // Prints an array
    static void printArray(int array[])
    {
        int n = array.length;
        for (int i = 0; i < n; ++i)
            System.out.print(array[i] + " ");
        System.out.println();
    } 
    // Main method
    public static void main(String args[])
    {
        System.out.println();
        System.out.println("* Array sorting demo *");
        System.out.println();
        // Declare the array of sorting method instances
        SortingMethod sortingMethods[] = {
            new QuickSort(),
            new CountingSort(),
            new ShellSort()
        };
        // Demonstrate that methods work
        for (int i = 0; i < sortingMethods.length; i++){
            System.out.println(sortingMethods[i].getClass().toString());
            // Declare the initial array
            int array[] = { 9, 8, 10, 3, 4 };
            // Show initial array
            System.out.print("Initial array: ");
            printArray(array);
            // Store startTime
            double startTime = System.nanoTime();
            // Sort the array and show its new state
            sortingMethods[i].sort(array);
            // Measure method execution time
            double endTime = System.nanoTime();
            double duration = (endTime - startTime) / 1000000;
            System.out.print("Resulting array: ");
            printArray(array);
            System.out.println("Execution time, ms: " + duration);
            System.out.println();
        }      
    }
}

Для компиляции откройте командную строку и перейдите в каталог, в котором находится папка sorting (в ее родительский каталог).

Для компиляции всех файлов запустите команду:

javac sorting/*.java

И, наконец, запустите демо:

java sorting.SortingDemo

Результат:

Ещё статьи
Экспертки о том, как оценивают кандидатов на нетехнических интервью
Часть 2. Работа с записями: вставка, чтение, изменение и удаление