Реализовываете трехстороннее разбиение Bentley-McIlroy с использованием итераторов STL? - PullRequest
1 голос
/ 01 сентября 2011

В своем выступлении «Быстрая сортировка оптимальна» Седжвик и Бентли ссылаются на модифицированную версию шага быстрого разделения, называемую трехсторонним разделением Бентли-Макилроя. Эта версия шага разбиения изящно адаптируется к входам, содержащим равные ключи, всегда вытаскивая копии элемента сводки из того, что осталось, гарантируя, что при вызове массивов, содержащих дубликаты, алгоритм по-прежнему работает хорошо.

C-код для этого шага разбиения перепечатан здесь:

void threeWayPartition(Item a[], int l, int r)
{ 
  int i = l-1, j = r, p = l-1, q = r; Item v = a[r];
  if (r <= l) return;
  for (;;)
    { 
       while (a[++i] < v) ;
       while (v < a[--j]) if (j == l) break;
       if (i >= j) break;
       exch(a[i], a[j]);
       if (a[i] == v) { p++; exch(a[p], a[i]); }
       if (v == a[j]) { q--; exch(a[j], a[q]); }
    } 
  exch(a[i], a[r]); j = i-1; i = i+1;
  for (k = l; k < p; k++, j--) exch(a[k], a[j]);
  for (k = r-1; k > q; k--, i++) exch(a[i], a[k]);
}

Я заинтересован в реализации этой версии быстрой сортировки в качестве алгоритма STL (только для моего собственного назидания, а не в качестве замены для очень быстрого std::sort). Чтобы сделать это, я бы идеально принял в качестве входных данных для алгоритма диапазон итераторов STL, определяющих диапазон, который должен быть отсортирован. Поскольку быстрая сортировка не требует произвольного доступа, я надеюсь, что эти итераторы будут двунаправленными итераторами, поскольку это сделает алгоритм более общим и позволит мне сортировать std::list s и другие контейнеры, поддерживающие только двунаправленный доступ.

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

int i = l-1, p = l-1;

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

У меня такой вопрос: без существенного переписывания ядра алгоритма , есть ли способ адаптировать этот код для использования итераторов в стиле STL, учитывая, что алгоритм начинается с резервного копирования итератора на начало диапазона? Прямо сейчас единственные мысли, которые у меня были, - это ввести дополнительные переменные, чтобы «притвориться», что мы сделали резервную копию итератора на первом шаге, или украсить итераторы специальными адаптерами итераторов, которые позволяют выполнять резервное копирование перед началом, просто отслеживая сколько у вас логических шагов до начала диапазона. Ничто из этого не кажется очень элегантным ... я что-то упустил? Есть ли простое решение?

Спасибо!

Ответы [ 4 ]

2 голосов
/ 05 марта 2015

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

Это реализует 3-стороннее разбиение Bentley-McIlroy оптимальным образом.для использования с двунаправленными итераторами:

template <typename Bi1, typename Bi2>
  Bi2 swap_ranges_backward(Bi1 first1, Bi1 last1, Bi2 last2)
  {
        typedef typename std::reverse_iterator<Bi1> ri1;
        typedef typename std::reverse_iterator<Bi2> ri2;
        return std::swap_ranges(ri1(last1), ri1(first1), ri2(last2)).base();
  }

template <typename Bi, typename Cmp>
  std::pair<Bi, Bi>
  partition3(Bi first, Bi last,
    typename std::iterator_traits<Bi>::value_type pivot, Cmp comp)
  {
        Bi l_head = first;
        Bi l_tail = first;

        Bi r_head = last;
        Bi r_tail = last;

        while ( true )
         {
           // guarded to avoid overruns.
           //
           // @note this is necessary since ordered comparisons are
           // unavailable for bi-directional iterator types.
           while ( true )
              if (l_tail == r_head)
                 goto fixup_final;
              else if (comp(*l_tail, pivot))
                 ++l_tail;
              else
                 break;
           --r_head;
           while ( true )
              if (l_tail == r_head)
                 goto fixup_right;
              else if (comp(pivot, *r_head))
                 --r_head;
              else
                 break;

           std::iter_swap(l_tail, r_head);

           // compact equal to sequence front/back.
           if (!comp(*l_tail, pivot))
              std::iter_swap(l_tail, l_head++);
           if (!comp(pivot, *r_head))
              std::iter_swap(r_head, --r_tail);
           ++l_tail;
         }

fixup_right:
        // loop exited before chance to eval.
        if (!comp(pivot, *r_head))
           ++r_head;
fixup_final:
        // swap equal to partition point.
        if ((l_tail - l_head) <= (l_head - first))
           l_tail = std::swap_ranges(l_head, l_tail, first);
        else
           l_tail = swap_ranges_backward(first, l_head, l_tail);

        if ((r_tail - r_head) <= (last - r_tail))
           r_head = swap_ranges_backward(r_head, r_tail, last);
        else
           r_head = std::swap_ranges(r_tail, last, r_head);
        // equal range in values equal to pivot.
        return std::pair<Bi, Bi>(l_tail, r_head);
  }

Примечание: Это было проверено с использованием пакета проверки Bentley.Приятным побочным эффектом охраняемого аванса является то, что эта функция безопасна для общего назначения (без ограничений на pivot или длину последовательности).

Пример использования,

