Алгоритм быстрого выбора для односвязного списка C++

Мне нужен алгоритм, который может найти медиану односвязного списка с линейной временной сложностью O (n) и постоянной пространственной сложностью O (1).

РЕДАКТИРОВАТЬ: односвязный список представляет собой односвязный список в стиле C. Использование stl запрещено (контейнер, функции запрещены, все stl запрещено, например, std::forward_list). Не разрешено перемещать числа в любой другой контейнер (например, массив). Приемлемо иметь пространственную сложность O (logn), так как на самом деле это будет даже меньше 100 для моих списков. Также мне не разрешено использовать функции STL, такие как nth_element

В основном у меня есть связанный список с элементами 3 * 10 ^ 6, и мне нужно получить медиану за 3 секунды, поэтому я не могу использовать алгоритм сортировки для сортировки списка (это будет O (nlogn) и займет что-то вроде 10-14 секунд может быть).

Я провел некоторый поиск в Интернете и обнаружил, что можно найти медиану std::vector в пространстве O(n) и O(1) с помощью quickselect (наихудший случай находится в O(n^2), но встречается редко), например: https://www.geeksforgeeks.org/quickselect-a-simple-iterative-implementation/

Но я не могу найти алгоритм, который делает это для связанного списка. Проблема в том, что я могу использовать индекс массива для случайного доступа к вектору. Если я хочу изменить этот алгоритм, сложность будет намного больше, потому что. Например, когда я меняю сводной индекс влево, мне действительно нужно пройти по списку, чтобы получить этот новый элемент и пойти дальше (это даст мне как минимум O (kn) с большим k для моего списка, даже около O (n ^ 2)...).

РЕДАКТИРОВАТЬ 2:

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

#include <bits/stdc++.h>

using namespace std;

template <class T> class Node {
    public:
    T data;
    Node<T> *next;
};

template <class T> class List {
    public:
    Node<T> *first;
};

template <class T> T getMedianValue(List<T> & l) {
    Node<T> *crt,*pivot,*incpivot;
    int left, right, lung, idx, lungrel,lungrel2, left2, right2, aux, offset;
    pivot = l.first;
    crt = pivot->next;
    lung = 1;
//lung is the lenght of the linked list (yeah it's lenght in romanian...)
//lungrel and lungrel2 are the relative lenghts of the part of 
//the list I am processing, e.g: 2 3 4 in a list with 1 2 3 4 5
    right = left = 0;
    while (crt != NULL) { 
        if(crt->data < pivot->data){
            aux = pivot->data;
            pivot->data = crt->data;
            crt->data = pivot->next->data;
            pivot->next->data = aux;
            pivot = pivot->next;
            left++;
        }
        else right++;
       // cout<<crt->data<<endl;
        crt = crt->next; 
        lung++; 
    }
    if(right > left) offset = left;
//  cout<<endl;
//  cout<<pivot->data<<" "<<left<<" "<<right<<endl;
//  printList(l);
//  cout<<endl;
    lungrel = lung;
    incpivot = l.first;
   // offset = 0;
    while(left != right){
        //cout<<"parcurgere"<<endl;
        if(left > right){
            //cout<<endl;
            //printList(l);
            //cout<<endl;
            //cout<<"testleft "<<incpivot->data<<" "<<left<<" "<<right<<endl;
            crt = incpivot->next;
            pivot = incpivot;
            idx = offset;left2 = right2 = lungrel = 0;
            //cout<<idx<<endl;
            while(idx < left && crt!=NULL){
                 if(pivot->data > crt->data){
                   //  cout<<"1crt "<<crt->data<<endl;
                     aux = pivot->data;
                     pivot->data = crt->data;
                     crt->data = pivot->next->data;
                     pivot->next->data = aux;
                     pivot = pivot->next;
                     left2++;lungrel++;
                  }
                  else {
                      right2++;lungrel++;
                      //cout<<crt->data<<" "<<right2<<endl;
                  }
                  //cout<<crt->data<<endl;
                  crt = crt->next;
                  idx++;
             }
             left = left2 + offset;
             right = lung - left - 1;
             if(right > left) offset = left;
             //if(pivot->data == 18) return 18;
             //cout<<endl;
             //cout<<"l "<<pivot->data<<" "<<left<<" "<<right<<" "<<right2<<endl;
           //  printList(l);
        }
        else if(left < right && pivot->next!=NULL){
            idx = left;left2 = right2 = 0;
            incpivot = pivot->next;offset++;left++;
            //cout<<endl;
            //printList(l);
            //cout<<endl;
            //cout<<"testright "<<incpivot->data<<" "<<left<<" "<<right<<endl;
            pivot = pivot->next;
            crt = pivot->next;
            lungrel2 = lungrel;
            lungrel = 0;
           // cout<<"p right"<<pivot->data<<" "<<left<<" "<<right<<endl;
            while((idx < lungrel2 + offset - 1) && crt!=NULL){
                 if(crt->data < pivot->data){
                //     cout<<"crt "<<crt->data<<endl;
                     aux = pivot->data;
                     pivot->data = crt->data;
                     crt->data = (pivot->next)->data;
                     (pivot->next)->data = aux;
                     pivot = pivot->next;
                 //    cout<<"crt2 "<<crt->data<<endl;
                     left2++;lungrel++;
                  }
                  else right2++;lungrel++;
                  //cout<<crt->data<<endl;
                  crt = crt->next;
                  idx++;
             }
             left = left2 + left;
             right = lung - left - 1;
                 if(right > left) offset = left;
            // cout<<"r "<<pivot->data<<" "<<left<<" "<<right<<endl;
           //  printList(l);
        }
        else{
            //cout<<cmx<<endl;
            return pivot->data;
        }
    }
    //cout<<cmx<<endl;
    return pivot->data;
}
template <class T> void printList(List<T> const & l) {
    Node<T> *tmp;
    if(l.first != NULL){
        tmp = l.first;
        while(tmp != NULL){
            cout<<tmp->data<<" ";
            tmp = tmp->next;
        }
    }
}
template <class T> void push_front(List<T> & l, int x)
{
    Node<T>* tmp = new Node<T>;

    tmp->data = x;

    tmp->next = l.first;
    l.first = tmp;
}

int main(){
    List<int> l;
    int n = 0;
    push_front(l, 19);
    push_front(l, 12);
    push_front(l, 11);
    push_front(l, 101);
    push_front(l, 91);
    push_front(l, 21);
    push_front(l, 9);
    push_front(l, 6);
    push_front(l, 25);
    push_front(l, 4);
    push_front(l, 18);
    push_front(l, 2);
    push_front(l, 8);
    push_front(l, 10);
    push_front(l, 200);
    push_front(l, 225);
    push_front(l, 170);
    printList(l);
    n=getMedianValue(l);
    cout<<endl;
    cout<<n;

    return 0;
}

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


person alex cojocaru    schedule 20.03.2020    source источник
comment
Сортировка слиянием связанного списка (вы должны отсортировать, чтобы найти медиану). Вам придется использовать восходящую (итеративную) сортировку слиянием, чтобы не раздувать стек рекурсией с более чем ~ 100 тыс. узлов. Ваша сортировка произойдет менее чем за 3 секунды. время (своего рода узлы 3M должны занимать ~0,3 секунды). Вы можете искать и должны найти ряд примеров для обработки связанного списка.   -  person David C. Rankin    schedule 21.03.2020
comment
Я обновил сообщение с моим текущим кодом. Этот односвязный список является частью задачи, я не могу обновить этот список до двусвязного списка, но я могу вносить изменения в список, например перемещать в нем элементы. Я не могу выполнить полную сортировку из-за требуемой временной сложности (сравнительная сортировка не быстрее, чем O (nlogn)), я могу полагаться только на частичную сортировку, такую ​​как quickselect, которую я пытаюсь реализовать в своем коде. Я проверил свой код, и он работает для небольших списков с приличной сложностью. Примерно ~40 операций для списка из 18 элементов (несортированных).   -  person alex cojocaru    schedule 21.03.2020
comment
@alexcojocaru: Должно ли решение также эффективно обрабатывать отсортированные данные? Или решение может иметь временную сложность O (n ^ 2) в худшем случае (то есть, когда данные уже отсортированы)? Можем ли мы предположить, что данные не отсортированы?   -  person Andreas Wenzel    schedule 21.03.2020
comment
Наихудшая временная сложность O (n ^ 2) принимается для отсортированных данных (поскольку в большинстве случаев случай отсортированных данных не возникает, поскольку данные в связанном списке в основном случайны), но мне нужна средняя временная сложность O (kn), при этом k‹=5 для большинства случаев. Было бы лучше, если бы сложность O(kn) гарантировалась для всех возможных случаев. Но из того, что я искал, мы можем сделать это только с помощью алгоритма опорного выбора, такого как медиана медиан. Это увеличит среднюю временную сложность примерно до 24n, что недопустимо в моем случае. Предпочтителен случайный выбор поворота, но его сложно реализовать.   -  person alex cojocaru    schedule 21.03.2020
comment
@alexcojocaru: Можем ли мы предположить, что в списке нет повторяющихся элементов, то есть все значения уникальны? Я спрашиваю, потому что, если все числа одинаковы, в зависимости от алгоритма, это может привести к наихудшей временной сложности.   -  person Andreas Wenzel    schedule 23.03.2020
comment
Да, мы можем это предположить.   -  person alex cojocaru    schedule 24.03.2020
comment
@alexcojocaru: Насколько я могу судить, правило медианы трех лучше всего подходит для выбора опорной точки. Даже если это не гарантирует, что не будет наихудшей временной сложности O (n ^ 2), это, по крайней мере, делает ее маловероятной. Поэтому это то, что я использовал в своем ответе.   -  person Andreas Wenzel    schedule 29.03.2020


