Почему мой алгоритм рекурсивной быстрой сортировки имеет такие перекошенные разделы?

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

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

Включает в себя:

#include<algorithm>
#include<cstdlib>
#include<ctime>
#include<iostream>
unsigned int recurse(int*a,unsigned int length)
{
    int value=0;
    unsigned int depth=0;
    if(length<=1)
    {
        depth=1;
    }
    else if(length==2)
    {
        if(a[0]>a[1])
        {
            value=a[0];
            a[0]=a[1];
            a[1]=value;
        }
        depth=1;
    }
    else if(length==3)
    {
        if(a[0]>a[1])
        {
            value=a[0];
            a[0]=a[1];
            a[1]=value;
        }
        if(a[0]>a[2])
        {
            value=a[0];
            a[0]=a[2];
            a[2]=value;
        }
        if(a[1]>a[2])
        {
            value=a[1];
            a[1]=a[2];
            a[2]=value;
        }
        depth=1;
    }
    else
    {
        //unsigned int fifth=(length>>3)+(length>>4)+(length>>6);
        unsigned int middle=length>>1;
        unsigned int choices[5]={0,middle>>1,middle,middle+(middle>>1),length-1};
        unsigned int left=0;
        unsigned int right=length-1;
        unsigned int index=0;
        for(unsigned int i=0;i<4;i++)
        {
            index=i;
            while(a[choices[index]]>a[choices[index+1]])
            {
                value=a[choices[index]];
                a[choices[index]]=a[choices[index+1]];
                a[choices[index+1]]=value;
                if(index>0)
                {
                    index--;
                }
            }
        }
        while(right>left)
        {
            while((a[++left]<=a[middle])&&right>left);
            while((a[--right]>=a[middle])&&right>left);
            if(right>left)
            {
                value=a[left];
                a[left]=a[right];
                a[right]=value;
            }
        }
        value=a[middle];
        if(left<middle&&right<middle)
        {
            a[middle]=a[left];
            a[left]=value;
            middle=left;
        }
        else if(left>middle&&right>middle)
        {
            a[middle]=a[left-1];
            a[left-1]=value;
            middle=left-1;
        }
        long double y;
        x++;
        ratio+=y=fabs((long double)(length>>1)-(long double)middle)/length;
        if(length>=1048576)
        {
            cout<<middle<<" "<<length<<" "<<y<<endl;
        }
        depth=max(recurse(a,middle),recurse(a+middle+1,length-middle-1))+1;
    }
    return depth;
}

Глубина была только для подсчета глубины рекурсии. Я пытался понять, было ли это проблемой.

int main(int argl,char**argv)
{
    unsigned int length=0;
    cin>>length;
    cout<<length<<endl;
    int*numbers=new int[length];
    for(unsigned int i=0;i<length;i++)
    {
        numbers[i]=(int)rand()%(length<<1);
    }
    time_t start=time(0);
    mergesort(numbers,length);
    time_t end=time(0);
    cout<<end-start<<endl;
    for(unsigned int i=0;i<length;i++)
    {
        numbers[i]=(int)rand()%(length<<1);
    }
    start=time(0);
    unsigned int depth=recurse(numbers,length);
    end=time(0);
    cout<<end-start<<endl;
    cout<<"Depth: "<<depth<<endl;
    return 0;
}

Изменить: код для сортировки слиянием, как и требовалось:

void mergesort(int*a,unsigned int length)
{
    if(length<=1)
    {
        return;
    }
    else if(length==2)
    {
        if(a[0]>a[1])
        {
            int value=a[0];
            a[0]=a[1];
            a[1]=value;
        }
    }
    else
    {
        unsigned int index1=0,index2=0;
        unsigned int divide1=1,divide2=1;
        unsigned int merge=2;
        unsigned int start=0;
        int*b=new int[length];
        while(merge<=length)
        {
            while(index1<divide1&&index2<divide2)
            {
                if(a[start+index1]>a[start+divide1+index2])
                {
                    b[start+index1+index2]=a[start+divide1+index2++];
                }
                else
                {
                    b[start+index1+index2]=a[start+index1++];
                }
            }
            if(index1<divide1)
            {
                for(unsigned int i=index1;index1<divide1;index1++)
                {
                    b[start+index1+index2]=a[start+index1];
                }
            }
            else
            {
                for(unsigned int i=index2;index2<divide2;index2++)
                {
                    b[start+index1+index2]=a[start+divide1+index2];
                }
            }
            if(start+merge>=length)
            {
                if(start==0)
                {
                    merge<<=1;
                }
                else
                {
                    start=0;
                    index1=0;
                    index2=0;
                    divide1=merge;
                    divide2=merge<<1>length?length-divide1:merge;
                    merge=divide1+divide2;
                }
                for(unsigned int i=0;i<length;i++)
                {
                    a[i]=b[i];
                }
            }
            else
            {
                start+=merge;
                index1=0;
                index2=0;
                divide1=start+divide1>length?length-start:divide1;
                divide2=start+merge>length?max((int)(length-(start+divide1)),0):divide2;
            }
        }
    }
}

