0

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)

2 Answers2

3

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)); }
};
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Oooh. So in the example mentioned in my question, how would the set be constructed (the actual code, I mean)? – Muhammad Irham Rasyidi Mar 17 '17 at 11:57
  • @MuhammadIrhamRasyidi: whoops, I misread your question - you're already passing a comparator to `std::set<...>`... Well, there's no way of using a different comparator than `yCompare` when calling `std::set::lower_bound`. – Vittorio Romeo Mar 17 '17 at 11:59
  • Aw, man. One of my ideas was to traverse the Binary Search Tree manually from root to leaf, but I have no idea how to do that. – Muhammad Irham Rasyidi Mar 17 '17 at 12:13
  • @MuhammadIrhamRasyidi: you could simply iterate through each value `for(const auto& v : fooSetY) { if(v.x == 10) { /* ... */ } }` - could that work for your use case? – Vittorio Romeo Mar 17 '17 at 12:18
  • However, that will run in O(N) instead of O(log N). Forgot to mention that in the question - time complexity is also an issue. – Muhammad Irham Rasyidi Mar 17 '17 at 12:23
  • @MuhammadIrhamRasyidi: if I get you correct, the idea is to store the objects somewhere, and then set up another way for further access. (E.g. store your objects in a `std::set`, and then set up a map with a different key-type which hold iterators to the respective elements in the set. (In a database framework, this would be similar to indices) – davidhigh Mar 17 '17 at 12:23
  • @MuhammadIrhamRasyidi: if that's the case, you need 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. – Vittorio Romeo Mar 17 '17 at 12:24
  • @MuhammadIrhamRasyidi: I updated my answer with a possible idea for an implementation – Vittorio Romeo Mar 17 '17 at 12:28
1

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);
Caleth
  • 52,200
  • 2
  • 44
  • 75