Ответы (2)


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

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

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

Если вы помните конец списка при первом его обходе, сохраняя указатель на конец, вам никогда не придется проходить его снова, чтобы найти конец. Кроме того, если вы используете стандартную схему разделов Lomuto (которую я не использую для причинам, изложенным ниже), то вы также должны поддерживать два указателя в списке, которые представляют индексы i и j стандартной схемы разделов Lomuto. Используя эти указатели, никогда не придется проходить список для доступа к одному элементу.

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

Теперь я создал свою собственную реализацию алгоритма QuickSelect для связанных списков, которую я разместил ниже. .

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

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

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

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

Одна существенная проблема, с которой я столкнулся, заключается в том, что для четного числа элементов медиана определяется как среднее арифметическое двух средних или медианных элементов. По этой причине я не могу просто написать функцию, аналогичную std::nth_element, потому что если, например, общее количество элементов равно 14, то я буду искать 7-й и 8-й по величине элемент. Это означает, что мне пришлось бы вызывать такую ​​функцию дважды, что было бы неэффективно. Поэтому вместо этого я написал функцию, которая может искать два медианных элемента одновременно. Хотя это делает код более сложным, потери производительности из-за дополнительной сложности кода должны быть минимальными по сравнению с преимуществом отсутствия необходимости вызывать одну и ту же функцию дважды.

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

В следующем коде я использую только стандартную библиотеку шаблонов (в частности, функцию std::nth_element) для тестирования моего алгоритма и проверки результатов. Я не использую ни одну из этих функций в моем реальном алгоритме.

#include <iostream>
#include <iomanip>
#include <cassert>

// The following two headers are only required for testing the algorithm and verifying
// the correctness of its results. They are not used in the algorithm itself.
#include <random>
#include <algorithm>

// The following setting can be changed to print extra debugging information
// possible settings:
// 0: no extra debugging information
// 1: print the state and length of all partitions in every loop iteraton
// 2: additionally print the contents of all partitions (if they are not too big)
#define PRINT_DEBUG_LEVEL 0

template <typename T>
struct Node
{
    T data;
    Node<T> *next;
};

// NOTE:
// The return type is not necessarily the same as the data type. The reason for this is
// that, for example, the data type "int" requires a "double" as a return type, so that 
// the arithmetic mean of "3" and "6" returns "4.5".
// This function may require template specializations to handle overflow or wrapping.
template<typename T, typename U>
U arithmetic_mean( const T &first, const T &second )
{
    return ( static_cast<U>(first) + static_cast<U>(second) ) / 2;
}