Результаты для 33554432 номеров: Изменить, изменить мой код, обновить результаты:

33554432
33554432
8
22
Depth: 765
0.4437349830864823

Последнее число — это среднее расстояние от центра до центра. Это почти 0,45, почти один к двадцати.


person DSOI__UNUNOCTIUM    schedule 06.04.2019    source источник
comment
Рекурсия достигает глубины 599 в случае размера 33554432 и 332 в случае размера 16777216. Я не думаю, что это вызовет временную сложность O (n ^ 2).   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
Это очень сложная быстрая сортировка. Я рекомендую сравнить то, что у вас есть, с эталонной реализацией.   -  person user4581301    schedule 06.04.2019
comment
@ user4581301 Я просмотрел один из алгоритмов разбиения, и он похож на тот, который я использовал, однако, посмотрев на то, как другие люди реализовали свою быструю сортировку, я думаю, что у меня есть небольшая разница в моем коде. Я постараюсь исправить это. Надеюсь это работает.   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
max(recurse(a,middle),recurse(a+middle+1,length-middle-1)) — это не std::max, поэтому я предполагаю, что существует макрос max, который оценивает свои параметры более одного раза.   -  person Raymond Chen    schedule 06.04.2019
comment
@RaymondChen Предполагается, что это максимальная функция <cmath>. Он берет более глубокий из двух рекурсивных вызовов и добавляет к нему один. Также он не может оценивать свои параметры более одного раза, так как оценка выполняется, и передаются только значения, функция max будет видеть только результирующие значения из рекурсивных вызовов.   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
Я не вижу функции max в <cmath>. cppreference и cplusplus отрицают существование такой функции.   -  person Raymond Chen    schedule 06.04.2019
comment
@RaymondChen Вы правы, я думаю, что использую максимум <algorithm> или ‹bits/stdc++.h›. Хотя я не верю, что это проблема.   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
@dsoi перестань верить и начни доказывать, так или иначе. Все должно быть правильно, чтобы код был быстрым и правильным; одна неправильная вещь делает ваш код медленным или неправильным. Здесь есть куча опасных вещей, от использования пространства имен std, необработанного нового/удаления, использования необработанного указателя, ручных свопов, многих разветвленных ifs... Я был бы удивлен, если бы код сделал то, что задумал автор, а не наоборот. Начните добавлять комментарии, которые логически доказывают инварианты, которые вместе доказывают правильность. Или найдите известный рабочий псевдокод и выровняйте свой код с комментариями.   -  person Yakk - Adam Nevraumont    schedule 06.04.2019
comment
Как прокомментировал пользователь 4581301, эта реализация сложна. Похоже, это усложненная версия схемы разделения Hoare. Ключевое отличие состоит в том, что обычная схема разделения Хоара использует узкие циклы (два оператора while) для сканирования массива с концов, прежде чем выполнять обмен, и продолжается до тех пор, пока индексы не пересекутся где-то внутри массива, в то время как ваш код использует дополнительные переменные и условные операторы (если), которые замедляют процесс.   -  person rcgldr    schedule 06.04.2019
comment
Я изменил свой код на то, что вы сказали @rcgldr, но он все еще примерно в три раза медленнее, чем сортировка слиянием.   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
Конечно, было бы неплохо, если бы вы включили включение. Представленный код не компилируется.   -  person Gardener    schedule 06.04.2019
comment
@Gardener Хорошо, я отредактировал свой пост, чтобы включить его.   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
Все еще ищет включение сортировки слиянием. Сортировка слиянием из stdlib.h имеет другую подпись. Я, наверное, просто глуп, чтобы не видеть, как это скомпилируется.   -  person Gardener    schedule 06.04.2019
comment
@Gardener Я не использовал встроенную сортировку слиянием, я реализовал свою собственную сортировку слиянием, чтобы проверить ее на быструю сортировку. Я вставлю код для этого.   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
Посмотрите на вывод отсортированных массивов для вашего алгоритма recurse для lengths 50 и 60. Они отсортированы? Anddddd, вы также можете проверить вывод mergesort.   -  person eric    schedule 06.04.2019
comment
@eric Мне кажется, все в порядке. 1 5 6 6 8 16 18 23 23 23 26 29 29 29 29 31 35 37 37 38 39 40 40 41 41 42 42 44 44 46 47 48 48 50 54 56 57 59 62 64 66 70 76 78 82 84 88 90 90 93 для 50 и 6 9 9 10 11 17 17 18 21 23 24 24 26 26 28 30 33 33 34 35 35 36 38 39 40 42 42 43 44 45 46 48 50 56 57 57 58 62 64 65 66 68 69 69 72 76 79 80 84 86 88 90 93 101 101 106 110 110 112 113   -  person DSOI__UNUNOCTIUM    schedule 06.04.2019
comment
Следите за <bits/stdc++.h>. Все в каталоге битов GCC является внутренней реализацией и не предназначено для прямого использования. <bits/stdc++.h>, например, должен помочь в предварительно скомпилированных заголовках и ускорить компиляцию. При неправильном использовании сильно замедляет компиляцию. Кроме того, задействовав всю стандартную библиотеку, вы добавили десятки тысяч идентификаторов, которые не используете, что привело к возникновению минного поля, от которого вас могут спасти только пространства имен. И если вам using namespace std;, вы лишились этой защиты.   -  person user4581301    schedule 07.04.2019
comment
Да, я не включаю это, теперь включено в пост.   -  person DSOI__UNUNOCTIUM    schedule 07.04.2019
comment
Глубина рекурсии 599 для набора данных размером 33554432 неразумна. Это означает, что вы в среднем разбиваете объекты на 3% и 97%. Если бы вы разбивали пополам, вы бы ожидали глубину 25. Это говорит о том, что в вашем коде разбиения есть недостаток.   -  person Raymond Chen    schedule 08.04.2019
comment
@RaymondChen, вы, вероятно, правы, я думаю, я проверю разделы и то, где заканчивается стержень.   -  person DSOI__UNUNOCTIUM    schedule 08.04.2019
comment
@RaymondChen Я проверил, и он довольно неравномерный, делящий массив в соотношении 1 к 24. Тем не менее, я пытался использовать средний элемент только в качестве опорного элемента, а также случайный опорный элемент, оба они были одинаково односторонними. Что теперь?   -  person DSOI__UNUNOCTIUM    schedule 12.04.2019
comment
Убедитесь, что результат разбиения правильный. Например, начните с массива, который уже отсортирован. Медиана из трех должна делиться на две равные половины. Это просто отладка.   -  person Raymond Chen    schedule 12.04.2019
comment
@RaymondChen Да, он делит уже отсортированный массив на две равные половины, однако я понял, что мой алгоритм даже не сортирует правильно. Теперь у меня есть большая проблема, которую нужно решить.   -  person DSOI__UNUNOCTIUM    schedule 12.04.2019


