无聊写排序之 ----第K最值(nth_element)

之前partial_sort有讲述了如果从一个无序序列中快速获得top m的所有有序数列m个。我们也常常会遇到这样的问题:如何在一个乱序的序列集合中,找到第K大的或者排名第K的那个元素?或者找到前K个排名的元素,但是这K个数之间的顺序对我们来说并不重要。比如篮球比赛中总胜利场数最多的20支球队进入总决赛,而这20支球队之前各自胜利多少的排名并不重要,因为决赛要重新比。 这个问题我们同样可以用排序来解决,最简单的方法也是将所有序列集合进行排序,直接取第K个位置上的数即可,但是这里我们要介绍一种线性时间复杂度(O(N))的算法:nth_element 该算法也来自于STL的算法库,在研究STL源码时看到的,瞬间眼前一亮,这里分享出来。

nth_element算法将重新排列区间[first, last)的序列元素, 使得第k个元素位置的元素在最终算法执行完毕后和整个区间完全排序后该位置元素具有相同的值,也就是说完全排序后该位置上出现的值,将会在该算法执行完成后依然在该位置。但是该算法并不保证位于第K个元素两边的区间的元素有序,只保证[ k, last)区间内没有元素不小于第K个元素位置的值。 看上去和我们之前讲解的partial_sort做的事情很像,但是他们有一个很大的区别在于。nth_element对于除第K位置的元素之外的区间的顺序不做保证,而partial_sort排序后前m个数的子区间是有序的。正因为如此在需要无序的前top k个值时 nth_element相对于partial_sort要更快。

    假如有序列vc = { 41 67 34 0 69 24 78 58 62 64 } 我们用nth_element求第5小的元素,使用nth_element(vc, vc+4, vc+10); 该算法执行完毕后第5小的元素便是vc[4] = 58 剩余小于58的元素都放在该元素左边区间,大于58的元素都放在该元素右边区间。当然不保证维持原有的相对位置。获得结果为 vc={0 24 34 41 58 62 64 67 69 78} 这与调用sort算法进行全部排序后的vc[4]的值保持了一致。

        nth_element的算法原理:

                  1》从给定的区间[first, last)内使用三点取值法获取一个基准值(三点中值)pivot。

                  2》 然后将小于pivot的元素调整到左边区间,大于等于pivot的元素调整到右边区间。

 3》 经过2步骤之后形成了两个子区间,左边区间都小于povit,右边区间的值均大于等于povit。再判断nth是处在左边区间还是右边区间,然后对nth所在的区间重复进行上述操作,知道该区间的元素数不大于3.

4》经过上述三步操作,nth所在的区间元素已经不大于4,此时对该区间进行一次全排序编可固定nth所处位置的元素,STL使用插入排序。


                            无聊写排序之 ----第K最值(nth_element)_第1张图片


算法使用演示如下:

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
	vector vc;
	for (int i = 0; i < 10; i++)
	{
		vc.push_back(rand()%100);
	}

 	for (int i = 0; i < vc.size(); i++)
		cout << vc[i] << " ";
 	cout << endl;

	nth_element(vc.begin(), vc.begin()+4, vc.end());

	for (int i = 0; i < vc.size(); i++)
		cout << vc[i] << " ";
	cout << endl;

	sort(vc.begin(), vc.end());
	for (int i = 0; i < vc.size(); i++)
		cout << vc[i] << " ";
	cout << endl;
	return 0;
}
执行结果:

                                 无聊写排序之 ----第K最值(nth_element)_第2张图片


STL源码剖析:



template 
inline void nth_element(_RandomAccessIter __first, _RandomAccessIter __nth,
	_RandomAccessIter __last) {
		__STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
		__STL_REQUIRES(typename iterator_traits<_RandomAccessIter>::value_type,
			_LessThanComparable);
		__nth_element(__first, __nth, __last, __VALUE_TYPE(__first));
}


template 
void __nth_element(_RandomAccessIter __first, _RandomAccessIter __nth,
	_RandomAccessIter __last, _Tp*) {
		while (__last - __first > 3) {    // 划分区间知道区间元素不大于3
			_RandomAccessIter __cut =
				__unguarded_partition(__first, __last, // 三点均值法 划分区间 返回区间分割后第二个区间的位置
				_Tp(__median(*__first,
				*(__first + (__last - __first)/2),
				*(__last - 1))));
			if (__cut <= __nth)   // nth落在在第二个区间 
				__first = __cut;
			else 
				__last = __cut;  // nth落在第一个区间
		}
		__insertion_sort(__first, __last); // 插入排序 处理区间不大于3的子区间进行全排序
}


template 
_RandomAccessIter __unguarded_partition(_RandomAccessIter __first, 
	_RandomAccessIter __last, 
	_Tp __pivot) 
{
	while (true) {
		while (*__first < __pivot)
			++__first;
		--__last;
		while (__pivot < *__last)
			--__last;
		if (!(__first < __last))
			return __first;
		iter_swap(__first, __last);
		++__first;
	}
}

        学习STL不得不佩服前辈大牛们卓越的思想,望其项背 努力学习。。。。


你可能感兴趣的:(sort,algorithm)