Почему моя реализация python сортировки по основанию работает медленнее, чем быстрая сортировка?

Я переписал исходный алгоритм сортировки по основанию для Python из Wikipedia, используя массивы из SciPy, чтобы повысить производительность и уменьшить длину кода, что мне удалось. Затем я взял классический (в памяти, на основе сводных данных) алгоритм быстрой сортировки из Literate Programming и сравнил их производительность.

Я ожидал, что радиксная сортировка превзойдет быструю сортировку за пределами определенного порога, но этого не произошло. Кроме того, я нашел блог Эрика Горсета задавая вопрос "Быстрее ли сортировка по основанию для целочисленных массивов, чем быстрая сортировка?". Ответ такой:

... тест показывает, что сортировка по основанию MSB на месте более чем в 3 раза быстрее, чем быстрая сортировка для больших массивов.

К сожалению, воспроизвести результат не удалось; разница в том, что (а) Эрик выбрал Java, а не Python, и (б) он использует сортировку по основанию счисления MSB, тогда как я просто заполняю корзины внутри словаря Python .

Согласно теории, поразрядная сортировка должна быть более быстрой (линейной) по сравнению с быстрой сортировкой; но, видимо, это во многом зависит от реализации. Так в чем же моя ошибка?

Вот код, в котором сравниваются оба алгоритма:

from sys   import argv
from time  import clock

from pylab import array, vectorize
from pylab import absolute, log10, randint
from pylab import semilogy, grid, legend, title, show

###############################################################################
# radix sort
###############################################################################

