nth_element是stl中的一个库函数,该函数可以从某个序列中找到第 n 小的元素 K,并将 K 移动到序列中第 n 的位置处。不仅如此,整个序列经过 nth_element() 函数处理后,所有位于 K 之前的元素都比 K 小,所有位于 K 之后的元素都比 K 大。

但这个函数与完整排序的区别在于:

  1.它只关注第n个,只保证小于该值的元素在其左边,大于等于的在其右边,但并不保证其完全有序。

  2.它的时间复杂度在O(N),而许多排序所用的复杂度为O(NlogN)。对于特定的找到第k大数的问题,所花时间是好于排序的。

 

它的原理思想如图

  这个算法的思路很像快排中partation函数。首先有一个first ,nth,last,首先找到这三个所在位置值的中位数,这里22,40,20中位数为22。

  然后对于22进行第一轮,方法同partation,22则作为锚点。

  然后这里找到右端的第一个(如果是数组为0-10装着上图的值),这里右端第一个即数组下标为5的数字40,而传入我们要找的nth也为5(其实即找到0-10中第6大的数),因此其小于等于右端起点,于是进入右段进行下一轮。

  下一轮同理40,30,22取30 进行交换使cut两端被分割。

  这时候再右端第一个和nth再进入,这时判断到子序列已经不大于3了,直接用插入排序。最后即得到第nth(这里传入nth为5,实际即第六大的数)为22.

 

以下是代码的解析:

下面是nth_element在本人电脑中stl的源码(可以跳过)

template<typename _RandomAccessIterator, typename _Compare>
    inline void
    nth_element(_RandomAccessIterator __first, _RandomAccessIterator __nth,
        _RandomAccessIterator __last, _Compare __comp)
    {
      // concept requirements
      __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
                  _RandomAccessIterator>)
      __glibcxx_function_requires(_BinaryPredicateConcept<_Compare,
        typename iterator_traits<_RandomAccessIterator>::value_type,
        typename iterator_traits<_RandomAccessIterator>::value_type>)
      __glibcxx_requires_valid_range(__first, __nth);
      __glibcxx_requires_valid_range(__nth, __last);

      if (__first == __last || __nth == __last)
    return;

      std::__introselect(__first, __nth, __last,
             std::__lg(__last - __first) * 2,
             __gnu_cxx::__ops::__iter_comp_iter(__comp));
    }

 

其中直接讲其核心的__introselect函数部分

template<typename _RandomAccessIterator, typename _Size, typename _Compare>
    void
    __introselect(_RandomAccessIterator __first, _RandomAccessIterator __nth,
          _RandomAccessIterator __last, _Size __depth_limit,
          _Compare __comp)
    {
      while (__last - __first > 3)
    {
      if (__depth_limit == 0)
        {
          std::__heap_select(__first, __nth + 1, __last, __comp);
          // Place the nth largest element in its final position.
          std::iter_swap(__first, __nth);
          return;
        }
      --__depth_limit;                                               
      _RandomAccessIterator __cut =      
        std::__unguarded_partition_pivot(__first, __last, __comp); //这里函数返回的是分割后右段的第一个元素。后面会详细讲解一下这个的功能
      if (__cut <= __nth)  //如果右段起点小于等于nth 
        __first = __cut;   //进入右段,对右段进行下次的分割
      else
        __last = __cut;
    }
      std::__insertion_sort(__first, __last, __comp);
    }

这个算法的思路很像快排中partation函数的思路。但对于段长度在3时候,就选择使用插入排序。

 

这里的关键是__unguarded_partition_pivot函数部分

 template<typename _RandomAccessIterator, typename _Compare>
    inline _RandomAccessIterator
    __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,    //对first last mid排序找到中位值
                  __comp);                                                    //这里的中位值即下面的pivot点
      return std::__unguarded_partition(__first + 1, __last, __first, __comp);  //这里即实现了对于类似快排若左边大于pivot则与右边小于pivot的交换
    }                                                                           //并返回的是右端第一个