Ответы (1)


Нет другого способа сделать это, кроме как ответить здесь. Если вы собираетесь позвонить new, сделайте соответствующий звонок delete.

rand() не такой случайный, рассмотрите возможность использования случайной библиотеки С++ 11.

Для краткости я не включил ваш алгоритм, но и не внес в него никаких изменений. Я запускал этот код как на Ubuntu, так и на MacOS.

int main() {
    // Here are 50 random numbers I get when I use your method, I have put
    // them in a vector.
    std::vector<int> v = {7, 49, 89, 74, 34, 8, 24, 62, 35, 13, 24, 53,12,
                    2, 51, 71, 55, 49, 88, 52, 15, 49, 45, 5, 88, 21,
                    75, 54, 8, 7, 25, 50, 8, 19, 2, 33, 19, 13, 3, 69,
                    31, 80, 49, 72, 77, 65, 44, 43};

    // I set the size of the vector to be the size of this raw array.
    unsigned int length = v.size();

    int* numbers= new int[length];

    for(int i = 0; i < length; i++)
        numbers[i] = v[i];

    // 7 49 89 74 34 8 24 62 35 13 24 53 12 2 51 71 55...
    for (int i = 0; i < length; i++)
        std::cout << numbers[i] << " ";
    std::cout << std::endl;

    unsigned int depth = recurse(numbers, length);

    // 5 2 3 2 7 7 8 12 8 13 8 13 15 19 19 24...
    for (int i = 0; i < length; i++)
        std::cout << numbers[i] << " ";

    delete[] numbers;
    return 0;
}

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

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

Лучший,

person eric    schedule 06.04.2019
comment
Это очень интересно. Я не уверен, что на самом деле здесь происходит, но я думаю, что проверю это. Подождите, я не уверен, что вы подразумеваете под нулем всего в массиве, кроме последнего элемента. Не могли бы вы уточнить? - person DSOI__UNUNOCTIUM; 06.04.2019
comment
Он берет заданный массив и перезаписывает исходные значения таким образом, что оно становится чем-то вроде [0, 0, 0, 0, 0, 0... 89]. Я просто запускаю то, что вы нам дали, так что mergesort(numbers,length), но на числах, которые rand() выдает на моей машине, которые я дал вам для справки. - person eric; 06.04.2019