劍指Offer(java版):最小的k個數

題目:輸入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;  }

相關文章
相關標籤/搜索