Почему моя реализация Python Radix сортировки медленнее, чем быстрая сортировка? - PullRequest
3 голосов
/ 26 ноября 2011

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

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

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

К сожалению, я не смог воспроизвести результат; Различия заключаются в том, что (а) Эрик выбрал Java, а не Python, и (б) он использует радикальную сортировку на месте MSB , тогда как я просто заполняю buckets внутри словаря 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), но радикальная сортировка просто немного скачет, но в остальном качественно следует тому же пути, что и быстрая сортировка, только намного медленнее!

Ответы [ 2 ]

3 голосов
/ 26 ноября 2011

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

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

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

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

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

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), поэтому я должен сказать «пропорционально двойному».)

0 голосов
/ 26 ноября 2011

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

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

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

...