Давайте узнаем, как реализовать мощный алгоритм сортировки на C, используя 2 стека и ограниченное количество движений.

Push_swap — первый алгоритмический проект Школы 42. Мы можем реализовать любой алгоритм для сортировки чисел, но лучшие получают более высокую оценку.
Я попробовал 3 разных алгоритма для решения проблемы push_swap: сортировка по основанию, быстрая сортировка и, наконец, самодельный алгоритм, который я покажу вам сегодня.
Этот алгоритм позволил мне получить высшую оценку за проект, и вы увидите, что его очень легко понять!
Понимание push_swap
Числа задаются в качестве аргументов программы на C, и мы должны преобразовать их с помощью функции atoi.
После этого мы должны сохранить эти числа, в идеале, в связанном списке, чтобы избежать освобождения и выделения массивов. Вот структура, которую я использовал для хранения своих чисел:
typedef struct node
{
int nb;
struct node *next;
struct node *prev;
} t_node;
- nb — целое число в нашем стеке.
- next — следующий элемент структуры в нашем двусвязном списке.
- prev — предыдущий элемент структуры в нашем двусвязном списке.
Для сортировки чисел мы можем использовать 2 стопки (стек A и стек B, как показано ниже). Связанный список представляет стек A, и мы можем создать другой связанный список для стека B и присвоить ему NULL, поскольку он пуст.
У нас также есть несколько доступных движений, и это делает проект немного сложнее! Эти движения учитываются, и окончательный счет определяет нашу оценку.
- sa: поменять местами первые два элемента стека A.
- sb: поменять местами первые два элемента стека B.
- ra: первый элемент стека A становится последним.
- rb: первый элемент стека B становится последним.
- rr: повернуть стек A и B (ra и rb).
- rra: последний элемент стека A становится первым.
- rrb: последний элемент стека B становится первым.
- rrr: повернуть A и B в обратном порядке (rra и rrb).
- pa: поместить в стек A.
- pb: поместить в стек B.

Каждое движение должно отображаться в stdout, а отсортированные числа должны храниться в стеке A в конце программы. Стек B — это просто временный стек, помогающий нам сортировать небольшие диапазоны чисел.
Сортировка 3 чисел
Небольшая часть кода посвящена обработке 3 чисел. Вы увидите, что это будет очень полезно для алгоритма, используемого для сортировки 5 чисел.
Вам нужно отсортировать только 3 числа, поэтому 3! = 3 * 2 * 1 = 6 возможных комбинаций. Это очень легко реализовать, и вы можете сделать это в одиночку, но не забывайте, что вы никогда не должны использовать для этого более 3 ходов.
Сортировка 5 номеров
Сортировка 5 чисел в порядке возрастания может быть немного сложнее, и я советую вам справиться с этим отдельно.
Обычный способ сделать это — поместить первые 2 числа в стек B, а затем повторно использовать написанный ранее код для сортировки 3 оставшихся чисел в стеке A. После этого все, что вам нужно сделать, отталкивает числа из стека B в стек A.

