【LeetCode】中位數(資料流、滑動視窗、兩個正序陣列)
文章目錄
資料流中的中位數★★★
LeetCode 劍指 Offer 41. 資料流中的中位數
【題目】如何得到一個數據流中的中位數?如果從資料流中讀出奇數個數值,那麼中位數就是所有數值排序之後位於中間的數值。如果從資料流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。
例如,
[2,3,4] 的中位數是 3
[2,3] 的中位數是 (2 + 3) / 2 = 2.5
設計一個支援以下兩種操作的資料結構:
void addNum(int num)
double findMedian()
- 返回目前所有元素的中位數。
【示例】
輸入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
輸出:[null,null,null,1.50000,null,2.00000]
【解題思路】堆
建立兩個堆,其中一個為大根堆,另一個為小根堆。大根堆中儲存較小的數,小根堆中儲存較大的數。大根堆的隊頭接小根堆的隊頭正好形成升序排列的一組數。
每次加入數字x時,先將x加入大根堆,然後大根堆出堆一個數進入小根堆,保持大根堆數字數目 + 1
始終等於小根堆數字數目
或者大根堆數字數目
始終等於小根堆數字數目
class MedianFinder {
PriorityQueue<Integer> pa; //大根堆
PriorityQueue<Integer> pb; //小根堆
public MedianFinder() {
pa = new PriorityQueue<Integer>((o1, o2) -> Integer.compare(o2, o1));
pb = new PriorityQueue<Integer>();
}
public void addNum(int num) {
pa.offer(num);
pb.offer(pa.poll());
if(pb.size() - 1 > pa.size()) pa.offer(pb.poll());
}
public double findMedian() {
return pa.size() == pb.size() ? (double)(pa.peek() + pb.peek()) / 2 : pb.peek();
}
}
滑動視窗中位數★★★
【題目】中位數是有序序列最中間的那個數。如果序列的大小是偶數,則沒有最中間的數;此時中位數是最中間的兩個數的平均數。
例如:
[2,3,4],中位數是 3
[2,3],中位數是 (2 + 3) / 2 = 2.5
給你一個數組 nums,有一個大小為 k 的視窗從最左端滑動到最右端。視窗中有 k 個數,每次視窗向右移動 1 位。你的任務是找出每次視窗移動後得到的新視窗中元素的中位數,並輸出由它們組成的陣列。
【示例】
給出 nums = [1,3,-1,-3,5,3,6,7]
,以及 k = 3。
視窗位置 中位數
--------------- -----
[1 3 -1] -3 5 3 6 7 1
1 [3 -1 -3] 5 3 6 7 -1
1 3 [-1 -3 5] 3 6 7 -1
1 3 -1 [-3 5 3] 6 7 3
1 3 -1 -3 [5 3 6] 7 5
1 3 -1 -3 5 [3 6 7] 6
因此,返回該滑動視窗的中位數陣列 [1,-1,-1,3,5,6]。
【解題思路】
方法一:思路同上尋找資料流中位數一樣,使用兩個堆。
需要注意的是,在堆中刪除資料時要維護兩個堆的大小平衡。
class Solution {
Queue<Integer> lo; //大根堆
Queue<Integer> hi; //小根堆
public double[] medianSlidingWindow(int[] nums, int k) {
lo = new PriorityQueue<Integer>((o1, o2) -> Integer.compare(o2, o1));
hi = new PriorityQueue<Integer>();
double[] res = new double[nums.length - k + 1];
for(int i = 0; i < nums.length; i++) {
addNum(nums[i]);
if(i < k - 1) continue;
res[i + 1 - k] = getMedium(k);
delNum(nums[i + 1 - k], k);
}
return res;
}
private void addNum(int num) {
lo.offer(num);
hi.offer(lo.poll());
if(hi.size() - 1 > lo.size()) lo.offer(hi.poll());
}
private void delNum(int num, int k) {
if(lo.contains(num)) {
lo.remove(num);
if(k % 2 == 1) lo.offer(hi.poll());
}else {
hi.remove(num);
if(k % 2 == 0) hi.offer(lo.poll());
}
}
private double getMedium(int k) {
if(k % 2 == 0) {
return ((double)lo.peek() + hi.peek()) / 2;
}else {
return (double)hi.peek();
}
}
}
方法二:插入排序+二分查詢
用插入排序維護一個視窗,每次插入資料時使用二分查詢尋找插入位置
class Solution {
public double[] medianSlidingWindow(int[] nums, int k) {
List<Integer> list = new ArrayList<Integer>();
double[] res = new double[nums.length - k + 1];
for(int i = 0; i < nums.length; i++) {
int pos = binarySearch(list, nums[i]);
list.add(pos, nums[i]);
if(list.size() < k) continue;
if(k % 2 == 0) {
res[i + 1 - k] = ((double)list.get(k / 2 - 1) + list.get(k / 2)) / 2;
}else {
res[i + 1 - k] = (double)list.get(k / 2);
}
list.remove((Integer)nums[i + 1 - k]);
}
return res;
}
//找到大於它的最小數的下標即為插入下標
private int binarySearch(List<Integer> list, int val) {
int le = 0, ri = list.size() - 1;
while(le <= ri) {
int mid = le + (ri - le) / 2;
if(list.get(mid) <= val) {
le = mid + 1;
}else {
ri = mid - 1;
}
}
return le;
}
}
尋找兩個正序陣列的中位數★★★
【題目】給定兩個大小為 m 和 n 的正序(從小到大)陣列 nums1 和 nums2。請你找出並返回這兩個正序陣列的中位數。
進階:你能設計一個時間複雜度為 O(log (m+n)) 的演算法解決此問題嗎?
【示例】
輸入:nums1 = [1,2], nums2 = [3,4]
輸出:2.50000
解釋:合併陣列 = [1,2,3,4] ,中位數 (2 + 3) / 2 = 2.5
【解題思路】
方法一:雙指標合併
時間複雜度:O(m + n)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
double res = 0.0;
int i = 0, j = 0, k = 0;
int n = nums1.length + nums2.length;
while(k < n) {
double val = 0;
if(i >= nums1.length) {
val = nums2[j++];
}else if(j >= nums2.length) {
val = nums1[i++];
}else if(nums1[i] < nums2[j]) {
val = nums1[i++];
}else {
val = nums2[j++];
}
k++;
if(n % 2 == 0) {
if(k == n / 2) res = (double)val;
if(k == n / 2 + 1) {
res = (res + val) / 2;
break;
}
}else {
if(k == n / 2 + 1) {
res = (double)val;
break;
}
}
}
return res;
}
}
方法二:二分查詢
時間複雜度:O(log(m + n))
未完待續