为啥“快速排序”算法的这两种变体在性能上差别如此之大?

Posted

技术标签:

【中文标题】为啥“快速排序”算法的这两种变体在性能上差别如此之大?【英文标题】:Why do these two variations on the "quick sorting" algorithm differ so much in performance?为什么“快速排序”算法的这两种变体在性能上差别如此之大? 【发布时间】:2019-08-10 07:06:29 【问题描述】:

我最初想出一些排序算法来用 C++ 编写代码以供练习。人们告诉我这是非常低效的(事实上,对几百个数字进行排序大约需要 10 秒)。该算法是记住向量中的第一个元素(“pivot”),然后解析所有其他元素,如果每个元素较小,则将其移动到枢轴的左侧,否则不做任何事情。这会将列表拆分为较小的列表进行排序;其余的通过递归完成。

所以现在我知道将列表分成两部分并进行这样的递归本质上就是快速排序所做的(尽管在如何进行分区方面有很多变化)。我不明白为什么我的原始代码效率如此之低,所以我写了一个新的。有人提到这是因为 insert() 和 erase() 函数,所以我确保不使用它们,而是使用 swap()。

旧(慢):

void sort(vector<T>& vec)
  int size = vec.size();
  if (size <= 1) //this is the most basic case
    return;
  

  T pivot = vec[0];
  int index = 0; //to help split the list later
  for (int i = 1; i < size; ++i) //moving (or not moving) the elements
    if (vec[i] < pivot)
      vec.insert(vec.begin(), vec[i]);
      vec.erase(vec.begin() + i + 1);
      ++index;
    
  

  if (index == 0) //in case the 0th element is the smallest
    vec.erase(vec.begin());
    sort(vec);
    vec.insert(vec.begin(), pivot);
  
  else if(index == size - 1) //in case the 0th element is the largest
    vec.pop_back();
    sort(vec);
    vec.push_back(pivot);
  

  //here is the main recursive portion
  vector<T> left = vector<T>(vec.begin(), vec.begin() + index);
  sort(left);
  vector<T> right = vector<T>(vec.begin() + index + 1, vec.end());
  sort(right);

  //concatenating the sorted lists together
  left.push_back(pivot);
  left.insert(left.end(), right.begin(), right.end());

  vec = left;

新的(快速):

template <typename T>
void quickSort(vector<T>& vec, const int& left, const int& right)
  if (left >= right) //basic case
    return;
  
  T pivot = vec[left];
  int j = left; //j will be the final index of the pivot before the next iteration

  for (int i = left + 1; i <= right; ++i)
    if (vec[i] < pivot)
      swap(vec[i], vec[j]); //swapping the pivot and lesser element
      ++j;
      swap(vec[i], vec[j]); //sending the pivot next to its original spot so it doesn't go the to right of any greater element
    
  

  //recursion
  quickSort(vec, left, j - 1);
  quickSort(vec, j + 1, right);

性能上的差异是疯狂的;较新的版本可以在不到一秒钟的时间内对数万个数字进行排序,而第一个版本无法对 100 个数字进行排序。确切地说,erase() 和 insert() 做了什么来减慢它的速度?真的是 erase() 和 insert() 导致了瓶颈,还是我还缺少其他东西?

【问题讨论】:

insert() 增加了向量的大小,因此可能会分配内存,将现有元素复制到新内存,复制要插入的元素,并释放旧内存。调用 erase() 中的元素向量的中间将所有后续元素洗牌到被擦除元素占用的空间中,并在最后销毁相同数量的元素。内存分配和释放、重排元素和销毁元素都是相对较慢的操作。交换两个元素不会调整大小,因此不会分配内存或调用析构函数。 【参考方案1】:

首先,是的,insert()erase() 会比 swap() 慢很多。

insert() 在最好的情况下会要求将插入向量中的点之后的每个元素移动到向量中的下一个点。想想如果你把自己挤到拥挤的人群中间会发生什么——你身后的每个人都必须后退一步为你腾出空间。在最坏的情况下,由于插入向量会增加向量的大小,因此向量可能会耗尽其当前内存位置的空间,从而导致整个向量(逐个元素)被复制到有空间容纳的新空间中新插入的项目。当向量中间的元素为erase()'d 时,必须将其后的每个元素复制并向上移动一个空格;就像如果你离开这条线,你后面的每个人都会向前迈出一步。相比之下,swap() 只移动正在交换的两个元素。

除此之外,我还注意到两个代码示例之间的另一个主要效率改进:

在第一个代码示例中,您有:

vector<T> left = vector<T>(vec.begin(), vec.begin() + index);
sort(left);
vector<T> right = vector<T>(vec.begin() + index + 1, vec.end());
sort(right);

它使用 C++ 向量的 范围构造函数。每次代码到达这一点时,当它创建leftright 时,它都会遍历整个vec 并将每个元素一个接一个地复制到两个新向量中。

在更新、更快的代码中,没有任何元素永远被复制到新的向量中;整个算法发生在原始数字所在的确切内存空间中。

【讨论】:

UV 提供精心编写和格式正确的答案(以及信息丰富)。 真的很喜欢排队类比的人。【参考方案2】:

向量是数组,因此在结束位置以外的位置插入和删除元素是通过将位置之后的所有元素重新定位到它们的新位置来完成的。

【讨论】:

以上是关于为啥“快速排序”算法的这两种变体在性能上差别如此之大?的主要内容,如果未能解决你的问题,请参考以下文章

OpenCL:为啥这两种情况的性能差异如此之大?

快速排序:要想提高效率就要少做事情

高级排序算法之归并排序,快速排序

为啥看和说序列的这两种实现有不同的执行时间?

挖掘算法中的数据结构:O(n*logn)排序算法之 快速排序(随机化二路三路排序) 及衍生算法

线性时间求取第 K 大数