算法題解:最小的K個數(海量數據Top K問題)

題目

輸入 n 個整數,找出其中最小的 k 個數。例如輸入四、五、一、六、二、七、三、8 這8個數字,則最小的4個數字是一、二、三、4。java

初窺

這道題最簡單的思路莫過於把輸入的 n 個整數排序,排序以後位於最前面的 k 個數就是最小的 k 個數。這種思路的時間複雜度是 O(nlogn)。面試

解法一:脫胎於快排的O(n)的算法

若是基於數組的第 k 個數字來調整,使得比第 k 個數字小的全部數字都位於數組的左邊,比第 k 個數字大的全部數字都位於數組的右邊。這樣調整以後,位於數組中左邊的 k 個數字就是最小的 k 個數字(這 k 個數字不必定是排序的)。下面是基於這種思路的參考代碼:算法

public class LeastK {

    public static void getLeastNumbers(int[] input, int[] output) {
        if (input == null || output == null || output.length <= 0 || input.length < output.length) {
            throw new IllegalArgumentException("Invalid args");
        }

        int start = 0;
        int end = input.length - 1;
        int index = partition(input, start, end); //切分後左子數組的長度
        int target = output.length - 1; //K-1

        //若切分後左子數組長度不等於K
        while (index != target) {
            //若切分後左子數組長度小於K,那麼繼續切分右子數組,不然繼續切分左子數組
            if (index < target) {
                start = index + 1;
            } else {
                end = index - 1;
            }
            index = partition(input, start, end);
        }

        System.arraycopy(input, 0, output, 0, output.length);
    }

    private static int partition(int arr[], int left, int right) {
        int i = left;
        int j = right + 1;
        int pivot = arr[left];

        while (true) {
            //找到左邊大於pivot的數據,或者走到了最右邊仍然沒有找到比pivot大的數據
            while (i < right && arr[++i] < pivot) { //求最大的k個數時,arr[++i] > pivot
                if (i == right) {
                    break;
                }
            }
            //找到右邊小於pivot的數據,或者走到了最左邊仍然沒有找到比pivot小的數據
            while (j > left && arr[--j] > pivot) { //求最大的k個數時,arr[--j] < pivot
                if (j == left) {
                    break;
                }
            }
            //左指針和右指針重疊或相遇,結束循環
            if (i >= j) {
                break;
            }
            //交換左邊大的和右邊小的數據
            swap(arr, i, j);
        }
        //此時的 a[j] <= pivot,交換之
        swap(arr, left, j);
        return j;
    }

    private static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

}

採用上面的思路是有限制的,好比須要修改輸入的數組,由於函數 Partition 會調整數組中的順序,固然了,這個問題徹底能夠經過事先拷貝一份新數組來解決。值得說明的是,這種思路是不適合處理海量數據的。如果遇到海量數據求最小的 k 個數的問題,可使用下面的解法。數組

解法二:適合處理海量數據的O(nlogk)的算法

咱們能夠先建立一個大小爲K的數據容器來存儲最小的 k 個數字,接下來咱們每次從輸入的 n 個整數中讀入一個數。若是容器中已有的數字少於 k 個,則直接把此次讀入的整數放入容器之中;若是容器中已有 k 個數字了,也就是容器已滿,此時咱們不能再插入新的數字而只能替換已有的數字。找出這已有的 k 個數中的最大值,而後拿此次待插入的整數和最大值進行比較。若是待插入的值比當前已有的最大值小,則用這個數替換當前已有的最大值;若是待插入的值比當前已有的最大值還要大,那麼這個數不多是最小的 k 個整數之一,因而咱們能夠拋棄這個整數。ide

所以當容器滿了以後,咱們要作 3 件 事情:一是在 k 個整數中找到最大數;二是有可能在這個容器中刪除最大數;三是有可能要插入一個新的數字。若是用一個二叉樹來實現這個數據容器,那麼咱們能在O(logk)時間內實現這三步操做。所以對於 n 個輸入數字而言,總的時間效率就是O(nlogk)。函數

