6
\$\begingroup\$

This is a follow-up to

Quicksort function

Like the previous code, this does not optimize for containers with large numbers of duplicate elements. How can I improve this code, particularly with iterators?

template<typename C>
void insertion_sort(C low, C high) {
    for (auto i = low; i < high; ++i) {
        for (auto j = i; j != low && *j < *(j-1); --j) {
            std::swap(*j, *(j-1));
        }
    }
}

template<typename C, typename I>
I partition(C& c, I low, I high) {
    auto p = *low;
    I i = low;
    I j = high;
    while (true) {
        while (i+1 != std::end(c) && *(++i) < p) {
            if (i == high) {
                break;
            }
        }
        while (p < *(--j)) { }
        if (i >= j) {
            break;
        }
        std::swap(*i, *j);
    }
    std::swap(*low, *j);
    return j;
}

template<typename C, typename I>
void sort(C& c, I low, I high) {
    if (high <= low+15) {
        insertion_sort(low, high);
        return;
    }
    I p = partition(c, low,high);
    sort(c, low, p);
    sort(c, p+1,high);
}

template<typename C>
void sort(C& c) {
    if (std::begin(c) == std::end(c)) {
        throw std::out_of_range("no size");
    }
    if (c.size() == 1) {
        return;
    }
    std::random_shuffle(std::begin(c), std::end(c));
    sort(c, std::begin(c), std::end(c));
}
\$\endgroup\$
1
  • \$\begingroup\$ You could fix most pathological cases with a Dual Pivot Quicksort. \$\endgroup\$
    – Veedrac
    Commented Jan 4, 2015 at 7:42

2 Answers 2

6
\$\begingroup\$

Sorting an empty array is perfectly valid.

if (c.size() == 0) {
    throw std::out_of_range("no size");
}

The result id an empty array. An exception is not the correct way to go.

OK optimize. An array of size 0 or 1 is already sorted.

if (c.size() == 1) {  // I would make this c.size() <= 1 (as I would remove the exception)
    return;
}

Special casing the std::end(c) just looks wrong.

   while (i+1 != std::end(c) && *(++i) < p) {

I would say that if i+1 == high would be a better test. Also adding the ++ and * operators into the expression makes it really hard to read. I would go with:

    while ((i < high) && (*i < p)) { ++i;}

The decrementing the j pointer should look exactly the same as the increment.

This is not tested (so may have bugs).
But this is how I would write partition.

// Note: this function require std::distance(low, high) >= 3
// Note: it is called from sort when std::distance(low, high) > 15
template<typename I>
std::pair<I,I> partition(I low, I high) {
    --high;
    I   pivot   = high;

    I   highBot = low;
    I   lowTop  = high;
    --lowTop;
    while (highBot < lowTop) {
        while ((highBot < high)  && (*highBot <= *pivot))   { ++highBot;}
        while ((lowTop >= low)   && (*pivot < *lowTop))     { --lowTop;}
        if (highBot < lowTop) {
            std::swap(*highBot, *lowTop);
        }
    }
    // At this point highBot and lowTop have just passed each other.
    // Thus highBot points to the first element greater than *pivot
    // and lowTop points to the last element smaller or equal to *pivot
    swap(*highBot, *pivot);

    // Now: highBot points to the pivot value and the value that was
    // at highBot is in the top end of the high range.

    // We want to to remove pivot value from the range of elements to be sorted.
    // so we want to sort the two ranges likes this:
    //
    //      [low..lowTop+1)
    //      highBot                 The pivot point removed from further sorting.
    //                              Note: lopTop+1 == highBot
    //      [highBot+1..high)
    //

    // Small optimization
    // If the value at lowTop is also equal to the pivot value
    // Then we can remove it from further sorting.
    for(; (lowTop >= low) && (*p == *lowTop); --lowTop) {}

    return std::make_pair(lowTop+1, highBot+1);
}

Usage:

std::pair<I,I> part = partition(low, high);
sort(c, low, part.first);
sort(c, part.second, high);
\$\endgroup\$
4
\$\begingroup\$

@Loki already covered the most important points, but there are still a few things that you could change to improve the algorithm further (provided you're still interested in this question one year later):

  • A well-designed sorting algorithm should be able to take advantage of user-defined swap functions instead of simply using std::swap everywhere; these user-defined overloads might be optimized. We can use argument-dependent lookup to do the job:

    using std::swap;
    swap(*it1, *it2);
    

    The first line tells the compiler to take std::swap into account for unqualified calls to swap. The second line makes an unqualified call to swap: the compiler will check the types of *it1 and *it2 and search for a swap function in their associated namespace. If the lookup finds such a function, it will use it, otherwise it will use std::swap instead. Note that you can also use std::iter_swap which does exactly that:

    std::iter_swap(it1, it2);
    
  • Algorithms that care about comparison generally allow to pass an additional compare parameter to use instead of operator<. It allows to sort types that do not define operator< and to sort things differently (for example, it allows to use std::greater<> to sort a collection in reverse order. Here is a comparison-enhanced insertion sort:

    template<typename C, typename Compare=std::less<>>
    void insertion_sort(C low, C high, Compare compare={}) {
        for (auto i = low; i < high; ++i) {
            for (auto j = i; j != low && compare(*j, *(j-1)); --j) {
                std::iter_swap(j, j - 1);
            }
        }
    }
    
  • std::sort is required to work with move-only types, and quicksort generally requires to copy the pivot before the partitioning operation to avoid tricky problems. To implement a quicksort without compying the pivot, the trick either to choose std::prev(high) as the pivot or to swap the pivot with std::prev(high) before the partitioning step.

  • It's a mere detail, but the condition if (high <= low+15) is not the easiest to reason about. I would use if (std::distance(low, high) <= 15), which tells more about the reasoning in my opinion.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.