1. 程式人生 > 其它 >【LeetCode】中位數(資料流、滑動視窗、兩個正序陣列)

【LeetCode】中位數(資料流、滑動視窗、兩個正序陣列)

技術標籤:LeetCodeleetcode

文章目錄


資料流中的中位數★★★

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(); } }

滑動視窗中位數★★★

LeetCode 480. 滑動視窗中位數

題目】中位數是有序序列最中間的那個數。如果序列的大小是偶數,則沒有最中間的數;此時中位數是最中間的兩個數的平均數。

例如:

[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;
    }
}

尋找兩個正序陣列的中位數★★★

LeetCode 4. 尋找兩個正序陣列的中位數

題目】給定兩個大小為 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))
在這裡插入圖片描述

未完待續