無序數組求第K大的數


作者:Grey

原文地址: 無序數組求第K大的數

問題描述

無序數組求第K大的數,其中K從1開始算。

例如:[0,3,1,8,5,2]這個數組,第2大的數是5

OJ可參考:LeetCode_0215_KthLargestElementInAnArray

堆解法

設置一個小根堆,先把前K個數放入小根堆,對於這前K個數來說,堆頂元素一定是第K大的數,接下來的元素繼續入堆,但是每入一個就彈出一個,最后,堆頂元素就是整個數組的第K大元素。代碼如下:

    public static int findKthLargest3(int[] nums, int k) {
        PriorityQueue<Integer> h = new PriorityQueue<>();
        int i = 0;
        // 經歷這個循環,前K個數的第K大的數就是h的堆頂元素
        while (i < k) {
            h.offer(nums[i++]);
        }
        // 每次入一個,出一個,這樣就保證了堆頂元素永遠保持第K大的元素
        while (i < nums.length) {
            h.offer(nums[i++]);
            h.poll();
        }
        return h.peek();
    }

由於每次堆需要logK的調整代價, 所以這個解法的時間復雜度為O(N*logK)

改進快排算法

快速排序中,有一個partition的過程, 代碼如下,注:以下代碼是從大到小排序的partition過程

    private static int[] partition(int[] nums, int l, int r, int pivot) {
        int i = l;
        int more = l - 1;//大於區域
        int less = r + 1; // 小於區域
        while (i < less) {
            if (nums[i] > pivot) {
                swap(nums, i++, ++more);
            } else if (nums[i] < pivot) {
                swap(nums, i, --less);
            } else {
                i++;
            }
        }
        return new int[]{more + 1, less - 1};
    }

這個過程主要的作用是將nums數組的l...r區間內的數,將:

  • 小於pivot的數放右邊

  • 大於pivot的數放左邊

  • 等於pivot的數放中間

返回兩個值,一個是左邊界和一個右邊界,位於左邊界和右邊界的值均等於pivot,小於左邊界的位置的值都大於pivot,大於右邊界的位置的值均小於pivot。簡言之:如果要排序,pivot這個值在一次partition以后,所在的位置就是最終排序后pivot應該在的位置。

所以,如果數組中某個數在經歷上述partion之后正好位於K-1位置,那么這個數就是整個數組第K大的數。

完整代碼如下:

public class LeetCode_0215_KthLargestElementInAnArray {
    
    // 快排改進算法
    // 第K小 == 第 nums.length - k + 1 大
    public static int findKthLargest2(int[] nums, int k) {
        return p(nums, 0, nums.length - 1, k - 1);
    }
    // nums在L...R范圍上,如果要排序(從大到小)的話,請返回index位置的值
    public static int p(int[] nums, int L, int R, int index) {
        if (L == R) {
            return nums[L];
        }
        int pivot = nums[L + (int) (Math.random() * (R - L + 1))];
        int[] range = partition(nums, L, R, pivot);
        if (index >= range[0] && index <= range[1]) {
            return pivot;
        } else if (index < range[0]) {
            return p(nums, L, range[0] - 1, index);
        } else {
            return p(nums, range[1] + 1, R, index);
        }
    }
    private static int[] partition(int[] nums, int l, int r, int pivot) {
        int i = l;
        int more = l - 1;//大於區域
        int less = r + 1; // 小於區域
        while (i < less) {
            if (nums[i] > pivot) {
                swap(nums, i++, ++more);
            } else if (nums[i] < pivot) {
                swap(nums, i, --less);
            } else {
                i++;
            }
        }
        return new int[]{more + 1, less - 1};
    }

    public static void swap(int[] nums, int t, int m) {
        int tmp = nums[m];
        nums[m] = nums[t];
        nums[t] = tmp;
    }
}

其中p方法表示:numsL...R范圍上,如果要排序(從大到小)的話,請返回index位置的值。