//the main loop of the function find_median can be in one of the following three states
enum LoopState
{
    //we are looking for one median value
    LOOPSTATE_LOOKINGFORONE,

    //we are looking for two median values, and the returned median
    //will be the arithmetic mean of the two
    LOOPSTATE_LOOKINGFORTWO,

    //one of the median values has been found, but we are still searching for
    //the second one
    LOOPSTATE_FOUNDONE
};

template <
    typename T, //type of the data
    typename U  //type of the return value
>
U find_median( Node<T> *list )
{
    //This variable points to the pointer to the first element of the current partition.
    //During the partition phase, the linked list will be broken and reassembled afterwards, so
    //the pointer this pointer points to will be nullptr until it is reassembled.
    Node<T> **pp_start = &list;

    //This pointer represents nothing more than the cached value of *pp_start and it is
    //not always valid
    Node<T> *p_start = *pp_start;

    //These pointers are maintained for accessing the middle of the list for selecting a pivot
    //using the "median-of-three" rule.
    Node<T> *p_middle;
    Node<T> *p_end;

    //result is not defined if list is empty
    assert( p_start != nullptr );

    //in the main loop, this variable always holds the number of elements in the current partition
    int num_total = 1;

    // First, we must traverse the entire linked list in order to determine the number of elements,
    // in order to calculate k1 and k2. If it is odd, then the median is defined as the k'th smallest
    // element where k = n / 2. If the number of elements is even, then the median is defined as the
    // arithmetic mean of the k'th element and the (k+1)'th element.
    // We also set a pointer to the nodes in the middle and at the end, which will be required later
    // for selecting a pivot according to the "median-of-three" rule.
    p_middle = p_start;
    for ( p_end = p_start; p_end->next != nullptr; p_end = p_end->next )
    {
        num_total++;
        if ( num_total % 2 == 0 ) p_middle = p_middle->next;
    }   

    // find out whether we are looking for only one or two median values
    enum LoopState loop_state = num_total % 2 == 0 ? LOOPSTATE_LOOKINGFORTWO : LOOPSTATE_LOOKINGFORONE;

    //set k to the index of the middle element, or if there are two middle elements, to the left one
    int k = ( num_total - 1 ) / 2;

    // If we are looking for two median values, but we have only found one, then this variable will
    // hold the value of the one we found. Whether we have found one can be determined by the state of
    // the variable loop_state.
    T val_found;

    for (;;)
    {
        //make p_start cache the value of *pp_start again, because a previous iteration of the loop
        //may have changed the value of pp_start
        p_start = *pp_start;

        assert( p_start   != nullptr );
        assert( p_middle  != nullptr );
        assert( p_end     != nullptr );
        assert( num_total != 0 );

        if ( num_total == 1 )
        {
            switch ( loop_state )
            {
            case LOOPSTATE_LOOKINGFORONE:
                return p_start->data;
            case LOOPSTATE_FOUNDONE:
                return arithmetic_mean<T,U>( val_found, p_start->data );
            default:
                assert( false ); //this should be unreachable
            }
        }

        //select the pivot according to the "median-of-three" rule
        T pivot;
        if ( p_start->data < p_middle->data )
        {
            if ( p_middle->data < p_end->data )
                pivot = p_middle->data;
            else if ( p_start->data < p_end->data )
                pivot = p_end->data;
            else
                pivot = p_start->data;
        }
        else
        {
            if ( p_start->data < p_end->data )
                pivot = p_start->data;
            else if ( p_middle->data < p_end->data )
                pivot = p_end->data;
            else
                pivot = p_middle->data;
        }

#if PRINT_DEBUG_LEVEL >= 1
        //this line is conditionally compiled for extra debugging information
        std::cout << "\nmedian of three: " << (*pp_start)->data << " " << p_middle->data << " " << p_end->data << " ->" << pivot << std::endl;
#endif

        // We will be dividing the current partition into 3 new partitions (less-than,
        // equal-to and greater-than) each represented as a linked list. Each list
        // requires a pointer to the start of the list and a pointer to the pointer at
        // the end of the list to write the address of new elements to. Also, when
        // traversing the lists, we need to keep a pointer to the middle of the list,
        // as this information will be required for selecting a new pivot in the next
        // iteration of the loop. The latter is not required for the equal-to partition,
        // as it would never be used.
        Node<T> *p_less    = nullptr, **pp_less_end    = &p_less,    **pp_less_middle    = &p_less;
        Node<T> *p_equal   = nullptr, **pp_equal_end   = &p_equal;
        Node<T> *p_greater = nullptr, **pp_greater_end = &p_greater, **pp_greater_middle = &p_greater;

        // These pointers are only used as a cache to the location of the end node.
        // Despite their similar name, their function is quite different to pp_less_end
        // and pp_greater_end.
        Node<T> *p_less_end    = nullptr;
        Node<T> *p_greater_end = nullptr;

        // counter for the number of elements in each partition
        int num_less = 0;
        int num_equal = 0;
        int num_greater = 0;

        // NOTE:
        // The following loop will temporarily split the linked list. It will be merged later.

        Node<T> *p_next_node = p_start;

        //the following line isn't necessary; it is only used to clarify that the pointers no
        //longer point to anything meaningful
        *pp_start = p_start = nullptr;

        for ( int i = 0; i < num_total; i++ )
        {
            assert( p_next_node != nullptr );

            Node<T> *p_current_node = p_next_node;
            p_next_node = p_next_node->next;

            if ( p_current_node->data < pivot )
            {
                //link node to pp_less
                assert( *pp_less_end == nullptr );
                *pp_less_end = p_less_end = p_current_node;
                pp_less_end = &p_current_node->next;
                p_current_node->next = nullptr;

                num_less++;
                if ( num_less % 2 == 0 )
                {
                    pp_less_middle = &(*pp_less_middle)->next;
                }
            }
            else if ( p_current_node->data == pivot )
            {
                //link node to pp_equal
                assert( *pp_equal_end == nullptr );
                *pp_equal_end = p_current_node;
                pp_equal_end = &p_current_node->next;
                p_current_node->next = nullptr;

                num_equal++;
            }
            else
            {
                //link node to pp_greater
                assert( *pp_greater_end == nullptr );
                *pp_greater_end = p_greater_end = p_current_node;
                pp_greater_end = &p_current_node->next;
                p_current_node->next = nullptr;

                num_greater++;
                if ( num_greater % 2 == 0 )
                {
                    pp_greater_middle = &(*pp_greater_middle)->next;
                }
            }
        }

        assert( num_total == num_less + num_equal + num_greater );
        assert( num_equal >= 1 );

#if PRINT_DEBUG_LEVEL >= 1
        //this section is conditionally compiled for extra debugging information
        {
            std::cout << std::setfill( '0' );
            switch ( loop_state )
            {
            case LOOPSTATE_LOOKINGFORONE:
                std::cout << "LOOPSTATE_LOOKINGFORONE k = " << k << "\n";
                break;
            case LOOPSTATE_LOOKINGFORTWO:
                std::cout << "LOOPSTATE_LOOKINGFORTWO k = " << k << "\n";
                break;
            case LOOPSTATE_FOUNDONE:
                std::cout << "LOOPSTATE_FOUNDONE k = " << k << " val_found = " << val_found << "\n";
            }
            std::cout << "partition lengths: ";
            std::cout <<
                std::setw( 2 ) << num_less    << " " <<
                std::setw( 2 ) << num_equal   << " " <<
                std::setw( 2 ) << num_greater << " " <<
                std::setw( 2 ) << num_total   << "\n";
#if PRINT_DEBUG_LEVEL >= 2
            Node<T> *p;
            std::cout << "less: ";
            if ( num_less > 10 )
                std::cout << "too many to print";
            else
                for ( p = p_less; p != nullptr; p = p->next ) std::cout << p->data << " ";
            std::cout << "\nequal: ";
            if ( num_equal > 10 )
                std::cout << "too many to print";
            else
                for ( p = p_equal; p != nullptr; p = p->next ) std::cout << p->data << " ";
            std::cout << "\ngreater: ";
            if ( num_greater > 10 )
                std::cout << "too many to print";
            else
                for ( p = p_greater; p != nullptr; p = p->next ) std::cout << p->data << " ";
            std::cout << "\n\n" << std::flush;
#endif
            std::cout << std::flush;
        }
#endif

        //insert less-than partition into list
        assert( *pp_start == nullptr );
        *pp_start = p_less;

        //insert equal-to partition into list
        assert( *pp_less_end == nullptr );
        *pp_less_end = p_equal;

        //insert greater-than partition into list
        assert( *pp_equal_end == nullptr );
        *pp_equal_end = p_greater;

        //link list to previously cut off part
        assert( *pp_greater_end == nullptr );
        *pp_greater_end = p_next_node;

        //if less-than partition is large enough to hold both possible median values
        if ( k + 2 <= num_less )
        {
            //set the next iteration of the loop to process the less-than partition
            //pp_start is already set to the desired value
            p_middle = *pp_less_middle;
            p_end = p_less_end;
            num_total = num_less;
        }

        //else if less-than partition holds one of both possible median values
        else if ( k + 1 == num_less )
        {
            if ( loop_state == LOOPSTATE_LOOKINGFORTWO )
            {
                //the equal_to partition never needs sorting, because all members are already equal
                val_found = p_equal->data;
                loop_state = LOOPSTATE_FOUNDONE;
            }
            //set the next iteration of the loop to process the less-than partition
            //pp_start is already set to the desired value
            p_middle = *pp_less_middle;
            p_end = p_less_end;
            num_total = num_less;
        }

        //else if equal-to partition holds both possible median values
        else if ( k + 2 <= num_less + num_equal )
        {
            //the equal_to partition never needs sorting, because all members are already equal
            if ( loop_state == LOOPSTATE_FOUNDONE )
                return arithmetic_mean<T,U>( val_found, p_equal->data );
            return p_equal->data;
        }

        //else if equal-to partition holds one of both possible median values
        else if ( k + 1 == num_less + num_equal )
        {
            switch ( loop_state )
            {
            case LOOPSTATE_LOOKINGFORONE:
                return p_equal->data;
            case LOOPSTATE_LOOKINGFORTWO:
                val_found = p_equal->data;
                loop_state = LOOPSTATE_FOUNDONE;
                k = 0;
                //set the next iteration of the loop to process the greater-than partition
                pp_start = pp_equal_end;
                p_middle = *pp_greater_middle;
                p_end = p_greater_end;
                num_total = num_greater;
                break;
            case LOOPSTATE_FOUNDONE:
                return arithmetic_mean<T,U>( val_found, p_equal->data );
            }
        }

        //else both possible median values must be in the greater-than partition
        else
        {
            k = k - num_less - num_equal;

            //set the next iteration of the loop to process the greater-than partition
            pp_start = pp_equal_end;
            p_middle = *pp_greater_middle;
            p_end = p_greater_end;
            num_total = num_greater;
        }
    }
}


