[劍指offer題解][Java]最小的k個數

前言

衆所周知,《劍指offer》是一本「好書」。java

爲何這麼說?git

由於在面試老鳥眼裏,它裏面羅列的算法題在面試中出現的頻率是很是很是高的。github

有多高,以我目前很少的面試來看,在全部遇到的面試算法題中,出現原題的機率大概能有6成,若是把基於原題的變種題目算上,那麼這個出現機率能到達9成,10題中9題見過。面試

若是你是個算法菜雞(和我同樣),那麼最推薦的是先把劍指offer的題目搞明白。算法

至於爲何給「好書」這兩個字打引號,由於這本書成了面試官的必備,若是考生不會這本書上的題目,就極可能獲得面試官負面的評價。這本書快要成爲評判學生算法能力的惟一標準,這使得考前突擊變成了一個慣例,反而讓投機倒把成了必要,並不必定能真正的考察考生的算法能力。後端

對於劍指offer題解這個系列,個人寫文章思路是,對於看了文章的讀者,可以:數組

  • 迅速瞭解該題常看法答思路(奇技淫巧不包括在內,節省你們時間,實在有研究需求的人能夠查閱其它資料)
  • 思路儘可能貼近原書(例如書中提到的面試官常常會要求不改變原數組,或者有空間限制等,儘可能體如今代碼中,保證讀者能夠不漏掉書中細節)
  • 儘可能精簡話語,避免冗長解釋
  • 給出代碼可運行,註釋齊全,對細節進行解釋

題目介紹

輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4,。安全

分類:優化時間和空間效率bash

解題思路

初始思路:直接排序 O(nlogn)

直接對數組排序,排序後前k個數就是答案,排序通常較快的是O(nlogn),顯然這並非時間複雜度最優解。微信

方法一:基於快速排序的變種 O(n)

思路

該方法須要改變原數組。

還記得上一題:數組中超過一半的數字麼?這一題的思路和上題相似,僅僅是換成了

這種算法是受快速排序算法的啓發。

在隨機快速排序算法中,咱們先數組中隨機選擇一個數字,而後調整數組中數字的順序,使得比選中的數字小的數字都排在它的左邊,比選中的數字大的數字都排在它的右邊。若是這個選中的數字的下標恰好是k,咱們就獲得了k個小的數字,這些數字在k的左邊,而且沒有通過排序,可是都比k小。

若是它的下標大於k,咱們能夠接着在它的左邊部分的數組中查找。

若是它的下標小於k,那麼中位數應該位於它的右邊,咱們能夠接着在它的右邊部分的數組中查找。

這是一個典型的遞歸過程

詳細細節見代碼註釋。

代碼

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        // 因爲本題須要返回ArrayList<Integer>,因此新建之
        ArrayList<Integer> list = new ArrayList<>();
        // 若輸入數組長度小於k。直接返回數空的ArrayList
        if(input.length < k){
            return list;
        }

        findKMin(input,0,input.length-1,k);
        for(int i = 0; i < k; i++){
            list.add(input[i]);
        }
        return list;
    }

    private void findKMin(int[] a, int start, int end, int k){
        if(start < end){
            int pos = partition(a, start, end);
            if(pos == k-1){
                return ;
            }else if(pos < k-1){
                findKMin(a,pos+1,end,k);
            }else{
                findKMin(a,start,pos-1,k);
            }
        }
    }

    // 快排中的每次排序實現(挖坑填數法),返回的是交換後start位置(快排一次後的中軸點,中軸點左邊全是小於它的,右邊都是大於它的)
    public int partition(int[] a, int start, int end){
        int pivot = a[start];
        while(start < end){
            while(start < end && a[end] >= pivot){end--;};
            a[start] = a[end];
            while(start < end && a[start] <= pivot){start++;};
            a[end] = a[start];
        }
        a[start] = pivot;
        return start;
    }
}
複製代碼

方法二:適合海量數據的最大堆 O(nlogk)

思路

該方法不改變原數組,但時間複雜度比O(n)略微複雜了些。

構造一個最大堆,最大堆的性質就是堆頂是全部堆中數字的最大值,那麼放入k個數字,隨後將數字中k個數字以後的數字依次和堆中的最大數字比較(也就是和堆頂數字比較),若是小於他,就把堆頂數字彈出,放入小的數字,這樣遍歷一邊數組後,獲得一個k個數字的最大堆,這個最大堆裏存的是最小的k個數。

最大堆的性質由Java中的優先隊列,經過天然數的逆序順序進行維護,也就是下面這句構造:

Queue<Integer> queue = new PriorityQueue<>(k, Collections.reverseOrder());
複製代碼

有的小夥伴會問,爲啥最大堆是最小的k個數?

答:說明你對堆還不夠了解,惡補一下堆的性質吧~

代碼

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution_2(int [] input, int k) {
        // 因爲本題須要返回ArrayList<Integer>,因此新建之
        ArrayList<Integer> res = new ArrayList<>();
        // 幾種特殊狀況
        if (k > input.length|| k == 0) {
            return res;
        }
        // 構造優先隊列,排序方法是天然數順序的逆序,因此是個最大堆,這樣這個堆的堆頂就是全部數中的最大數
        Queue<Integer> queue = new PriorityQueue<>(k, Collections.reverseOrder());

        for (int i = 0; i < input.length; i++) {
            // 最大堆內數字個數少於k,一直添加到k個
            if (queue.size() < k) {
                queue.add(input[i]);
            }
            else {
                // 若堆內最大的數字大於數組中的數字,則將數字出堆,並放入這個小的數
                if (input[i] < queue.peek()) {
                    queue.remove();
                    queue.add(input[i]);
                }
            }
        }

        // 結束上面循環後,堆內就是最小的k個數
        while (!queue.isEmpty()) {
            res.add(queue.remove());
        }
        return res;
    }


    public static void main(String[] args) {
        int[] a = {4,5,1,6,2,7,3,8};
        Solution_40 solution_40 = new Solution_40();
        System.out.println(solution_40.GetLeastNumbers_Solution(a,4));
    }
}
複製代碼

總結

書中提到,第二種堆的方法適合海量數據求k個最小。由於k個數的堆,空間是固定的,當數組超級大,那麼全存入內存都變得不可行的時候,就須要從外存中慢慢讀取數字,而後和這個堆進行比較。

而方法一就必須吧整個數組放入內存中,才能運行,因此不適合海量數據。

關注我

我目前是一名後端開發工程師。技術領域主要關注後端開發,數據爬蟲,數據安全,5G,物聯網等方向。

微信:yangzd1102

Github:@qqxx6661

我的博客:

原創博客主要內容

  • Java知識點複習全手冊
  • Leetcode算法題解析
  • 劍指offer算法題解析
  • SpringCloud菜鳥入門實戰系列
  • SpringBoot菜鳥入門實戰系列
  • Python爬蟲相關技術文章
  • 後端開發相關技術文章

我的公衆號:Rude3Knife

我的公衆號:Rude3Knife

若是文章對你有幫助,不妨收藏起來並轉發給您的朋友們~

相關文章
相關標籤/搜索