Почему длительность алгоритма быстрой сортировки увеличивается, когда массив имеет повторяющиеся значения? - PullRequest
3 голосов
/ 29 марта 2019

Я пытаюсь измерить длительность функций сортировки слиянием и быстрой сортировки, используя вычисления времени std :: chrono и используя случайно сгенерированные массивы целых чисел в некотором диапазоне [A, B], размеры массивов варьируются от 5000 до100 000 целых чисел.

Цель моего кода - доказать, что когда метод выбора (сводной) в быстрой сортировке улучшается, функция быстрой сортировки в конечном итоге занимает меньше времени для обработки массива, чем сортировка слиянием,способ, которым я выбираю сводку, использует метод случайного индекса, чтобы минимизировать вероятность наличия сложности (n ^ 2). Однако в некоторых случаях, которые я опишу ниже, быстрая сортировка заканчивается больше времени, чем сортировка слиянием, и яхотел бы знать, почему это происходит.

case 1: диапазон чисел в массиве мал, что увеличивает вероятность наличия дублирующихся чисел в массиве.

case 2: когда Iиспользуйте локальную среду разработки, например clion, функция быстрой сортировки занимает намного больше времени, чем сортировка слиянием, однакоНесмотря на то, что компилятор, такой как IDEONE.com, дает одинаковые результаты в обоих алгоритмах сортировки (даже когда диапазон генерируемых целых чисел мал)

вот результаты, которые я получил в упомянутых случаях (первая строка чисел - сортировка слияниемРезультаты, вторая строка результатов быстрой сортировки):

Результаты 1-clion узкий диапазон чисел (-100, 600) clion results narrow range of numbers (-100, 600)

Результаты 2-clion с широким диапазоном чисел(INT_MIN, INT_MAX) clion results with a wide range of numbers (INT_MIN, INT_MAX)

3-IDEONE результаты с узким диапазоном чисел (-100, 600) IDEONE results with a narrow range of numbers (-100, 600)

4-IDEONE результаты с широким диапазоном чисел (INT_MIN, INT_MAX) IDEONE results with a wide range of numbers (INT_MIN, INT_MAX)

#include <bits/stdc++.h>
#include <chrono>
#include <random>

using namespace std;

mt19937 gen(chrono::steady_clock::now().time_since_epoch().count());
int* generateArray(int size)
{
    int* arr = new int[size];
    uniform_int_distribution<> distribution(INT_MIN, INT_MAX);
    for (int i=0; i < size; ++i)
    {
        arr[i] = distribution(gen);
    }
    return arr;
}
void merge(int* leftArr, int nL, int* rightArr, int nR, int* mainArr)
{
    int i=0, j=0, k=0;
    while (i < nL && j < nR)
    {
        if (leftArr[i] < rightArr[j]) { mainArr[k++] = leftArr[i++]; }
        else { mainArr[k++] = rightArr[j++]; }
    }
    while (i < nL){ mainArr[k++] = leftArr[i++]; }
    while (j < nR){ mainArr[k++] = rightArr[j++]; }
}
void mergeSort (int* mainArray, int arrayLength)
{
    if (arrayLength < 2) { return; }
    int mid = arrayLength/2;
    int* leftArray = new int[mid];
    int* rightArray = new int[arrayLength - mid];
    for (int i=0; i<mid; ++i) {leftArray[i] = mainArray[i];}
    for (int i = mid; i<arrayLength; ++i) {rightArray[i - mid] = mainArray[i];}
    mergeSort(leftArray, mid);
    mergeSort(rightArray, arrayLength-mid);
    merge(leftArray, mid, rightArray, arrayLength-mid, mainArray);
    delete[] leftArray;
    delete[] rightArray;
}


int partition (int* arr, int left, int right)
{
    uniform_int_distribution<> distribution(left, right);
    int idx = distribution(gen);
    swap(arr[right], arr[idx]);
    int pivot = arr[right];
    int partitionIndex = left;
    for (int i = left; i < right; ++i)
    {
        if (arr[i] <= pivot)
        {
            swap(arr[i], arr[partitionIndex]);
            partitionIndex++;
        }
    }
    swap(arr[right], arr[partitionIndex]);
    return partitionIndex;
}
void quickSort (int* arr, int left, int right)
{
    if(left < right)
    {
        int partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex-1);
        quickSort(arr, partitionIndex+1, right);
    }
}

int main()
{
    vector <long long> mergeDuration;
    vector <long long> quickDuration;
    for (int i = 5000; i<= 100000; i += 5000)
    {
        int* arr = generateArray(i);
        auto startTime = chrono::high_resolution_clock::now();
        quickSort(arr, 0, i - 1);
        auto endTime = chrono::high_resolution_clock::now();
        long long duration = chrono::duration_cast<chrono::milliseconds>(endTime - startTime).count();
        quickDuration.push_back(duration);
        delete[] arr;
    }
    for (int i = 5000; i <= 100000; i += 5000 )
    {
        int* arr = generateArray(i);
        auto startTime = chrono::high_resolution_clock::now();
        mergeSort(arr, i);
        auto endTime = chrono::high_resolution_clock::now();
        long long duration = chrono::duration_cast<chrono::milliseconds>(endTime - startTime).count();
        mergeDuration.push_back(duration);
        delete[] arr;
    }
    for (int i = 0; i<mergeDuration.size(); ++i)
    {
        cout << mergeDuration[i] << " ";
    }
    cout  << endl;
    for (int i = 0; i<quickDuration.size(); ++i)
    {
        cout << quickDuration[i] << " ";
    }
}

Ответы [ 2 ]

4 голосов
/ 29 марта 2019

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

Повторные элементы

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

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

algorithm quicksort(A, lo, hi) is
    if lo < hi then
        p := pivot(A, lo, hi)
        left, right := partition(A, p, lo, hi)  // note: multiple return values
        quicksort(A, lo, left - 1)
        quicksort(A, right + 1, hi)

Алгоритм разбиения возвращает индексы первому («крайнему левому») и до последнего ('самого правого') пункта среднего раздела. Каждый предмет раздел равен p и поэтому сортируется. Следовательно, элементы раздела не должны быть включены в рекурсивные вызовы быстрая сортировка.

Следующая модификация quickSort дает намного лучшие результаты:

pair<int,int> partition(int* arr, int left, int right)
{
    int idx = left + (right - left) / 2;
    int pivot = arr[idx]; // to be improved to median-of-three
    int i = left, j = left, b = right - 1;
    while (j <= b) {
        auto x = arr[j];
        if (x < pivot) {
            swap(arr[i], arr[j]);
            i++;
            j++;
        } else if (x > pivot) {
            swap(arr[j], arr[b]);
            b--;
        } else {
            j++;
        }
    }
    return { i, j };
}
void quickSort(int* arr, int left, int right)
{
    if (left < right)
    {
        pair<int, int> part = partition(arr, left, right);
        quickSort(arr, left, part.first);
        quickSort(arr, part.second, right);
    }
}

Выход:

0 1 2 3 4 5 6 7 8 9 11 11 12 13 14 15 16 19 18 19
0 0 0 1 0 1 1 1 1 1 2 3 2 2 2 2 3 3 3 3

0 1 2 3 4 5 6 6 8 8 9 12 11 12 13 14 16 17 18 19
0 0 1 1 1 2 3 3 3 4 4 4 5 6 5 6 7 7 8 8

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

2 голосов
/ 31 марта 2019

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

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

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

...