1. 程式人生 > 實用技巧 >劍指Offer_#40_最小的k個數

劍指Offer_#40_最小的k個數

劍指Offer_#40_最小的k個數

劍指offer

Contents

題目

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

示例 1:

輸入:arr = [3,2,1], k = 2
輸出:[1,2] 或者 [2,1]

示例 2:

輸入:arr = [0,1,2,1], k = 1
輸出:[0]

限制:

0 <= k <= arr.length <= 10000
0 <= arr[i]<= 10000

思路分析

方法1:利用快排中的切分partition()函式

快排當中的partition函式,可以基於陣列中某個元素v作為標準,將比v小的元素放到v的前面,將比v大的元素放到v的後面。我們可以不斷呼叫這個函式,縮小切分範圍,直到v元素的索引剛好是k-1。此時陣列的前k項就是結果。
與快排不同的是,快排的出口條件是hi = lo + 1,即需要不斷縮小範圍,直到指標相鄰,最終整個陣列都是有序的。
在本題中,返回的陣列不需要是有序的,所以跟快排有所不同。
關於快排及partition

函式,參考《演算法第四版》2.3節

方法2:大根堆輔助(可用Java中的PriorityQueue實現)

用一個數據容器來儲存最小的k個數字,最合適的是大根堆,每次可以自動彈出最大值。Java中的PriorityQueue可以實現。
在堆的元素個數小於k時,按順序將數組裡數字壓入。
堆的元素超過k時,如果數組裡的數字小於堆裡最大值,替換掉堆裡的最大值。

解答

解法1:利用快排中的切分partition()函式

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if
(arr.length == 0 || k == 0) return new int[0]; int start = 0; int end = arr.length - 1; int index = partition(arr, start, end); while(index != k - 1){ if(index > k - 1){ end = index - 1; index = partition(arr, start, end); } else{ start = index + 1; index = partition(arr, start, end); } } //返回nums陣列的前k個元素 return Arrays.copyOf(arr, k); } int partition(int[] nums,int lo,int hi){ int v = nums[lo]; //左右掃描指標 //i不需要初始化為lo-1,因為第一個nums[lo]是k,不需要掃描這個元素 int i = lo,j = hi + 1; while(true){ //錯誤寫法:當k == nums.length時,會導致陣列越界 //while(nums[++i] < v) if(i == hi) break; //while(nums[--j] > v) if(j == lo) break; //i從lo + 1開始向後掃描,直到遇到 大於等於v的數字,指標i停留在此 while(++i <= hi && nums[i] < v); //j從hi開始向前掃描,直到遇到 小於等於v的數字,指標j停留在此 while(--j >= lo && nums[j] > v); //i > j的情況,已經切分好左右子陣列,不可以再交換了 //i = j的情況,無需交換 if(i >= j) break; exch(nums, i, j); } exch(nums, lo, j);//將v放入正確的位置 return j; } //陣列nums作為形參,屬於引用傳遞,所以函式執行後可以改變實參 void exch(int[] nums, int idxA, int idxB){ int tmp = nums[idxA]; nums[idxA] = nums[idxB]; nums[idxB] = tmp; } }

複雜度分析

時間複雜度:,因為每次遍歷的範圍可以看作上次的一半,
空間複雜度:不需要藉助額外變數,但是改變了陣列

解法2:大根堆輔助(可用Java中的PriorityQueue實現)

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(k == 0 || arr.length == 0) return new int[0];
        //使用lambda表示式過載了PriorityQueue當中的compare方法
        Queue<Integer> pq = new PriorityQueue<>((v1,v2) -> v2-v1);
        //因為這裡不修改陣列元素,所以可以採用foreach迴圈
        for(int num:arr){
            if(pq.size() < k) pq.offer(num);
            else if(num < pq.peek()){
                pq.poll();
                pq.offer(num);
            }
        }
        //遍歷pq取出所有元素到res陣列
        int[] res = new int[k];
        int idx = 0;
        for(int num:pq){
            res[idx++] = num;
        }
        return res;
    }
}

用lambda表示式可以過載PriorityQueue中的compare方法。以下是一些相關的參考文章。

lambda表示式

PriorityQueue

複雜度分析

時間複雜度:
空間複雜度:,只使用了大小為k的優先佇列