В отличии от квадратичных сортировок, сортировка слиянием (англ. merge sort) созволяет сортировать массив за время $O(n \log n)$. Главное в этой сортировке - её простота в понимании и реализции.

Merge

Для начала разберем простой вспомогательный алгоритм, который назовём merge. Он принимает два ОТСОРТИРОВАННЫХ массива и выдает отсортированный массив, состоящий из элементов обоих массивов. Работу функции лучше всего описать гифкой:

Merge function

Как вы видите, если обозначить за $n$ размер первого массива, а за $m$ размер второго массива, то алгоритм работает за $O(n + m)$.

Прокомментирую, как работает данный алгоритм:

  • Поддерживаем два указателя на текущий элемент в каждом массиве - $i$ и $j$.
  • Сравниваем два текущих элемента и выписываем в ответ меньший из них
  • Сдвигаем соответствующий указатель
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> merge(const vector<int>& a, const vector<int>& b) {
    int n = a.size(), m = b.size();
    vector<int> res(n + m);
    for (int i = 0, j = 0; i < n && j < m; ) {
        if (i < n && j < m && a[i] < b[j] || j == m) {
            res[i + j] = a[i];
            ++i;
        } else {
            res[i + j] = b[j];
            ++j;
        }
    }
    return c;
}

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

Merge sort

Теперь воспользуемся техникой “Разделяй и властвуй” (англ. Divide and Conqueror). Что же мы будем разделять и как нам над этим властвовать?

  • Divide. Разделим массив на две равные части. Отсортируем каждую из частей по-отдельности рекурсивно
  • Conqueror. Теперь, когда каждая из частей массива отсортированная, мы можем их склеить с помощью функции merge.

Merge sort

Напишем код и воспользуемся встроенной функцией merge и copy:

1
2
3
4
5
6
7
8
9
10
11
12
void sort(vector<int>& a, int l, int r) { // сортируем интервал [l; r)
    if (r - l == 1) {
        return; // массив из одного элемента отсортирован
    } else {
        int m = (r + l) / 2;
        sort(a, l, m);
        sort(a, m, r);
        vector<int> res(r - l);
        merge(a.begin() + l, a.begin() + m, a.begin() + m, a.begin() + r, res.begin());
        copy(res.begin(), res.end(), a.begin() + l);
    }
}

Обратите внимание! Вы не можете класть результат сразу в a.begin() + l. В этом случае вы можете затереть значения левого массива! Поэтому я создаю временный массив и только потом из него копирую элементы в исходный.

Время работы

Merge sort

Картинка сверху наглядно демонстрирует, что суммарное время работы - $O(n \log n)$. Почему? Потому что всего у нас есть $\log n$ уровней, на каждом из которых мы делаем $O(n)$ операций суммарно в процессе merge. Таким образом, суммарное время работы - $O(n \log n)$.

Упражнения

Инверсией в массиве называется пара (i, j), для которой верно, что $i < j$ и $a_i > a_j$. Посчитайте количество инверсий за время $O(n \log n)$.