PHPとJavaScriptで入れ子配列を平坦にする方法(再帰処理)
今回は [1,[4,9],16,[25,[36,49]],64] みたいな恰好をした入れ子の配列を順番をそのままで一階層の配列に直す方法を考えました。もっとスマートな関数があるようですがとりあえずは再帰処理の練習ということで、ご興味ある方はお付き合いください。言語はPHPとJavaScriptをそれぞれでやります。
1.PHPの場合
まず完成形を示します。
<?php
$in = [1,[4,9],16,[25,[36,49]],64];
function flat($in){
static $i = 0;
static $out = [];
if(is_array($in)){
foreach($in as $key => $val){
flat($in[$key]); //これが再帰(関数内で同名の関数を使う)
}
}elseif(is_int($in)){
$out[$i] = $in;
$i++;
}
return $out;
}
echo "<pre>";
var_dump(flat($in)); //->[1,4,9,16,25,36,49,64]
echo "</pre>";
再帰関数を使っています。つまり、関数flat
内でflat
を呼び出しているということです。
再帰って少し理解しやすいコツがあって、
function hoge(){
A1; //1は第1階層
hoge();
B1;
}
このhoge
関数内のhoge()
にファンクション全体を代入すればよいのです。
なお、このexample.php
はこのままでは無限ループになります。
function hoge(){
A1; //1は第1階層
A2; //2は第2階層
hoge();
B2;
B1;
}
どんどんやって
function hoge(){
A1; //1は第1階層
A2; //2は第2階層
A3; //3は第3階層
hoge();
B3;
B2;
B1;
}
みたいになります。
A1→A2→A3と実行していって、
なんらかの条件判定などでこの第3階層で処理が終わるとすると、
その後は
B3→B2→B1
と処理されて関数が終わります。
次に注目していただきたいのは、
static宣言です。これは関数実行の際に、
最初の一回だけ実行される処理です。
つまり、再帰処理で何回もこの行は通過しますが、
もしこのstaticがないと通過するたびに初期化され
せっかく入れたデータが消えてしまいます。
ところがstaticを付けておくと初回の初期化だけで済むめっちゃ便利な宣言です。
(なお筆者の未熟の故、このstaticを使わない方法は分かりませんでした。)
そしてif文のところに来ると、まあ最初は$in
は配列になっているでしょう。
そこでif(is_array($in))
に引っ掛かります。
foreach($in as $key => $val){
flat($in[$key]); //これが再帰(関数内で同名の関数を使う)
}
が実行されます。foreachのなかでは $in[$key]
は一つ上の階層の配列もしくは整数になります。
$in = [1,[2,3]];
$in[$key] → 1 or [2,3]
みたいなことですね。
直前の例で言うと1の値のようにもはや$in
が配列ではなく数値になったとき、
elseif(is_int($in))
が引っ掛かります。
ここで
$out[$i] = $in;
$i++;
を使って、$out
という配列に添え字をインクリメントしながら、
順番に入れて、最後に第一階層のreturn
で完成した$out
を返します。
(恐らく第二階層以上でもreturn
は実行されていますが、foreach内のhoge()
では返り値を使用しておらず捨てています。)
これでめでたく平坦な配列が手に入りました。
2.JavaScriptの場合(その1)
これもまずコードを示します。これはグローバルスコープの変数i,b
にアクセスしてあまりよろしくないのでその1としておきます。
let i=0;
let b=[];
function flafla(arr){
if(typeof(arr)=="object"){
for(var j in arr){
console.log("forward");
console.log(arr[j]);
flafla(arr[j]);
console.log("backward");
console.log(arr[j]);
}
}else if(typeof(arr)=="number"){
b[i] = arr;
i++;
}
}
let a = [[1,2],3,[4,5,[6,7],8],[9]];
flafla(a);
console.log(b); //->[1,2,3,4,5,6,7,8,9]
これもアルゴリズム自体はPHPと同じです。
ただし、jsではstatic宣言が使えません。
よって、関数外のグローバルスコープの変数i,b
に
b[i] = arr;
i++;
で無理やり成果を入れてしまうという恐ろしいことをしています。
なお、for in文内でconsole.log
を書いていますが、これの挙動を把握することは非常に勉強になると思います。
3.JavaScriptの場合(その2)
さて、本命のきれいな関数です。クロージャという"関数を返す関数"を使っています。
hoge
関数内かつflafla
関数外のところに変数値を保持するスコープがあるわけです。
このクロージャのスコープではflafla
関数内からアクセスできますが、flafla
関数が終わってもhoge
関数が生きているので変数値は保持されます。
function hoge(){
let i=0;
let b=[];
return function flafla(arr){
if(typeof(arr)=="object"){
for(var j in arr){
console.log("forward");
console.log(arr[j]);
flafla(arr[j]);
console.log("backward");
console.log(arr[j]);
}
}else if(typeof(arr)=="number"){
b[i] = arr;
i++;
}
return b;
}
};
let a = [[1,2],3,[4,5,[6,7],8],[9]];
console.log(hoge()(a));//->[1,2,3,4,5,6,7,8,9]
hoge
関数内でflafla
関数を使って、再帰になっているのです。
flafla
関数が無名関数では再帰処理ができないものと思われます。
let i,b
がPHPでいうstatic
な値の保持をしています。
アルゴリズム的にはこれも他と同じです。
これもまた重要なのは最後の
hoge()(a)
という関数呼び出しでしょう。これはhoge()
が関数flafla
を返しているので、
hoge()(a) → flafla(a)
を意味していることになります。hoge
のスコープの中で変数i,b
を保持しつつflafla
を実行できるなんて嬉しいですね~。(その1のflafla
に対するグローバルスコープが、このその2のflafla
に対するhoge
のスコープが対応しています。)
4.おまけのJS(今までの流れぶち壊し)
flat
関数というアレイメソッドがあります。
これは引数の階層分までの配列を平坦にします。
完全に平坦にするにはInfinity
値を入れればよく、
const arr = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr.flat(Infinity);
だけでよいです(白目)
5.最後に
アルゴリズム分かれば楽勝と思っていましたが、
言語のクセってのはあるものですね。
今日はここまで、お疲れさまでした。
Discussion
関数を返す関数を作らなくてもこうすればよいのではないかと思いました。
なんと、こんなきれいにかけるのですね!
勉強になります。ありがとうございますm(_ _)m