Search code examples
c++sortingc++11c++17stl-algorithm

partitioning a range of sorted elements into adjacent groups


I have a sorted list of items below. I need to determine how to pick [inclusive, exclusive) item pairs from this list such that the difference between them exceeds some fixed value (for example 5 in the example below).

This should therefore result in partitioning the list into adjacent ranges (no element left out).

I figured out a brute force way of doing this (see live COLIRU demo), but I am sure there must be a more elegant solution and I am probably missing some edge cases (for example 1 a list containing a single value should result in the empty pair). I was thinking that some variant of the stl range algorithms, std::adjacent_find or a combination of std::lower_bound/std::upper_bound could be used to determine these inclusive/exclusive pairs - for example using or some range based search in a loop or some sort - but I cannot figure it out.

The live searches the values { 100, 104, 108, 112, 116, 120 } resulting in the following non overlapping ranges. Note that the last pair (the difference is 4 (which is < 5) is a special case (see the code).

[100,104),[108,112),[116,120)

The code to do this is as follows:

#include <iostream>
#include <algorithm>
#include <experimental/iterator>
#include <string>
#include <vector>

int main()
{
    std::vector<int> elements = { 100, 104, 108, 112,  116, 120 };
    std::vector<std::pair<int, int>> result;
    auto current = elements.begin();
    while (current != std::prev(elements.cend())) {
        auto next = std::next(current);
        while (((*next - *current) < 5) && (next != std::prev(elements.cend()))) {
            ++next;
        }
        // consider edge case where we are at the end of the list
        if (next != std::prev(elements.cend())) {
            result.emplace_back(*current, *std::prev(next));
        } else {
            result.emplace_back(*current, *next);
        }
        current = next;
    }
    std::transform( result.cbegin(), result.cend(), std::experimental::make_ostream_joiner(std::cout, ","), 
       [](const auto& next){ return std::string("[") + std::to_string(next.first) + ',' +  std::to_string(next.second) + ')'; } );  
}

Solution

  • auto next = std::next(current);
    while (((*next - *current) < 5) && (next != std::prev(elements.cend()))) {
        ++next;
    }
    

    In a sorted list, we are looking for the first element at least 5 bigger than our current element right? That's precisely what std::lower_bound is for - which does a binary search instead of a linear one:

    auto next = std::lower_bound(std::next(current), elements.end(), *current + 5);
    

    Combine that with fixing your loop condition to go until the end of the list rather than one before the end (which just... looks wrong and needs some serious justification), and the whole body can just be:

    while (current != elements.end()) {
        auto next = std::lower_bound(std::next(current), elements.end(), *current + 5);
        result.emplace_back(*current, *std::prev(next));
        current = next;
    }
    

    Side-note. This:

    std::transform( result.cbegin(), result.cend(), std::experimental::make_ostream_joiner(std::cout, ","),·
       [](const auto& next){ return std::string("[") + std::to_string(next.first) + ',' +  std::to_string(next.second) + ')'; } );
    

    Does not really seem better to me than, say, this:

    bool first = true;
    for (auto const& [first, second] : result) {
        if (!first) std::cout << ',';
        first = false;
        std::cout << '[' << first << '',' << second << ']';
    }
    

    YMMV. I know people like saying "no raw loops", but I rarely see transform lead to readable code....