// NOTE:
// The following code is not part of the algorithm, but is only intended to test the algorithm

// This simple class is designed to contain a singly-linked list
template <typename T>
class List
{
public:
    List() : first( nullptr ) {}

    // the following is required to abide by the rule of three/five/zero
    // see: https://en.cppreference.com/w/cpp/language/rule_of_three
    List( const List<T> & ) = delete;
    List( const List<T> && ) = delete;
    List<T>& operator=( List<T> & ) = delete;
    List<T>& operator=( List<T> && ) = delete;

    ~List()
    {
        Node<T> *p = first;

        while ( p != nullptr )
        {
            Node<T> *temp = p;
            p = p->next;
            delete temp;
        }
    }

    void push_front( int data )
    {
        Node<T> *temp = new Node<T>;

        temp->data = data;

        temp->next = first;
        first = temp;
    }

    //member variables
    Node<T> *first;
};

int main()
{
    //generated random numbers will be between 0 and 2 billion (fits in 32-bit signed int)
    constexpr int min_val = 0;
    constexpr int max_val = 2*1000*1000*1000;

    //will allocate array for 1 million ints and fill with random numbers
    constexpr int num_values = 1*1000*1000;

    //this class contains the singly-linked list and is empty for now
    List<int> l;
    double result;

    //These variables are used for random number generation
    std::random_device rd;
    std::mt19937 gen( rd() );
    std::uniform_int_distribution<> dis( min_val, max_val );

    try
    {
        //fill array with random data
        std::cout << "Filling array with random data..." << std::flush;
        auto unsorted_data = std::make_unique<int[]>( num_values );
        for ( int i = 0; i < num_values; i++ ) unsorted_data[i] = dis( gen );

        //fill the singly-linked list
        std::cout << "done\nFilling linked list..." << std::flush;
        for ( int i = 0; i < num_values; i++ ) l.push_front( unsorted_data[i] );

        std::cout << "done\nCalculating median using STL function..." << std::flush;

        //calculate the median using the functions provided by the C++ standard template library.
        //Note: this is only done to compare the results with the algorithm provided in this file
        if ( num_values % 2 == 0 )
        {
            int median1, median2;

            std::nth_element( &unsorted_data[0], &unsorted_data[(num_values - 1) / 2], &unsorted_data[num_values] );
            median1 = unsorted_data[(num_values - 1) / 2];
            std::nth_element( &unsorted_data[0], &unsorted_data[(num_values - 0) / 2], &unsorted_data[num_values] );
            median2 = unsorted_data[(num_values - 0) / 2];

            result = arithmetic_mean<int,double>( median1, median2 );
        }
        else
        {
            int median;

            std::nth_element( &unsorted_data[0], &unsorted_data[(num_values - 0) / 2], &unsorted_data[num_values] );
            median = unsorted_data[(num_values - 0) / 2];

            result = static_cast<int>(median);
        }

        std::cout << "done\nMedian according to STL function: " << std::setprecision( 12 ) << result << std::endl;

        // NOTE: Since the STL functions only sorted the array, but not the linked list, the 
        //       order of the linked list is still random and not pre-sorted.

        //calculate the median using the algorithm provided in this file
        std::cout << "Starting algorithm" << std::endl;
        result = find_median<int,double>( l.first );
        std::cout << "The calculated median is: " << std::setprecision( 12 ) << result << std::endl;

        std::cout << "Cleaning up\n\n" << std::flush;
    }
    catch ( std::bad_alloc )
    {
        std::cerr << "Error: Unable to allocate sufficient memory!" << std::endl;
        return -1;
    }

    return 0;
}

Я успешно протестировал свой код с миллионом случайно сгенерированных элементов, и он практически мгновенно нашел правильную медиану.

person Andreas Wenzel    schedule 28.03.2020
comment
Обратите внимание, что в коде моего первоначального ответа была ошибка, которую я исправил. В ревизии 17 истории редактирования я добавил две строки кода для исправления ошибки. - person Andreas Wenzel; 01.04.2020

Итак, что вы можете сделать, это использовать итераторы для удержания позиции. Я написал приведенный выше алгоритм для работы с std::forward_list. Я знаю, что это не идеально, но написал это быстро и надеюсь, что это поможет.

    int partition(int leftPos, int rightPos, std::forward_list<int>::iterator& currIter, 
    std::forward_list<int>::iterator lowIter, std::forward_list<int>::iterator highIter) {
        auto iter = lowIter;
        int i = leftPos - 1;
        for(int j = leftPos; j < rightPos - 1; j++) {
           if(*iter <= *highIter) {
               ++currIter;
               ++i;
               std::iter_swap(currIter, iter);
           }
           iter++;
        }
        std::forward_list<int>::iterator newIter = currIter;
        std::iter_swap(++newIter, highIter);
        return i + 1;
    }

   std::forward_list<int>::iterator kthSmallest(std::forward_list<int>& list, 
   std::forward_list<int>::iterator left, std::forward_list<int>::iterator right, int size, int k) {
       int leftPos {0};
       int rightPos {size};
       int pivotPos {0};

       std::forward_list<int>::iterator resetIter = left;
       std::forward_list<int>::iterator currIter = left;
       ++left;
       while(leftPos <= rightPos) {
           pivotPos = partition(leftPos, rightPos, currIter, left, right);

           if(pivotPos == (k-1)) {
               return currIter;
           } else if(pivotPos > (k-1)) {
               right = currIter;
               rightPos = pivotPos - 1;
           } else {
               left = currIter;
               ++left;
               resetIter = left;
               ++left;
               leftPos = pivotPos + 1;
           }

           currIter = resetIter;
       }

       return list.end();
  }

