正如 @joran 所指出的那样,你的代码非常专门化,一般来说,不太通用的函数、算法等往往更具性能。看看 median.default
:
median.default
为了适应缺失值的可能性,有几个操作已经放置。这些操作肯定会影响函数的整体执行时间。由于您的函数没有复制此行为,因此可以消除许多计算,但是对于带有缺失值的向量将不会提供相同的结果:
median(c(1, 2, NA))
median2(c(1, 2, NA))
还有一些因素可能没有处理NA
那么重要,但是值得指出:
median
和它使用的几个函数都是S3通用函数,所以会花费一点时间在方法派发上
median
不仅适用于整数和数字向量;它还可以处理Date
,POSIXt
和可能还有其他类,并正确地保留属性:
median(Sys.Date() + 0:4)
median(Sys.time() + (0:4) * 3600 * 24)
编辑: 需要注意的是下面的函数将会导致原始向量被排序,因为NumericVector
是代理对象。如果你想避免这种情况,你可以使用Rcpp::clone
来克隆输入向量并对克隆进行操作,或者使用原始签名(使用std::vector<double>
),它在从SEXP
到std::vector
的转换中隐式地需要一次副本。
还要注意,你可以通过使用NumericVector
而不是std::vector<double>
来节省更多的时间:
#include <Rcpp.h>
double cpp_med(Rcpp::NumericVector x){
std::size_t size = x.size();
std::sort(x.begin(), x.end());
if (size % 2 == 0) return (x[size / 2 - 1] + x[size / 2]) / 2.0;
return x[size / 2];
}
microbenchmark::microbenchmark(
median(x),
median2(x),
cpp_med(x),
times = 200L
)
在上面的评论中,Yakk提出了一个很好的观点 - 也由Jerry Coffin详细阐述 - 关于进行完整排序的低效性。这里是使用std :: nth_element
重写的代码,在一个更大的向量上进行基准测试:
#include <Rcpp.h>
double cpp_med2(Rcpp::NumericVector xx) {
Rcpp::NumericVector x = Rcpp::clone(xx);
std::size_t n = x.size() / 2;
std::nth_element(x.begin(), x.begin() + n, x.end());
if (x.size() % 2) return x[n];
return (x[n] + *std::max_element(x.begin(), x.begin() + n)) / 2.;
}
set.seed(123)
xx <- rnorm(10e5)
all.equal(cpp_med2(xx), median(xx))
all.equal(median2(xx), median(xx))
microbenchmark::microbenchmark(
cpp_med2(xx), median2(xx),
median(xx), times = 200L
)
median.default
实际做了什么,然后尝试使用更公平的东西进行测试。 - joranstd::nth_element
可以比排序更快地完成这个任务。如果想要在R中实现它,可以使用递归中位数-中位数-5和分区来高效地实现,并使用一个短长度的备用算法。其次,在std::vector
迭代器上明确地使用std::sort
(没有保证它们被定义在namespace std
中,而您的代码依赖于此)。 - Yakk - Adam Nevraumontmedian.default
函数使用了sort
的partial
参数,这个参数执行的操作与你所描述的类似。 - jorannth_element
仅将其分区,以便第n个元素位于位置n,并且所有在其之前的元素都比它小,而所有在其之后的元素都比它大。您可以使用中位数的中位数方法比半排序更快地找到一个几乎是中位数的元素,然后进行分区以找出它的位置。重复此过程,直到找到第n个元素。 - Yakk - Adam Nevraumont