題目:輸入n個整數,找出其中最小的k個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4java
這道題最簡單的思路莫過於把輸入的n個整數排序,排序以後位於最前面的k個數就是最小的k個數。這種思路的時間複雜度是O(nlogn),面試官會提示咱們還有更快的算法。面試
解法一:O(n)的算法,只有當咱們能夠修改輸入的數組時可用算法
從上一題中咱們能夠獲得啓發,咱們一樣能夠基於Partition函 數來解決這個問題。若是基於數組的第k個數字來調整,使得比第k個數字小的全部數字都位於數組的左邊,比第k個數字大的全部數字都位於數組的右邊。這樣調 整以後,位於數組中左邊的k個數字就是最小的k個數字。下面是基於這種思路的Java代碼:數組
package cglib;函數
public class DeleteNode{
// 使用partition函數
public int partition(int[] arr, int left, int right) {
int result = arr[left];
if (left > right)
return -1;
while (left < right) {
while (left < right && arr[right] >= result) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] < result) {
left++;
}
arr[right] = arr[left];
}
arr[left] = result;
return left;
}
public int[] getLeastNumbers(int[] input,int k){
if(input.length == 0 || k<= 0)
return null;
int[] output = new int[k];
int start = 0;
int end = input.length-1;
int index = partition(input,start,end);
while(index != k-1){
if(index > k-1){
end = index -1;
index = partition(input,start ,end);
}
else{
start = index+1;
index = partition(input,start ,end);
}
}
for(int i = 0;i<k;i++){
output[i] = input[i];
}
return output;
}
public static void main(String[] args){
int[] arr= {4,5,1,6,2,7,3,8};
DeleteNode test = new DeleteNode();
int[] output=test.getLeastNumbers(arr, 4);
for(int i = 0;i<output.length ;i++){
System.out.print(output[i]+",");
}
}
}
大數據
輸出:ui
3,2,1,4,spa
採用這種思路是有限制的。咱們須要修改輸入的數組,由於函數Partition會調整數組中數字的順序。若是面試官要求不能修改輸入的數組 。排序
解法二:O(nlogk)的算法,特別適用處理海量數據內存
咱們能夠先建立一個大小爲k的數據容器來存儲最小的k個數字,接下來 咱們每次從輸入的n個整數中讀入一個數。若是容器中已有數字少於k個,則直接把此次讀入的整數放入容器中;若是容器中已有k個數字了,也就是容器已滿,此 時咱們不能再插入新的數字了而只能替換已有的數字。找出這已有的k個數中的最大值,而後拿此次待插入的整數和最大值進行比較。若是待插入的值比當前已有的 最小值小,則用這個數替換當前已有的最大值;若是待插入的值比當前已有的最大值還大,那麼這個數不多是最小的k個整數之一,因而咱們能夠拋棄這個整數。
所以當容器滿了以後,咱們要作3件事;一是在k個整數中找到最大數; 二是有可能在這個容器中刪除最大數;三是有可能要插入一個新的數字。若是用一個二叉樹來實現這個容器,那麼咱們能在O(logk)時間內實現這三步操做。 所以對於n個輸入的數字而言,總的時間效率是O(nlogk).
咱們能夠選擇用不一樣的二叉樹來實現這個數據容器。因爲每次都須要找到 k個整數中的最大數字,咱們很容易想到用最大堆。在最大堆中,根節點的值老是大於它的子樹中的任意結點的值。因而咱們每次能夠在O(1)獲得已有的k個數 字中的最大值,但須要O(logk)時間完成刪除及插入操做。
下面是Java代碼實現步驟:
package cglib;
import java.util.Arrays;
public class DeleteNode{
public void buildMaxHeap(int[] arr,int lastIndex){
System.out.println("lastIndex="+lastIndex);
System.out.println("(lastIndex-1)/2="+((lastIndex-1)/2));
for(int i = (lastIndex-1)/2;i>=0;i--){
int k = i;
System.out.println("k="+k);
System.out.println("2*k+1="+(2*k+1));//4,5,1,6
while(2*k+1 <= lastIndex){
int biggerIndex = 2*k+1;
System.out.println("biggerIndex="+biggerIndex);
System.out.println("lastIndex="+lastIndex);
System.out.println("arr[biggerIndex]="+arr[biggerIndex]);
System.out.println("arr[biggerIndex+1]="+arr[biggerIndex+1]);
System.out.println("arr[k]="+arr[k]);
if(biggerIndex <lastIndex){
if(arr[biggerIndex]< arr[biggerIndex+1])
biggerIndex++;
System.out.println("biggerIndex="+biggerIndex);
}
if(arr[k] < arr[biggerIndex]){
System.out.println("biggerIndex="+biggerIndex);
System.out.println("k="+k);
System.out.println("哈哈進入swap");
swap(arr,k,biggerIndex);
System.out.println("換後arr[biggerIndex]="+arr[biggerIndex]);
System.out.println("換後arr[k]="+arr[k]);
k = biggerIndex;
System.out.println("換後k="+k);
}
else
break;
}
}
}
public static void swap(int[] arr,int i ,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public void heapSort(int[] arr){
for(int i = 0;i<arr.length-1;i++){
System.out.println("i="+i);
System.out.println("arr.length="+arr.length);
System.out.println("arr.length-i-1="+(arr.length-i-1));
System.out.println("進到buildMaxHeap");
buildMaxHeap(arr,arr.length-i-1);
System.out.println("進入swap");
swap(arr,0,arr.length-i-1);
}
}
public void getLeastNumbers(int[] arr,int k){
if(arr == null || k<0 || k>arr.length)
return;
//根據輸入數組前k個數遍歷最大堆
//從k+1個數開始與根節點比較
//大於根節點,捨去
//小於,取代根節點,重建最大堆
int[] kArray = Arrays.copyOfRange(arr, 0, k);
System.out.println("第一次進到heapSort");
heapSort(kArray);
for(int i = k;i<arr.length;i++){
System.out.println("arr[i="+i+"]:"+arr[i]);
System.out.println("kArray[k-1="+(k-1)+"]:"+kArray[k-1]);
if(arr[i]<kArray[k-1]){
kArray[k-1] = arr[i];
System.out.println("賦值:kArray[k-1="+(k-1)+"]:"+kArray[k-1]);
System.out.println("第二次進到heapSort");
heapSort(kArray);
}
}
System.out.println("這k個數是:");
for(int i:kArray)
System.out.print(i);
}
public static void main(String[] args){
int[] arr= {4,5,1,6,2,7,3,8};
DeleteNode test = new DeleteNode();
test.getLeastNumbers(arr, 3);
}
}
輸出:
第一次進到heapSort
i=0
arr.length=3
arr.length-i-1=2
進到buildMaxHeap
lastIndex=2
(lastIndex-1)/2=0
k=0
2*k+1=1
biggerIndex=1
lastIndex=2
arr[biggerIndex]=5
arr[biggerIndex+1]=1
arr[k]=4
biggerIndex=1
biggerIndex=1
k=0
哈哈進入swap
換後arr[biggerIndex]=4
換後arr[k]=5
換後k=1
進入swap
i=1
arr.length=3
arr.length-i-1=1
進到buildMaxHeap
lastIndex=1
(lastIndex-1)/2=0
k=0
2*k+1=1
biggerIndex=1
lastIndex=1
arr[biggerIndex]=4
arr[biggerIndex+1]=5
arr[k]=1
biggerIndex=1
k=0
哈哈進入swap
換後arr[biggerIndex]=1
換後arr[k]=4
換後k=1
進入swap
arr[i=3]:6
kArray[k-1=2]:5
arr[i=4]:2
kArray[k-1=2]:5
賦值:kArray[k-1=2]:2
第二次進到heapSort
i=0
arr.length=3
arr.length-i-1=2
進到buildMaxHeap
lastIndex=2
(lastIndex-1)/2=0
k=0
2*k+1=1
biggerIndex=1
lastIndex=2
arr[biggerIndex]=4
arr[biggerIndex+1]=2
arr[k]=1
biggerIndex=1
biggerIndex=1
k=0
哈哈進入swap
換後arr[biggerIndex]=1
換後arr[k]=4
換後k=1
進入swap
i=1
arr.length=3
arr.length-i-1=1
進到buildMaxHeap
lastIndex=1
(lastIndex-1)/2=0
k=0
2*k+1=1
biggerIndex=1
lastIndex=1
arr[biggerIndex]=1
arr[biggerIndex+1]=4
arr[k]=2
進入swap
arr[i=5]:7
kArray[k-1=2]:4
arr[i=6]:3
kArray[k-1=2]:4
賦值:kArray[k-1=2]:3
第二次進到heapSort
i=0
arr.length=3
arr.length-i-1=2
進到buildMaxHeap
lastIndex=2
(lastIndex-1)/2=0
k=0
2*k+1=1
biggerIndex=1
lastIndex=2
arr[biggerIndex]=2
arr[biggerIndex+1]=3
arr[k]=1
biggerIndex=2
biggerIndex=2
k=0
哈哈進入swap
換後arr[biggerIndex]=1
換後arr[k]=3
換後k=2
進入swap
i=1
arr.length=3
arr.length-i-1=1
進到buildMaxHeap
lastIndex=1
(lastIndex-1)/2=0
k=0
2*k+1=1
biggerIndex=1
lastIndex=1
arr[biggerIndex]=2
arr[biggerIndex+1]=3
arr[k]=1
biggerIndex=1
k=0
哈哈進入swap
換後arr[biggerIndex]=1
換後arr[k]=2
換後k=1
進入swap
arr[i=7]:8
kArray[k-1=2]:3
這k個數是:
123
解法比較:
基於函數Partition的第一種解法的平均時間複雜度是O(n),比第二種思路要快,但同時它也有明顯的限制,好比會修改輸入的數組。
第二種解法雖然要慢一點,但它有兩個明顯的優勢。一是沒有修改輸入的 數據。二是該算法適合海量數據的輸入(包括百度在內的多家公司很是喜歡與海量數據相關的問題)。假如題目是要求從海量的數據中找出最小的k個數字,因爲內 存的大小是有限的,有可能不能把這些海量數據一次性所有加載入內存。這個時候,咱們能夠輔助存儲空間(好比磁盤)中每次讀入一個數字,根據 GetLeastNumbers的方式判斷是否是須要放入容器LeastNumbers便可。這種思路只要求內存可以容納leastNumbers便可。 所以它適合的情形就是n很大而且k較小的問題。
以下圖比較兩種算法:
因爲這兩種算法各有優缺點,各自適用於不一樣的場合,所以應聘者在動手寫代碼以前要清楚題目的要求,包括輸入的數據量有多大,可否一次性載入內存,是否容許交換輸入數據中數字的順序等。
總結:
1. 先對數據排序,而後取出前K個數。排序算法不少,什麼插入排序、快速排序等等,不過都不可能最壞也能到達O(N);
2. 開一個 |K| 大小的數組,先從這堆數據中裝入前K個數,找出這K個數中的最大數Max(K),而後從第K+1個數開始向後找,若是有小於這個Max(K)的,則替換掉 這個數,而後從這K個數中從新找出最大的Max(K)。這樣一直向後掃描,獲得結果。這個算法的複雜度最壞是O(Kn);
3. 用到堆!先用數據中的前K個數建一個最大堆,建堆複雜度是O(K),而後從第K+1個數開始向後掃描,遇到小於堆頂元素時替換掉堆頂元素,更新堆,這個操做的複雜度是O(logK)。因此總的時間是O(K+(n-K)*logK)=O(n*logK),比方法2的O(nK)稍微好一點。
這種方法有個好處,就是當數據量很大時,若是內存放不下全部的數據,用這方法能夠解決這個問題。先讀出一部分數據,建堆,處理完這部分數據,再讀出一部分數據,如此循環下去,直達數據處理完爲止。
4. 也是用堆,不過是對整個數據建一個最小堆(O(n)),而後取出堆頂元素,每取完一次更新一次堆(O(logn)),取K次,因此總的複雜度是O(n+K*logn);
能夠證實O(n+K*logn) < O(n*logK),即創建n個元素是最小堆而後取前K個堆頂元素的方法比創建一個K個元素的最大堆而後比較全部數據獲得最小的K個數的方法在時間複雜度上稍微優越一點,但二者其實是一個數量級,在那篇博文裏面,做者特地寫了實現了這兩種方法去處理一組大數據,結果代表兩種方法的時間實際上相差很少。
但在空間上,最大堆只須要O(K)的空間複雜度,而最小堆須要O(n),因此綜合來說,最大堆解決這種方法比最小堆有優點。
算法改進:每次取走堆頂元素更新堆時,正常是把堆中最後一個元素放到堆頂(暫且稱爲 !Top),而後調整堆把 !Top下調到他應該在的位置。改進後, !Top不用下調到他原所應該在的位置,而是下調頂多K次就能夠了。具體以下:
創建n 的最小堆以後,取走堆頂元素(第一個數),而後將最後的數 !Top調到堆頂,把 !Top下調至多K-1層造成新的堆;接着取走堆頂元素(第二個數), 一樣,更新堆的時候 !Top下調至多K-2層...直到取走第K個數時,再也不更新堆(此時的堆已經不是最小堆),算法結束,已經取得最小的K個數,最後 的「堆」是否是堆已經跟我不要緊了。
改進後的複雜度:建堆O(n),更新堆O(K),K次更新爲O(K*K)=O(K^2),因此總的複雜度是O(n+K^2),比改進前的O(n+K*logn)要好。
5. 用快速排序的思想,先選取一個數做爲基準比較數(做者稱爲「樞紐元」,即pivot),用快排方法把數據分爲兩部分Sa和Sb。
若是K< |Sa|( |Sa|表示Sa的大小),則對Sa部分用一樣的方法繼續操做;
若是K= |Sa|,則Sa是所求的數;
若是K= |Sa| + 1,則Sa和這個pivot一塊兒構成所求解;
若是K> |Sa| + 1,則對Sb部分用一樣的方法查找最小的(K- |Sa|-1)個數(其中Sa和pivot已是解的一部分了)。
與快排不一樣,快排每次都要對劃分後的兩部分數據都繼續進行一樣的快排操做,快速選擇(暫時這麼稱呼這種算法吧)不一樣,只對其中一部分進行操做便可。
BFPRT算法就是在這個方法的基礎上進行改進的。BFPRT算法主要改進是在選取pivot上面,通常快排是在數據堆取第一個或最後一個數最爲pivot,而BFPRT算法採用「五分化中位數的中位數」方法取得這個pivot,從而使算法複雜度下降到O(N),具體方法以下:
以5個數爲一組對數據進行劃分,最後一組數據的個數爲n%5,而後對每組數據用插入排序方法選出中位數,對選出的中位數用一樣的方法繼續選,最後選出這些數的中位數做爲pivot,便可達到O(N)的效率。
快速排序:
public static void quickSort(int[] arr, int start, int end) {
if (start < end) {
int key = arr[start];
int right = start;
int left = end;
while (right < left) {
while (right < left && arr[left] > key) {
left --;
}
if (right < left) {
arr[right] = arr[left];
}
while (right < left && arr[right] <= key) {
right ++;
}
if (right < left) {
arr[left] = arr[right];
}
}
arr[right] = key;
quickSort(arr, start, right-1);
quickSort(arr, left+1, end);
}
}
快速排序以後,數組會是有序的,上面的排序是從小到大的,因此咱們輸出應該是下面這樣
複製代碼 代碼以下:
int k = 4;
for (int i=arr.length-1; i>=arr.length-k; i--) {
System.out.println(arr[i]+" ");
}
部分排序
public static int[] selectSortK(int[] arr, int k) { if(arr == null || arr.length == 0) { return null; } int[] newArr = new int[k]; List<Integer> list = new ArrayList<Integer>();//記錄每次最大數的下標 for (int i=0; i<k; i++) { int maxValue = Integer.MIN_VALUE; //最大值 int maxIndex = i; for (int j=0; j<arr.length; j++) { if (arr[j] > maxValue && !list.contains(j) ) { maxValue = arr[j]; maxIndex = j; } } if (!list.contains(maxIndex)) {//若是不存在,就加入 list.add(maxIndex); newArr[i] = maxValue; } } return newArr; }