咱們能夠選擇用不一樣的二叉樹來實現這個數據容器。因爲每次都須要找到 k 個整數中的最大數字,咱們很容易想到用最大堆。在最大堆中,根結點的值老是大於它的子樹中任意結點的值。因而咱們每次能夠在 O(1) 獲得已有的 k 個數字中的最大值,但須要 O(logk) 時間完成刪除及插入操做。學習

咱們本身從頭實現一個最大堆須要必定的代碼,這在面試短短的幾十分鐘內很難完成。咱們還能夠採用 Java 提供的具備優先級的隊列來實現咱們的容器。this

public class LeastK {
  
    public static Integer[] getLeastNumbers(int[] nums, int k) {
        // 默認天然排序,需手動轉爲降序
        PriorityQueue<Integer> maxQueue = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 > o2) {
                    return -1;
                } else if (o1 < o2) {
                    return 1;
                }
                return 0;
            }
        });
        for (int num : nums) {
            if (maxQueue.size() < k || num < maxQueue.peek()) { // peek():返回隊列頭部的值,也就是隊列最大值
                // 插入元素
                maxQueue.offer(num);
            }
            if (maxQueue.size() > k) {
                // 刪除隊列頭部
                maxQueue.poll();
            }
        }
        return maxQueue.toArray(new Integer[0]);
    }
  
}

海量數據Top K

Top K 問題是在面試中常常被問到的問題,好比:從20億個數字的文本中,找出最大的前100個。指針

如果遇到此類求海量數據中最大的 k 個數的問題,能夠參考上面的求最小的 k 個數,改用最小堆,實現以下的 Java 代碼:code

public class TopK {
  
    public Integer[] getLargestNumbers(int[] nums, int k) {
        PriorityQueue<Integer> minQueue = new PriorityQueue<>(k); // 默認天然排序
        for (int num : nums) {
            if (minQueue.size() < k || num > minQueue.peek()) { // peek():返回隊列頭部的值,也就是隊列最小值
                // 插入元素
                minQueue.offer(num);
            }
            if (minQueue.size() > k) {
                // 刪除隊列頭部
                minQueue.poll();
            }
        }
        return minQueue.toArray(new Integer[0]);
    }
  
}

最大堆源碼

若是對最大堆的實現源碼比較感興趣的話,能夠參考下面的代碼自行學習。

public class MaxHeapAndTopK {
  
    /**
     * 大頂堆
     *
     * @param <T> 參數化類型
     */
    private final static class MaxHeap<T extends Comparable<T>> {
        // 堆中元素存放的集合
        private List<T> items;
        // 用於計數
        private int cursor;

        /**
         * 構造一個椎,始大小是32
         */
        public MaxHeap() {
            this(32);
        }

        /**
         * 造詣一個指定初始大小的堆
         *
         * @param size 初始大小
         */
        public MaxHeap(int size) {
            items = new ArrayList<>(size);
            cursor = -1;
        }

        /**
         * 向上調整堆
         *
         * @param index 被上移元素的起始位置
         */
        public void siftUp(int index) {
            T intent = items.get(index); // 獲取開始調整的元素對象

            while (index > 0) { // 若是不是根元素
                int parentIndex = (index - 1) / 2; // 找父元素對象的位置
                T parent = items.get(parentIndex);  // 獲取父元素對象
                if (intent.compareTo(parent) > 0) { //上移的條件,子節點比父節點大
                    items.set(index, parent); // 將父節點向下放
                    index = parentIndex; // 記錄父節點下放的位置
                } else { // 子節點不比父節點大,說明父子路徑已經按從大到小排好順序了,不須要調整了
                    break;
                }
            }

            // index此時記錄是的最後一個被下放的父節點的位置(也多是自身),因此將最開始的調整的元素值放入index位置便可
            items.set(index, intent);
        }