def splitmerge0 (ls, digit): ## python (pure!)

    seq = map (lambda n: ((n // 10 ** digit) % 10, n), ls)
    buf = {0:[], 1:[], 2:[], 3:[], 4:[], 5:[], 6:[], 7:[], 8:[], 9:[]}

    return reduce (lambda acc, key: acc.extend(buf[key]) or acc,
        reduce (lambda _, (d,n): buf[d].append (n) or buf, seq, buf), [])

def splitmergeX (ls, digit): ## python & numpy

    seq = array (vectorize (lambda n: ((n // 10 ** digit) % 10, n)) (ls)).T
    buf = {0:[], 1:[], 2:[], 3:[], 4:[], 5:[], 6:[], 7:[], 8:[], 9:[]}

    return array (reduce (lambda acc, key: acc.extend(buf[key]) or acc,
        reduce (lambda _, (d,n): buf[d].append (n) or buf, seq, buf), []))

def radixsort (ls, fn = splitmergeX):

    return reduce (fn, xrange (int (log10 (absolute (ls).max ()) + 1)), ls)

###############################################################################
# quick sort
###############################################################################

def partition (ls, start, end, pivot_index):

    lower = start
    upper = end - 1

    pivot = ls[pivot_index]
    ls[pivot_index] = ls[end]

    while True:

        while lower <= upper and ls[lower] <  pivot: lower += 1
        while lower <= upper and ls[upper] >= pivot: upper -= 1
        if lower > upper: break

        ls[lower], ls[upper] = ls[upper], ls[lower]

    ls[end] = ls[lower]
    ls[lower] = pivot

    return lower

def qsort_range (ls, start, end):

    if end - start + 1 < 32:
        insertion_sort(ls, start, end)
    else:
        pivot_index = partition (ls, start, end, randint (start, end))
        qsort_range (ls, start, pivot_index - 1)
        qsort_range (ls, pivot_index + 1, end)

    return ls

def insertion_sort (ls, start, end):

    for idx in xrange (start, end + 1):
        el = ls[idx]
        for jdx in reversed (xrange(0, idx)):
            if ls[jdx] <= el:
                ls[jdx + 1] = el
                break
            ls[jdx + 1] = ls[jdx]
        else:
            ls[0] = el

    return ls

def quicksort (ls):

    return qsort_range (ls, 0, len (ls) - 1)

###############################################################################
if __name__ == "__main__":
###############################################################################

    lower = int (argv [1]) ## requires: >= 2
    upper = int (argv [2]) ## requires: >= 2
    color = dict (enumerate (3*['r','g','b','c','m','k']))

    rslbl = "radix sort"
    qslbl = "quick sort"

    for value in xrange (lower, upper):

        #######################################################################

        ls = randint (1, value, size=value)

        t0 = clock ()
        rs = radixsort (ls)
        dt = clock () - t0

        print "%06d -- t0:%0.6e, dt:%0.6e" % (value, t0, dt)
        semilogy (value, dt, '%s.' % color[int (log10 (value))], label=rslbl)

        #######################################################################

        ls = randint (1, value, size=value)

        t0 = clock ()
        rs = quicksort (ls)
        dt = clock () - t0

        print "%06d -- t0:%0.6e, dt:%0.6e" % (value, t0, dt)
        semilogy (value, dt, '%sx' % color[int (log10 (value))], label=qslbl)

    grid ()
    legend ((rslbl,qslbl), numpoints=3, shadow=True, prop={'size':'small'})
    title ('radix & quick sort: #(integer) vs duration [s]')
    show ()

###############################################################################
###############################################################################

И вот результат сравнения продолжительности сортировки в секундах (логарифмическая вертикальная ось) для целочисленных массивов размером от 2 до 1250 (горизонтальная ось); нижняя кривая относится к быстрой сортировке:

Быстрая сортировка работает плавно при изменении мощности (например, при 10, 100 или 1000), но сортировка по основанию только немного прыгает, но в остальном качественно следует по тому же пути, что и быстрая сортировка, только намного медленнее!


person hsk81    schedule 26.11.2011    source источник
comment
Массив из 1250 элементов на самом деле не является большим массивом. Какой результат вы получите, если отсортируете 1000000 элементов?   -  person sth    schedule 26.11.2011
comment
Что произойдет, если вы добавите сюда список из 1 000 000 или 10 000 000 значений? Похоже, у вас ужасно неэффективная реализация поразрядной сортировки (например, вычисление 10 ** цифр во внутреннем цикле и множество ненужных вызовов функций), поэтому теоретическая эффективность может быть не видна, когда у вас всего 1250 элементов. Сортировать.   -  person Duncan    schedule 26.11.2011
comment
Кстати, ваш код очень трудно читать, но мне кажется, что вы зависите от итерации по dict, которая дает вам ключи в числовом порядке. Это означает, что вы зависите от неопределенного поведения, поэтому ваш код может сломаться в любой момент.   -  person Duncan    schedule 26.11.2011
comment
@Duncan: Ну, я предварительно вычислил полномочия на цифру и использовал поиск для увеличения производительности, но это не помогло; Я не смог обнаружить каких-либо значительных улучшений. Что вы имеете в виду под ненужными вызовами функций? Лямбда-выражения?   -  person hsk81    schedule 26.11.2011
comment
@sth: Проблема в том, что реализация, по-видимому, настолько медленная, что мне удается отсортировать списки до 10000, после чего это ужасно медленно. Даже при скорости 10000 быстрая сортировка была быстрее, хотя относительное расстояние между алгоритмами, похоже, сократилось.   -  person hsk81    schedule 26.11.2011
comment
Я просто заполняю ведра внутри словаря Python. Вы понимаете эффективность словаря? (Я не знаю, потому что я питон-придурок.) Вы уверены, что не выполняете какие-то медленные операции, скрытые за одной из умных функций питонов?   -  person dmckee --- ex-moderator kitten    schedule 26.11.2011


Ответы (2)


У вас тут несколько проблем.

Прежде всего, как указано в комментариях, ваш набор данных слишком мал, чтобы теоретическая сложность могла преодолеть накладные расходы в коде.

Затем ваша реализация со всеми этими ненужными вызовами функций и копированием списков будет очень неэффективной. Написание кода в простой процедурной манере почти всегда будет быстрее, чем функциональное решение (то есть для Python, другие языки здесь будут отличаться). У вас есть процедурная реализация быстрой сортировки, поэтому, если вы напишете свою радикальную сортировку в том же стиле, она может оказаться быстрее даже для небольших списков.

Наконец, может случиться так, что когда вы попробуете использовать большие списки, накладные расходы на управление памятью начнут преобладать. Это означает, что у вас есть ограниченное окно между небольшими списками, где эффективность реализации является доминирующим фактором, и большими списками, где управление памятью является доминирующим фактором.

Вот код, который использует вашу быструю сортировку, но простую радикальную сортировку, написанную процедурно, но пытающуюся избежать такого большого копирования данных. Вы увидите, что даже для коротких списков она превосходит быструю сортировку, но, что более интересно, по мере увеличения размера данных увеличивается и соотношение между быстрой сортировкой и сортировкой по основанию, а затем оно снова начинает падать, поскольку управление памятью начинает доминировать (простые вещи, такие как освобождение список из 1000000 элементов занимает значительное время):

from random import randint
from math import log10
from time import clock
from itertools import chain

def splitmerge0 (ls, digit): ## python (pure!)

    seq = map (lambda n: ((n // 10 ** digit) % 10, n), ls)
    buf = {0:[], 1:[], 2:[], 3:[], 4:[], 5:[], 6:[], 7:[], 8:[], 9:[]}

    return reduce (lambda acc, key: acc.extend(buf[key]) or acc,
        reduce (lambda _, (d,n): buf[d].append (n) or buf, seq, buf), [])

def splitmerge1 (ls, digit): ## python (readable!)
    buf = [[] for i in range(10)]
    divisor = 10 ** digit
    for n in ls:
        buf[(n//divisor)%10].append(n)
    return chain(*buf)

def radixsort (ls, fn = splitmerge1):
    return list(reduce (fn, xrange (int (log10 (max(abs(val) for val in ls)) + 1)), ls))

###############################################################################
# quick sort
###############################################################################

def partition (ls, start, end, pivot_index):

    lower = start
    upper = end - 1

    pivot = ls[pivot_index]
    ls[pivot_index] = ls[end]

    while True:

        while lower <= upper and ls[lower] <  pivot: lower += 1
        while lower <= upper and ls[upper] >= pivot: upper -= 1
        if lower > upper: break

        ls[lower], ls[upper] = ls[upper], ls[lower]

    ls[end] = ls[lower]
    ls[lower] = pivot

    return lower

def qsort_range (ls, start, end):

    if end - start + 1 < 32:
        insertion_sort(ls, start, end)
    else:
        pivot_index = partition (ls, start, end, randint (start, end))
        qsort_range (ls, start, pivot_index - 1)
        qsort_range (ls, pivot_index + 1, end)

    return ls

def insertion_sort (ls, start, end):

    for idx in xrange (start, end + 1):
        el = ls[idx]
        for jdx in reversed (xrange(0, idx)):
            if ls[jdx] <= el:
                ls[jdx + 1] = el
                break
            ls[jdx + 1] = ls[jdx]
        else:
            ls[0] = el

    return ls

def quicksort (ls):

    return qsort_range (ls, 0, len (ls) - 1)

if __name__=='__main__':
    for value in 1000, 10000, 100000, 1000000, 10000000:
        ls = [randint (1, value) for _ in range(value)]
        ls2 = list(ls)
        last = -1
        start = clock()
        ls = radixsort(ls)
        end = clock()
        for i in ls:
            assert last <= i
            last = i
        print("rs %d: %0.2fs" % (value, end-start))
        tdiff = end-start
        start = clock()
        ls2 = quicksort(ls2)
        end = clock()
        last = -1
        for i in ls2:
            assert last <= i
            last = i
        print("qs %d: %0.2fs %0.2f%%" % (value, end-start, ((end-start)/tdiff*100)))

Результат, когда я запускаю это:

C:\temp>c:\python27\python radixsort.py
rs 1000: 0.00s
qs 1000: 0.00s 212.98%
rs 10000: 0.02s
qs 10000: 0.05s 291.28%
rs 100000: 0.19s
qs 100000: 0.58s 311.98%
rs 1000000: 2.47s
qs 1000000: 7.07s 286.33%
rs 10000000: 31.74s
qs 10000000: 86.04s 271.08%

Изменить: просто для пояснения. Реализация быстрой сортировки здесь очень удобна для памяти, она сортируется на месте, поэтому независимо от того, насколько велик список, он просто перетасовывает данные, а не копирует их. Исходная система radixsort эффективно копирует список дважды для каждой цифры: один раз в меньшие списки, а затем еще раз, когда вы объединяете списки. Использование itertools.chain позволяет избежать этой второй копии, но по-прежнему происходит много выделения / освобождения памяти. (Также «дважды» является приблизительным, поскольку добавление списка требует дополнительного копирования, даже если оно амортизировано O (1), поэтому я, возможно, должен сказать «пропорционально дважды».)

person Duncan    schedule 26.11.2011
comment
Я не знал функции цепочки из itertools, но проверю ее. Мое основное предположение заключалось в том, что в Python циклы for на самом деле медленные, поэтому я намеренно все выразил функционально. Кроме того, чтобы избежать копирования списков, я использовал уловки вроде acc.extend (buf [key]) или acc, когда я возвращаю аккумулятор для уменьшения. Думаю, мне нужно больше узнать о внутреннем устройстве функционального Python, чтобы увидеть соответствующие подводные камни. - person hsk81; 26.11.2011
comment
В общем случае предположим: Fast: map или reduce с функцией, написанной на C. Медленнее: Python for loop. Еще медленнее: map или reduce вызов функции, написанной на Python (сортировка аргументов - дорогостоящая операция). Также помните, что между lambda и def нет разницы, проще def foo(acc, key): acc.extend(buf[key]); return acc, чем запутать с помощью lambda и or между выражениями. - person Duncan; 26.11.2011

Ваше представление данных очень дорогое. Почему вы используете hashmap для своих сегментов? Зачем использовать представление base10, для которого вам нужно вычислять логарифмы (= дорогое вычисление)?

Избегайте лямбда-выражений и тому подобного, я не думаю, что python пока может хорошо их оптимизировать.

Возможно, вместо этого начните с сортировки 10-байтовых строк для теста. И: никаких хэш-карт и подобных дорогих структур данных.

person Has QUIT--Anony-Mousse    schedule 26.11.2011
comment
Я не уверен, что 10-байтовые строки для теста подходят, когда утверждается, что radixsort превосходит быструю сортировку для массивов integer. - person Rob; 26.11.2011
comment
Спасибо за подсказку относительно лямбда-выражений, я попробую ... хотя я немного разочарован тем, что Python так плохо работает, когда вы используете лямбда-выражения. :( - person hsk81; 26.11.2011
comment
@Anonymouse: Что бы вы предложили вместо хэш-карты, когда я хочу отсортировать целые числа и поэтому не хочу преобразовывать их в 10-байтовую строку? Далее: насколько я могу судить, логарифм вычисляется только один раз, так что отказ от этого не принесет особой выгоды, не так ли? - person hsk81; 26.11.2011
comment
Используйте, например, массив вместо хэш-карты. - person Has QUIT--Anony-Mousse; 26.11.2011
comment
Попробуйте использовать пример кода в Википедии, en.wikipedia.org/wiki/Radix_sort#Example_in_Python чтобы увидеть, как он работает по сравнению с вашим. - person Has QUIT--Anony-Mousse; 26.11.2011