简体   繁体   中英

C++ std::set with a custom lower_bound

How would I perform a find() or lower_bound() function on a std::set using a comparator function that is independent of its key such that it still runs in O(log N) time?

Suppose I define a data type foo with two variables x and y and have a std::set<foo> that uses x as the key value.

struct foo {
    int x, y;
    foo(int x, int y) : x(x), y(y) {}
};

struct xCompare {
    bool operator() (const foo& i, const foo& j) const {
        return i.x < j.x;
    }
};

// Within main()
std::set<foo, xCompare> fooSetX;

Is it possible to perform a binary search using lower_bound() or some other function that compares the values of y ?

For the sake of this argument, assume that x and y are unique and independent of each other, and that given two foo variables foo1 and foo2 , if foo1.x < foo2.x , then foo1.y < foo2.y . This means that I cannot express y as a function of x , but is also ordered by y within fooSetX .

For example, given three foo(x,y) values (2,5), (3,9) and (5,10) within fooSet , a lower_bound() that takes y = 7 as the search term would return an iterator pointing to (3,9).

Currently, the way I go about solving this is by having two std::set<foo> s, ordered by x and y respectively. Whenever I need to search by y , I use the second std::set .

struct yCompare {
    bool operator() (const foo& i, const foo& j) const {
        return i.y < j.y;
    }
};

// Within main()
std::set<foo, yCompare> fooSetY;

// Inserting elements
fooSetX.insert(foo(2,5)); fooSetY.insert(foo(2,5));
fooSetX.insert(foo(3,9)); fooSetY.insert(foo(3,9));
fooSetX.insert(foo(5,10)); fooSetY.insert(foo(5,10));

// lower_bound() with y = 7
std::set<foo>::iterator it = fooSetY.lower_bound(foo(0,7)); // points to (3,9)

You cannot directly pass a custom comparator to std::set::lower_bound - you need to pass it to the class template itself, as it will be internally used to maintain the order of the objects (consequently making std::set::lower_bound work) .

Here's how the std::set template is defined :

template<
    class Key,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<Key>
> class set;

Compare is the only ordering customization point that allows you to provide a function object that will compare your objects as desired in place of std::less<Key> .

There's no way of adding additional ordering predicates to an std::set .


If you want an additional ordering on your objects that will allow you to achieve O(log N) lookups, you could use another ordered structure that is kept in sync with the original one. A std::set of pointers to objects in the first set that uses a different comparator could work. Example:

class MySet
{
private:
    std::set<Item, Comparator0> _set0;
    std::set<decltype(_set0.begin()), Comparator1> _set1;

public:
    void insert(Item x) 
    {
        auto res = _set0.insert(x);
        assert(res.second);

        _set1.insert(res.first);
    }

    const auto& lookup0(Key0 x) { return _set0.lower_bound(x); }
    const auto& lookup1(Key1 x) { return *(_set1.lower_bound(x)); }
};

Not with std::set, as @Vittorio Romeo points out in his answer.

There is a boost datastructure that can lookup by unrelated members, which you would define like

struct foo {
    int x, y;
    foo(int x, int y) : x(x), y(y) {}
};

// helpers
struct x_tag {}; 
struct y_tag {};

boost::multi_index_container<
    foo,
    indexed_by<
        ordered_unique<tag<x_tag>, boost::multi_index::member<foo, int, &foo::x>>, // std::less<int> applied to foo::x
        ordered_unique<tag<y_tag>, boost::multi_index::member<foo, int, &foo::y>> // std::less<int> applied to foo::y
    >
> fooSet;

int an_x, an_y;
// lookup by x
fooSet.get<x_tag>().find(an_x);
fooSet.get<y_tag>().find(an_y);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM