C++(std::sort)

发布于:2025-07-04 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

一 快速排序

二 std::sort

1. 接口

2. sort()底层原理

3. 源码解析

三 整体的逻辑


一 快速排序

1. 快排时间复杂度

快排的基本原理也就是选取一个基准值,把小于他的放左边,大于他的放右边,反之也可以,当一趟划分出来的左右区间元素和相等,即最好的情况,类似二分,但如果一趟之后分出来的区间比如:左区间为0,右区间为所有元素的个数,看下图:

可以看出来每次一趟的快排,都会丢一个区间,转而去遍历右边区间的全部元素,每一次快排都会遍历mid+1个元素,总共就有n躺,也就是说,O(n)*O(n-i)次,和冒泡一样,那么这种情况 是怎么发生的呢?当数据完全有序,或者接近有序,快排复杂度就会退化成O(n^2)。

2. 优化

上面说明的是数据完全有序或者接近有序就会导致快排的时间复杂度增高,本质是因为每次选择了整个数组的最小或者最大,也就是说选取基准值是固定的。

优化1:不采用固定的基准值,采用随机选取3个数,取中间不大不小的数字,或者随机取整个数组的某个元素,这样一趟快排就不会出现某个区间为0了。

优化2:如果数据大量重复,优化1虽然选取了随机数,但数据重复多,一样会退化成最快的情况,所以可以采用三路划分的方式,比如:把小于key的放右边,大于key的放右边,等于的key不管,当大于小于key的存放完毕,等于key的自然的就被急到中间去了,后续递归就不用递归相等的区间了。

优化3:当一个子区间的元素个数少于一定的数量,就可以用插入排序优化,插入排序不是O(n^2)吗?当数据完全有序,插入排序的当前遍历元素的左区间就不用遍历了,因为完全有序,左区间元素一定比右边元素小,不用进行逐个比较。

优化4:当出现了最坏的情况,递归层数为O(n),递归调用函数会创建栈帧,不断的递归可能会导致栈大小满了,直到栈溢出,这时候可以转堆排,比如根据数组元素个数定义出最大的递归层数,当减到0了,就转堆排,避免了大量的函数调用的开销。

二 std::sort

前置:借鉴STL源码解析。

1. 接口

采用模版来接受不同的参数类型。

template <class RandomAccessIterator>
  void sort (RandomAccessIterator first, RandomAccessIterator last);

template <class RandomAccessIterator, class Compare>
  void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

可以看出来,sort()是一个重载的排序函数,第一个传入随机访问迭代器。

什么是随机访问迭代器?

字面意思就是能随机访问元素的迭代器:vector,deque。

至于list,forword_list,其内部内置了排序接口,map,set,可以自己传自定义比较函数,stack,queue,priority_queue,虽然底层是适配的vector/deque,但都要保持该有的特征,不能随机访问。

第一个函数:2个参数,都是随机访问的迭代器,默认是升序排序。

第二个函数:前2个参数和第一个一样,第3个参数传入自定义比较函数,比如:泛函数,lambda表达式,函数指针等。

2. sort()底层原理

sort()采用内省排序,也就是混合排序,结合快排,堆排,插排,对数据进行排序,主要用来解决快排最坏的情况带来的影响,比如递归层数过深采用堆排,每一端区间的数据量小采用插排优化。

3. 源码解析

1. 入口函数(这里介绍不传自定义比较方法的sort函数版本)

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

      std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());
    }
  • __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept< _RandomAccessIterator>)

        1. 校验传入的对象是否是随机访问迭代器。

  •       __glibcxx_function_requires(_LessThanComparableConcept<typename iterator_traits<_RandomAccessIterator>::value_type>)

        2. 校验该对象是否有重载 < 方法。 

  •       __glibcxx_requires_valid_range(__first, __last);

        3. 校验迭代器范围是否有效。

  •       __glibcxx_requires_irreflexive(__first, __last);

        4. 校验比较函数是否正确,比如 比较的时候按照 <= ,如果左右操作数相等 而重载的是 < 符号,但里面的比较是 <= ,如果左右操作数相等,理论上说是false,也就是不小于,但实现的时候是<=,则是true了,不符合预期。

可以看出来函数里的前4个接口是用来作用于前置工作的,都是检测传入的迭代器本身,迭代器区间,比较方法,比较方法的实现逻辑是否符合预期。

而最后一个 std::__sort()才是真正的排序入口。

std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());

        1. __first, __last, 迭代器的首和尾,尾为开区间。

        2.  __gnu_cxx::__ops::__iter_less_iter(),比较方法。

来看看std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter())的实现。

template<typename _RandomAccessIterator, typename _Compare>
    _GLIBCXX20_CONSTEXPR
    inline void __sort(_RandomAccessIterator __first, _RandomAccessIterator __last,_Compare                                                         
                       __comp)
    {
        if (__first != __last)
	        {
	            std::__introsort_loop(__first, __last,std::__lg(__last-__first)*2,__comp);
	            std::__final_insertion_sort(__first, __last, __comp);
	        }
    }

if (__first != __last):迭代去相遇代表排序结束。

std::__introsort_loop(__first, __last,std::__lg(__last-__first)*2,__comp):内省排序的入口            1. first/last首尾迭代器。

      2. __lg(__last-__first)*2 根据迭代器区间的大小计算最坏的快排的深度。

        

template<class Size>
inline Size __lg(Size n)
{
    Size k;
    for(k = 0;n >1;n >>= 1)++k;
    return k;
}

实际转换后的公式 2^k <= n,比如 n == 40

for(k = 0;n >1;n >>= 1)++k; => for(k = 0;40 >1;40 >>= 1)++k

第一次循环:40 => 20

第二次循环:20 => 10

第三次循环:10 => 5

第四次循环:5 => 2

第六次循环:2 => 1 => k = 5;

2^5 = 32 <= 40。

实际的深度为 5 * 2 => __lg(__last-__first) *2 = 5 *2。

参数介绍完了,再来看看内省排序的实现

__introsort_loop(_RandomAccessIterator __first, _RandomAccessIterator __last,
		         _Size __depth_limit, _Compare __comp)
{
    while (__last - __first > int(_S_threshold))
	{
	    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);
	    __last = __cut;
    }
}

参数:

        1. __first/__last:首尾迭代器。

        2. __depth_limit:递归最大深度。

        3. __Comp:比较函数。

函数体: 

        while (__last - __first > int(_S_threshold));

                int(_S_threshold:枚举类型为16。

                如果区间元素个数小于这个区间则出循环体,执行内省排序后面的函数__final_insertion_sort,也就是插入排序。

                否则执行循环体:

        if (__depth_limit == 0) 如果深度减到0则转堆排
        {
            std::__partial_sort(__first, __last, __last, __comp);
            return;
        }

        --__depth_limit; 每执行一次快排则减减深度。

        

        

        _RandomAccessIterator __cut = std::__unguarded_partition_pivot(__first, __last, __comp);选择一个key,并进行快排流程分区操作,__cut是key的迭代器。

        std::__unguarded_partition_pivot(__first, __last, __comp):看下图

        

__unguarded_partition_pivot(_RandomAccessIterator __first,_RandomAccessIterator __last,       
                           _Compare __comp)

    {

      _RandomAccessIterator __mid = __first + (__last - __first) / 2;

      std::__move_median_to_first(__first, __first + 1, __mid, __last - 1,

          __comp);

      return std::__unguarded_partition(__first + 1, __last, __first, __comp);

    }

_RandomAccessIterator __mid = __first + (__last - __first) / 2; 取中间值。

std::__move_median_to_first(__first, __first + 1, __mid, __last - 1, __comp); 

       传入3个迭代器进行比较取中间大的元素,并和起始位置交换__first。

       __first:首位置,用来和后面三数取到的值进行交换。

       __first + 1:首+1位置。

       __mid:中间位置。

       __last - 1:尾位置。

std::__unguarded_partition(__first + 1, __last, __first, __comp); 根据key进行分区间。

template<typename _RandomAccessIterator, typename _Compare>
    _GLIBCXX20_CONSTEXPR
    _RandomAccessIterator
    __unguarded_partition(_RandomAccessIterator __first,
			              _RandomAccessIterator __last,
			              _RandomAccessIterator __pivot, _Compare __comp)
    {
        while (true)
	    {
	        while (__comp(__first, __pivot)) ++__first; 从左往右找大
	        
            --__last; __last 是开区间,下一个while循环会用到,要进行减减到有效的区间,跳过已经交换过的元素
	  
            while (__comp(__pivot, __last)) --__last; 从右往左找小
	      
            if (!(__first < __last)) 如果相遇结束返回
	            return __first; 最终相遇的位置返回出去
	      
            std::iter_swap(__first, __last); 交换找到的值
	        ++__first; 交换后的元素不需要在进行比较,直接跳过
	    }
    }

std::__introsort_loop(__cut, __last, __depth_limit, __comp); 递归右区间

        这里和传统的递归左右双区间不一样,采用的是递归右区间,但效果是和递归左右区间是一样的。

    __last = __cut; 当右区间递归结束,就把cur赋给__last,让__last当作左区间的右开区间

比如:[3,1,2,4] 和 [5,8] => 递归完右区间,让__last=__cur ,也就是5,而5就是左区间的开区间,下一次循环则处理右区间,相当于把左区间转换成了右区间统一处理。

 

std::__final_insertion_sort(__first, __last, __comp); 转插入排序。

三 整体的逻辑

当进入sort()函数首先进行校验迭代器的合法性,比较方法的合法性等...

进入__sort(),首先判断迭代器是否相等,相等则结束,否则进入内省排序__introsort_loop(),并计算最大深度。

__introsort_loop():首先判断区间元素个数是否小于16,是就结束执行__introsort_loop()下一个函数__final_insertion_sort()插入排序,否则,先判断深度是否为0,是就转堆排,否则,减减深度,然后__unguarded_partition_pivot(),进行三数取中选取key,并划分左右区间,然后__introsort_loop(),递归右区间,然后让右迭代器=key,让他作为左区间的右开区间,即左区间最后一个元素+1的位置,然后执行下一次循环。

总的来说std::sort(),与传统的快排不一样:

1. 结合了堆/插排的优点,限制递归深度和区间元素个数小于16转插排。

2. 递归区间也不是传统的递归左右区间,而是递归右区间,在让左区间变成右区间,继续递归右区间。

3. 三数取中:first last-1 mid,位置取中间的迭代器。


网站公告

今日签到

点亮在社区的每一天
去签到