В зависимости от того, как вы это сделаете, вы можете превысить 12 ходов, что является максимумом, разрешенным 42 School для 5 номеров.
Не волнуйтесь, вы можете использовать мой тестер, чтобы убедиться, что вы не превысите 12 номеров. Он проверяет все комбинации из 5 чисел (5! = 120 комбинаций), так что у вас не будет сюрпризов во время оценки.
Алгоритм для больших стеков (100–500 чисел)
Чтобы отсортировать большое количество целых чисел с помощью моего метода, вам понадобится третий стек. Не волнуйтесь, это не сложно, и вы очень скоро поймете его назначение!
Этот стек, называемый stack C, не отображает никаких сообщений, и checker не знает об этом — это как внутренний связанный список, который мы будем использовать для определенного цель.
Push_swap — это не введение во временную сложность. Наоборот, это алгоритм, который должен максимально эффективно подстраиваться под требования субъекта: делать как можно меньше перемещений между стопками.
Зная это, наш стек C позволит нам впервые отсортировать наши числа, не отображая движения. Идея состоит в том, чтобы получить окончательную позицию каждого числа после их сортировки.
Для этого вам нужно клонировать числа A стека с помощью следующей функции. Обратите внимание, что тип стека C — int *, а не t_node *, потому что мы просто используем целые числа и не выполняем никакого перемещения.
int *stack_dup(t_node *stack)
{
int i;
t_node *node;
int *array;
i = 0;
node = stack;
array = malloc(sizeof(int) * stack_size(stack));
if (!array)
return (NULL);
while (node->next != stack)
{
array[i++] = node->nb;
node = node->next;
}
array[i] = node->nb;
return (array);
}
Эта функция подтверждает, что вы используете циклический двусвязный список, как и я. Не стесняйтесь редактировать его в соответствии с вашей структурой кода.
Получив массив целых чисел, вы можете реализовать простой алгоритм, такой как быстрая сортировка, для сортировки чисел. Вот моя реализация:
void swap(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int partition(int *array, int start, int end)
{
int pivot;
int i;
int j;
pivot = array[end];
i = start - 1;
j = start;
while (j <= end - 1)
{
if (array[j] < pivot)
{
i++;
swap(&array[i], &array[j]);
}
j++;
}
swap(&array[i + 1], &array[end]);
return (i + 1);
}
void quicksort(int *array, int start, int end)
{
int pivot_index;
if (start < end)
{
pivot_index = partition(array, start, end);
quicksort(array, start, pivot_index - 1);
quicksort(array, pivot_index + 1, end);
}
}
Теперь мы знаем точное положение каждого числа в итоговом связанном списке даже до того, как напечатаем одно движение. Благодаря стеку C (массиву целых чисел) теперь мы можем присвоить каждому элементу в стеке A целое число, представляющее его позицию в отсортированном массиве. Назовем его index.
typedef struct node
{
int nb;
int index; // Position in the sorted array.
struct node *next;
struct node *prev;
} t_node;
На данный момент нам просто нужно написать ядро алгоритма. Но перед этим позвольте мне объяснить вам, как это работает!
Зная, что у нас уже есть конечная позиция каждого элемента в стеке A, мы можем поместить все числа в стек B (с помощью pb). Таким образом, мы можем вычислить для каждого элемента в стеке B, сколько ходов потребуется, чтобы поместить его в нужное место в стеке A.

Как вы можете видеть на рисунке, сначала мы выберем 6, потому что это тот, который делает наименьшее количество движений для правильного размещения в стеке A . Теперь мы можем выполнить правильные движения, чтобы сделать ход 6 в хорошей позиции.
Нам просто нужно выполнить следующие шаги в цикле, пока все числа не будут сохранены обратно в стек A:
- Подсчитайте для каждого элемента в стеке B, сколько ходов потребуется, чтобы переместить его в отсортированную позицию в стеке A.
- Выберите, чтобы вернуть в стек A число, требующее наименьшего количества ходов.
- Повторяйте до тех пор, пока стек B не станет пустым.
Чтобы запомнить движения во время ваших вычислений, я советую вам добавить целые числа в вашу структуру, чтобы использовать их для нахождения лучшего узла для возврата в стек A, например, вот так:
typedef struct node
{
int nb;
int index;
struct node *next;
struct node *prev;
int value;
int sa;
int sb;
int ra;
int rb;
int rra;
int rrb;
} t_node;
В конце цикла случается так, что стек A отсортирован, но минимум находится не в первой позиции, как вы можете видеть на следующем изображении.

Чтобы решить эту проблему, вам нужно найти индекс минимального значения в стеке A, а затем выполнить правильное количество ra или rra. (используйте ra если индекс ‹ stack_a_size или используйте rra если индекс ≥ stack_a_size) для соблюдения допустимого возрастающего порядка.
Вы можете найти полный код моего алгоритма push_swap в моей учетной записи GitHub: https://github.com/julien-ctx/push-swap
Оптимизация push_swap для максимальной оценки
Вот еще несколько вещей, которые вы ДОЛЖНЫ сделать, чтобы повысить производительность вашего алгоритма и иметь 100/100:
- При выборе наилучшего числа для возврата в стек A, всякий раз, когда у вас одновременно есть ra и rb или rra и rrb одновременно, не забывайте использовать rr или ррр. Они помогут вам получить максимальную оценку в проекте push_swap.
- Вместо того, чтобы помещать все в стек B в начале программы, вы можете найти самую длинную последовательность чисел, которая уже отсортирована в том порядке, в котором они будут отсортированы в конце. Вы должны сохранять отсортированную последовательность только в том случае, если она имеет тот же порядок в стеке C, а не числа, которые расположены только в порядке возрастания!
Тестирование вашего алгоритма сортировки
Как и во всех алгоритмических проектах, знать, работает ли ваша программа, может быть непростой задачей. Таким образом, вы можете использовать тестер push_swap, чтобы убедиться, что ваша программа работает.
Этот тестер был сделан мной и позволяет вам проверить как ваш парсинг, так и ваш алгоритм на нескольких последовательностях чисел (3, 4, 5, 100, 500 чисел). Целые числа генерируются случайным образом, чтобы избежать какой-либо систематической ошибки в тестах.
Вы можете использовать простую команду Bash для запуска тестера:
- Для Mac:
git clone [email protected]:julien-ctx/push-swap-tester.git && mv push-swap-tester/tester.py . && mv push-swap-tester/checker_Mac . && rm -rf push-swap-tester && chmod 777 checker_Mac && make && python3 tester.py
- Для Linux:
git clone [email protected]:julien-ctx/push-swap-tester.git && mv push-swap-tester/tester.py . && mv push-swap-tester/checker_linux . && rm -rf push-swap-tester && chmod 777 checker_linux && sed -i -- 's/checker_Mac/checker_linux/g' tester.py && make && python3 tester.py
Понравилась ли вам эта статья? Вы можете сообщить мне об этом разными способами:
- Похлопайте статье.
- Напишите, что вы думаете о моей истории.
- Подписаться на мой аккаунт.
Вы также можете подписаться на мою рассылку, чтобы получать мои последние сообщения в свой почтовый ящик.