浅谈sort函数底层(一道c++面试的天坑题)

简介: 浅谈sort函数底层(一道c++面试的天坑题)

浅谈sort函数底层

sort函数的底层用到的是内省式排序以及插入排序,那么什么是内省式排序呢?和插入排序又是如何组合的呢?

根据维基百科描述:内省排序(英语:Introsort)是由David Musser在1997年设计的排序算法。这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。

先来回顾一下以上提到的3中排序方法:

快速排序:先选一个基准值(一般为首值),将比它大的数置于其右侧,将比它小的数置于它左侧,那么这个基准值所在的位置定是整个数组的有序位。然后递归该基准左右两子数组。算法复杂度为nlogn;

堆排序:将数组建立成大顶堆,重复从堆顶取出数值最大的结点(把根结点和最后一个结点交换,把交换后的最后一个结点移出堆,移出的这个数值为未排序数组的最后),并让残余的堆维持大顶堆的性质。时间复杂度为nlogn;

插入排序:对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。时间复杂度为n2;

其中先讲下快排和堆排,快排的平均复杂度为nlogn,但是它的复杂度是根据基准值来决定的,基准值选择的不好,最坏的复杂度会达到n2,而堆排序的复杂度是一定的为nlogn,那为什么不直接使用堆排序呢,是因为在将堆顶值与最后一个结点值交换并移除最后一个值后,在重新建堆的过程中,交换到堆顶的值显然比每个结点要小,但还是要经过对比判断,这个判断其实是多余的,因此这是它相比快排较慢的原因。

于是,内省式排序结合了快排和堆排的特点,当快速排序到大一定深度(2logn)时,采用堆排序,以维持nlogn的复杂度。

在sort函数中,内省排序过程中子数组长度小于16时,采用的是插入排序,为什么呢?因为当数组长度较短时,就是数组已经大致排序过了,对大致有序的数组(即逆序对不多了)用插入排序的算法复杂度会很小,可以想象成理牌的过程。

让我们看一下sort函数的底层代码:

inline void
    __sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
     _Compare __comp)
    {
      if (__first != __last)
  {
  //sort函数默认为内省排序,当子数组小于16时返回
    std::__introsort_loop(__first, __last,
        std::__lg(__last - __first) * 2,
        __comp);
  //对大致排序过的数组进行插入排序操作
    std::__final_insertion_sort(__first, __last, __comp);
  }
    }

内省排序:

void
    __introsort_loop(_RandomAccessIterator __first,
         _RandomAccessIterator __last,
         _Size __depth_limit, _Compare __comp)
    {
    //当子数组长度<=16就返回,返回之后就会执行上一组代码的插入排序
      while (__last - __first > int(_S_threshold))
  {
  //控制深度,快排深度达2logn时,进行堆排序
    if (__depth_limit == 0)
      {
        std::__partial_sort(__first, __last, __last, __comp);
        return;
      }
    --__depth_limit;
   //这里进行了基准值的选择和简单的交换
    _RandomAccessIterator __cut =
      std::__unguarded_partition_pivot(__first, __last, __comp);
   //对右子数组进行内省排序
    std::__introsort_loop(__cut, __last, __depth_limit, __comp);
   //对左子数组进行while循环(这里很巧的避免了一次内省函数的调用,节省了时间)
    __last = __cut;
  }
    }

综上所述,sort函数的底层不只是快速排序,而是由内省排序和插入排序的组合,这也解释了为什么当数组长度短的时候,sort函数是稳定的排序,而当数组长度较大时,排序要记录元素的游标值记录避免快排引起的不稳定排序。

 

相关文章
|
3天前
|
编译器 C++
【C++核心】函数的应用和提高详解
这篇文章详细讲解了C++函数的定义、调用、值传递、常见样式、声明、分文件编写以及函数提高的内容,包括函数默认参数、占位参数、重载等高级用法。
12 3
|
1月前
|
编译器 C++ 容器
【C++】String常见函数用法
【C++】String常见函数用法
16 1
|
1月前
|
机器学习/深度学习
【机器学习】如何判断函数凸或非凸?(面试回答)
文章介绍了如何判断函数是凸函数还是非凸函数,包括凸函数的定义、几何意义、判定方法(一元函数通过二阶导数判断,多元函数通过Hessian矩阵的正定性判断),以及凸优化的概念和一些经典的凸优化问题。
78 1
【机器学习】如何判断函数凸或非凸?(面试回答)
|
1月前
|
C++
c++常见函数及技巧
C++编程中的一些常见函数和技巧,包括生成随机数的方法、制表技巧、获取数字的个位、十位、百位数的方法、字符串命名技巧、避免代码修改错误的技巧、暂停和等待用户信号的技巧、清屏命令、以及避免编译错误和逻辑错误的建议。
19 6
|
1月前
|
存储 C++
c++学习笔记05 函数
C++函数使用的详细学习笔记05,包括函数的基本格式、值传递、函数声明、以及如何在不同文件中组织函数代码的示例和技巧。
28 0
c++学习笔记05 函数
|
1月前
|
JavaScript
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
这篇文章解释了为什么在Vue中组件的`data`属性必须是一个函数而不是一个对象。原因在于组件可能会有多个实例,如果`data`是一个对象,那么这些实例将会共享同一个`data`对象,导致数据污染。而当`data`是一个函数时,每次创建组件实例都会返回一个新的`data`对象,从而确保了数据的隔离。文章通过示例和源码分析,展示了Vue初始化`data`的过程和组件选项合并的原理,最终得出结论:根实例的`data`可以是对象或函数,而组件实例的`data`必须为函数。
【Vue面试题八】、为什么data属性是一个函数而不是一个对象?
|
2月前
|
C++ 运维
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
37 11
|
2月前
|
C++ 运维
开发与运维函数问题之C++类的简单示例如何解决
开发与运维函数问题之C++类的简单示例如何解决
53 10
|
1月前
|
存储 C++
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
31 0
|
2月前
|
存储 C++ 运维
开发与运维函数问题之使用C++标准库中的std::function来简化回调函数的使用如何解决
开发与运维函数问题之使用C++标准库中的std::function来简化回调函数的使用如何解决
41 6