尋找最大的K個數

編程之美有一道考察多種排序的題目,題目如下:
有一個長度爲N的無序數組,假定其中的每一個元素都各不相等,求其中最大的K個數。
作者對於此題目結合各種排序算法給出了五種解法思路。
解法一:
使用快速排序或堆排序對它們元素進行排序,整個排序的時間複雜度爲O(N*log2 N),然後取出前K個,時間複雜度爲O(K),總時間複雜度O(N*log2 N)+O(K)=O(N*log2 N).根據題目要求,並不需要後N-KN-K個數有序,而只需要尋找最大K的個數,因此可以用選擇排序和交換排序進行部分排序來篩選最大的K個數,其時間複雜度爲O(N*K).
解法二:
利用快速排序思想對數據進行分組,讓其一組數據的任何一個數都比另一組中的任意數大,整個算法的時間複雜度是O(N*log2 K),其情況如下:
假設N個數存儲在數組S中,我們從數組S中隨機的找到一個元素X,把數組分成兩部分Sa和Sb。Sa中的元素大於等於X,Sb中的元素小於X。
(1)Sa中的元素個數小於K,Sa中所有的數和Sb中最大的K-|Sa|個元素就是數組S中最大的K個數。
(2)Sa中的元素的個數大於等於K,則需要返回Sa中最大的K個元素。
代碼實現:

#include <iostream>
#include<stdlib.h>
#include<stdio.h>
using namespace std;
//返回基準索引
int partition(int a[],int low,int height)
{
   //存儲基準值對應數組的索引
   int index;
   //利用隨機選擇比較基準值,以避免特殊數據下的算法退化
    swap(a[low],a[low+rand()%(height-low+1)]);
    int p=a[low];//比較的基準值
    int i=low,j=height;
    do
    {
       //從右端開始尋找大於基準值的第一個值
      while(i<j&&a[j]<p)j--;
      //比基準值大的數往左邊移
      if(i<j)
         a[i++]=a[j];
      //從左端端開始尋找小於基準值的第一個值
      while(i<j&&a[i]>=p)i++;
       //比基準值小的數往右邊移
      if(i<j)
         a[j--]=a[i];
    }while(i<j);
    a[i]=p;
    index=i;
    return index;
}
int Kbig(int a[],int low,int height,int k)
{
   if(low>=height)
      return a[low];
   int index=partition(a,low,height);
   //獲得數組連續最大的數目
   int count=index-low+1;
   //獲得數組連續最大的數目等於K則輸出
   // 若小於K則是第一種情況
   //若大於K則是第二種情況
   if(count==k)
      return a[index];
   if(count<k)
      return Kbig(a,index+1,height,k-count);//後面部分尋找最大的K個數
   else
      return Kbig(a,low,index-1,k);//前面部分尋找最大的K個數
}
//交換數據
void swap(int *a,int *b)
{
   int temp=*a;
   *a=*b;
   *b=temp;
}
int main()
{
   int a[]={100,2000,45,68,5,700,9,50,45,89,87};
   int len=sizeof(a)/sizeof(int);
   int k=5;
   cout<<Kbig(a,0,len-1,k)<<endl;
   for(int i=0;i<k;i++)
      cout<<a[i]<<"\t";
    return 0;
}

解法三思路:
尋找N個數中最大的K個數,本質上就是尋找最大的K個數中最小的那個,也就是第K大的數,因此可以使用二分搜索的策略,對一個給定的數p,可以在O(N)的時間複雜度內找出所有不小於P的數,假如N個常數最大的數爲Vmax,最小的數爲Vmin,那麼這N個數中的第K大數一定在區間[Vmin,Vmax]之間,整個算法的時間複雜度也爲O(N*log2 N).
代碼實現:

#include <iostream>
#include<stdlib.h>
#include<stdio.h>
using namespace std;
int find(int*a,int x,int len)
{
   int number=0;
   for(int i=0;i<len;i++)
      if(a[i]>=x)
         number++;
   return number;
}
int findKMax(int *a,int max,int min,int k,int len,int delta)
{
   while(max-min>delta)
   {      
      int mid=min+(max-min)/2;
       //若後面部分大於mid的個數大於K,那麼查找後半部分
      if(find(a,mid,len)>=k)
         min=mid;
      //若後面部分大於mid的個數小於K,那麼查找前半部分
      else
         max=mid;
   }
   return min;
}
int main()
{
   int a[]={100,2000,45,68,5,700,9,50,45,89,87};
   int len=sizeof(a)/sizeof(int);
   int k=5;
   //該取值要比所有N個數中的任意兩個不相等的元素差值之最小值都要小
   int delta=1;
   cout<<findKMax(a,2000,5,5,len,delta)<<endl;   
    return 0;
}