        /**
         * 向下調整堆
         *
         * @param index 被下移的元素的起始位置
         */
        public void siftDown(int index) {
            T intent = items.get(index);  // 獲取開始調整的元素對象
            int leftIndex = 2 * index + 1; // // 獲取開始調整的元素對象的左子結點的元素位置

            while (leftIndex < items.size()) { // 若是有左子結點
                T maxChild = items.get(leftIndex); // 取左子結點的元素對象,而且假定其爲兩個子結點中最大的
                int maxIndex = leftIndex; // 兩個子節點中最大節點元素的位置,假定開始時爲左子結點的位置

                int rightIndex = leftIndex + 1;  // 獲取右子結點的位置
                if (rightIndex < items.size()) {  // 若是有右子結點
                    T rightChild = items.get(rightIndex);  // 獲取右子結點的元素對象
                    if (rightChild.compareTo(maxChild) > 0) {  // 找出兩個子節點中的最大子結點
                        maxChild = rightChild;
                        maxIndex = rightIndex;
                    }
                }

                // 若是最大子節點比父節點大,則須要向下調整
                if (maxChild.compareTo(intent) > 0) {
                    items.set(index, maxChild); // 將子節點向上移
                    index = maxIndex; // 記錄上移節點的位置
                    leftIndex = index * 2 + 1; // 找到上移節點的左子節點的位置
                } else { // 最大子節點不比父節點大,說明父子路徑已經按從大到小排好順序了,不須要調整了
                    break;
                }
            }

            // index此時記錄是的最後一個被上移的子節點的位置(也多是自身),因此將最開始的調整的元素值放入index位置便可
            items.set(index, intent);
        }

        /**
         * 向堆中添加一個元素
         *
         * @param item 等待添加的元素
         */
        public void add(T item) {
            items.add(item); // 將元素添加到最後
            siftUp(items.size() - 1); // 循環上移,以完成重構
        }

        /**
         * 刪除堆頂元素
         *
         * @return 堆頂部的元素
         */
        public T deleteTop() {
            if (items.isEmpty()) { // 若是堆已經爲空,就報出異常
                throw new RuntimeException("The heap is empty.");
            }

            T maxItem = items.get(0); // 獲取堆頂元素
            T lastItem = items.remove(items.size() - 1); // 刪除最後一個元素
            if (items.isEmpty()) { // 刪除元素後,若是堆爲空的狀況,說明刪除的元素也是堆頂元素
                return lastItem;
            }

            items.set(0, lastItem); // 將刪除的元素放入堆頂
            siftDown(0); // 自上向下調整堆
            return maxItem; // 返回堆頂元素
        }

        /**
         * 獲取下一個元素
         *
         * @return 下一個元素對象
         */
        public T next() {

            if (cursor >= items.size()) {
                throw new RuntimeException("No more element");
            }
            return items.get(cursor);

        }

        /**
         * 判斷堆中是否還有下一個元素
         *
         * @return true堆中還有下一個元素,false堆中無下五元素
         */
        public boolean hasNext() {
            cursor++;
            return cursor < items.size();
        }

        /**
         * 獲取堆中的第一個元素
         *
         * @return 堆中的第一個元素
         */
        public T first() {
            if (items.size() == 0) {
                throw new RuntimeException("The heap is empty.");
            }
            return items.get(0);
        }

        /**
         * 判斷堆是否爲空
         *
         * @return true是,false否
         */
        public boolean isEmpty() {
            return items.isEmpty();
        }

        /**
         * 獲取堆的大小
         *
         * @return 堆的大小
         */
        public int size() {
            return items.size();
        }

        /**
         * 清空堆
         */
        public void clear() {
            items.clear();
        }

        @Override
        public String toString() {
            return items.toString();
        }
    }

    /**
     * 題目: 輸入n個整數,找出其中最小的k個數
     *
     * @param input  輸入數組
     * @param output 輸出數組
     */
    public static void getLeastNumbers(int[] input, int[] output) {
        if (input == null || output == null || output.length <= 0 || input.length < output.length) {
            throw new IllegalArgumentException("Invalid args");
        }

        MaxHeap<Integer> maxHeap = new MaxHeap<>(output.length);
        for (int i : input) {
            if (maxHeap.size() < output.length) {
                maxHeap.add(i);
            } else {
                int max = maxHeap.first();
                if (max > i) {
                    maxHeap.deleteTop();
                    maxHeap.add(i);
                }
            }
        }

        for (int i = 0; maxHeap.hasNext(); i++) {
            output[i] = maxHeap.next();
        }
    }
  
}

參考資料

[1] 《劍指offer》

相關文章
相關標籤/搜索