Задачи модуля "Сортировки": от пузырьковой сортировки до слияния
Задача 1: Устойчивость в базе данных
Условие: Представьте, что у вас есть таблица пользователей интернет-магазина. Изначально таблица отсортирована по "Общей сумме покупок" по убыванию (самые дорогие клиенты сверху). Теперь вам нужно отсортировать базу по "Городу" (в алфавитном порядке). Какую сортировку - вставкой или выборкой - вы бы применили, если хотите, чтобы внутри каждого города клиенты по-прежнему шли в порядке убывания суммы покупок?
Подсказка
Вспомните понятие "устойчивость" сортировки. Какая из этих двух сортировок не меняет относительный порядок равных элементов?
Разбор
Сортировка вставкой.
Нам требуется устойчивая сортировка, которая не переставляет равные по ключу элементы относительно друг друга (в нашем случае ключ - это название города, например, Москва и Казань). Сортировка вставкой является устойчивой, поэтому внутри этих городов клиенты останутся отсортированными по сумме покупок. Сортировка выборкой - неустойчивая, и нарушила бы изначальный порядок внутри групп (городов).
Задача 2: Восстановление кучи
Условие: В массиве лежит Max-Куча: [10, 8, 7, 4, 3]. Мы решили применить Пирамидальную сортировку. Опишите ровно один шаг сортировки (то есть извлечение максимума и одно просеивание). Как будет выглядеть массив после этого одного шага?
Подсказка
Сначала максимум (корень) меняется местами с последним элементом кучи, а затем новый корень просеивается вниз (sift down), чтобы восстановить свойство Max-Кучи.
Разбор
Ответ: [8, 4, 7, 3, 10] (при этом активная куча уменьшается на 1 элемент, а 10 остается на законном последнем месте).
- Шаг 1: Меняем корень
10с последним элементом3. Массив:[3, 8, 7, 4, 10]. (Куча теперь работает только с[3, 8, 7, 4]). - Шаг 2 (SiftDown): Новый корень
3нарушает свойства пирамиды. Его левый ребенок8, правый7. Максимум из детей -8. Меняем3и8местами. - Массив становится
[8, 3, 7, 4, 10]. - Шаг 3: Элемент
3(индекс 1) имеет ребенка4(индекс 3).4 > 3, меняем их местами. - Окончательный массив (для оставшейся кучи):
[8, 4, 7, 3, 10].
Задача 3. Пузырьковая сортировка с оптимизацией
Условие: Реализуйте алгоритм пузырьковой сортировки. Добавьте в него оптимизацию: если за весь проход по массиву не было совершено ни одного обмена элементов, алгоритм должен досрочно завершить работу, так как массив уже отсортирован.
Подсказка
Заведите логическую переменную (флаг), которую будете сбрасывать в начале каждого внешнего цикла и устанавливать при любом обмене элементов во внутреннем цикле.
Разбор
Обычная пузырьковая сортировка всегда выполняет O(N²) операций. Оптимизация с флагом позволяет улучшить время работы до O(N) в лучшем случае (когда массив уже отсортирован или почти отсортирован).
Если во внутреннем цикле элементы ни разу не поменялись местами, это значит, что каждый элемент меньше или равен следующему, то есть массив отсортирован.
Код на C
#include <stdio.h>
#include <stdbool.h>
void BubbleSortOptimized(int arr[], int n) {
bool swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j+1]) {
// Обмен элементов
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = true;
}
}
// Если обменов не было - массив отсортирован
if (!swapped) {
break;
}
}
}
Задача 4. Сортировка Хоара (Quick Sort)
Условие: Реализуйте алгоритм быстрой сортировки Хоара для массива. В качестве опорного элемента (pivot) всегда выбирайте последний элемент текущего подмассива. Напишите функцию разделения (partition) и саму рекурсивную функцию сортировки.
Подсказка 1
Функция partition должна перебрать все элементы подмассива и перенести те, что меньше опорного, в левую часть. Затем опорный элемент ставится между левой (меньшие) и правой (большие) частями.
Подсказка 2
Заведите указатель i (индекс меньшего элемента), который изначально равен low - 1. Пройдите циклом j от low до high - 1.
Подсказка 3
Внутри цикла, если arr[j] <= pivot, увеличивайте i и меняйте местами arr[i] и arr[j]. В конце не забудьте поменять arr[i+1] с arr[high] (опорным).
Разбор
Сортировка Хоара работает по принципу "разделяй и властвуй".
- Выбираем опорный элемент (pivot).
- Перемещаем элементы так, чтобы слева от pivot оказались элементы меньше него, а справа - больше.
- Рекурсивно сортируем левую и правую части.
Сложность: O(N log N) в среднем, но O(N²) в худшем случае (когда массив уже отсортирован, а мы берем последний элемент как pivot).
Код на C
#include <stdio.h>
void Swap(int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}
int Partition(int arr[], int low, int high) {
int pivot = arr[high]; // Выбираем последний элемент опорным
int i = (low - 1); // Индекс меньшего элемента
for (int j = low; j <= high - 1; j++) {
// Если текущий элемент меньше или равен pivot
if (arr[j] <= pivot) {
i++;
Swap(&arr[i], &arr[j]);
}
}
// Ставим pivot на правильное место
Swap(&arr[i + 1], &arr[high]);
return (i + 1);
}
void QuickSort(int arr[], int low, int high) {
if (low < high) {
// pi - индекс опорного элемента после разделения
int pi = Partition(arr, low, high);
// Рекурсивно сортируем элементы до и после разделения
QuickSort(arr, low, pi - 1);
QuickSort(arr, pi + 1, high);
}
}
Задача 5. Сортировка слиянием (Merge Sort)
Условие: Напишите рекурсивную сортировку слиянием. Массив нужно поделить на две половины, отсортировать их, а затем слить в один отсортированный массив. Слияние должно использовать дополнительный временный массив.
Подсказка 1
Для слияния заведите два указателя: один на начало левой половины, второй на начало правой. На каждом шаге выбирайте меньший элемент и копируйте его во временный массив.
Подсказка 2
После прохода может оказаться так, что элементы в одной из половин закончились, а во второй еще остались. Их нужно просто скопировать в хвост результирующего массива.
Подсказка 3
В языке C вы можете использовать malloc для создания временных массивов размера n1 = m - l + 1 и n2 = r - m. Не забудьте освободить их память с помощью free().
Разбор
Сортировка слиянием также использует "разделяй и властвуй", но в отличие от сортировки Хоара, деление всегда происходит ровно пополам.
Алгоритм стабильно работает за время O(N log N) как в лучшем, так и в худшем случае, что делает его предсказуемым. Однако, в отличие от алгоритмов, сортирующих "на месте", слияние требует дополнительной памяти O(N) для временного массива.
Код на C
#include <stdio.h>
#include <stdlib.h>
void Merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1;
int n2 = r - m;
int* L = (int*)malloc(n1 * sizeof(int));
int* R = (int*)malloc(n2 * sizeof(int));
// Копируем данные во временные массивы
for (int i = 0; i < n1; i++) L[i] = arr[l + i];
for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];
// Сливаем временные массивы обратно в arr
int i = 0, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// Копируем оставшиеся элементы L[]
while (i < n1) {
arr[k] = L[i];
i++; k++;
}
// Копируем оставшиеся элементы R[]
while (j < n2) {
arr[k] = R[j];
j++; k++;
}
free(L);
free(R);
}
void MergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
MergeSort(arr, l, m);
MergeSort(arr, m + 1, r);
Merge(arr, l, m, r);
}
}