2016-04-05 3 views
11

Я тестировал временную сложность различных алгоритмов сортировки для разных порядков чисел, и все было хорошо, пока я не получил результаты быстрого сортирования (с центром в центре) для последовательностей, которые на одну половину по возрастанию а другой - нисходящий. График:Сложная временная сложность Quicksort, C++

enter image description here

(Под «V», я имею в виду последовательность, в которой первая половина опускается, а другой по возрастанию, а «А» Я имею в виду последовательность, где первая половина поднимается, а другая половина убывает.)

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

void quicksort(int l,int p,int *tab) 
{ 
int i=l,j=p,x=tab[(l+p)/2],w; //x - pivot 
do 
{ 
    while (tab[i]<x) 
    { 
     i++; 
    } 
    while (x<tab[j]) 
    { 
     j--; 
    } 
    if (i<=j) 
    { 
     w=tab[i]; 
     tab[i]=tab[j]; 
     tab[j]=w; 
     i++; 
     j--; 
    } 
} 
while (i<=j); 
if (l<j) 
{ 
    quicksort(l,j,tab); 
} 
if (i<p) 
{ 
    quicksort(i,p,tab); 
} 
} 

Есть ли у кого-нибудь идеи, что вызвало такие странные результаты?

+3

Известный недостаток в среднем с быстрой сортировки поворота. Классическая. – SergeyA

+0

@SergeyA Я искал интернет для подобных графиков и ничего не нашел. Есть ли место, где я мог бы прочитать, почему он прыгает между сложностями? – vois

+0

Ну, какова ваша метрика для создания такого эпического графа? Вы измеряете время? Как? – tomascapek

ответ

0

Quicksort имеет худшую временную сложность O (n^2) и среднее значение O (n log n) для n записей в наборе данных. Более подробная информация о анализе временной сложности можно найти здесь:

https://www.khanacademy.org/computing/computer-science/algorithms/quick-sort/a/analysis-of-quicksort

и здесь:

http://www.cise.ufl.edu/class/cot3100fa07/quicksort_analysis.pdf

+0

Да, но должен ли алгоритм переходить между сложностями для одного и того же типа последовательности? – vois

+0

@vois Я думаю, это не должно. См. Мой ответ. – tomascapek

1

в QuickSort одна из главных вещей, которые влияют на время работы является сделать входной режим.

Обычно выбор стержня в определенном положении может быть не самым лучшим, за исключением того, что вход случайным образом перетасован. Использование median of three partition является одним из широко используемых средств, чтобы удостовериться, что стержень является случайным числом. Из вашего кода вы его не реализовали.

Кроме того, когда рекурсивная быстрая сортировка будет испытывать некоторые накладные расходы с момента ее использования внутреннего стека (придется генерировать несколько функций и назначать параметры), поэтому рекомендуется, чтобы при сохранении размера оставшихся данных около 10 - 20 вы можете использовать другой алгоритм сортировки как InsertionSort, так как это будет примерно 20% быстрее.

void quicksort(int l,int p,int *tab){ 
    if (tab.size <= 10){ 

     IntersionSort(tab); 
    } 
.. 
..} 

Что-то в этом роде.

В целом лучшее время бега для быстрой сортировки является NlogN худший случай времени работы является п^2 часто вызывается non-random входов или duplicates входов

+0

Может ли случайный вход заставлять его перескакивать между nlogn и n^2 конкретичностью? Если да, то почему? – vois

+0

Да, неслучайный ввод может привести к его переходу с nlogn на n^2, однако это зависит от реализации. но быть в безопасности, поэтому некоторые люди сначала перетасовывают входные данные. если вход не является случайным выбором, среднее число не имеет большого значения, потому что последовательность результатов будет наклонена в одну сторону. не сможет дать здесь подробного объяснения, но вы можете проверить «выбивание части pivot» https://en.wikipedia.org/wiki/Quicksort –

1

