寻找第K大的数字

寻找第k大的数字,有很多方法,最基本的就是将数组按照从大到小的顺序排列,找出第k个元素即可。但是这种方法的时间复杂度为o(nlog(n)),我们还能找到更好地方法。下面我们将介绍另外两种办法,一种是基于快排Partition的方法,一种是基于partial_sort的方法。

基于快排partition的查找法

利用快速排序的思想,从数组S中随机找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。这时有两种情况:

  1. Sa中元素的个数小于k,则Sb中的第k-|Sa|个元素即为第k大数;
  2. Sa中元素的个数大于等于k,则返回Sa中的第k大数。时间复杂度近似为O(n)

递归法

#include <iostream>

using namespace std;

//快速排序的划分函数
int partition(int data[],int i,int j)
//将>=x的元素换到左边区域
//将<=x的元素换到右边区域
{
    if(data==NULL||i<0)
        throw std::exception("invalued parameter");
    int index = i+rand()%(j-i+1);//生产随机数
    int pivot=data[index];
    swap(data[i],data[index]);
    while(i<j)
    {
        while(i<j&&pivot>=data[j])
        {
            j--;
        }
        if (i<j)
        {
            data[i++]=data[j];
        }
        while(i<j&&pivot<=data[i])
        {
            i++;
        }
        if (i<j)
        {
            data[j--]=data[i];
        }
    }
    data[i]=pivot;
    return i;
}


//线性寻找第k大的数,递归法
int random_select(int a[],int l,int r,int k)
{
    if(k>(r-l+1)||k<1)//k应该在[1,r-l+1]之间
        throw std::exception("invalued k");

    int i,j;
    if (l == r) //递归结束
    {
        return a[l];
    }
    i = partition(a,l,r);//划分
    j = i-l+1;//距开始的长度,i为第j大的数字
    if(k == j) //递归结束,找到第K大的数
        return a[i];
    if(k < j)
    {
        return random_select(a,l,i-1,k);//递归调用,在前面部分查找第K大的数
    }
    else
        return random_select(a,i+1,r,k-j);//递归调用,在后面部分查找第K大的数
}

int main()
{
    int a[]={1,2,3,4,6,6,7,8,10,10};

    cout<<random_select(a,0,9,1)<<endl;
    cout<<random_select(a,0,9,5)<<endl;
    return 0;
}

循环法

#include <iostream>

using namespace std;

//快速排序的划分函数
int partition(int data[],int len, int i, int j)
//将>=x的元素换到左边区域
//将<=x的元素换到右边区域
{
    if (data == NULL ||len<=0|| i < 0||j>=len)
        throw std::exception("invalued parameter");
    int index = i + rand() % (j - i + 1);//生产随机数
    int pivot = data[index];
    swap(data[i], data[index]);
    while (i < j)
    {
        while (i < j&&pivot >= data[j])
        {
            j--;
        }
        if (i < j)
        {
            data[i++] = data[j];
        }
        while (i < j&&pivot <= data[i])
        {
            i++;
        }
        if (i<j)
        {
            data[j--] = data[i];
        }
    }
    data[i] = pivot;
    return i;
}


//线性寻找第k大的数,循环法
int random_select(int a[],int len, int k)
{

    if (a == NULL || len <= 0 || k > len || k <= 0)
    {
        throw std::exception("invalued parameter");
    }
    int i = 0;
    int j = len - 1;
    int index = partition(a, len, i, j);
    while (index+1!= k)
    {
        if (index+1> k)
        {
            index = partition(a, len, i, index - 1);
        }
        else
        {
            index = partition(a, len, index + 1, j);
        }
    }
    int result = a[k-1];
    return result;

}

int main()
{
    int a[] = { 1, 2, 3, 4, 6, 6, 7, 8, 10, 10 };

    cout << random_select(a,10, 1) << endl;
    cout << random_select(a, 10,3) << endl;
    return 0;
}

