劍指Offer-31-最小的K個數

SpecialYang發表於2019-03-03

題目

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

解析

思路一

顯然最簡答做法就是對原陣列排序,取前k個就行。

Note: 這裡可以分情況的:

  1. 如果k遠小於n, 可以利用一次冒泡或者選擇演算法,選擇出當前序列中最小的值,複雜度為O(nk)
  2. 如果k沒有遠小於n, 那麼選擇O(nlogn)演算法最佳
    /**
     * 排序的做法
     * @param input
     * @param k
     * @return
     */
    public static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        if(input == null || k <= 0 || k > input.length) {
            return result;
        }
        Arrays.sort(input);
        for(int i = 0; i < k; i++) {
            result.add(input[i]);
        }
        return result;
    }
複製程式碼

思路二

我們注意到題目並沒有要求輸出的最小k個數必須是有序的,所以我們可以利用快排中partion函式的思想來做做題。
因為partion可以使得序列分為2部分:左邊的值都小於哨兵,右邊的值都大於哨兵。所以我們只要找到處於第k位置的哨兵即可,也就是說找到第k大的值所在的位置即可,那麼它的左邊的k-1值都小於等於第k大值。顯然,前k個值即為我們所求的最小k個數。在我們的劃分過程有3種情況:

  1. 哨兵的位置大於k,說明第k大的數在左邊,繼續遞迴處理左部分即可。
  2. 哨兵的位置小於k,說明第K大的數在右邊,繼續遞迴處理有部分即可。
  3. 哨兵的位置等於k,說明該哨兵即為第K大的值,其左邊k-1個數都小於等於它,因此輸出前k個即為所求的結果。
    /**
     * 基於快排的劃分函式的思想來做的。
     * @param input
     * @param k
     * @return
     */
    public static ArrayList<Integer> GetLeastNumbers_Solution2(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        if(input == null || k <= 0 || k > input.length) {
            return result;
        }
        findKthValue(input, 0, input.length - 1, k - 1);
        for(int i = 0; i < k; i++) {
            result.add(input[i]);
        }
        return result;
    }

    public static void findKthValue(int[] input, int low, int high, int k) {
        if(low < high) {
            int pivot = new Random().nextInt(high - low + 1) + low;
            swap(input, pivot, high);
            int index = low;
            for(int i = low; i < high; i++) {
                if(input[i] < input[high]) {
                    swap(input, i, index);
                    index++;
                }
            }
            swap(input, index, high);
            if(index > k) {
                findKthValue(input, low, index - 1, k);
            }else if(index < k) {
                findKthValue(input, index + 1, high, k);
            }
        }
    }
複製程式碼

思路3

這是典型的Top-K問題,即從n個數中找出最小的k個數或者最大的k個數問題。
我們通常的做法用一個容量為k的容器來存放這k個最小的值。我們只需遍歷一遍原陣列,就能得到最小的k個數。

  1. 起初容器是空,當已遍歷的數的個數小於容器的容量k時,直接向容器中新增該值。
  2. 當容器的容量已滿,則判斷該容器中最大值是否大於待插入的點:
    1. 若大於,則從容器中刪除該最大值,新增待插入的點
    2. 若小於或者等於,則不做任何操作,繼續遍歷下一個值

問題轉化為如何高效率得到容器中的最大值。一個優雅的資料結構完美的解決此題,即堆結構,分為大根堆或者小根堆。顯然這裡應該選擇大根堆。在大根堆中,根節點大於左子樹和右子樹中所有點,所以我們只需訪問根節點即可得到k容量的最大值,且資料結構可以對插入的值進行動態調整堆結構,使得滿足大根堆。關於堆的具體程式碼,以後我單獨寫一個部落格,這裡不再累述了。
在Java中,沒有專門的堆資料結果,不過有基於堆結構的優先佇列,所以這裡採用優先佇列並自定義比較器,來滿足大根堆的需求。

     /**
     * Topk問題
     * @param input
     * @param k
     * @return
     */
    public static ArrayList<Integer> GetLeastNumbers_Solution3(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        if(input == null || k <= 0 || k > input.length) {
            return result;
        }
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k, new Comparator<Integer>() {

            //因為要滿足大根堆需求,所以使用自定義比較器,比較策略為o1大於o2時,o1放o2的前面
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        for(int i = 0; i < input.length; i++) {
            if(i < k) {
                priorityQueue.add(input[i]);
            } else if(input[i] < priorityQueue.peek()) {
                priorityQueue.poll();
                priorityQueue.add(input[i]);
            }
        }
        result.addAll(priorityQueue);
        return result;
    }
複製程式碼

總結

多結合排序演算法和常見的資料結構來簡化題目。


文章收錄在[個人專欄(upadating)](https://blog.csdn.net/column/details/23876.html),期待與你一起KO常見面試題。

相關文章