Задача сортировки массива заключается в том, чтобы расставить его элементы в определённом порядке (чаще всего — по неубыванию: каждый элемент должен быть больше или равен предыдущему).
Будет полезно вместе с описанем алгоритмов смотреть их визуализацию.
Наш первый подход будет заключаться в следующем: обозначим за
Как каждую итерацию максимальный элемент «всплывает» словно пузырек к концу массива — отсюда и название.
<int>& array) {
void bubble_sort(vectorint n = (int) array.size();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - 1; j++) {
// сравниваем элемент со следующим
// и меняем местами, если следующий меньше
if (array[j] > arr[j + 1]) {
+ 1]);
swap(array[j], array[j
}
}
}
}
<int> a = {1, -3, 7, 88, 7};
vector;
bubble_sort(a)for (auto elem : a) {
<< elem << " ";
cout
}// -3 1 7 7 88
После
Упражнение. Алгоритм можно немного ускорить. Подумайте, какие лишние элементы мы перебираем. Как нужно изменить границы в двух циклах for
, чтобы не делать никаких бесполезных действий?
Другим способом является сортировка выбором минимума (или максимума).
Чтобы отсортировать массив, просто
Содержательная часть будет выглядеть так:
for (i = 0; i < n - 1; i++) {
for (j = i + 1; j < n; j++) {
if (a[i] > a[j]) {
(a[j], a[i]);
swap}
}
}
Определение. Префиксом длины
Тогда пусть на
for (int i = 1; i < n; i++) {
for (int j = i; j > 0; j--) {
if (a[j - 1] < a[j]) {
break;
}
swap(a[j], a[j - 1]);
}
}
Предыдущие три алгоритма работали с массивами, в которых лежат абсолютно любые объекты, которые можно сравнивать: любые числа, строки, пары, другие массивы — почти все что угодно.
Но особых случаях, когда элементы принадлежат какому-то маленькому множеству, можно использовать другой алгоритм — сортировку подсчетом.
Пусть, например, нам гарантируется, что все числа натуральные и лежат в промежутке от
Создадим массив размера
Пройдемся по всем числам исходного массива и увеличим соответствующее значение массива на
После того, как мы посчитали, сколько раз каждое число встретилось, можно просто пройтись по этому массиву и вывести
Время работы такого алгоритма составляет
Часто требуется оценить, сколько времени работает алгоритм. Но тут возникают проблемы:
Зачастую основной задачей программиста становится оптимизировать алгоритм, выполнение которого займёт тысячи лет, до какого-нибудь адекватного времени работы. Поэтому хотелось бы уметь предсказывать, сколько времени займёт выполнение алгоритма ещё до того, как мы его запустим.
Для этого сначала попробуем оценить число операций в алгоритме. Возникает вопрос: какие именно операции считать. Как один из вариантов — учитывать любые элементарные операции:
+, -, *, /
<, >, <=, >=, ==, !=
a[0] = 3
При этом надо учитывать, как реализованы некоторые отдельные вещи в самом языке. Например, в питоне срезы массива (array[3:10]
) копируют этот массив, то есть этот срез работает за 7 элементарных действий. А swap
, например, может работать за 3 присваивания.
Упражнение. Попробуйте посчитать точное число сравнений и присваиваний в сортировках пузырьком, выбором, вставками и подсчетом в худшем случае (это должна быть какая формула, зависящая от
Чтобы учесть вообще все элементарные операции, ещё надо посчитать, например, сколько раз прибавилась единичка внутри цикла for
. А ещё, например, строчка n = len(array)
— это тоже действие. Поэтому даже посчитав их, сразу очевидно, какой из этих алгоритмов работает быстрее — сравнивать формулы сложно. Хочется придумать способ упростить эти формулы так, чтобы:
Для этого придумали
Определение. Пусть
В таких обозначениях можно сказать, что
Это обозначение удобно тем, что оно короткое и понятное, а также оно не зависит от умножения на константу или прибавления константы. Например, если алгоритм работает за
Все рассуждения про то, сколько операций в swap
или считать ли отдельно присваивания, сравнения и циклы — отпадают. Каков бы ни был ответ на эти вопросы, они меняют ответ лишь на константу, а значит асимптотическое время работы алгоритма никак не меняется.
Первые три сортировки именно поэтому называют квадратичными — они работают за
Упражнение. Найдите асимптотику данных функций, маскимально упростив ответ (например, до
Упражнение. Найдите асимптотическое время работы данных функций:
def f(n):
= 0
s for i in range(n):
for j in range(n):
+= i * j
s return s
def g(n):
= 0
s for i in range(n):
+= i
s for i in range(n):
+= i * i
s return s
def h(n):
if n == 0:
return 1
return h(n - 1) * n
Упражнение. Найдите лучшее время работы алгоритмов, решающих данные задачи:
Сортировка очень часто применяется как часть решения олимпиадных задач. В таких случаях обычно не пишут её заново, а используют встроенную.
Пример на Python:
= [1, 5, 10, 5, -4]
a a.sort()
Пример на C++:
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
<int> v = {1, 5, 10, 5, -4};
vector(v.begin(), v.end());
sort}
В разных языках она может быть реализована по-разному, но везде она работает за
Найти количество пар элементов
и в отсортированном массиве, такие что .
Наивное решение: бинарный поиск. Будем считать, что массив уже отсортирован. Для каждого элемента
А можно ли быстрее?
Да, давайте перебирать два указателя — два индекса
int second = 0, ans = 0;
for (int first = 0; first < n; ++first) {
while (second != n && a[second] - a[first] <= r) {
++;
second}
+= n - second;
ans }
За сколько же работает это решение? С виду может показаться, что за
Это называется метод двух указателей — так как мы двигаем два указателя first и second одновременно слева направо по каким-то правилам. Обычно его используют на одном отсортированном массиве.
Давайте разберем еще примеры.
Еще пример двух указателей на нескольких массивах.
Пусть у нас есть два отсортированных по неубыванию массива размера
Пусть первый указатель будет указывать на начало первого массива, а второй, соответственно, на начало второго. Из двух текущих элементов, на которые указывают указатели, выберем наименьший и положим на соответствующую позицию в новом массиве, после чего сдвинем указатель. Продолжим этот процесс пока в обоих массивах не закончатся элементы. Тогда код будет выглядеть следующим образом:
int a[n + 1], b[m + 1], res[n + m];
[n] = INF; // Создаем в конце массива фиктивный элемент, который заведомо больше остальных
a[m] = INF; // Чтобы избежать лишних случаев
b
for (int i = 0; i < n; ++i) {
>> a[i];
cin }
for (int j = 0; j < m; ++j) {
>> a[j];
cin }
int i = 0, j = 0;
for (int k = 0; k < n + m; ++k) {
if (a[i] < b[j]) {
[k] = a[i];
res++;
i} else {
[k] = b[j];
res++;
j}
}
Итоговая асимптотика:
Давайте подробно опишем как использовать операцию слияния для сортировки за
Пусть у нас есть какой-то массив.
int a[8] = {7, 2, 5, 6, 1, 3, 4, 8};
Сделаем такое предположение. Пусть мы уже умеем как-то сортировать массив размера
// (7 2 5 6 1 3 4 8)
// (7 2 5 6)(1 3 4 8)
// (7 2)(5 6)(1 3)(4 8)
// (2 7)(5 6)(1 3)(4 8)
// (2 5 6 7)(1 3 4 8)
// (1 2 3 4 5 6 7 8)
#include // Воспользуемся встроенной функцией merge
void merge_sort(vector &v, int l, int r) { // v - вектор, который хотим отсортировать
if (r - l == 1) { // l и r - полуинтервал, который хотим отсортировать
return;
}
int mid = (l + r) / 2;
(v, l, mid);
merge_sort(v, mid, r);
merge_sort(r - l); // временный вектор
vector temp(v.begin() + l, v.begin() + mid, v.begin() + mid, v.begin() + r, c.begin());
mergefor (int i = 0; i < r - l; ++i) {
[i + l] = temp[i];
v}
return;
}
Так сколько же работает это решение?
Пускай
Пусть у нас есть некоторая перестановка
Найти количество инверсий в данной перестановке.
Очевидно, что эта задача легко решается обычным перебором двух индексов за
int a[n], ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (a[i] > a[j]) {
++;
ans}
}
}
<< ans << endl; cout
Внезапно эту задачу можно решить используя сортировку слиянием, слегка модифицируя её. Оставим ту же идею. Пусть мы умеем находить количество инверсий в массиве размера
Заметим, что мы уже знаем количество инверсий в левой половине и в правой половине массива. Осталось лишь посчитать число инверсий, где одно число лежит в левой половине, а второе в правой половине. Как же их посчитать?
Давайте подробнее рассмотрим операцию merge левой и правой половины (которую мы ранее заменили на вызов встроенной функции merge). Первый указатель указывает на элемент левой половины, второй указатель указывает на элемент второй половины, мы смотрим на минимум из них и этот указатель вдигаем вправо.
Рассмотрим число
Значит в тот момент, когда мы добавляем число
Быстрая сортировка заключается в том, что на каждом шаге мы находим опорный элемент, все элементы, которые меньше его кидаем в левую часть, остальные в правую, а затем рекурсивно спускаемся в обе части.
void quicksort(int l, int r){
if (l < r){
int index = (l + r) / 2; /* index - индекс опорного элемента для
начала сделаем его равным середине отрезка*/
= divide(l, r, index); /* divide - функция разбивающие элементы
index на меньшие и больше/равные a[index],
при этом функция возвращает границу разбиения*/
(l, index);
quicksort(index + 1, r);
quicksort}
}
Давайте оценим асимптотику данной сортировки. На случайных данных она работает за
Существуют несколько выходов из этой ситуации :
Давайте если быстрая сортировка работает долго, то запустим любую другую сортировку за
Давайте делить массив не на две, а на три части(меньше, равны, больше).
Чтобы избавиться от проблемы с максимумом/минимумом в середине, давайте брать случайный элемент.
Пусть дан массив
Давайте поймем, что в быстрой сортировке мы можем узнать, сколько элементов меньше данного, тогда рассмотрим три случая
количество чисел, меньше данного =
количество чисел, меньше данного >=
количество чисел, меньше данного <
За сколько же это работает, из быстрой сортировки мы имеем, что размер убывает приблизительно в 2 раза, то есть мы имеем сумму
Также в C++ эта функция уже реализована и называется nth_element
.