Методы сортировки данных. Алгоритмы поиска и сортировки | OTUS
Скидка до 15% на курсы декабря и января
❄️ До 28.12 Забрать скидку! →
Выбрать курс

Методы сортировки данных. Алгоритмы поиска и сортировки

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

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

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

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

Что же, теперь можно приступать и к рассмотрению алгоритмов сортировки. Начнём с пузырьковой.

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

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

Представим, что у нас есть массив целых чисел:

1-20219-7a560c.jpg

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

2-20219-2d35ce.jpg

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

3-20219-664e30.jpg

Однако пока обмен происходит, сортировка продолжается, в результате чего перемещается 6:

4-20219-d810d3.jpg

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

public void Sort(T[] items)
{
    bool swapped;

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

Сортировка данных вставками

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

5-20219-3a5c0c.jpg

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

Приведём пример. Вот неотсортированный массив:

6-20219-f9b31e.jpg

Алгоритм сортировки начнёт работать с индекса «ноль» и значения «три». Т. к. это 1-й индекс, массив до него включительно будет считаться уже отсортированным.

Потом перейдём к семёрке. Семь больше любого значения в отсортированной части, значит, осуществляется переход к последующему элементу. Отметим, что на прошедшем этапе были отсортированы компоненты с индексами 0..1, про компоненты с индексами 2..n мы пока ничего не знаем.

Теперь алгоритм проверяет четвёрку. Четыре меньше, чем 7, поэтому переносится на другую, более правильную позицию, которая находится в отсортированной части массива. Позиция определяется методом FindInsertionIndex. Метод сравнивает переданное значение (в нашем случае это 4) с каждым значением из отсортированной части и так до тех пор, пока не будет найдено место для вставки.

Так для вставки был определён индекс 1. Вставка осуществляется методом Insert. Вставляемое значение удаляется из массива, все остальные цифры сдвигаются вправо, начиная с индекса для вставки. Вот как стал выглядеть массив после сортировки:

7-20219-c78b95.jpg

Итог работы алгоритма сортировки вставками очевиден:

8-20219-363db1.jpg

public void Sort(T[] items)
{
    int sortedRangeEndIndex = 1;

    while (sortedRangeEndIndex < items.Length)
    {
        if (items[sortedRangeEndIndex].CompareTo(items[sortedRangeEndIndex - 1]) < 0)
        {
            int insertIndex = FindInsertionIndex(items, items[sortedRangeEndIndex]);
            Insert(items, insertIndex, sortedRangeEndIndex);
        }

        sortedRangeEndIndex++;
    }
}

private int FindInsertionIndex(T[] items, T valueToInsert)
{
    for (int index = 0; index < items.Length; index++) {
        if (items[index].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[indexInsertingAt];

    // Шаг 2.

    itemArray[indexInsertingAt] = itemArray[indexInsertingFrom];

    // Шаг 3.
    for (int current = indexInsertingFrom; current > indexInsertingAt; current--)
    {
        itemArray[current] = itemArray[current - 1];
    }

    // Шаг 4.
    itemArray[indexInsertingAt + 1] = temp;
}

Сортировка данных выбором

Сортировка выбором — некий гибрид между сортировкой вставками и пузырьковой сортировкой. Давайте посмотрим, как работает эта сортировка на нашем массиве:

9-20219-46eae6.jpg

Во время первого же прохода алгоритм посредством метода FindIndexOfSmallestFromIndex пробует найти самое меньшее значение для перемещения его в начало массива.

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

10-20219-87f8d8.jpg

И ещё после 2-х проходов алгоритм сортировки завершит работу:

11-20219-673c99.jpg

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[sortedRangeEnd];
    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» («разделяй и властвуй»). Она работает путём разделения крупной задачи на более мелкие, которые решаются проще.

Отвлечёмся на минутку

Представьте, что вы работаете на крыше или стройплощадке, и у вас повредился электрокабель, от которого запитывается важный инструмент. Строительные и кровельные кабели очень длинные и часто достигают 100 и более метров. Вам нужно срочно окончить работу, но у вас нет средств диагностики, чтобы починить провод. Неопытные работники просто прекращают все действия, доложив начальству. Мастера-сдельщики режут кабель пополам, получая в 99 % случаев 50 метров работающего провода. Если нужно, они режут пополам и неработающую часть, что позволяет либо достаточно быстро найти место внешнего повреждения, внимательно изучив четверть кабеля (25 м), либо получить в итоге 75 метров, которых хватит для выполнения большинства строительных задач. Всё, что потребуется, — нож и моток изоленты.

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

Итак, наш массив:

12-20219-d1ff2e.jpg

Он делится наполовину:

13-20219-449466.jpg

И потом опять, и опять наполовину:

14-20219-57b1de.jpg

Потом сливание/соединение в верном порядке:

15-20219-1f05fd.jpg

И результат:

16-20219-61f3da.jpg

Алгоритм работает путём реализации следующих операций: 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[leftSize];
    T[] right = new T[rightSize];
    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[targetIndex] = right[rightIndex++];
        }
        else if (rightIndex >= right.Length)
        {
            items[targetIndex] = left[leftIndex++];
        }
        else if (left[leftIndex].CompareTo(right[rightIndex]) < 0)
        {
            items[targetIndex] = left[leftIndex++];
        }
        else
        {
            items[targetIndex] = right[rightIndex++];
        }

        targetIndex++;
        remaining--;
    }
}

Быстрая сортировка данных

Быстрая сортировка тоже представляет собой алгоритм, отвечающий типу «разделяй и властвуй». Сортировка данных происходит рекурсивно и поэтапно: 1. Выбирается ключевой индекс, массив делится по нему на 2 части. Выбор осуществляется разными способами, мы же возьмём случайное число. 2. Все элементы, которые больше ключевого, перемещаются в правую часть массива, которые меньше — в левую. Теперь ключевой элемент больше любого значения слева и меньше любого значения справа. 3. Первые два шага повторяются до полной сортировки массива.

Смотрим на работу алгоритма:

17-20219-f18cc8.jpg

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

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

18-20219-fd215f.jpg

И переносим значения справа от ключевого индекса, располагая их в верном положении (используем метод partition).

19-20219-c4d883.jpg

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

20-20219-c10add.jpg

И снова быстрая сортировка:

21-20219-bea468.jpg

И опять:

22-20219-b5b82e.jpg

В итоге работа алгоритма завершается.

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[pivotIndex];

    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;
}

Статья подготовлена специально для OTUS по материалам «Sorting Algorithms».

Хотите знать больше? Записывайтесь на курс "Алгоритмы для разработчиков"!

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто
Новогодние скидки в Otus!-15% ❄️
Успейте забрать свою скидку до 28.12 →