上面两种办法均是基于partiton的方法,实际上在C++ STL中也有一个实现查找第k大元素的方法:nth_element.

nth_element

nth_element用于排序一个区间,它使得位置n上的元素正好谁全排序情况下的第n个元素,而且,当nth_element返回的时候,所有按照全排序规则排在位置n之前的元素也都排在位置n之前,按照全排序规则排在n之后的元素全都排在位置n之后。
所以,我们使用nth_element既可以寻找最好的前k个元素,也可以寻找第k大元素。

先看示例:

#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
int main()
{
    int a[10] = { 8, 9, 1, 2, 4, 3, 5, 6, 7, 10 };
    nth_element(a, a + 5, a + 10, std::greater<int>());
    cout << "数组中的中间元素是" << a[5] << endl;
    nth_element(a, a, a + 10, std::greater<int>());
    cout << "数组中第1大元素为" << a[0] << endl;
    nth_element(a, a + 1, a + 10, std::greater<int>());
    cout << "数组中第2大元素是" << a[1] << endl;
}

结果为:

数组中的中间元素是5
数组中第1大元素为10
数组中第2大元素是9
请按任意键继续. . .

使用说明:

default (1) 
template <class RandomAccessIterator>
  void nth_element (RandomAccessIterator first, RandomAccessIterator nth,
                    RandomAccessIterator last);
custom (2)  
template <class RandomAccessIterator, class Compare>
  void nth_element (RandomAccessIterator first, RandomAccessIterator nth,
                    RandomAccessIterator last, Compare comp);

default (1) 使用‘<’作为比较符
custom (2) 可以指定比较运算
nth:实际指的是nth+1大(小)元素,也就是如果你要找前20个最大的,nth=19.

基于partial_sort的查找法

用O(4*n)的方法对原数组建最大堆,然后pop出k次即可。时间复杂度为O(4*n + k*logn)
在C++ STL中同样有一个部分排序的函数就是partial_sort。
用法:

default (1) 
template <class RandomAccessIterator>
  void partial_sort (RandomAccessIterator first, RandomAccessIterator middle,
                     RandomAccessIterator last);
custom (2)  
template <class RandomAccessIterator, class Compare>
  void partial_sort (RandomAccessIterator first, RandomAccessIterator middle,
                     RandomAccessIterator last, Compare comp);

default (1) :使用‘<’作为比较符
custom (2) :可以指定比较运算
middle:将前middle个元素排序,比如说我们要找到前10个最小的元素,而且从小到大排列,这时可以使用partial_sort,middle=10.
注意
partial_sort与上面的nth_element相比,需要对前面最好的n个元素排序,而nth_element只需要找到前面最好的n个元素就可以,不需要知道顺序。
而且关于第二个参数的使用上截然不同:partial_sort使用第1个和第2个迭代器指明一段需要排序的区间,根据STL关于区间的定义,第2个参数应该是目标区间外的第一个元素(比如widgets.begin()+20实际指的是第21个元素),而nth_element则使用第2个参数标识出容器中的某个特定位置(比如widgets.begin()+19实际指的是第20个元素)
详细情况请参考:《理解你的排序操作[(stable_sort,sort,partial_sort,nth_element,stable_partition,partition)》
因此我们可以先部分排序,再直接取第k个元素即可。

#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
int main()
{
    int a[10] = { 8, 9, 1, 2, 4, 3, 5, 6, 7, 10 };
    partial_sort(a, a + 6, a + 10, std::greater<int>());
    cout << "数组中的中间元素是" << a[5] << endl;
    partial_sort(a, a + 2, a + 10, std::greater<int>());
    cout << "数组中第1大元素为" << a[0] << endl;
    partial_sort(a, a + 3, a + 10, std::greater<int>());
    cout << "数组中第2大元素是" << a[1] << endl;
}

你可能感兴趣的:(快速排序,STL,nth-elemen)