int pivot = nums[L + (int) (Math.random() * (R - L + 1))];

這一行表示隨機取一個值pivot出來,用這個值做后續的partition操作,如果index恰好在pivot這個值做partition的左右邊界范圍內,則pivot就是排序后第index+1大的數(從1開始算)。

bfprt算法

brfpt算法和改進快排算法主流程上基本一致,只是在選擇pivot的時候有差別,快排改進是隨機取一個數作為pivot, 而bfprt算法是根據一定的規則取pivot,偽代碼表示為:

public class LeetCode_0215_KthLargestElementInAnArray {
     
    public static int findKthLargest2(int[] nums, int k) {
        return bfprt(nums, 0, nums.length - 1, k - 1);
    }

    // nums在L...R范圍上,如果要排序(從大到小)的話,請返回index位置的值
    public static int bfprt(int[] nums, int L, int R, int index) {
        if (L == R) {
            return nums[L];
        }
        //int pivot = nums[L + (int) (Math.random() * (R - L + 1))];
        int pivot = medianOfMedians(nums, L, R);
        int[] range = partition(nums, L, R, pivot);
        if (index >= range[0] && index <= range[1]) {
            return pivot;
        } else if (index < range[0]) {
            return bfprt(nums, L, range[0] - 1, index);
        } else {
            return bfprt(nums, range[1] + 1, R, index);
        }
    }
    ....
}

其中

 int pivot = medianOfMedians(nums, L, R);

就是bfprt算法最關鍵的步驟,mediaOfMedians這個函數表示:

num分成每五個元素一組,不足一組的補齊一組,並對每組進行排序(由於固定是5個數一組進行排序,所以排序的時間復雜度O(1)),取出每組的中位數,組成一個新的數組, 對新的數組求其中位數,這個中位數就是我們需要的值pivot

    public static int medianOfMedians(int[] arr, int L, int R) {
        int size = R - L + 1;
        int offSize = size % 5 == 0 ? 0 : 1;
        int[] mArr = new int[size / 5 + offSize];
        for (int i = 0; i < mArr.length; i++) {
            // 每一組的第一個位置
            int teamFirst = L + i * 5;
            int median = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));
            mArr[i] = median;
        }
        return bfprt(mArr, 0, mArr.length - 1, (mArr.length - 1) / 2);
    }

    public static int getMedian(int[] arr, int L, int R) {
        Arrays.sort(arr, L, R);
        return arr[(R + L) / 2];
    }

注:mediaOfMedians方法中最后一句:

return bfprt(mArr, 0, mArr.length - 1, (mArr.length - 1) / 2);

就是利用bfprt算法拿整個元素中間位置的值。

關於bfprt算法的兩個問題

  1. 為什么是5個一組

  2. 為什么嚴格收斂到O(N)

請參考:

BFPRT算法原理

BFPTR算法詳解+實現+復雜度證明

三種解法復雜度分析

算法 時間 空間
O(N*logK) O(N)
快排改進 概率上收斂到:O(N) O(1)
bfprt 嚴格收斂到:O(N) O(N)

相關題目

LeetCode_0004_MedianOfTwoSortedArrays

第K小的數值對

長度為N的數組arr,一定可以組成N^2個數值對。例如arr = [3,1,2],數值對有(3,3) (3,1) (3,2) (1,3) (1,1) (1,2) (2,3) (2,1) (2,2),也就是任意兩個數都有數值對,而且自己和自己也算數值對。數值對怎么排序?規定,第一維數據從小到大,第一維數據一樣的,第二維數組也從小到大。所以上面的數值對排序的結果為:(1,1)(1,2)(1,3)(2,1)(2,2)(2,3)(3,1)(3,2)(3,3), 給定一個數組arr,和整數k,返回第k小的數值對。

更多

算法和數據結構筆記

參考資料

程序員代碼面試指南(第2版)

算法和數據結構體系班-左程雲

BFPRT算法原理

BFPTR算法詳解+實現+復雜度證明


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM