Cайт программиста Ruby, веб-разработчика Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Алгоритм "Быстрая сортировка", Quicksort, Обменная сортировка с разделением. Ч. Э. Р. Хоар. Р. Седжвик, Дональд Э. Кнут. Ruby

# encoding: utf-8
# frozen_string_literal: true
#
# @author ESV Corp. (C) 21.09.2026
#
# алгоритм: сортировка методом "быстрая сортировка" Quicksort, Обменная сортировка с разделением
# Ч. Э. Р. Хоар (C. A. R. Hoare)
# Р. Седжвик "Алгоритмы",
# Д. Кнут "Искусство программирования", т.3 "Сортировка и поиск",
# глава 5.2.2, раздел "Обменная сортировка"
#
# сначала происходит разделение массива на подмассивы, размер которых
# подходит для быстрой сортировки методом вставок, далее каждый подмассив
# сортируется методом вcтавки
# сортировка всего массива производится "по месту", т.е. без создания дополнительных
# массивов
#

M = 5 # размер подмассива для сортировки методом вставок

# ---
# "разделение" массива на подмассивы:
# в результате возвращается индекс (index) в массиве, такой, что
# элементы list[l...index] <= list[index] <= list[index+1..r]
#
# Parameters
#
# @param l [Integer] левая (меньшая) граница исходного массива
# @param r [Integer] правая (большая) граница исходного массива
# @param list [Array<Integer>] изменяемый массив, который разделяется
#
# Returns
#
# @return [Integer] индекс элемент в массиве, который делит массив на левую и правую части
#
def split(l, r, list)

  i = l
  j = r + 1

  k = list[l]

  while i < j

    # гарантированный сдвиг индексов
    # !!! важно: этот код выполняется и после обмена соседних элементов [i] и [j]
    # при этом более важен j -= 1 , т.к. после этого он будет указывать
    # на элемент, который подлежит обмену с "центральным" элементом после
    # выхода из цикла while i < j
    i += 1
    j -= 1

    while i < r && k > list[i]
      i += 1
    end

    while j > l && k < list[j]
      j -= 1
    end

    # достаточно условия i < j , но дополнительно list[i] != list[j], чтобы не менять
    # одинаковые элементы
    if i < j && list[i] != list[j]
      # обмен элементов из левой и правой частей
      list[i], list[j] = list[j], list[i]
    end

  end

  # обмен левого элемента и "центрального", который должен находится между разделёнными массивами
  # тут можно тоже в условие добавить && list[i] != list[j] , но для наглядности:
  # алгоритм Кнута и Седжвика не делает эту проверку
  if l != j
    # установим "центральный" элемент между массивами "меньше" и "больше"
    list[l], list[j] = list[j], list[l]
  end

  j

end


# ---
# сортировка методом вставок
def sort_by_inserts(r, li, ri)

  puts "sort_by_inserts: nothing to sort" unless (ri - li + 1) > 1

  puts "Sort by inserts source: (#{li}-#{ri}) #{r[li..ri].inspect}"

  j = 1 + li

  while j <= ri

    t = r[j]
    i = j

    while i > li

      check = r[i-1]

      break if check < t

      r[i] = check
      i -= 1

    end

    # в оригинальном алгоритме Кнута значение может быть
    # записано в ту же самую позицию
    # видимо потому, что лишняя проверка индексов дополнительно занимает
    # память программы, и ещё и для выполнения требует время
    r[i] = t

    j += 1

  end

  r

end


# ---
# быстрая сортировка
# рекурсивный вызов
def quicksort_recursive(l, r, list)

  if l >= r
    return list
  end

  # разделяем на подмассивы
  s = split(l, r, list)

  center_elem = list[s]
  left_slice  = list[l...s]
  right_slice = list[s+1..r]

  puts("---")
  puts("splited:")
  puts("index = #{s}, list[s] = #{center_elem}")
  puts("left  = #{left_slice.inspect}")
  puts("right = #{right_slice.inspect}")
  puts("#{left_slice.inspect} #{center_elem} #{right_slice.inspect}")
  puts("===")

  left_size  = s - l
  right_size = r - s

  # сортировка левой части
  if left_size > 1

    if left_size > M
      quicksort_recursive(l, s - 1, list)
    else
      sort_by_inserts(list, l, s - 1)
    end

  end

  # сортировка правой части
  if right_size > 1

    if right_size > M
      quicksort_recursive(s + 1, r, list);
    else
      sort_by_inserts(list, s + 1, r)
    end

  end

  list

end


# ---
# быстрая сортировка
# использование стека индексов
def quicksort_stack(l, r, list)

  if l >= r
    return list
  end

  qs_stack = []
  ins_stack = []

  qs_stack.push([l, r])

  while qs_stack.any?

    l, r = qs_stack.pop

    if (r - l + 1) <= M
      ins_stack.push([l, r])
      next
    end

    # разделяем на подмассивы
    s = split(l, r, list)

    center_elem = list[s]
    left_slice  = list[l...s]
    right_slice = list[s+1..r]

    puts("---")
    puts("splited:")
    puts("index = #{s}, list[s] = #{center_elem}")
    puts("left  = #{left_slice.inspect}")
    puts("right = #{right_slice.inspect}")
    puts("#{left_slice.inspect} #{center_elem} #{right_slice.inspect}")
    puts("===")

    left_size  = s - l
    right_size = r - s

    if left_size > 1
      if left_size > M
        qs_stack.push([l, s - 1])
      else
        ins_stack.push([l, s - 1])
      end
    end

    if right_size > 1
      if right_size > M
        qs_stack.push([s + 1, r])
      else
        ins_stack.push([s + 1, r])
      end
    end

  end

  # сортировка коротких участков методом вставки
  while ins_stack.any?
    l, r = ins_stack.pop
    sort_by_inserts(list, l, r)
  end

  list

end


# ===
# основная программа

puts "Robert Sedgewick, Donald E. Knuth algorithm test: quicksort"

src_list = [5, 3, 2, 7, 1, -5, 10, 4, -3, 0, 12, 6, 7, 9, 11, 8, 7, 20, -1, 2, 100]

# в процессе сортировки модифицируем исходный массив, поэтому нужна копия
list = src_list.dup

sorted_list = quicksort_recursive(0, list.size-1, list)

puts "source: #{src_list.inspect}"
puts "recursive sorted itself: #{list.inspect}"
puts "recursive sorted: #{sorted_list.inspect}"

puts "\n#{'*' * 30}\n\n"

list = src_list.dup

sorted_list = quicksort_stack(0, list.size-1, list)

puts "source: #{src_list.inspect}"
puts "stack sorted itself: #{list.inspect}"
puts "stack sorted: #{sorted_list.inspect}"