【手写快排与Sort】

写在前面:本博客仅作记录学习之用,部分图片来自网络,如需引用请注明出处,同时如有侵犯您的权益,请联系删除!


文章目录

  • 快速排序(Quicksort)
  • 复杂度
  • 代码
    • 基准函数
    • 递归快排
    • 非递归快排
  • 手写快排存在的问题
  • Sort的底层实现
  • 致谢
  • 参考

快速排序(Quicksort)

基于分治(Divide and Conquer)思想。它的核心思想是选择一个基准元素,通过将数组分割成两个子数组,使得左边的子数组中的所有元素都小于或等于基准值,右边的子数组中的所有元素都大于基准值。然后递归地对这两个子数组进行排序,最终完成整个数组的排序。

具体步骤如下:

  • 选择一个基准元素(pivot),通常可以选择数组的第一个元素、最后一个元素、中间元素等作为基准。
  • 定义两个指针,一个指向数组的起始位置(左指针),另一个指向数组的结束位置(右指针)。
  • 不断地移动左指针和右指针,并交换它们指向的元素,直到左指针和右指针相遇:
  • 将基准元素与左指针指向的元素进行交换,使基准元素位于正确的位置上。
  • 对基准元素左边的子数组和右边的子数组分别递归地执行上述步骤,直到子数组的长度为1或0,递归终止。

快速排序的关键在于如何选择基准元素和如何分割子数组。通常情况下,选择一个合适的基准元素可以使得分割后的子数组尽可能平衡,以提高算法的效率。例如,可以采用三数取中的方法,在数组的起始、中间和结束位置分别取样,然后选择其中的中位数作为基准元素,以减少最坏情况的出现。

复杂度

快速排序是一种原地排序算法,不需要额外的辅助空间。

  • 平均时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn) , 最坏情况下(当数组已经有序或近乎有序)的时间复杂度: O ( n 2 ) O(n^2) O(n2) ,但通过合理选择基准元素可以避免最坏情况的发生。
  • 空间复杂度: O ( l o g n ) O(logn) O(logn),一般来说主要是递归来带的栈空间的消耗。
  • 非稳定排序算法,因为在交换过程中可能改变相等元素的相对位置。

代码

基准函数

// 返回分界线
int get_mid(vector<int>& nums, int left, int right){
    int i = left, j = right;
    while(i < j){
    	// 降序
        while(i < j && nums[j] <= nums[left]) j--;
        while(i < j && nums[i] >= nums[left]) i++;
        // 升序
		// while(i < j && nums[j] >= nums[left]) j--;
        // while(i < j && nums[i] <= nums[left]) i++;
        swap(nums[i], nums[j]);
    }
    swap(nums[i], nums[left]);
    return i;
}

递归快排

void quickSort(vector<int>& nums, int left, int right){
  if(left > right) return;
     int mid = get_mid(nums, left, right);
     quickSort(nums, left, mid - 1);
     quickSort(nums, mid + 1, right);
 }

非递归快排

 void quicksort(vector<int>& nums, const int& size){
     stack<pair<int, int>> lr_stack;
     lr_stack.push(make_pair(0, size));
     while(!lr_stack.empty()){
         auto tmp = lr_stack.top();
         lr_stack.pop();
         int mid = get_mid(nums, tmp.first, tmp.second);
         if(tmp.first < mid - 1)  lr_stack.push(make_pair(tmp.first, mid - 1));
         if(mid + 1 < tmp.second) lr_stack.push(make_pair(mid + 1, tmp.second));
     }
 }

手写快排存在的问题

  • 快速排序适合所有情况?数据量少时,效率可能低于简单排序。
  • 时间复杂度不是稳定的 O ( n l o g n ) O(nlogn) O(nlogn),最坏情况会退化为冒泡 O ( n 2 ) O(n^2) O(n2)
  • 递归层次过深会导致性能下降、栈溢出甚至程序崩溃

因此有必要了解和Sort的区别。 库函数 sort 和上述快排代码的区别主要在以下几个方面:

实现方式不同: sort 函数通常采用一种混合了多种排序算法的方式来实现数组的排序,如快速排序、归并排序、堆排序等等。具体采用哪种排序算法,取决于数组的大小和数据分布情况。

代码实现复杂度不同: sort 库函数的实现是高度优化的,如采用了分段递归、尾递归优化、使用小范围插入排序等等。

排序速度和效率不同: 由于 sort 库函数使用了多种排序算法的混合方式,同时采用了许多优化措施,在大多数情况下, sort 函数的速度和效率通常是要优于手写的快速排序算法的。

