获取位于特定区间的已排序值列表的子列表的最短方法

Shortest way to obtain a sublist of a sorted list of values lying in a certain interval

本文关键字:列表 方法 排序 于特定 区间 获取      更新时间:2023-10-16

今天我问自己,在排序向量std::vector<double>中获取所有值的最短代码是什么,这些值大于或等于a,小于或等于b

我的第一个方法是这样的:

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
// Returns all values in sortedValues being greater equal start and smaller equal end;
std::vector<double> cutValues(const std::vector<double>& sortedValues, double start, double end) {
std::vector<double> ret;
auto startIter=std::lower_bound(sortedValues.begin(), sortedValues.end(), start);
auto stopIter = std::upper_bound(sortedValues.begin(), sortedValues.end(), end);
std::copy(startIter, stopIter, std::back_inserter(ret));
return ret;
}
int main(int argc, char **args) {
{
auto ret = cutValues({ 0.1,0.2,0.3 }, 0.1, 0.3);
std::copy(ret.begin(), ret.end(), std::ostream_iterator<double>(std::cout, ","));
std::cout << std::endl;
}
{
auto ret = cutValues({ 0.12,0.2,0.31 }, 0.1, 0.3);
std::copy(ret.begin(), ret.end(), std::ostream_iterator<double>(std::cout, ","));
std::cout << std::endl;
}
{
auto ret = cutValues({ 0.1,0.2,0.3 }, 0.2, 0.2);
std::copy(ret.begin(), ret.end(), std::ostream_iterator<double>(std::cout, ","));
std::cout << std::endl;
}
}

我的第二个想法非常简单,如下所示:

std::vector<double> cutValues2(const std::vector<double>& sortedValues, double start, double end) {
std::vector<double> ret;
std::copy_if(sortedValues.begin(), sortedValues.end(), std::back_inserter(ret), [&start, &end](auto v) { return v >= start && v <= end; });
return ret;
}

但是考虑到仅从非常大的载体中去除一小部分的情况,它可能有一些效率问题。

现在我问自己,有没有更好的方法可以做到这一点?

第一个版本稍有改动:

std::vector<double> cutValues(const std::vector<double>& sortedValues, double start, double end) {
auto startIter = std::lower_bound(sortedValues.begin(), sortedValues.end(), start);
auto stopIter = std::upper_bound(startIter, sortedValues.end(), end);
return std::vector<double>(startIter, stopIter);
}

不是更短,但通常更有效:使用std::lower_bound找到"有趣"区间的开始,只要元素小于最大值,就继续复制;这样你就可以快速定位(O(log N((开始,即使是大向量,你也不会浪费时间再次进行二叉搜索寻找区间的结束 - 无论如何你都会在复制循环中找到它。

无论如何,可能的额外比较实际上是免费的,因为比较本身非常便宜double分支被很好地预测(在副本结束之前总是相同的(,并且依赖于缓存中已有的数据;与额外的二进制搜索相比,后者在内存中"跳来跳去"(更差的缓存局部性(并且具有更难预测的分支。

std::vector<double> cutValues(const std::vector<double>& sortedValues, double minv, double maxv) {
std::vector<double> ret;
auto end = sortedValues.end();
for(auto it = std::lower_bound(sortedValues.begin(), end, minv);
it != end && *it <= max; ++it) {
ret.push_back(*it);
}
return ret;
}

这可以以类似于使用 STL 算法的cutValues2方式重写,但我不确定它是否有多大的改进。

std::vector<double> cutValues(const std::vector<double>& sortedValues, double minv, double maxv) {
std::vector<double> ret;
std::copy_if(
std::lower_bound(sortedValues.begin(), sortedValues.end(), minv),
sortedValues.end(),
std::back_inserter(ret),
[=](double v) { return v <= maxv; });
return ret;
}

你可以编写你的函数 STL 风格,让它与任何类型的任何容器一起工作(恕我直言,它会更优雅(。

template <typename ForwardIt, typename T>
auto bounded_range(ForwardIt first, ForwardIt last, T lb, T ub) {
return std::pair{std::lower_bound(first, last, lb), std::upper_bound(first, last, ub)};
}
std::vector<double> v1{0.1, 0.1, 0.2, 0.3, 0.3, 0.5, 0.7, 0.9};
auto [beg, end] = bounded_range(std::begin(v), std::end(v), 0.3, 0.5);
std::vector<double> v2{beg, end};
// Or using 'std::make_from_tuple'.
auto v3 = std::make_from_tuple<std::vector<double>>(bounded_range(std::begin(v), std::end(v), 0.3, 0.5));

注意:示例对类模板和结构化绑定使用 C++17 模板参数推导。

一旦你知道你的间隔从哪里开始,就没有必要再次查看孔向量来找到结束迭代器。改为在start_iterator后开始搜索

auto startIter = std::lower_bound(sortedValues.begin(), sortedValues.end(), start);
if (startIter == sortedValues.end()) {
// either the last element was inside the interval (return it) or not (return empty vector)
return *(--startIter) == start ? {start} : {}; 
}
auto stopIter = std::upper_bound(++startIter, sortedValues.end(), end);
// start from here
std::copy(--startIter, stopIter, std::back_inserter(ret));    

其他优化将取决于内容。 例如,如果您知道区间内的最后一个元素位于向量的末尾,那么向后迭代它会更有意义。

您也可以考虑先检查间隔,以防止在 a 大于或等于 b 时不必要地执行。