任意に入れ子になった整数の配列の配列を平坦化する

Write a piece of functioning code that will flatten an array of arbitrarily nested arrays of integers into a flat array of integers. e.g. [[1,2,[3]],4] -> [1,2,3,4].

* tests use Jasmine can be found  http://jsbin.com/harofo/4/edit?html,js,output
* @input  array
* @output array
*/

function flatten(arr) {
   //this function is called in recursion mode
    let result = [];
    if (arr && arr.length > 0) {
        arr.forEach(function(value) {
            if(Array.isArray(value)) {
                result = result.concat(flatten(value));
            } else {
                result.push(value);
            }
        });
    }    
    return result;

};
3

6 答え

The check for arr.length > 0 is not needed: forEach() can be called on an empty array (and does nothing in that case).

チェック if(arr)は、未定義の引数での呼び出しから保護します。 しかし、配列でない呼び出しには反対です。例として、 flatten(123) JavaScriptエラーが発生します。だからあなたが決める必要があります:

  • flatten()の引数が配列であることがわかっている場合 外側の if ステートメントはまったく必要ありません。
  • flatten()を確認する必要がある以外の引数で呼び出すことができる場合 forEach()を呼び出す前に配列を探します。

reduce()配列を平坦にするもご覧ください>ループの代わりに使用することができます。