template<typename Bi, typename Cmp>
  void qsort_bi(Bi first, Bi last, Cmp comp)
  {
        auto nmemb = std::distance(first, last);
        if (nmemb <= 1)
           return;
        Bi pivot = first;
        std::advance(pivot, std::rand() % nmemb);

        std::pair<Bi, Bi> equal = partition3(first, last, *pivot, comp);
        qsort_bi(first, equal.first, comp);
        qsort_bi(equal.second, last, comp);
  }

template<typename Bi>
  void qsort_bi(Bi first, Bi last)
  {
        typedef typename std::iterator_traits<Bi>::value_type value_type;
        qsort_bi(first, last, std::less<value_type>());
  }

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

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

Моя рекомендация?Восходящая итеративная сортировка, используемая sgi STL.Это доказано, стабильно, просто и быстро (гарантировано n * log (n)).К сожалению, у этого алгоритма нет уникального имени, и мне не удалось найти ссылку на реализацию в отдельности, поэтому он повторяется здесь.

Это очень удобный алгоритм, он работает в некотором родеаналог бинарного счетчика (с непустыми списками, равными единице).Счетчик содержит списки размеров 2 ^ index (то есть 1,2,4,8 ...).При добавлении каждого элемента (бита) может быть инициирован перенос, который будет каскадно добавляться в списки более высокого порядка (двоичное сложение).

template <typename Tp>
  void msort_list(std::list<Tp>& in)
  {
        std::list<Tp> carry;
        std::list<Tp> counter[64];
        int fill = 1;

        while (!in.empty()) {
            carry.splice(carry.begin(), in, in.begin());
            int i = 0;
            for (; !counter[i].empty(); i++) {
                // merge upwards for stability.
                counter[i].merge(carry);
                counter[i].swap(carry);
            }
            counter[i].swap(carry);
            if (i == fill) ++fill;
        }
        for (int i = 1; i < fill; i++)
            counter[i].merge(counter[i-1]);
        in.swap(counter[fill-1]);
  }

Примечание: Эта версия отличается от оригинала впара способов. 1) Мы начинаем fill с единицы вместо нуля, это позволяет нам пропустить проверку размера и выполнить финальную замену, не влияя иначе на поведение. 2) Исходное условие внутреннего цикла добавляет i < fill, эта проверка является посторонней (вероятно, удержание от версии, где массив счетчиков был динамическим).

1 голос
/ 01 сентября 2011

without substantially rewriting the core of the algorithm

Это в значительной степени ограничивает вас попыткой разобраться с проблемой границы, поэтому вам потребуется использовать пользовательский адаптер итератора или обернуть итераторв boost::optional или что-то подобное, чтобы вы знали, когда это первый доступ.

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

Я не знаю, является ли правильным , но он описывает алгоритм по-другому, не требуя, чтобы итератор выходил изграницы.


Редактировать: Сказав это, я попробовал.Этот код не проверен, так как я не знаю, как должен выглядеть исходящий результат при вводе - смотрите комментарии для деталей.Это дало бы шанс работать только для итераторов двунаправленного / произвольного доступа.
#include <algorithm>
#include <iterator>

template <class Iterator>
void three_way_partition(Iterator begin, Iterator end)
{
    if (begin != end)
    {
        typename Iterator::value_type v = *(end - 1);

        // I can initialise it to begin here as its first use in the loop has
        // changed to post-increment (its pre-increment in your original
        // algorithm).
        Iterator i = begin;

        Iterator j = end - 1;

        // This should be begin - 1, but thats not valid, I set it to end
        // to act as a sentinal value, that way I know when im incrementing
        // p for the first time, and can set it to begin.
        Iterator p = end;

        Iterator q = end - 1;

        for (;;)
        {
            while (*(i++) < v);

            while (v < *(--j))
            {
                if (j == begin)
                {
                    break;
                }
            }

            if (std::distance(i, j) <= 0)
            {
                break;
            }

            if (*i == v)
            {
                if (p == end)
                {
                    p = begin;
                }
                else
                {
                    ++p;
                }

                std::iter_swap(p, i);
            }

            if (v == *j)
            {
                --q;
                std::iter_swap(j, q);
            }
        }

        std::iter_swap(i, end - 1);

        j = i - 1;
        i++;

        for (Iterator k = begin; k < p; ++k, --j)
        {
            std::iter_swap(k, j);
        }

        for (Iterator k = end - 2; k > q; --k, ++i)
        {
            std::iter_swap(i, k);
        }
    }
}
0 голосов
/ 04 марта 2015

Учитывая все перестановки, которые выполняет функция, не будет ли проще (и, возможно, более эффективно) просто выполнить следующее,

template <typename For, typename Cmp>
  std::pair<For, For>
  partition_3way(For first, For last,
          typename std::iterator_traits<For>::value_type pivot, Cmp comp)
  {
        For lower = std::partition(first, last, std::bind2nd(comp, pivot));
        For upper = std::partition(lower, last,
                std::not1(std::bind1st(comp, pivot)));
        // return equal range for elements equal to pivot.
        return std::pair<For, For>(lower, upper);
  }
0 голосов
/ 01 сентября 2011

К сожалению, выражение "k

if (i >= j) break; 

должен уйти и быть заменен на

if (i == j) break;

, что означает, что вам нужно будет добавить дополнительные условия во "внутренние" циклы, чтобы гарантировать, что j (в частности) не будет уменьшено слишком далеко. Нетто / нет ваше ограничение «без существенной перезаписи» не может быть выполнено при выполнении этого алгоритма для двунаправленных итераторов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...