Skip to content

Commit f6bcf27

Browse files
committed
[libc++] Introduce one-sided binary search for lower_bound on non-random iterators.
One-sided binary search, aka meta binary search, has been in the public domain for decades, and has the general advantage of being Ω(1) rather than the classic algorithm's Ω(log(n)), with the downside of executing at most 2*log(n) comparisons vs the classic algorithm's exact log(n). There are two scenarios in which it really shines: the first one is when operating over non-random iterators, because the classic algorithm requires knowing the container's size upfront, which adds Ω(n) iterator increments to the complexity. The second one is when you're traversing the container in order, trying to fast-forward to the next value: in that case, the classic algorithm would yield Ω(n*log(n)) comparisons and, for non-random iterators, Ω(n^2) iterator increments, whereas the one-sided version will yield O(n) operations on both counts, with a Ω(log(n)) bound on the number of comparisons.
1 parent b65415f commit f6bcf27

File tree

2 files changed

+110
-6
lines changed

2 files changed

+110
-6
lines changed

libcxx/include/__algorithm/iterator_operations.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,53 @@ struct _IterOps<_ClassicAlgPolicy> {
8787
std::advance(__iter, __count);
8888
}
8989

90+
// advance with sentinel, a la std::ranges::advance
91+
// it's unclear whether _Iter has a difference_type and whether that's signed, so we play it safe:
92+
// use the incoming type for returning and steer clear of negative overflows
93+
template <class _Iter, class _Distance>
94+
_LIBCPP_HIDE_FROM_ABI constexpr static _Distance advance(_Iter& __iter, _Distance __count, const _Iter& __sentinel) {
95+
return _IterOps::__advance(__iter, __count, __sentinel, typename iterator_traits<_Iter>::iterator_category());
96+
}
97+
98+
// advance with sentinel, a la std::ranges::advance -- InputIterator specialization
99+
template <class _InputIter, class _Distance>
100+
_LIBCPP_HIDE_FROM_ABI constexpr static _Distance
101+
__advance(_InputIter& __iter, _Distance __count, const _InputIter& __sentinel, input_iterator_tag) {
102+
_Distance __dist{};
103+
for (; __dist < __count && __iter != __sentinel; ++__dist)
104+
++__iter;
105+
return __count - __dist;
106+
}
107+
108+
// advance with sentinel, a la std::ranges::advance -- BidirectionalIterator specialization
109+
template <class _BiDirIter, class _Distance>
110+
_LIBCPP_HIDE_FROM_ABI constexpr static _Distance
111+
__advance(_BiDirIter& __iter, _Distance __count, const _BiDirIter& __sentinel, bidirectional_iterator_tag) {
112+
_Distance __dist{};
113+
if (__count >= 0)
114+
for (; __dist < __count && __iter != __sentinel; ++__dist)
115+
++__iter;
116+
else
117+
for (__count = -__count; __dist < __count && __iter != __sentinel; ++__dist)
118+
--__iter;
119+
return __count - __dist;
120+
}
121+
122+
// advance with sentinel, a la std::ranges::advance -- RandomIterator specialization
123+
template <class _RandIter, class _Distance>
124+
_LIBCPP_HIDE_FROM_ABI constexpr static _Distance
125+
__advance(_RandIter& __iter, _Distance __count, const _RandIter& __sentinel, random_access_iterator_tag) {
126+
auto __dist = _IterOps::distance(__iter, __sentinel);
127+
_LIBCPP_ASSERT_UNCATEGORIZED(
128+
__count == 0 || (__dist < 0) == (__count < 0), "__sentinel must precede __iter when __count<0");
129+
if (__count < 0)
130+
__dist = __dist > __count ? __dist : __count;
131+
else
132+
__dist = __dist < __count ? __dist : __count;
133+
__iter += __dist;
134+
return __count - __dist;
135+
}
136+
90137
// distance
91138
template <class _Iter>
92139
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14

libcxx/include/__algorithm/lower_bound.h

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727

2828
_LIBCPP_BEGIN_NAMESPACE_STD
2929