При вызове k-го итератора левый итератор должен быть на единицу меньше, чем вы намереваетесь начать. Это позволяет нам быть на одну позицию позади low в partition(). Вот пример его выполнения:

int main() {
    std::forward_list<int> list {10, 12, 12, 13, 4, 5, 8, 11, 6, 26, 15, 21};
    auto startIter = list.before_begin();
    int k = 6;
    int size = getSize(list);

    auto kthIter = kthSmallest(list, startIter, getEnd(list), size - 1, k);
    std::cout << k << "th smallest: " << *kthIter << std::endl;

    return 0;
}

6-й по величине: 10

person Brandon Manning    schedule 20.03.2020
comment
Если количество элементов четное, то медиана обычно определяется как среднее арифметическое двух средних значений. - person Andreas Wenzel; 20.03.2020
comment
Я просто давал ему версию quickselect на основе ссылки, которую он разместил с помощью std::forward_list, вы можете сделать вызов kthSmallest для двух средних значений, а затем получить среднее арифметическое, используя это. - person Brandon Manning; 21.03.2020
comment
Спасибо за Ваш ответ. К сожалению, мой связанный список является «настраиваемым», он не использует ничего из stl. Например. функция push_back() определена, я не использую stl и т. д. Может быть, я мог бы попытаться адаптировать код, который вы отправили в свой список... Мне действительно удалось настроить своего рода быстрый выбор для связанных списков. Проблема в том, что мне нужно лучше выбирать опорные точки, потому что, когда я их устанавливаю, он выбирает первый элемент в качестве опорной... - person alex cojocaru; 21.03.2020
comment
Без проблем. Если вы можете обновить свой вопрос с требованиями к списку, я могу помочь с улучшением этого решения. Если в вашем списке используется LegacyForwardIterator и у вас есть доступ к begin(), это может сработать. Просто нужно заменить там, где у меня есть forward_list, на ваш пользовательский. Определенно интересная задача под рукой. - person Brandon Manning; 21.03.2020
comment
@BrandonManning: В своем ответе я написал функцию, которая может искать два значения одновременно вместо одного, так что одну и ту же функцию не нужно вызывать дважды. Однако даже если неэффективно вызывать функцию дважды, эта неэффективность не меняет временной сложности алгоритма. Поэтому я голосую за ваш ответ. - person Andreas Wenzel; 29.03.2020
comment
@BrandonManning: я только что заметил, что вызов вашей функции kthSmallest дважды, как вы предлагаете, изменяет временную сложность на O (n ^ 2). Это связано с тем, что вы просто выбираете последний элемент в качестве опорного вместо использования правила медианы трех (которое я использую в своей реализации). Это приводит к наихудшей временной сложности O (n ^ 2) для уже отсортированных данных. Поскольку данные сортируются при первом вызове функции, вызов функции во второй раз с почти тем же k означает, что функция будет иметь дело с уже отсортированными данными при втором вызове функции. - person Andreas Wenzel; 01.04.2020
comment
@AndreasWenzel Я ценю обратную связь. Я читал о медиане из трех, чтобы гарантировать O (n), но у меня не было возможности добавить это к моему ответу. Я знаю, что определенно хочу продолжать улучшать то, что было сделано, хотя бы с возможностью использовать инструменты, доступные в C++. Я тоже проголосовал за вас, это была отличная работа. Другое возможное решение — использование процентилей и получение статистической медианы. - person Brandon Manning; 01.04.2020