Это часть моей серии статей об алгоритмах и структурах данных под названием «Алгоритмы с тетушкой Аей». Эта серия представляет собой введение в структуры данных и алгоритмы для людей, которые никогда их не изучали или забыли.
Алгоритмы с тетушкой Аей: Сортировка
Алгоритмы сортировки составляют большую часть любого курса алгоритмов. Они также часто появляются в интервью. По моему не столь скромному мнению, правильный ответ, когда кто-то просит вас что-то отсортировать на собеседовании, — использовать любую сортировку, встроенную в ваш язык. Сортировка обманчиво сложна. Многие считают, что это должно быть легко. Ведь 2-х и 3-х летние дети умеют сортировать вещи. Оказывается, есть много тонкостей, которые следует учитывать при написании алгоритма сортировки. Одним из распространенных примеров являются ошибки один за другим (ошибки столбов забора).
Сортировка вставками — это первая сортировка, о которой я расскажу. Есть несколько более простых алгоритмов сортировки (например, пузырьковая сортировка), но сортировку вставками объяснить несложно. Вы, вероятно, используете метод, похожий на сортировку вставками, когда сортируете вещи в повседневной жизни. Кроме того, он достаточно хорошо работает с небольшими наборами данных, и вам не нужно иметь все элементы заранее. Вы можете размещать новые предметы в правильном месте по мере их появления. Это делает его онлайн алгоритмом сортировки.
Настройка сортировки вставками проста. У вас есть две коллекции. Один содержит несортированные элементы. Другой содержит элементы, которые отсортированы. Если это сбивает с толку, подумайте о заказе колоды карт. Перетасованные (неотсортированные) карты находятся в колоде, а те, которые вы расставили по порядку, разложены на столе.
При запуске коллекция отсортированных элементов пуста. Чтобы отсортировать, вы смотрите на первый элемент в несортированной коллекции и помещаете его в нужное место в отсортированной коллекции. Затем вы кладете следующий элемент на место. Повторяйте, пока не отсортируете.
Код
В духе TDD я начну с трех основных тестов. Сортировка пустого массива, сортировка массива длины один и сортировка массива с более чем одним элементом.
require 'minitest/autorun' require './insertion_sort' class TestInsertionSort < Minitest::Test def test_sort_empty_list assert_equal [], [].insertion_sort end def test_sort_list_length_one assert_equal [3], [3].insertion_sort end def test_sort_random_list assert_equal [1, 2, 3, 4, 5], [3, 2, 5, 1, 4].insertion_sort end end
Вот базовая реализация сортировки вставками. Я обезьяньи исправления (повторно открываю) класс Array, потому что мои тесты вызывают сортировку вставками в массиве.
class Array def insertion_sort sorted = [] self.each do |item| (0..sorted.length).each do |index| if sorted.fetch(index, Float::INFINITY) > item then sorted.insert index, item break end end end sorted end end
В этих 18 строках происходит довольно много. В строке 3 я создаю новый массив с именем sorted, который будет содержать отсортированные результаты (те, что на столе, по аналогии с карточками). Далее (строка 5) я перебираю все элементы self, которые представляют собой несортированный массив.
Строки 6–13 просматривают отсортированный массив и пытаются найти первый элемент, который больше, чем элемент, на который я сейчас смотрю. Как только он найден (строка 9), я использую Array#insert, который вставляет элемент в массив перед указанным индексом.
Если элемент, который я пытаюсь найти, больше, чем все остальные элементы в отсортированном массиве, индекс в конечном итоге будет sorted.length. Это потому, что я использую две точки для диапазона. Две точки означают, что включены оба конца диапазона. Если index имеет значение sorted.length, то sorted[index] будет равен нулю. Я ловлю этот случай в строке 8 и использую вставку, чтобы добавить элемент в конец отсортированного массива.
Сортировка на месте
Приведенная выше версия сортировки вставками работает, но создает копию массива и оставляет оригинал нетронутым. Если я хочу отсортировать исходный массив, я могу написать #insertion_sort!. Вот новые тесты:
def test_sort_bang_empty_list ary = [] ary.insertion_sort! assert_equal [], ary end def test_sort_bang_list_length_one ary = [3] ary.insertion_sort! assert_equal [3], ary end def test_sort_bang_random_list ary = [3, 2, 5, 1, 4] ary.insertion_sort! assert_equal [1, 2, 3, 4, 5], ary end
Поскольку я тестирую метод, который изменяет массив, мне нужно присвоить массив, вызвать мой метод, а затем подтвердить, что он сделал правильно. Вот почему эти тесты длиннее, чем тесты для insertion_sort выше.
Вот код:
def insertion_sort! (1...self.length).each do |i| j = i while j > 0 and self[j - 1] > self[j] self[j], self[j - 1] = self[j - 1], self[j] j -= 1 end end end
Эта версия намного короче. Начиная со второго элемента массива (индекс 1), я беру этот элемент и сравниваю его со всеми элементами массива перед ним. Когда я нахожу тот, который меньше, я вставляю текущий элемент сразу после него. Если я сделаю это полностью вперед, текущий элемент должен быть наименьшим, поэтому я помещаю текущий элемент в начало массива.
Вывод
Сортировка вставками не является особенно эффективным алгоритмом, но его легко понять и относительно просто написать. Существует несколько различных способов его реализации, и я рекомендую вам пройти тесты, которые я предоставил, и попытаться найти другой способ его реализации.
Код, использованный в этом посте, доступен на Github.
08/28/16
Первоначально опубликовано на www.thagomizer.com 28 августа 2016 г.