The best approach here is indeed a binary search, but any efficient order-based search will do perfectly well. You don't really have to implement the search and the data structure explicitly. You can use it indirectly by employing a standard associative container instead.
Since your ranges don't overlap, the solution is very simple. You can immediately use a std::map
for this problem to solve it in just a few lines of code.
For example, this is one possible approach. Let's assume that we are mapping an [ int, int ]
range to an int
value. Let's represent our ranges as closed-open ranges, i.e. if the original range is [0, 70]
, let's consider a [0, 71)
range instead. Also, let's use the value of 0
as a "reserved" value that means "no mapping" (as you requested in your question)
const int EMPTY = 0;
All you need to do is to declare a map from int
to int
:
typedef std::map<int, int> Map;
Map map;
and fill it with each end of your closed-open ranges. The left (closed) end should be mapped to the desired value the entire range is mapped to, while the right (open) end should be mapped to our EMPTY
value. For your example, it will look as follows
map[0] = r_a;
map[71] = EMPTY;
map[101] = r_b;
map[251] = EMPTY;
map[260] = r_c; // 260 adjusted from 201
map[401] = EMPTY;
(I adjusted your last range, since in your original example it overlapped the previous range, and you said that your ranges don't overlap).
That's it for initialization.
Now, in order to determine where a given value of i
maps to all you need to do is
Map::iterator it = map.upper_bound(i);
If it == map.begin()
, then i
is not in any range. Otherwise, do
--it;
If the it->second
(for the decremented it
) is EMPTY
, then i
is not in any range.
The combined "miss" check might look as follows
Map::iterator it = map.upper_bound(i);
if (it == map.begin() || (--it)->second == EMPTY)
/* Missed all ranges */;
Otherwise, it->second
(for the decremented it
) is your mapped value
int mapped_to = it->second;
Note that if the original ranges were "touching", as in [40, 60]
and [61, 100]
, then the closed-open ranges will look as [40, 61)
and [61, 101)
meaning that the value of 61
will be mapped twice during map initialization. In this case it is important to make sure that the value of 61
is mapped to the proper destination value and not to the value of EMPTY
. If you map the ranges as shown above in the left-to-right (i.e. increasing) order it will work correctly by itself.
Note, that only the endpoints of the ranges are inserted into the map, meaning that the memory consumption and the performance of the search depends only on the total number of ranges and completely independent of their total length.
If you wish, you can add a "guard" element to the map during the initialization
map[INT_MIN] = EMPTY;
(it corresponds to "negative infinity") and the "miss" check will become simpler
Map::iterator it = map.upper_bound(i);
assert(it != map.begin());
if ((--it)->second == EMPTY)
/* Missed all ranges */;
but that's just a matter of personal preference.
Of course, if you just want to return 0
for non-mapped values, you don't need to carry out any checking at all. Just take the it->second
from the decremented iterator and you are done.