問題描述
無序陣列求第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
方法表示:nums
在L...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演算法的兩個問題
-
為什麼是5個一組
-
為什麼嚴格收斂到O(N)
請參考:
三種解法複雜度分析
演算法 | 時間 | 空間 |
---|---|---|
堆 | 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小的數值對。