标准库 之 nth_element

STL库中实现了nth_element函数,实现的功能是 “返回n个元素中的第k小的元素”。


首先,头脑风暴一下“返回n个元素中的第k小的元素”的算法:


1    排序 ,首选快排  O(n*logn),取出第k个即可。

2 其次,是维护一个大小为k的数组,找出数组中的最大值kmax,然后依次遍历剩下的 n-k 个元素,如果小雨kmax,则替换掉kmax

元素,然后再找出其中的最大值,重复上述过程,时间复杂度为 O(n*k)。

3 对2的改进,使用大小为k个大顶堆,时间复杂度为 O(n*logk),代码实现如下。

void max_heapify(vector &A, int cur, int len)
{
    int left = 2*cur+1, right = 2*cur+2;
    if(left >= len) return;
    int largest = cur;
    if(left < len && A[largest] < A[left])
        largest = left;
    if(right < len && A[largest] < A[right])
        largest = right;
    if(largest != cur)
    {
        swap(A[largest], A[cur]);
        max_heapify(A,largest, len);
    }
}

void buildHeap(vector &A)
{
    int len = A.size();
    for(int i = len/2 ; i >= 0 ; --i)
    {
        max_heapify(A,i,len);
    }
    copy(A.begin(), A.end(), ostream_iterator(cout,"\t"));
    cout << endl;
}

vector kMin(vector & A, const int k)
{
    int len = A.size();
    if(k >= len) return A;
    vector ret;

    buildHeap(A);
    for(int i = k ; i > 0 ; --i)
    {
        ret.push_back(A[0]);
        swap(A[0],A[len-1]);
        --len;
        max_heapify(A,0, len);
    }
    return ret; //此处返回的是前k小的元素
}

上述函数实现的是返回前k小的元素。


4 便是此处要提到的STL库中的nth_element函数的实现了。

原理:

在STL库中,nth_element的实现是基于快速排序的partition的过程。

0)分两种,第一,如果处理的元素的个数小于某一个阈值(此处为3)。那么使用插入排序,否则,转1)

1)选出pivot,使用的是first,last,以及中间元素的中间值,(其实,在快速排序中,这样选择pivot可能导致最坏时间复杂度的

概率已经是可以忽略不计了),根据该pivot的值将数组进行划分,得到A[ first ,.... ,cut] , 与 A[cut  ... last]

2) 如果小于pivot的元素个数已经是K,直接返回。

3) 如果小于pivot的元素小于K,那么在 大于pivot的部分寻找第K - ( cut - first +1 ) 大的元素。转入1),递归即可。

4) 如果小于pivot的元素大于K,那么在 小于pivot的部分继续寻找第 K大 的 元素。转入1),转入1),递归即可。


模拟实现:

下面是自己实现的代码:


const int K = 3;
void insertionSort(vector &A, int left, int right)
{
    for(int i = left + 1; i < right; ++i)
    {
        int t = A[i];
        int j = i-1;

        for( ; j >= left && A[j] > t; --j )
            A[j+1] = A[j];
        //循环不变式为:A[j+1...]是大于或等于t的元素!!
        A[j+1] = t;
    }
}

void kMin2QuickSelect(vector &A, int K , int left, int right)
{
    //元素小于CUTOFF时,选择插入排序
    if(right - left < CUTOFF)
    {
        insertionSort(A,left,right);
    }
    else
    {
        int pivot = A[right-1];
        int i=left;
        for(int k = left ; k < right-1; ++k)
        {
            if(A[k] < pivot)
            {
                swap(A[i++],A[k]);
            }
        }
        swap(A[i],A[right-1]);

        if((i-left+1) == K || (i-left+1) == K+1) return;

        if((i-left+1) < K )
        {
            kMin2QuickSelect(A,i+1,right,(K-(i-left+1)));
        }
        else
        {
            kMin2QuickSelect(A,left,i, K);
        }
    }
}


上述函数只想完成之后,返回的数组的前K个元素就是最小的K个元素,并且第K小的元素在其位置上。


源码剖析:

代码为:

//实现为模板函数。可以看到,只有随机迭代器才支持该调用,
//Compare对象是函数对象,或者是函数指针,此处默认为 operator< 操作符。
template 
void __nth_element(_RandomAccessIter __first, _RandomAccessIter __nth,
                   _RandomAccessIter __last, _Tp*, _Compare __comp) {
    //如果元素个数大于3,才使用partition,否则直接使用插入排序。
  while (__last - __first > 3) {
    _RandomAccessIter __cut =
      __unguarded_partition(__first, __last,
                            _Tp(__median(*__first,
                                         *(__first + (__last - __first)/2), 
                                         *(__last - 1),
                                         __comp)),
                            __comp);
    if (__cut <= __nth)
      __first = __cut;
    else 
      __last = __cut;
  }
  __insertion_sort(__first, __last, __comp); //实现见下
}

//partition函数
template 
_RandomAccessIter __unguarded_partition(_RandomAccessIter __first, 
                                        _RandomAccessIter __last, 
                                        _Tp __pivot, _Compare __comp) 
{
  while (true) {
    while (__comp(*__first, __pivot))
      ++__first;
    --__last;
    while (__comp(__pivot, *__last))
      --__last;
    if (!(__first < __last))
      return __first;
    iter_swap(__first, __last);
    ++__first;
  }
}
//插入排序
template 
void __insertion_sort(_RandomAccessIter __first,
                      _RandomAccessIter __last, _Compare __comp) {
  if (__first == __last) return;
  for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i)
    __linear_insert(__first, __i, __VALUE_TYPE(__first), __comp);
}

//插排
template 
inline void __linear_insert(_RandomAccessIter __first, 
                            _RandomAccessIter __last, _Tp*, _Compare __comp) {
  _Tp __val = *__last;
  if (__comp(__val, *__first)) { //如果当前元素小于第一个元素,那么也就是说该元素应该插入到最前面,直接copy即可。
    copy_backward(__first, __last, __last + 1);
    *__first = __val;
  }
  else
    __unguarded_linear_insert(__last, __val, __comp); //如果该元素不小于第一个元素。
}
//当前元素应该插入到已有序的子数组的中间部分。因此,可以不适用first指针,因为肯定不会越界
template 
void __unguarded_linear_insert(_RandomAccessIter __last, _Tp __val, 
                               _Compare __comp) {
  _RandomAccessIter __next = __last;
  --__next;  
  while (__comp(__val, *__next)) {
    *__last = *__next;
    __last = __next;
    --__next;
  }
  *__last = __val;
}

上述的STL库中的代码,显得很繁琐,其实主要是都是模板函数。原理其实很简单。


其实,归根结底,就以下几点:

如果元素个数比较多的时候,那么利用快排的partition的思想,就可以将问题的规模降低。

如果元素个数比较少的时候,那么直接使用插入排序,是很好的选择, 在数组元素个数比较少或者是数字基本有序的时候,插入排

序是很好的选择。





你可能感兴趣的:(Standard,Library,C++,标准库)