Timsort
Timsort
Timsort — гибридный алгоритм сортировки, сочетающий сортировку вставками и сортировку слиянием.
Данный алгоритм был изобретен в 2002 году Тимом Петерсом(в честь него и назван) и основывается на том, что в реальном мире сортируемые массивы данных часто содержат в себе упорядоченные подмассивы. На таким данных алгоритм Тима Петерса существенно быстрее многих других алгоритмов сортировки. В настоящее время Timsort является стандартной сортировкой в Python, OpenJDK 7 и реализован в Android JDK 1.5.
Основная идея алгоритма
Алгоритм Timsort состоит из нескольких шагов:
- Шаг №1: по специальному алгоритму входной массив разделяется на подмассивы.
- Шаг №2: каждый подмассив сортируется сортировкой вставками или сортировкой выбором.
- Шаг №3: отсортированные подмассивы собираются в единый массив с помощью модифицированной сортировки слиянием.
Алгоритм
Используемые понятия и комментарии
- — размер входного массива.
-
- строго по убыванию .
- нестрого по возрастанию .
— некоторый подмассив во входном массиве, который обязан быть упорядоченным одним из двух способов:
- — минимальный размер подмассива, описанного в предыдущем пункте.
Шаг №1. Вычисление minrun
Число
определяется на основе , исходя из принципов:- Не должно быть слишком большим, поскольку к подмассиву размера будет в дальнейшем применена сортировка вставками (эффективна только на небольших массивах).
- Оно не должно быть слишком маленьким, так как чем меньше подмассив — тем больше итераций слияния подмассивов придётся выполнить на последнем шаге алгоритма. Оптимальная величина для — степень двойки. Это требование обусловлено тем, что алгоритм слияния подмассивов наиболее эффективно работает на подмассивах примерно равного размера.
- Согласно авторским экспериментам:
- При нарушается пункт .
- При нарушается пункт .
- Наиболее эффективные значения из диапозона .
- Исключение — если , тогда и Timsort превращается в сортировку вставками.
Таким образом, алгоритм расчета
не так уж сложен: берем старшие 6 бит числа и добавляем единицу, если в оставшихся младших битах есть хотя бы один ненулевой.int GetMinrun(int n) { int flag = 0; /* станет 1 если среди сдвинутых битов есть хотя бы 1 ненулевой */ while (n >= 64) { flag |= n & 1; n >>= 1; } return n + flag; }
Шаг №2. Алгоритм разбиения на подмассивы и их сортировка
На данном этапе у нас есть входной массив, его размер и вычисленное число . Обратим внимание, что если данные изначального массива достаточно близки к случайным, то размер упорядоченных подмассивов близок к ,. Но если в изначальных данных были упорядоченные диапазоны, то упорядоченные подмассивы могут иметь размер, превышающий .- Начало.
- Указатель текущего элемента ставится в начало входного массива.
- Начиная с текущего элемента, идет поиск во входном массиве упорядоченного подмассива . По определению, в однозначно войдет текущий элемент и следующий за ним. Если получившийся подмассив упорядочен по убыванию — элементы переставляются так, чтобы они шли по возрастанию.
- Если размер текущего меньше , тогда выбираются следующие за найденным подмассивом элементы в количестве . Таким образом, на выходе будет получен подмассив размером большим или равный , часть которого (в лучшем случае — он весь) упорядочена.
- К данному подмассиву применяем сортировка вставками. Так как размер подмассива невелик и часть его уже упорядочена — сортировка работает эффективно.
- Указатель текущего элемента ставится на следующий за подмассивом элемент.
- Если конец входного массива не достигнут — переход к пункту 2
- Конец.
Шаг №3. Слияние
Нужно объединить полученные подмассивы для получения результирующего упорядоченного массива. Для достижения эффективности, объединение должно объединять подмассивы примерно равного размера и cохранять стабильность алгоритма (не делать бессмысленных перестановок).
- Начало.
- Создается пустой стек пар индекс начала подмассива — размер подмассива .
- Берется первый упорядоченный подмассив.
- Добавляется в стек пара данных индекс начала текущего подмассива — его размер .
- Пусть — длины верхних трех интервалов, которые лежат в стеке. Причем — это последний элемент стека.
- Повторяем пока выражение (
- Если размер стека не меньше 3 и — сливаем c .
- Иначе Если — сливаем c .
- Возвращаемся в п.6.
&& ) не станет истинным
- Конец
Основная цель этой процедуры — сохранение баланса. Изменения будут выглядеть как на картинке, а значит и размеры подмассивов в стеке эффективны для дальнейшей сортировки слиянием.
Описание процедуры слияния
- Создается временный массив в размере меньшего из сливаемых подмассивов.
- Меньший из подмассивов копируется во временный массив, но надо учесть, что если меньший подмассив правый, то ответ (результат сливания) формируется справа налево. Дабы избежать данной путаницы, лучше копировать всегда левый подмассив во временный. На скорости это практически не отразится.
- Ставятся указатели текущей позиции на первые элементы большего и временного массива.
- На каждом шаге рассматривается значение текущих элементов в большем и временном массивах, берется меньший из них, копируется в новый отсортированный массив. Указатель текущего элемента перемещается в массиве, из которого был взят элемент.
- Предыдущий пункт повторяется, пока один из массивов не закончится.
- Все элементы оставшегося массива добавляются в конец нового массива.
Модификация процедуры слияния подмассивов (Galloping Mode)
Рассмотрим процедуру слияния двух массивов:
Вышеуказанная процедура для них сработает, но каждый раз на её четвёртом пункте нужно будет выполнить одно сравнение и одно копирование. В итоге 10000 сравнений и 10000 копирований. Алгоритм Timsort предлагает в этом месте модификацию, которая получила называет «галоп». Алгоритм следующий:
- Начинается процедура слияния.
- На каждой операции копирования элемента из временного или большего подмассива в результирующий запоминается, из какого именно подмассива был элемент.
- Если уже некоторое количество элементов (в данной реализации алгоритма это число равно 7) было взято из одного и того же массива — предполагается, что и дальше придётся брать данные из него. Чтобы подтвердить эту идею, алгоритм переходит в режим «галопа», то есть перемещается по массиву-претенденту на поставку следующей большой порции данных бинарным поиском (массив упорядочен) текущего элемента из второго соединяемого массива.
- В момент, когда данные из текущего массива-поставщика больше не подходят (или был достигнут конец массива), данные копируются целиком.
Для вышеописанных массивов
алгоритм выглядит следующим образом: Первые 7 итераций сравниваются числа из массива с числом , так как больше, то элементы массива копируются в результирующий. Начиная со следующей итерации алгоритм переходит в режим «галопа»: сравнивает с числом последовательно элементы массива . ( сравнений). После того как конец массива достигнут и известно, что он весь меньше , нужные данные из массива копируются в результирующий.Доказательство времени работы алгоритма
Главный факт, который нам понадобится для доказательства нужной оценки времени работы в
— это то, что сливаемые массивы всегда имеют примерно одинаковую длинну. То есть подмассивы , такие, что будут слиты только тогда, когда размеры соответствующих подмассивов, в которые они войдут будут примерно равны. Например, если на вход подан массив , то, временно забыв про понятие , Timsort сделает следующие слияния:- 64, 32, 16, 8, 4, 2, 1, 1
- 64, 32, 16, 8, 4, 2, 2
- 64, 32, 16, 8, 4, 4
- 64, 32, 16, 8, 8
- 64, 32, 16, 16
- 64, 32, 32
- 64, 64
- 128
При сливании двух подмассивов
, будет произведенно операций сравнения. Результирующий подмассив будет удовлетворять выражению . Таким образом, каждый подмассив может поучаствовать в не более операций слияния, а значит и каждый элемент будет задействован в сравниниях не более раз. Элементов , откуда и получаем соответствующую оценку вИсточники
- Peter McIlroy "Optimistic Sorting and Information Theoretic Complexity", Proceedings of the Fourth Annual ACM-SIAM Symposium on Discrete Algorithms, ISBN 0-89871-313-7, Chapter 53, pp 467-474, January 1993.
- Magnus Lie Hetland Python Algorithms: Mastering Basic Algorithms in the Python Language. — Apress, 2010. — 336 с.