5
追加された
あなたは arr.length> 0 のチェックは必要ないと書いていますが、最初の(再帰的でない)呼び出しでは arr はそうではないかもしれないのでそれは全く本当ではありませんまったく配列です。 arr.length が未定義の場合、arr.length> 0`はfalseですが、 arr.forEach が未定義の場合、 arr.forEach()はスローされます。実際には、 arr.length> 0 は、少なくともある程度は、そしてある程度信頼性の低い方法で、配列以外の引数での呼び出しから保護します。 OPのコードで flatten(123)を実行してもエラーは発生しません - [] が返されます。
追加された 著者 flith,

再帰関数を使用する際に考慮する必要がある実際的な制限があります。それは、呼び出しスタックのサイズです。

入れ子になった配列の深さが制限されていない場合は、ほとんどの場合、インタプリタに末尾呼び出しの最適化があると想定できないため、JavaScriptで再帰的な解決策を使用することはできません。 /37224520/are-functions-in-javascript-tail-call-optimized"> https://stackoverflow.com/questions/37224520/are-functions-in-javascript-tail-call-optimized

そのため、入力配列が非常にネストされていると、メモリのために再帰的な解決策は失敗します。

したがって、私はスタックを使わない反復的な解決策を好むでしょう。 (スタックを使用する反復解法は再帰と同じ問題を抱えています)

function flatten(array) {
    let i = 0;
    while (i != array.length) {
        let valueOrArray = array[i];
        if (! Array.isArray(valueOrArray)) {
            i++;
        } else {
            array.splice(i, 1, ...valueOrArray);
        }
    }
    return array;
}

この機能は入力を変更することに注意してください。

Blindman67が指摘しているように、引数に巡回参照があると、この関数は終了しません。巡回参照を含む配列を有限時間で平坦化することは不可能であるため、これは正しいようです。ただし、循環参照を検出してエラーをスローする可能性があります。

もちろん、再帰的な解決策を使うことができますが、関数のドキュメントで入れ子になった配列の深さの制限を指定するべきです。

4
追加された
これはパフォーマンスや最適化に関するものではなく、堅牢性に関するものです。確かに、反復と再帰のどちらが速いのかわかりません。しかし、再帰的な実装はそれほど堅牢ではありません。
追加された 著者 dismal_denizen,
正しく、 "... valueOrArray"で配列を展開するとスタックが爆発します。私はこの特定の問題に関して、現実の配列は問題がないということに同意します。
追加された 著者 dismal_denizen,
私はよくc/c ++/java/python/javascript(など)が関数型プログラミングを可能にするマルチパラダイム言語であることを読みました。しかし、一般的なケースでは、それらのコンパイラ/インタプリタはほとんどの場合TCO(テールコール最適化)を持っていないので、関数型を使うのは危険です。
追加された 著者 dismal_denizen,
これは悪い答えです。配列が非常に深くネストされているため、再帰によってスタックが破壊されることはほとんどありません。この場合、再帰的な実装は繰り返しよりもはるかに簡単です。反復の使用は時期尚早の最適化です。
追加された 著者 flith,
ちなみに、あなたのソリューションはコールスタックのサイズに関しても深刻な問題を抱えています。約150000要素を超える配列では失敗します。 RangeError:最大呼び出しスタックサイズを超えました。テストコード: let a = []; (i = 0; i <200000; i ++とする)a.push(i); a = [a]; flatten(a); 理由: array.splice(i、1、... valueOrArray)は、すべての配列要素を呼び出しスタックにコピーします。 Node.js 8、9、10、11およびChrome 68でも同じエラーです。
追加された 著者 flith,
私は、約5000レベルの深さにネストされた配列に対して機能する再帰的な解決策を投稿しました。私が今まで書いたコードや私のコンピュータで現在実行されているプロセスがそのような深くネストされた配列に遭遇するとは思わない。実際には起こりません。それは配列が使われる方法ではありません。一方、150000個の要素を持つ配列は非常に一般的です。一言で言えば:現実の世界では、反復的なソリューションは再帰的なソリューションよりも堅牢性が劣ります。
追加された 著者 flith,
P.S:あなたのコードが好きです。それは賢いです。それがどのように機能するのかを理解するために少し時間をかけてください。一方、プロダクションコードを書き込もうとしている場合、これは一般的には良いことではありません。たとえば、引数値を変更したり、ループ内でループ変数を変更したりすることは避けてください。それが私がまだ再帰的な解決策を好む理由です - それらはより単純です。
追加された 著者 flith,
この場合、テールコールの最適化は役に立ちません。再帰呼び出しを末尾呼び出しにする方法はありません。入れ子になった配列の呼び出しから戻った後、外部配列の残りを処理する必要があります。
追加された 著者 flith,
関数型プログラミングは、ほとんどすべての言語で実行できます。初期化後に変数を変更しないでください。場合によっては(たとえば、何千もの要素を反復処理するなど)、TCOなしで再帰を実行するとスタックオーバーフローが発生する可能性がありますが、多くの場合、危険ではなく、遅くなります。また、TCOは、スタックなしの反復として表現できる場合にのみ可能です。再帰呼び出しから戻った後もまだ必要な変数値がある場合は、TCOは不可能です。 TCOを有効にするために機能が再編成されることがあります。 ja.wikipedia.org/wiki/Tail_call#Example_programs を参照してください。
追加された 著者 flith,

これは再帰関数で、基本的な考え方はあなたのものと似ています。

function flatten(a) {
  if (! Array.isArray(a)) throw new Error('flatten() called with non-array');
  const f = [];
  function p(a) {
    for (let e of a) if (Array.isArray(e)) p(e); else f.push(e);
  }
  p(a);
  return f;
}

このバージョンの利点:

  • It avoids copying the nested arrays twice (which happens in result.concat(flatten(value)) in your solution).
  • By using a plain for loop instead of forEach(function ...), the syntax becomes simpler, and we avoid an extra function call in each recursive call, which will probably make things a bit faster (but I haven't measured it).
    • (This also means that it can handle arrays that are nested about twice as deep before it exceeds the stack size, but that's a problem that is extremely unlikely to occur in real life.)
  • The main function flatten is not recursive. It just checks its argument, sets up a data structure, calls the recursive function p which does the main work, and returns the result.
    • Such a split between a 'public' function that is meant to be called by 'normal' users and an 'internal' recursive function that does the actual work is often helpful when you develop recursive functions. The 'public' function has a simple interface and does some housekeeping, the 'internal' function has fewer argument checks or maybe has additional arguments that are not meant to be used by 'normal' callers. This often makes the recursive function simpler and faster.

私のマシンでは、このバージョンでは数百万の要素と数千のネストレベルを持つ配列を数秒で処理できます。

関数の先頭に、引数が配列かどうかのチェックを追加しました。このチェックは1回しか適用されず、すべての再帰的呼び出しには適用されません。

引数が配列ではない場合の関数の動作も変更しました。

  • 元のバージョンでは、値が誤っているか、 length プロパティを持っていないか、その length が正でない場合、空の配列が返されます。正の length プロパティを持ち、 forEach()メソッドを持たない場合(例:空でない文字列)、エラーがスローされます。 length プロパティと forEach()メソッドがある場合は、そのメソッドが呼び出されますが、これは便利な場合とそうでない場合があります。
  • このバージョンでは、配列以外のすべての引数に対してエラーがスローされます。

私は一般的に「フェイルファスト」アプローチを好む - エラーが検出されたとき、動作し続けようとせず、ただ失敗する。しかし、それは好みの問題です。いくつかのケースでは、それはに意味があるかもしれません。成功してデフォルト値を返すには、 flatten({})flatten(123)、または flatten( 'foo')を入力します。空の配列、指定された値、または指定された値を含む配列


これは再帰的な生成関数を使った解です。

function flatten(a) {
  if (! Array.isArray(a)) throw new Error('flatten() called with non-array');
  function* f(a) {
    for (let e of a) if (Array.isArray(e)) yield* f(e); else yield e;
  }
  return Array.from(f(a));
}

ジェネレータ関数を使用すると、解決しにくい問題に対する洗練された解決策が可能になることがよくありますが、この場合(そしておそらく他の多くの場合)、パフォーマンスはかなり悪くなります。

3
追加された
再帰関数には明らかに利点がありますが、なぜこのコードが優れているのか明示的に述べてください。詳しくはこちらの回答をご覧ください。
追加された 著者 Sam Onela,
@SamOnelaリンクをありがとう!私はcodereviewについてはここではかなり新しい、おそらくまだSOモードで働いています。このソリューションでは、入れ子になった配列を2回コピーすることは避けられるという説明を追加しました。それで十分?
追加された 著者 flith,

再帰と巡回無限大に関する問題

ところで最良の解決策は array.flat(10)です; 10は再帰の深さです。新しい Array プロトタイプの詳細については、答えの最後をご覧ください。

呼び出しスタックのオーバーフロー

Your code is not protected from 呼び出しスタックのオーバーフロー,

amard's answer で正しく指摘されているように、再帰は呼び出しスタックの深さがjavaScript内から認識できないため危険です。我々は適切な末尾電話を受けるまで言語の大きな欠陥)。非常にエレガントな答えBTW + 1(それはそれ以上の票を持っていない驚いた)

残念ながら、彼の答えは、他の場所に影響を与える可能性のあるすべての配列に含まれています。

循環参照からのスタックオーバーフロー

循環参照の例

var a = [];
var b = [a];
a.push(b);
flatten(a);//will throw or 呼び出しスタックのオーバーフロー or worse if
           //a or b were large arrays crash the page with our of
           //memory error.

巡回参照は、未知のデータセットを反復処理するために再帰または再帰的な解決策を使用するときは常に考慮する必要があります。無限ループを処理できる時間やメモリはありません。

サイクリックセーフソリューション

  • Do as Array.prototype.flat does and use a depth argument to limit recursion depth
  • Track flattened arrays using a Set
  • Use type coercion, String.split, Array.map and Number if items only number like or arrays.

ワンライナー

Array.toString flattens an array to a string, items separated by a , comma. As coercion is automatic in JavaScript such transformations must be protected against cyclic references.

Method : First the array is converted to a string, you could use .toString but can be forced via a sting contact operator +. Then just split the new string at "," to convert back to array, and if needed convert the array of strings back to numbers with map(Number)

const arr = [1,2,3,[1,2,[4,5],3,4],2];
arr.push([1,2,3,[arr]]); //create cyclic ref

// one liner flatten array of numbers (or number like items)
const flat = ("" + arr).split(",").map(Number);

フラット化されたアイテムを記憶するために Set を使用

次の flattenSafeSet を使用してフラット化された配列を追跡します。また、再帰ごとに配列を作成するのではなく単一の結果配列にプッシュすることでメモリ全体を削減します前の繰り返し配列

スニペットの例には、上記のonelineメソッドも含まれています。

<div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false"> <div class="snippet-code">

//==========================================================
// Cyclic data to test on
const a = [1,2,3,4,5,[6,[110,123]]];
// create an array the contains a reference to array a
const b = [[7,8,9],[10,12,13,14,[21,22,a]]];
// create a cyclic reference in a by push b.
a.push(b);

//==========================================================
// Test data cyclic using the most ugly way NOTE this is only 
// situation and not a valid test, don't use it in production 
// code.
try { JSON.stringify(a); log("failed") } catch(e) { log("Cyclic JSON.stringify Throws : " + e.message) } 

//==========================================================
// flatten using toString
const flat = (""+a).split(",").map(Number);

//==========================================================
// recursive cyclic safe flatten
function flattenSafe(arr) {
    if (Array.isArray(arr)) {
        const usedArrays = new Set();//Note dont add array here
        usedArrays.add(arr);//mark incoming array as used
        const resArr = [];
        (function flatten(arr) {
            for (const item of arr) {
                if (Array.isArray(item)) {
                    if (! usedArrays.has(item) ) {//check this item has not been touched
                        usedArrays.add(item);//add to used Set
                        flatten(item);
                    }
                } else {
                    resArr.push(item);
                }
            }
        })(arr);
        return resArr;
    }
    return arr;
}

const safeFlat = flattenSafe(a);


//==========================================================
// Show results

log("Flat using string " + flat.join("-"));
log("Flat flattenSafe " + safeFlat.join("="));

// Join flattens so you should use console to examine results  
// On snippet console looks bad so I used  above.
// console.log(flat);
// console.log(safeFlat);



function log(str) { document.body.innerHTML += str + "
" }
</div> </div>

スタックを使用して再帰を避けます。

whileループとスタックを使用して配列と配列インデックスを保持することで、再帰を回避することもできます。 各項目について、その項目が配列でない限りフラット配列にプッシュします。そうであれば、現在の配列にプッシュして、スタックするインデックスを作成します。新しい配列を現在のものとして参照し、インデックスを0にリセットして、同じ規則で各項目を繰り返します。配列インデックスが最後を過ぎたときにスタックからポップし、スタックが空になるまで続けます。上の例のように、Setを使用して巡回配列をテストします。

JavaScript Array.prototype.flat and Array.prototype.flatMap

javascriptに不慣れで、命名 * についての長い議論の末、ようやく Array.flat および Array.flatMap 後者は、平坦化された各項目に変換関数を適用します。

使用されます

([1,2,[1,2]]).flat();//default depth 1
([1,2,[1,2,[1,2]]]).flat(2);// depth 2

* Common libraries use of flatten meant it would break the net and flat is not commonly 使用されます a verb (Verb in music. C flat to lower by a semitone, or language, flat that surface) and for a while it looked like it may have been Array.smoosh

Warning on performance testing re - webNeat's answer

テスト結果は2オーダーずれています

  • JSONメソッド5289ms
  • ループ方式23ミリ秒。

タイミングを開始する前に、常に数百回または1000回関数を実行してください。そのため、オプティマイザが使用している時間はテストされません。

常にグローバルソークを使用してください。外部状態* 1に影響を与えない関数は単純に実行されません。これはオプティマイザが実行できる最も基本的な最適化です。引数に関係なく同じ結果を生成する関数は、結果をキャッシュに入れます。毎回同じ入力に対して同じ結果を生成する関数は結果をキャッシュすることができ、そのためパフォーマンステストを偽造します)

* 1 注Stateは、単にJSコンテキストではなくブラウザの状態を意味するために使用されていました。

実行順序は、タイミングに影響を与え、テストをランダム化し、統計的平均を使って多くのテストの結果を推定することができます。

NOTE do to an ongoing and unresolved security issue performance.now can not be trusted to give anything above a 0.1-0.2ms accuracy.

3
追加された

私はあなたよりも良い解決策を見つけることはできませんが、読みやすさは劣りますがより簡潔なコードを提案することができます。

function flatten(arr) {
    if (!Array.isArray(arr)) { return []; }
    return arr.reduce(function(acc, x) {
        return acc.concat( Array.isArray(x) ? flatten(x) : [x] );
    }, []);
};
2
追加された

配列項目が整数であるこの特定の場合に対して、より簡単な解決策を見つけました。

const flatten = array =>
    JSON.stringify(array)
    .match(/\d+/g)
    .map(x => parseInt(x))

この解決策がループの解決策よりも速いか遅いかを知りたかっただから私はこのスクリプトを書いた

const generate = length =>
  length <= 0 ? []
  : (
    Math.random() < 0.7 
    ? [Math.floor(Math.random() * 1000)] 
    : [generate(length/2)]
  ).concat(generate(length - 1))

const flattenLoop = array => {
    let i = 0;
    while (i != array.length) {
        let valueOrArray = array[i];
        if (! Array.isArray(valueOrArray)) {
            i++;
        } else {
            array.splice(i, 1, ...valueOrArray);
        }
    }
    return array;
}

const flattenJSON = array =>
    JSON.stringify(array)
    .match(/\d+/g)
    .map(x => parseInt(x))

console.time('generate')
const input = generate(500)
console.timeEnd('generate')

console.time('flattenLoop')
flattenLoop(input)
console.timeEnd('flattenLoop')

console.time('flattenJSON')
flattenJSON(input)
console.timeEnd('flattenJSON')

そして私のコンピュータの出力は

generate: 2288.048ms
flattenLoop: 3810.580ms
flattenJSON: 2430.594ms

驚くべきことに、 JSON.stringify と正規表現を使うほうが速いです。私は何か見落としてますか?

2
追加された
JavaScript - 日本のコミュニティ
JavaScript - 日本のコミュニティ
2 参加者の

日本人コミュニティのjavascript