JavaScript
 Computer >> コンピューター >  >> プログラミング >> JavaScript

JavaScriptで再帰を使わずにネストされた配列をフラット化する方法

JavaScriptでは、任意の深さにネストされた配列を1次元の配列へ平坦化(フラット化)したいケースがよくあります。例えば、次のような入れ子になった数値の配列を考えてみましょう。

const arr = [1, 4, 5, [
    5, 6, [
       6, 19, 5, [5]
    ], [5, 7, 6, [6, 8]], 8
], 6];

ここで求められているのは、任意の階層にネストされた配列を受け取り、その平坦化した新しい配列を作成して返すJavaScript関数です。

課題:守るべき2つの制約

この関数を実装する際には、以下の2つの条件を満たす必要があります。

  • コード内で再帰呼び出し(自分自身を呼び出す関数)を使用しないこと

  • 組み込みメソッドのArray.prototype.flat()を使用しないこと

つまり、ループ処理だけでネスト構造を解きほぐす必要があります。

実装のアイデア:スタックで階層を管理する

再帰を使わずにネストを処理する定番の考え方は、反復処理(イテレーション)で再帰と同じ動作をシミュレートすることです。具体的には、現在参照している配列の階層(level)と、その階層での読み取り位置(counter)を変数として持ち、whileループで各要素を順番に走査していきます。

要素が配列であれば、その配列を次の階層として登録して一段深く進み、そうでなければ結果配列に値をpushします。階層の末尾まで読み終えたら、一段上の階層に戻る――この仕組みにより、関数の再帰呼び出しなしに深いネストも完全に展開できます。

サンプルコード

const arr = [1, 4, 5, [
    5, 6, [
       6, 19, 5, [5]
    ], [5, 7, 6, [6, 8]], 8
], 6];

const flattenWithoutRecursion = (arr = []) => {
    const res = [];
    let level = 0, ref = [arr], counter = [0];
    
    while (level >= 0) {
        // 現在の階層をすべて読み終えたら、一つ上の階層へ戻る
        if (counter[level] >= ref[level].length) {
            level--;
            continue;
        }
        // 要素が配列なら、次の階層として登録して一段深く進む
        if (Array.isArray(ref[level][counter[level]])) {
            ref[level + 1] = ref[level][counter[level]];
            counter[level]++;
            level++;
            counter[level] = 0;
            continue;
        }
        // 配列以外の要素は結果に追加
        res.push(ref[level][counter[level]]);
        counter[level]++;
    }
    return res;
};

console.log(flattenWithoutRecursion(arr));

実行結果

コンソールに出力される結果は以下の通りです。すべてのネストが解除され、1次元の配列になっていることが確認できます。

[
    1, 4, 5, 5, 6, 6,
    19, 5, 5, 5, 7, 6,
    6, 8, 8, 6
]

補足:他の再帰不要なアプローチ

同様の目的を達成する方法は他にもあります。代表的な例を紹介します。

Array.prototype.reduce() を使う方法

reduce()は高階関数であり、明示的な再帰関数を定義しないため、「再帰関数を書かない」という条件を満たす解釈も可能です。

const flatten = (arr) => arr.reduce(
    (acc, val) => acc.concat(Array.isArray(val) ? flatten(val) : val),
    []
);

ただし、この書き方は内部的に再帰的な構造を持つため、厳密に「再帰禁止」を守りたい場合は、前述のスタック方式のような反復処理ベースの実装が確実です。

スプレッド演算子とsome()を組み合わせる方法

const flatten = (arr) => {
    let result = [...arr];
    while (result.some(Array.isArray)) {
        result = [].concat(...result);
    }
    return result;
};

この方法では、配列の中にまだ配列が残っている限り、concat()による結合を繰り返します。シンプルで直感的ですが、要素数が多い配列では毎回新しい配列を生成するため、パフォーマンス面では先頭に紹介したスタック方式が有利です。

まとめ

再帰やflat()に頼らずとも、階層とカウンターを管理するwhileループを使えば、任意の深さのネストされた配列を安全かつ効率的に平坦化できます。大規模なデータや深いネスト構造を扱う場合には、スタック方式の反復処理が特に有効なアプローチです。

  1. JavaScriptの配列findIndex()関数の使い方をわかりやすく解説

    JavaScriptのfindIndex()関数は、配列の中から指定した条件を満たす最初の要素を見つけ、そのインデックス番号を返すメソッドです。条件に合う要素が見つかった場合、その要素のインデックスが返されます。もし条件を満たす要素がひとつも存在しない場合は、-1が返されるという特徴があります。findIndex()の基本的な使い方以下は、findIndex()関数を使ったサンプルコードです。ボタンをクリックすると、配列内の「lion」という要素のインデックスを取得して表示します。サンプルコード<!DOCTYPE html> <html lang="ja"

  2. JavaScriptの配列some()メソッドの使い方をわかりやすく解説

    JavaScriptのArray.some()メソッドは、配列の要素の中に、指定されたテスト(条件)を満たすものが「1つでも存在するかどうか」を判定するためのメソッドです。ここでいうテストとは、配列の各要素に対して順番に実行されるコールバック関数のことを指します。コールバック関数が1つでもtrueを返した時点でsome()はtrueを返し、すべての要素が条件を満たさなかった場合はfalseを返します。なお、空の配列に対して呼び出した場合は常にfalseを返すという仕様になっている点にも注意しましょう。some()メソッドのサンプルコード以下は、配列のsome()メソッドを使った具体的な実装例で