当数组具有重复值时,为什么快速排序算法的持续时间会增加?

时间:2019-03-29 10:53:44

标签: c++ random time-complexity quicksort mergesort

我正在尝试使用std :: chrono时间计算并使用在[A,B]范围内的随机生成的整数数组来测量Merge Sort和Quick Sort函数的持续时间,该数组的大小从5000到100,000个整数。

我的代码的目的是证明,当改进快速排序中拾取(枢轴)的方法时,快速排序功能最终比合并排序花费的时间更少,这是我选择枢轴的方式正在使用随机索引方法来最小化具有(n ^ 2)复杂度的可能性,但是在某些情况下(我将在下面描述),快速排序最终比合并排序要花更多的时间,我想知道为什么这样做发生。

情况1: 数组中数字的范围很小,这增加了在数组中重复数字的可能性。

情况2: 当我使用像clion这样的本地IDE时,快速排序功能比合并排序要花费更多的时间,但是,像IDEONE.com这样的在线编译器在两种排序算法中都会得到相似的结果(即使生成的整数的范围很小) / p>

这是我在上述情况下得到的结果(第一行是合并排序结果,第二行是快速排序结果):

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 个答案:

答案 0 :(得分:4)

众所周知,当输入集包含大量重复项时,Quicksort的性能较差。解决方案是使用Wikipedia中所述的三向分区:

  

重复的元素

     

使用上述分区算法(甚至   选择一个好的枢轴值),quicksort表现不佳   包含许多重复元素的输入的性能。的   当所有输入元素都相等时,问题显而易见:   每次递归时,左分区为空(输入值均小于该值)   而不是枢轴),而右分区只减少了一个   元素(枢轴已删除)。因此,该算法需要   二次时间对等值数组进行排序。

     

要解决此问题(有时称为Dutch national flag problem),可以使用替代的线性时间分区例程   将值分为三个组:值小于   枢轴,值等于枢轴,值大于枢轴。   ... 价值   等于枢轴的点已经被排序,所以只有小于和   大于分区需要进行递归排序。用伪代码,   快速排序算法变为

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

因此,现在有很多重复的运行要快得多。

答案 1 :(得分:2)

  

当数组具有重复值时,为什么快速排序算法的持续时间会增加?

仅当使用Lomuto类型分区方案时,这是正确的,其中重复的值会使拆分变得更糟。

如果使用Hoare分区方案,则当数组具有重复值时,算法持续时间通常会减少,因为拆分更接近于精确拆分为一半的理想情况,改进的拆分可补偿具有内存的典型系统上的额外交换缓存。