480.スライディングウィンドウの中央値
中央値は、順序付けられたシーケンスの中央の数値です。シーケンスのサイズが偶数の場合、中央値はありません。この場合、中央値は2つの中央値の平均です。
例えば:
[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]。
促す:
- kは常に有効であると想定できます。つまり、kは常に入力の空でない配列の要素数よりも少なくなります。
- 真の値の10-5以内の回答は、正解と見なされます。
方法1:優先キュー
問題解決のアイデア
この問題に対する強引な解決策は簡単に思い付くことができます。長さkの配列を維持し、左と右に並べ替えるたびに中央値を取得しますが、時間の複雑さはそれだけですO(klogk)
。これに基づいて、最適な解決策を考えてください。
要素の優先キューが順序付けられていることはわかっていますが、優先キューはインデックス値でサポートされていません。この目的のために、2つの優先度付きキューを維持smallHeap
しlargeHeap
、データの各半分を保存し、kが奇数の場合smallHeap
はaを配置します。注意:
smallHeap
データの小さい方の半分を維持することsmallHeap
です。ヒープのトップが最大であり、「ヒープのビッグトップ」です。largeHeap
データの大部分を維持するために、ヒープの上部が最小でlargeHeap
あり、「上部の小さなヒープ」です。- kは奇数、中央値は
smallHeap.peek()
;それ以外の場合、中央値は(smallHeap.peek() + largeHeap.peek()) / 2
ストレージの問題は解決されましたが、まだ削除されています。優先キューは、ヒープの最上位要素のみを削除できます。この目的のために、追加のマップを使用して、削除する必要のある要素を維持できます。キーは削除する番号であり、値は番号を削除する必要がある回数です。ヒープの上部にある要素がマップにある場合、ヒープの上部にある要素を削除する必要があることを意味します。
さらに、にsmallHeap.size()
等しいlargeHeap.size()
、または等しい2つのキューのバランスを取る必要もありますlargeHeap.size() + 1
。両方のキューに「削除する要素」がある可能性があるため、サイズを直接使用することはできず、2つのキューの「実際の要素」の数を手動で維持する必要があります。
参照コード
- コード包装
DualHeap
クラスを、#insert(int x)
、#remove(int x)
、#getMedian()
中央値を求めるインサート要素、削除要素を表します。 - 各挿入要素と削除要素の後に
#balance()
、2つのキューのバランスを取るためにメソッドを呼び出す必要があり、メソッド#checkTop()
がヒープ要素の先頭をチェックするかどうかを削除できます。
public double[] medianSlidingWindow(int[] nums, int k) {
int n = nums.length;
double[] ans = new double[n - k + 1];
DualHeap dualHeap = new DualHeap((k & 1) == 1);
for (int i = 0; i < n; i++) {
dualHeap.insert(nums[i]);
if (i < k - 1) {
continue;
}
ans[i - k + 1] = dualHeap.getMedian();
dualHeap.remove(nums[i - k + 1]);
}
return ans;
}
@SuppressWarnings("all")
class DualHeap {
// k 是否为奇数
private boolean odd;
private PriorityQueue<Integer> smallHeap;
private PriorityQueue<Integer> largeHeap;
private Map<Integer, Integer> lazyMap;
// 两个队列中真实的元素数量(去掉待删除的)
int smallSize, largeSize;
public DualHeap(boolean odd) {
this.odd = odd;
this.smallHeap = new PriorityQueue<>(Comparator.reverseOrder());
this.largeHeap = new PriorityQueue<>(Comparator.naturalOrder());
this.lazyMap = new HashMap<>();
smallSize = 0; largeSize = 0;
}
public void insert(int val) {
if (smallHeap.isEmpty() || val <= smallHeap.peek()) {
smallSize++;
smallHeap.offer(val);
} else {
largeSize++;
largeHeap.offer(val);
}
balance();
checkTop();
}
public void remove(int val) {
if (val <= smallHeap.peek()) {
smallSize--;
} else {
largeSize--;
}
lazyMap.put(val, lazyMap.getOrDefault(val, 0) + 1);
balance();
checkTop();
}
public double getMedian() {
return odd ? smallHeap.peek() : ((double) smallHeap.peek() + largeHeap.peek()) / 2;
}
// 执行插入和删除操作后,需要维持两个队列的平衡
private void balance() {
if (smallSize > largeSize + 1) {
smallSize--;
largeSize++;
largeHeap.offer(smallHeap.poll());
} else if (smallSize < largeSize) {
smallSize++;
largeSize--;
smallHeap.offer(largeHeap.poll());
}
}
// 如果堆顶元素是待删除元素则删除
private void checkTop() {
while (!smallHeap.isEmpty() && lazyMap.containsKey(smallHeap.peek())) {
int x = smallHeap.poll();
int count = lazyMap.get(x);
if (count == 1) {
lazyMap.remove(x);
} else {
lazyMap.put(x, count - 1);
}
}
while (!largeHeap.isEmpty() && lazyMap.containsKey(largeHeap.peek())) {
int x = largeHeap.poll();
int count = lazyMap.get(x);
if (count == 1) {
lazyMap.remove(x);
} else {
lazyMap.put(x, count - 1);
}
}
}
}
の結果