Sort的底层实现

template<typename _RandomAccessIterator>
    inline void
    sort(_RandomAccessIterator __first, _RandomAccessIterator __last)
    {
      typedef typename iterator_traits<_RandomAccessIterator>::value_type
        _ValueType;
      // concept requirements
      __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<_RandomAccessIterator>)
      __glibcxx_function_requires(_LessThanComparableConcept<_ValueType>)
      __glibcxx_requires_valid_range(__first, __last);

      if (__first != __last)
        {
        //快速排序+插入排序
          std::__introsort_loop(__first, __last, std::__lg(__last - __first) * 2);
        //插入排序
          std::__final_insertion_sort(__first, __last);
        }
    }

以上代码是 C++ STL(标准模板库)中的 sort 函数模板实现。它用于对给定范围内的元素进行排序,使用了快速排序和插入排序两种算法。

函数的参数包括一个随机访问迭代器 __first __last,表示待排序范围的起始和结束位置。在函数内部,首先使用 typedef 定义了一个类型 _ValueType,表示迭代器指向元素的值类型。接下来,使用一些宏定义进行了一些概念要求的检查,确保传入的迭代器满足要求,如可变的随机访问迭代器概念和可比较的值类型概念。然后,通过判断__first__last 是否相等来确定待排序范围是否为空。如果不为空,则执行排序操作。

排序操作分为两个阶段:
快速排序 + 插入排序:调用 std::__introsort_loop 函数进行快速排序和插入排序的混合排序。其中,std::__lg(__last - __first) * 2 表示递归深度的限制,根据待排序范围的大小计算出最大递归深度。
插入排序:调用 std::__final_insertion_sort 函数对剩余的待排序范围(如果有的话)进行插入排序。
整体上,sort 函数模板实现了一种高效的排序算法,通过混合使用快速排序和插入排序,可以在大部分情况下获得较好的性能。同时,根据待排序范围的大小,动态调整递归深度,以避免快速排序的最坏时间复杂度,并在数据规模较小时使用插入排序进行优化。

template<typename _RandomAccessIterator, typename _Size>
    void
    __introsort_loop(_RandomAccessIterator __first,
                     _RandomAccessIterator __last,
                     _Size __depth_limit)
    {
      typedef typename iterator_traits<_RandomAccessIterator>::value_type
        _ValueType;
    //_S_threshold=16,每个区间必须大于16才递归
      while (__last - __first > int(_S_threshold))
        {
        //达到指定递归深度,改用堆排序
          if (__depth_limit == 0)
            {
              std::partial_sort(__first, __last, __last);
              return;
            }
          --__depth_limit;
          _RandomAccessIterator __cut =
            std::__unguarded_partition(__first, __last,
                                       _ValueType(std::__median(*__first,
                                                                *(__first
                                                                  + (__last
                                                                  - __first)
                                                                  / 2),
                                                                *(__last
                                                                  - 1))));
          std::__introsort_loop(__cut, __last, __depth_limit);
          __last = __cut;
        }
    }

上述代码__introsort_loop函数采用了一种混合的排序算法,结合了快速排序、插入排序和堆排序,并通过递归和迭代的方式来进行排序。函数的命名为 __introsort_loop,参数包括一个随机访问迭代器 __first __last,表示待排序范围的起始和结束位置,以及一个整数__depth_limit,表示递归深度的限制。在函数内部,首先根据迭代器的类型,定义了一个 _ValueType 类型,它表示迭代器指向元素的值类型。然后,在一个循环中,判断待排序范围的大小是否大于一个阈值 _S_threshold(通常是 16)。如果不满足该条件,则将排序范围分为两部分,并对其中一部分进行递归排序,另一部分则留待下一次迭代处理。当递归深度达到限制时,会使用 std::partial_sort 函数对剩余的待排序范围进行排序,该函数会使用堆排序算法来进行排序操作。

整体上,通过快速排序和插入排序来处理较大的待排序范围,而使用堆排序来处理递归深度达到限制的情况,以保证排序的效率和性能。这种混合算法的设计可以根据不同大小的输入数据自适应地选择最优的排序方式,以达到较好的排序效果。

致谢

欲尽善本文,因所视短浅,怎奈所书皆是瞽言蒭议。行文至此,诚向予助与余者致以谢意。

参考

  1. 知乎:C++一道深坑面试题:STL里sort算法用的是什么排序算法?
  2. 力扣:排序后寻找

你可能感兴趣的:(#,力扣,算法,数据结构)