Все ответы здесь есть очень хорошие моменты. Моя идея состоит в том, что в алгоритме нет ничего плохого (поскольку проблема с опорой хорошо известна, и это является причиной O (n^2), но что-то не так с тем, как вы ее измеряете.

clock() - возвращает количество процессорных тактов, прошедшее с какой-то момент (вероятно, программа запуска не важно?)

Ваш способ измерения времени Relly на постоянной длине клеща, который я думаю, не гарантируется

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

Я попытался найти, какой макрос CLOCKS_PER_SEC на самом деле. Это текущие часы в секунду? Имеет ли он некоторые средние значения в течение какого-то загадочного периода времени? Я, к сожалению, не смог узнать. Поэтому я думаю, что ваш способ измерения времени может быть ошибочным.

Поскольку мой аргумент стоит на чем-то, я точно не знаю, Возможно, я ошибся.

Один из способов узнать, как много раз запускать несколько тестов с одними и теми же данными с разным общим использованием системы и видеть, если он ведет себя существенно различно каждый раз. Другой способ - установить частоту процессора вашего компьютера на некоторое статическое значение и протестировать его аналогичным образом.

IDEA Не было бы лучше измерить «время» в тиках?

EDIT 1 Благодаря @BeyelerStudios, теперь мы наверняка знаете, что вы не должны Релли на clock() на машинах Windows, так как он не соответствует стандарту C98. Source

Я надеюсь, что помог, если я ошибаюсь, , пожалуйста, исправьте меня - Я студент, а не специалист по HW.

+1

в библиотеках Microsoft «CLOCKS_PER_SEC» является постоянным значением (по крайней мере, до VS2013, не знаю о MSVC 14.0) – BeyelerStudios

+0

@BeyelerStudios Это может означать, что эта программа способна заставлять свою частоту процессора или что этот метод расчета времени просто не работает. – tomascapek

+0

Нормализовать это было бы сложно - как его нормализовать? Нет никакой (видимой) связи между 'clock()' и 'CLOCK_PER_SEC', что может означать, что внутри кода есть что-то магическое, и это не очень хорошо, поэтому я не думаю, что это работает так. Представьте себе так: для измерения времени вы дважды вызываете 'clock()'. Как знать, откуда начинать нормализацию? Каждый звонок? Это будет крайне неудачно ... – tomascapek

8

TL; DR: Проблема заключается в стратегии выбора стержня, которая делает неоднозначные варианты выбора этих типов входов (последовательности A- и V-образной формы). Это приводит к быстрому сортировке, что приводит к «неуравновешенным» рекурсивным вызовам, что, в свою очередь, приводит к тому, что алгоритм работает очень плохо (квадратичное время для A-образных последовательностей).

Поздравляем, вы обнаружили новый (или, скорее, семейство входов) для версии quicksort, которая выбирает средний элемент в качестве точки опоры.

Для справки, пример А-образной последовательности является 1 2 3 4 3 2 1, т.е. последовательность, которая увеличивается, достигает выбор в середине, а затем уменьшается; пример V-образной последовательности равен 4 3 2 1 2 3 4, то есть последовательность, которая уменьшается, достигает минимума в середине и затем увеличивается.

Подумайте о том, что происходит, когда вы выбираете средний элемент как ось A- или V-образной последовательности. В первом случае, когда вы передаете алгоритм A-образной последовательности 1 2 ... n-1 n n-1 ... 2 1, стержень является самым большим элементом массива --- это потому, что самый большой элемент A-образной последовательности является средним, и вы выбираете средний элемент в качестве поворота --- и вы будете делать рекурсивные вызовы в подмассивах размеров 0 (ваш код фактически не делает звонок по 0 элементам) и n-1. В следующем вызове в подмассиве размером n-1 вы выберете в качестве свода наибольший элемент подмассива (который является вторым по величине элементом исходного массива); и так далее. Это приводит к низкой производительности, так как время работы O (n) + O (n-1) + ... + O (1) = O (n^2), потому что на каждом шаге вы, по сути, проходите почти по всему массив (все элементы, за исключением точки поворота), другими словами, размеры массивов в рекурсивных вызовах сильно несбалансированы.

Вот след для А-образной последовательности 1 2 3 4 5 4 3 2 1:

[email protected]:/tmp$ ./test 
pivot=5 
    1 2 3 4 1 4 3 2 5 
pivot=4 
    1 2 3 2 1 3 4 4 
pivot=3 
    1 2 3 2 1 3 
pivot=3 
    1 2 1 2 3 
pivot=2 
    1 2 1 2 
pivot=2 
    1 1 2 
pivot=1 
    1 1 
pivot=4 
    4 4 
    1 1 2 2 3 3 4 4 5 

Вы можете видеть из след, что при рекурсивном вызове алгоритм выбирает большой элемент (может быть до двух крупнейших элементов, следовательно, статья a, а не ) в качестве стержня. Это означает, что время работы для A-образной последовательности действительно O (n) + O (n-1) + ... + O (1) = O (n^2). (В техническом жаргоне А-образная последовательность является примером состязательного ввода, который заставляет алгоритм работать плохо.)

Это означает, что если вы планируете время выполнения для «отлично» A-образных последовательностей формы

1 2 3 ... n-1 n n-1 ... 3 2 1 

для увеличения n, вы увидите хороший квадратичной функции. Вот график, я просто вычислить для n=5,105, 205, 305,...,9905 для А-образных последовательностей 1 2 ... n-1 n n-1 ... 2 1:

Running times for A-shaped sequences

Во втором случае, при передаче алгоритма последовательность V-образную форму, Вы выбираете наименьший элемент массива, что и pivot, и, таким образом, вызовет рекурсивные вызовы в подмассивах размеров n-1 и 0 (ваш код фактически не делает звонок по 0 элементам). В следующем вызове в подвале размером n-1 вы выберете в качестве свода самый большой элемент; и так далее. (Но вы не всегда будете делать такие ужасные выборы, и в этом случае сложно сказать больше). Это приводит к низкой производительности по аналогичным причинам. Этот случай немного сложнее (это зависит от того, как вы делаете шаг «перемещение»).

Посмотрите график времени работы для V-образных последовательностей n n-1 ... 2 1 2 ... n-1 n для n=5,105,205,...,49905. Время работы несколько менее регулярное, так как я сказал, что это сложнее, потому что вы не всегда выбираете наименьший элемент в качестве точки опоры. График:

Running times for V-shaped sequences for increasing sizes.

код, который я использовал для измерения времени:

double seconds(size_t n) { 
    int *tab = (int *)malloc(sizeof(int) * (2*n - 1)); 
    size_t i; 

    // construct A-shaped sequence 1 2 3 ... n-1 n n-1 ... 3 2 1 
    for (i = 0; i < n-1; i++) { 
     tab[i] = tab[2*n-i-2] = i+1; 
     // To generate V-shaped sequence, use tab[i]=tab[2*n-i-2]=n-i+1; 
    } 
    tab[n-1] = n; 
    // For V-shaped sequence use tab[n-1] = 1; 

    clock_t start = clock(); 
    quicksort(0, 2*n-2, tab); 
    clock_t finish = clock(); 

    free(tab); 

    return (double) (finish - start)/CLOCKS_PER_SEC; 
} 

Я приспособил свой код, чтобы напечатать «след» алгоритма, так что вы можете играть с ним самостоятельно и усиления понимание того, что происходит:

#include <stdio.h> 

void print(int *a, size_t l, size_t r); 
void quicksort(int l,int p,int *tab); 

int main() { 
    int tab[] = {1,2,3,4,5,4,3,2,1}; 
    size_t sz = sizeof(tab)/sizeof(int); 

    quicksort(0, sz-1, tab); 
    print(tab, 0, sz-1); 

    return 0; 
} 


void print(int *a, size_t l, size_t r) { 
    size_t i; 
    for (i = l; i <= r; ++i) { 
     printf("%4d", a[i]); 
    } 
    printf("\n"); 
} 

void quicksort(int l,int p,int *tab) 
{ 
int i=l,j=p,x=tab[(l+p)/2],w; //x - pivot 
printf("pivot=%d\n", x); 
do 
{ 
    while (tab[i]<x) 
    { 
     i++; 
    } 
    while (x<tab[j]) 
    { 
     j--; 
    } 
    if (i<=j) 
    { 
     w=tab[i]; 
     tab[i]=tab[j]; 
     tab[j]=w; 
     i++; 
     j--; 
    } 
} 
while (i<=j); 

print(tab, l, p); 
if (l<j) 
{ 
    quicksort(l,j,tab); 
} 
if (i<p) 
{ 
    quicksort(i,p,tab); 
} 
} 

Кстати, я думаю, что график, показывающий бегущих раз будет гладко, если вы взяли в среднем, скажем, в 100 раз бегущие для каждой входной последовательности.

Мы видим, что проблема заключается в стратегии выбора стержня. Позвольте мне заметить, что вы можете устранить проблемы с состязательными входами, используя случайный выбор. Самый простой подход - выбрать шарнир равномерно случайным образом (каждый элемент в равной степени может быть выбран в качестве стержня); вы можете показать, что алгоритм работает в O (n log n) время with high probability.(Заметим, однако, что, чтобы показать этот острый хвост связаны вам нужны некоторые предположения на входе, результат, безусловно, имеет место, если эти цифры различны, см., Например, Motwani и рандомизированные Алгоритмы книги Рагаван в)

Чтобы подтвердить мои претензии, вот график времени работы для тех же последовательностей, если вы выбираете шарнир равномерно случайным образом, с x = tab[l + (rand() % (p-l))]; (убедитесь, что вы в основном вызываете srand(time(NULL))). Для получения А-образных последовательностей: enter image description here

Для V-образных последовательностей:

enter image description here