JavaScriptでスライディングウィンドウごとの中央値を効率的に求める方法
中央値(Median)とは
数学における中央値とは、ソート済み(昇順に並べ替えた)数値リストの中間に位置する値のことです。
リストの要素数が偶数の場合、中間の値は1つに定まりません。その場合、中央値は中間にある2つの値の平均(相加平均)として定義されます。
問題の概要
ここでは、JavaScriptの関数を作成します。この関数は、整数の配列 arr を第1引数、ウィンドウサイズを表す数値 num(num ≤ arr.length)を第2引数として受け取ります。
配列 arr 内のサイズ num の各ウィンドウ(連続する部分配列)について中央値を計算し、その結果を新しい配列に順番に格納していきます。そして、反復処理が完了した時点で中央値の配列を返します。
たとえば、関数への入力が次のようだったとします。
const arr = [5, 3, 7, 5, 3, 1, 8, 9, 2, 4, 6, 8];
const num = 3;
この場合、出力は次のようになります。
const output = [5, 5, 5, 3, 3, 8, 8, 4, 4, 6];
出力の解説
各ステップでのウィンドウの状態と中央値は以下の通りです。
| 開始インデックス | 現在のウィンドウ | ソート後のウィンドウ | 中央値 |
|---|---|---|---|
| 0 | [5, 3, 7] | [3, 5, 7] | 5 |
| 1 | [3, 7, 5] | [3, 5, 7] | 5 |
| 2 | [7, 5, 3] | [3, 5, 7] | 5 |
| 3 | [5, 3, 1] | [1, 3, 5] | 3 |
| 4 | [3, 1, 8] | [1, 3, 8] | 3 |
| 5 | [1, 8, 9] | [1, 8, 9] | 8 |
| 6 | [8, 9, 2] | [2, 8, 9] | 8 |
| 7 | [9, 2, 4] | [2, 4, 9] | 4 |
| 8 | [2, 4, 6] | [2, 4, 6] | 4 |
| 9 | [4, 6, 8] | [4, 6, 8] | 6 |
実装例
この問題を解くコードは以下の通りです。
const arr = [5, 3, 7, 5, 3, 1, 8, 9, 2, 4, 6, 8];
const num = 3;
const binarySearch = (arr, target, l, r) => {
while (l < r) {
const mid = Math.floor((l + r) / 2);
if (arr[mid] < target) l = mid + 1;
else if (arr[mid] > target) r = mid;
else return mid;
};
if (l === r) return arr[l] >= target ? l : l + 1;
}
const medianSlidingWindow = (arr = [], num = 1) => {
let l = 0, r = num - 1, res = [];
const window = arr.slice(l, num);
window.sort((a, b) => a - b);
while (r < arr.length) {
const median = num % 2 === 0 ? (window[Math.floor(num / 2) - 1] + window[Math.floor(num / 2)]) / 2 : window[Math.floor(num / 2)];
res.push(median);
let char = arr[l++];
let index = binarySearch(window, char, 0, window.length - 1);
window.splice(index, 1);
char = arr[++r];
index = binarySearch(window, char, 0, window.length - 1);
window.splice(index, 0, char);
}
return res;
};
console.log(medianSlidingWindow(arr, num));
コードの解説
このソリューションの基本的なアイデアは、スライディングウィンドウを右へ移動させる際に、二分探索(バイナリサーチ)を使って、右側から入ってくる新しい要素を挿入し、左側から外れる要素を削除するというものです。
毎回ウィンドウ全体を再ソートする代わりに、ソート済みの状態を維持したまま要素の追加・削除を行うことで、計算量を抑えられます。二分探索による挿入位置の特定は O(log n)、splice による挿入・削除は O(n) となるため、全体的には単純な再ソート方式(O(n log n) × ウィンドウ数)よりも効率的な処理が可能です。
また、中央値の計算では num の偶奇を判定しています。偶数の場合は中間の2要素の平均を、奇数の場合は中間の1要素をそのまま中央値として採用します。
実行結果
コンソールへの出力は次のようになります。
[5, 5, 5, 3, 3, 8, 8, 4, 4, 6 ]
-
JavaScriptの「for...in」ステートメントとは?オブジェクトのプロパティをループ処理する方法を解説
JavaScriptのfor...in文は、オブジェクトが持つすべてのプロパティ(列挙可能なプロパティ)を順番に取り出して処理するためのループ構文です。オブジェクト内の各キー(プロパティ名)にアクセスしながら、対応する値を取得したい場合に非常に便利です。for...inの基本的な構文for (let 変数名 in オブジェクト) { // 各プロパティに対して実行したい処理 }ループ変数には、各反復ごとにオブジェクトのプロパティ名(キー)が文字列として代入されます。値そのものを取得するには、「オブジェクト[変数名]」のようにブラケット記法を使ってアクセスします。サンプルコード以下は、for
-
JavaScriptで連結リストの各ノードの「次に大きい値」を効率的に求める方法
問題概要JavaScriptで、連結リストの先頭ノード(head)を唯一の引数として受け取る関数を作成することを考えます。この連結リストには数値データが格納されており、リスト内の各ノードには「次に大きい値(next larger value)」が存在する場合があります。ノードiに対して next_larger(node_i) とは、j > i かつ node_j.val > node_i.val を満たすノードの中で、j が最小になるような node_j.val のことです。そのような j が存在しない場合、次に大きい値は 0 となります。つまり私たちの関数は、リスト内の各要素に対