排序优化

Sorting Optimization

本文关键字:优化 排序      更新时间:2023-10-16

我目前正在关注一个算法类,因此决定最好实现一些排序算法并进行比较。我实现了合并排序和快速排序,然后比较了它们的运行时间,以及std::sort:我的电脑不是最快的,但我平均在200次尝试后获得1000000个元素:

  1. std::sort->0.620342秒
  2. quickSort->2.2692
  3. 合并排序->2.19048

我想询问是否可以就如何改进和优化我的代码实现发表意见。

void quickSort(std::vector<int>& nums, int s, int e, std::function<bool(int,int)> comparator = defaultComparator){
if(s >= e)
return;
int pivot;
int a = s + (rand() % (e-s));
int b = s + (rand() % (e-s));
int c = s + (rand() % (e-s));
//find median of the 3 random pivots
int min = std::min(std::min(nums[a],nums[b]),nums[c]);
int max = std::max(std::max(nums[a],nums[b]),nums[c]);
if(nums[a] < max && nums[a] > min)
pivot = a;
else if(nums[b] < max && nums[b] > min)
pivot = b;
else
pivot = c;
int temp = nums[s];
nums[s] = nums[pivot];
nums[pivot] = temp;
//partition
int i = s + 1, j = s + 1;
for(; j < e; j++){
if(comparator(nums[j] , nums[s])){
temp = nums[i];
nums[i++] = nums[j];
nums[j] = temp;
}
}
temp = nums[i-1];
nums[i-1] = nums[s];
nums[s] = temp;
//sort left and right of partition
quickSort(nums,s,i-1,comparator);
quickSort(nums,i,e,comparator);

这里s是第一个元素的索引,e是最后一个元素之后的元素的索引。defaultComparator只是以下lambda函数:

auto-defaultComparator=[](int a,int b){return a<=b;};

std::vector<int> mergeSort(std::vector<int>& nums, int s, int e, std::function<bool(int,int)> comparator = defaultComparator){
std::vector<int> sorted(e-s);
if(s == e)
return sorted;
int mid = (s+e)/2;
if(s == mid){
sorted[0] = nums[s];
return sorted;
}
std::vector<int> left = mergeSort(nums, s, mid);
std::vector<int> right = mergeSort(nums, mid, e);
unsigned int i = 0, j = 0;
unsigned int c = 0;
while(i < left.size() || j < right.size()){
if(i == left.size()){
sorted[c++] = right[j++];
}
else if(j == right.size()){
sorted[c++] = left[i++];
}
else{
if(comparator(left[i],right[j]))
sorted[c++] = left[i++];
else
sorted[c++] = right[j++];
}
}
return sorted;

谢谢大家

我看到的第一件事是,您正在传递一个涉及虚拟调用的std::function<>,这是最昂贵的调用策略之一。只需使用一个模板T(可能是一个函数)尝试一下,结果将是直接调用函数。

第二件事,当优化时,不要在本地容器(vector<int> sorted;)中执行此操作,当存在就地变量时,不要执行。进行就地排序。客户应该意识到你做空了他们的矢量;如果他们愿意,他们可以提前复印一份。你采用非常数引用是有原因的。[1]

第三,与rand()相关的成本是远远不能忽略的。除非你确定你需要quicksort()的随机变体(以及它关于"没有太糟糕的序列"的好处),否则只使用第一个元素作为支点。或者中间。

使用std::swap()交换两个元素。很可能,它会被翻译成xchg(在x86/x64上)或类似的版本,这是很难击败的。优化器是否在不显式的情况下识别出您打算在这些地方交换,可以从程序集输出中进行验证。

找到三个元素的中值的方法充满了条件移动/分支。它只是nums[a] + nums[b] + nums[c] - max - min;但同时得到nums[...]minmax也可以进一步优化。

瞄准速度时应避开i++。虽然大多数优化器通常会创建好的代码,但它很有可能是次优的。优化时要明确(交换后的++i),但_only_when_optimizing_。

但最重要的是:valgrind/callgrind/kcachegrind。简介,简介,简介。只优化真正慢的东西。

[1] 这个规则有一个例外:从非常量容器构建的常量容器。这些通常是内部类型,并且在多个线程之间共享,因此最好将它们保持为const&需要修改时进行复制。在这种情况下,您将在函数中分配一个新容器(const或not),但为了API上的用户方便,您可能会保留const。

对于快速排序,请使用类似Hoare的分区方案。

http://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme

中位数3只需要3个if/swap语句(实际上是一种泡沫排序)。无需进行最小或最大检查。

if(nums[a] > nums[b])
std::swap(nums[a], nums[b]);
if(nums[b] > nums[c])
std::swap(nums[b], nums[c]);
if(nums[a] > nums[b])
std::swap(nums[a], nums[b]);
// use nums[b] as pivot value

对于合并排序,使用一个一次性创建工作向量的入口函数,然后通过引用将该向量传递给实际的合并排序函数。对于自上而下的合并排序,索引确定每个子向量的开始、中间和结束。

如果使用自上而下的合并排序,代码可以通过根据递归级别交替合并方向来避免复制数据。这可以使用两个相互递归的函数来完成,第一个函数的结果最终在原始向量中,第二个函数的最终结果在工作向量中。第一个调用第二个两次,然后从工作向量合并回原始向量,反之亦然。对于第二个,如果大小==1,则需要将1个元素从原始向量复制到工作向量。两个函数的替代方法是传递一个布尔值,用于合并哪个方向。

如果使用自下而上的合并排序(这会更快一点),则每次传递都会交换向量。所需的传递次数是预先确定的,在传递次数为奇数的情况下,第一个传递会交换到位,这样在完成所有合并传递后,数据最终会出现在原始向量中。