30-
template <class _AlgPolicy, class _Iter, class _Sent, class _Type, class _Proj, class _Comp>
31-
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20
32-
_Iter __lower_bound(_Iter __first, _Sent __last, const _Type& __value, _Comp& __comp, _Proj& __proj) {
33-
auto __len = _IterOps<_AlgPolicy>::distance(__first, __last);
34-
30+
template <class _AlgPolicy, class _Iter, class _Type, class _Proj, class _Comp>
31+
_LIBCPP_NODISCARD_EXT _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter __lower_bound_bisecting(
32+
_Iter __first,
33+
const _Type& __value,
34+
typename iterator_traits<_Iter>::difference_type __len,
35+
_Comp& __comp,
36+
_Proj& __proj) {
3537
while (__len != 0) {
3638
auto __l2 = std::__half_positive(__len);
3739
_Iter __m = __first;
@@ -46,13 +48,68 @@ _Iter __lower_bound(_Iter __first, _Sent __last, const _Type& __value, _Comp& __
4648
return __first;
4749
}
4850

51+
// One-sided binary search, aka meta binary search, has been in the public domain for decades, and has the general
52+
// advantage of being Ω(1) rather than the classic algorithm's Ω(log(n)), with the downside of executing at most
53+
// 2*(log(n)-1) comparisons vs the classic algorithm's exact log(n). There are two scenarios in which it really shines:
54+
// the first one is when operating over non-random iterators, because the classic algorithm requires knowing the
55+
// container's size upfront, which adds Ω(n) iterator increments to the complexity. The second one is when you're
56+
// traversing the container in order, trying to fast-forward to the next value: in that case, the classic algorithm
57+
// would yield Ω(n*log(n)) comparisons and, for non-random iterators, Ω(n^2) iterator increments, whereas the one-sided
58+
// version will yield O(n) operations on both counts, with a Ω(log(n)) bound on the number of comparisons.
59+
template <class _AlgPolicy, class _Iter, class _Sent, class _Type, class _Proj, class _Comp>
60+
_LIBCPP_NODISCARD_EXT _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter
61+
__lower_bound_onesided(_Iter __first, _Sent __last, const _Type& __value, _Comp& __comp, _Proj& __proj) {
62+
// static_assert(std::is_base_of<std::forward_iterator_tag, typename _IterOps<_AlgPolicy>::template
63+
// __iterator_category<_Iter>>::value,
64+
// "lower_bound() is a multipass algorithm and requires forward iterator or better");
65+
66+
using _Distance = typename iterator_traits<_Iter>::difference_type;
67+
for (_Distance __step = 1; __first != __last; __step <<= 1) {
68+
auto __it = __first;
69+
auto __dist = __step - _IterOps<_AlgPolicy>::advance(__it, __step, __last);
70+
// once we reach the last range where needle can be we must start
71+
// looking inwards, bisecting that range
72+
if (__it == __last || !std::__invoke(__comp, std::__invoke(__proj, *__it), __value)) {
73+
return std::__lower_bound_bisecting<_AlgPolicy>(__first, __value, __dist, __comp, __proj);
74+
}
75+
// range not found, move forward!
76+
__first = std::move(__it);
77+
}
78+
return __first;
79+
}
80+
81+
template <class _AlgPolicy, class _InputIter, class _Sent, class _Type, class _Proj, class _Comp>
82+
_LIBCPP_NODISCARD_EXT inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _InputIter __lower_bound(
83+
_InputIter __first, _Sent __last, const _Type& __value, _Comp& __comp, _Proj& __proj, std::input_iterator_tag) {
84+
return std::__lower_bound_onesided<_AlgPolicy>(__first, __last, __value, __comp, __proj);
85+
}
86+
87+
template <class _AlgPolicy, class _RandIter, class _Sent, class _Type, class _Proj, class _Comp>
88+
_LIBCPP_NODISCARD_EXT inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _RandIter __lower_bound(
89+
_RandIter __first,
90+
_Sent __last,
91+
const _Type& __value,
92+
_Comp& __comp,
93+
_Proj& __proj,
94+
std::random_access_iterator_tag) {
95+
const auto __dist = _IterOps<_AlgPolicy>::distance(__first, __last);
96+
return std::__lower_bound_bisecting<_AlgPolicy>(__first, __value, __dist, __comp, __proj);
97+
}
98+
99+
template <class _AlgPolicy, class _Iter, class _Sent, class _Type, class _Proj, class _Comp>
100+
_LIBCPP_NODISCARD_EXT inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Iter
101+
__lower_bound(_Iter __first, _Sent __last, const _Type& __value, _Comp&& __comp, _Proj&& __proj) {
102+
return std::__lower_bound<_AlgPolicy>(
103+
__first, __last, __value, __comp, __proj, typename _IterOps<_AlgPolicy>::template __iterator_category<_Iter>());
104+
}
105+
49106
template <class _ForwardIterator, class _Tp, class _Compare>
50107
_LIBCPP_NODISCARD_EXT inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20
51108
_ForwardIterator lower_bound(_ForwardIterator __first, _ForwardIterator __last, const _Tp& __value, _Compare __comp) {
52109
static_assert(__is_callable<_Compare, decltype(*__first), const _Tp&>::value,
53110
"The comparator has to be callable");
54111
auto __proj = std::__identity();
55-
return std::__lower_bound<_ClassicAlgPolicy>(__first, __last, __value, __comp, __proj);
112+
return std::__lower_bound<_ClassicAlgPolicy>(__first, __last, __value, std::move(__comp), std::move(__proj));
56113
}
57114

58115
template <class _ForwardIterator, class _Tp>

0 commit comments

Comments
 (0)