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的交換 } //並返回的是右端第一個