解法四思路
如果數據很大,100億多個,這個時候數據不能全部裝入內存,所以要求儘可能少地遍歷所有數據,如果能通過逐漸的加入元素來判斷,能就可以解決這問題了,因此可以用最小堆的結構來求出前面已加入元素的前K個數,因此可以構造K個元素的小頂堆來存儲最大的K個數,最小堆的堆頂(Y)就是最大的K個數中的最小的一個,對於每一個新加入的數X,如果X < Y,則不需要更新堆,如果X>Y,那麼用X替換掉堆頂的元素Y,然後調整小頂堆結構,整個過程遍歷一次數組元素就行了,因此整個算法的時間複雜度O(N *log2 K ).
代碼實現:

#include <iostream>
#include<stdlib.h>
#include<stdio.h>
using namespace std;
//調整堆
void adjustMinHeap(int *a,int m,int n)
{
   int i=m;
   int j=2*i+1;//左結點
   int temp;//臨時存儲
   while(j<n)
   {
      //比較左右結點得到較小的那個結點
      if(j+1<n&&a[j]>a[j+1])
         j++;
      //若父結點大於較小的那個子結點,則交換,否則就跳出循環
      if(a[i]>a[j])
      {
         temp=a[j];
         a[j]=a[i];
         a[i]=temp;
         i=j;
         j=2*i+1;
      }
      else
         break;
   }
}
//創建k個元素最小堆
void createMinHead(int *a,int k)
{
   //自下而上的建立堆,從最後一個非葉子結點開始初始化最小堆
   for(int i=k/2-1;i>=0;i--)
      adjustMinHeap(a,i,k);
}
void FindKMax(int *a,int n,int *kMax,int k)
{
   for(int i=0;i<k;i++)
      kMax[i]=a[i];
   //初始K個元素最小堆
   createMinHead(kMax,k);
   for(int i=k;i<n;i++)
   {
      //如果該元素大於她的最小堆則替換
      if(a[i]>kMax[0])
      {
         kMax[0]=a[i];
         adjustMinHeap(kMax,0,k);
      }
   }
}
int main()
{
   int a[]={100,2000,45,68,5,700,9,50,45,89,87};
   int len=sizeof(a)/sizeof(int);
   int k=5;
   int * kMax=(int*)malloc(k*sizeof(int));
   FindKMax(a,len,kMax,k);
   for(int i=0;i<k;i++)
      cout<<kMax[i]<<"\t";
    return 0;
}

第五中解法思路:
利用計數排序思想,如果所有N個數都是正整數,且它們的取值範圍不太大,可以考慮申請空間,記錄每個整數出現的次數,然後再從大到小取最大的K個,比如,所有整數都在(0,MAXN)區間中的話,利用一個數組count[MAXN]來記錄每個整數出現的個數(count[i]表示整數i在所有整數中出現的個數),我們只須掃描一遍就可以得到count數組,然後尋找第K大的元素,整個算法時間複雜度爲O(n).
代碼實現:

#include <iostream>
#include<stdlib.h>
#include<stdio.h>
using namespace std;
#define MAX 100000
int FindKMax(int *a,int k)
{
   int sumCount=0;
   int i;
   //從大到小開始計算其元素個數,直到爲K
   for(i=MAX-1;i>=0;i--)
   {
      sumCount+=a[i];
      if(sumCount>=k)
         break;
   }
   return i;
}
int main()
{
   int a[]={100,2000,45,68,5,700,9,50,45,89,87};
   int len=sizeof(a)/sizeof(int);
   int count[MAX]={0};
   int k=5;
   //統計a元素對應出現個數
   for(int i=0;i<len;i++)
   {
      count[a[i]]++;
   }
   cout<<FindKMax(count,k)<<endl;
    return 0;
}
發佈了119 篇原創文章 · 獲贊 10 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章