オライリーの JavaScript 第6版を図書館から借りて、 いろいろ調べている。 7 章の配列は知らないので、少し調べてみた。 以下の例で、配列の要素に使った名称はエスペラントである。
sort, reduce, join, indexOf, map, forEach, filter について調べる。
sort は、配列で表現された複数の値を破壊的に整列させる汎用メソッドである。次はその一例である
let a = new Array("pomo", "banano", "ĉerizo"); // リンゴ, バナナ, サクランボ
a.sort();
let s = a.join(","); // s == "banano, ĉerizo, pomo" バナナ, サクランボ, リンゴ
無名関数式も使える。特に sort は順番を決めるうえで大切だ。
let a = new Array(800, 108, 1003, 50000);
a.sort(); // 1003, 108, 50000, 800 辞書式。
a.sort(function(a, b) {
return a - b;
}); // 108, 800, 1003, 50000 数値順
大文字と小文字を区別せずにソートしたいときは、toLowerCaseを使う。
a = ['formiko', 'Besteto', 'kato', 'Hundo'] // それぞれ、アリ、虫、ネコ、イヌ
a.sort(); // ['Besteto', 'Hundo', 'formiko', 'kato'] 大文字小文字を区別
a.sort(function(s, t) {
let a = s.toLowerCase();
let b = t.toLowerCase();
if (a < b) return -1;
if (a > b) return 1;
return 0;
}); // ['Besteto', 'formiko', 'Hundo', 'kato']
reduce は、配列で表現された複数の値から単一の値を得るための汎用メソッドである。以下のかなりの部分は、
Array.prototype.reduce()
(developer.mozilla.org)に基づく。
配列の総和を得る
a = [1, 2, 3, 4]
const sumo = a.reduce((akum, val) => akum + val, 0) // 10 ( = 0 + 1 + 2 + 3 + 4)
これは次のように書いても同じだ。
a = [1, 2, 3, 4]
const sumo = a.reduce((akum, val) => akum + val) // 10 ( = 1 + 2 + 3 + 4)
配列の最大値を得る
a = [3, 1, 4, 1, 5, 9, 2]
const maksimumo = a.reduce((akum, val) => Math.max(akum, val) , -Infinity) // 9
配列の最大値を得る添字を求める
a = [3, 1, 4, 1, 5, 9, 2]
const maks_indekso = a.reduce((av, kv, ki, tabelo) => a[av] > kv ? ki : av, 0) //
上記の例では、配列の最大値自体は 9 だから、求める添字は 9 を値にとる 5 である(つまり、a[5] = 9 である)。 この最後の例はわかりにくいので説明する。まず、a.reduce のアロー関数に出ている引数は一般的に次で与えられる:
reduce((antaŭaValoro, komencaValoro, kurantaIndekso, tabelo) => { /* … */ }, komencaValoro)
これと照らしながら動作を見る。まず、komencaValoro が初期値として設定され、av の値となる。具体的には添字の値としての 0 である。 そして、tabelo[av] と kv の値が比較される。av は最大値を得る添字だから、tabelo[av] は前までに走査された配列のなかでの最大値そのものである。 それと現在の値 kv (= tabelo[ki]) が条件演算子(三項演算子)によって比較される。その結果、kv が tabelo[av] より大きいならば ki が次の動作の比較対象となる。 そうでないならば、つまり kv が tabelo[av] と等しいか tabelo[kv] より小さいならば、av がそのまま残り、次の動作の比較対象となる。 この動作が最終要素まで進んで配列の最大値が求められる。
reduceRight は、reduce と同じく、配列で表現された複数の値から単一の値を得るための汎用メソッドである。以下のかなりの部分は、
Array.prototype.reduceRight()
(developer.mozilla.org)に基づく。
reduceRightの適用例として、ガウスの消去法や LU 分解、QR 分解などで頻出する後退代入を取り上げよう。具体的には
`[[1,2,-1],[0,1,3],[0,0,1]] [[x_1],[x_2],[x_3]] = [[4],[5],[1]]` のような連立一次方程式を解く方法である。解は、
`x_3 = 1 / 1 = 1`
`x_2 = (5 - 3*x_3) / 1 = 2`
`x_1 = (4 - (2*x_2 - x_3)) / 1 = 1`
これを JavaScript で組むと次のようになる。`A` は `n` 次上三角行列を表す配列(第1行が長さ `n` の配列、第 2 行が長さ `n-1` の配列、`cdots`、第 `n` 行が長さ `1` の配列)、
`x` は未知の `n` 次配列、`b` は既知の `n` 次配列である。上記の場合を例にとると次のようになる。
const ip = (a, b) => a.reduce((akum, val, i) => akum + val * b[i], 0) // a と b の内積
const A = [[1,2,-1],[1,3],[1]];
const b = [4,5,1];
const x = A.reduceRight((akum, val, i) => {
const d = val[0];
return [...akum, (b[i] - ip(akum, val.reverse())) / d ]
}, []).reverse()
ほとんど判じ物である。以下注釈を付ける。
多次元配列 A は上三角行列を JavaScript で表現したものである。後退代入なので、配列の最後の要素から取り扱うのが素直だろう。そこで reduceRight を使う。 `x` の最終要素から決めていくわけだが、これは `a_(33) x_3 = b_3` から `x_3 = b_3 / a_(33)` である。b_3 は b[i] で表現されている。なぜなら、 a.reduce のcallbackFunction の引数 akum, val, i のうち i は 配列を array とすると array.length - 1 から始まるからである(初期値が指定されているため)。 また、内積をとる ip の値はここでは 0 である(後述)。それを val[0] を表す d で割っている。val は reduce で得られる最初の要素であり、この最初の要素は長さ 1 の配列だから、 対角要素である。よってこれが最初の解の最終要素 `x_3` となる。これが akum に蓄えられる(続く)。
join は、配列の全要素を区切り文字を挟んで文字列を順に連結し、連結した文字列を新たに作成して返すメソッドである。
indexOf メソッドでは引数に与えられた内容と同じ内容を持つ最初の配列要素の添字を返す。存在しない場合は -1 を返す。
const bestoj = ['azeno', 'bovo', 'cervo', 'ĉevalo', 'delfeno', 'elefanto']; // 動物:ロバ、ウシ、シカ、ウマ、イルカ、ゾウ console.log(bestoj.indexOf('bovo')); // 予想出力: 1
最後の配列要素の添字を返すには lastIndexOf を用いる。
map は、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成するメソッドである。
const a = [1, 2, 3, 4]; const b = [5, 6, 7, 8]; const c = a.map((x) => x * x); // map するために関数を通す console.log(c); // [1, 4, 9, 16] const d = a.map((x, i) => x + b[i]); console.log(d); // [6, 8, 10, 12]
forEach メソッドは、副作用のみを期待するループである。map や reduce とは異なり、値を出力することはない。
// for ループから forEach への変換
// js
const items = ["item1", "item2", "item3"];
const copyItems = [];
// 書き換え前
for (let i = 0; i < items.length; i++) {
copyItems.push(items[i]);
}
// 書き換え後
items.forEach((item) => {
copyItems.push(item);
});
filter メソッドは、指定された配列の中から指定された関数で実装されているテストに合格した要素だけを抽出する
const words = ['spray', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter((word) => word.length > 6);
console.log(result);
// Expected output: Array ["exuberant", "destruction", "present"]
要素のない配列を作ることも可能である。
let a = []; console.log(a.length) ; // 0 let b = [3]; console.log(b.length) ; // 1 b.pop(); // 3 console.log(a.length) ; // 0
配列を整列するときには、sort 関数と、sort 関数と一緒に使われる比較計算関数がある。 ここでは、String オブジェクトにある localCompare メソッドを使ってみた。 s に HTML の文字列が入る。
let z = [{n:1, c:'治'}, {n:2, c:'鷗外'}, {n:3, c:'百閒'}, {n:4, c:'漱石'}, {n:5, c:'弴'},
{n:6, c:'ばなな'}, {n:7, c:'オリザ'}];
let s = "";
for (let i = 0; i < z.length; i++){
s += `${z[i].n}:${z[i].c}`;
}
s += "<br>";
z.sort(function(x, y) {return x.c.localeCompare(y.c);});
for (let i = 0; i < z.length; i++){
s += `${z[i].n}:${z[i].c}`;
}
実行結果は次の通り。
配列を特定の値で初期化するには、Array.from() が使える。 たとえば長さ 5 の配列をすべて 0 で初期化するには次のようにする。
Array.from({ length: 5 }).fill(0); // [0, 0, 0, 0, 0]
アロー関数を使うと次のようにもかける。
Array.from({ length: 5 }, () => 0); // [0, 0, 0, 0, 0]
アロー関数は適用範囲が広い。たとえば、配列を連番で生成するには次のようにする。
Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]
二重配列(二次元配列)も可能である。
特定の初期値を与える例は次のとおりである。
let a = [
['formiko', 'Besteto', 'kato', 'Hundo'],
['pomo', 'banano', 'ĉerizo','']
];
上記の例を進めて、三重配列(三次元配列)や、さらに多重配列(多次元配列)も可能である。
Array コンストラクタのほか、Array.from(), Array.prototype.fill(), Array.prototype.map() などを駆使する。
const rows = 3; const cols = 4; const initialValue = 0; const array2D = Array.from(Array(rows), () => Array(cols).fill(initialValue)); // [[0, 0, 0, 0],[0, 0, 0, 0],[0, 0, 0, 0]]
要素を末尾に追加するには push() メソッドを用いる。戻り値は新しい配列の長さである。
let fruktoj = ['frago']; fruktoj.push('banano', 'pomo', 'persiko'); console.log(fruktoj.length); // 4
要素を先頭に追加するには unshift() メソッドを用いる。戻り値は新しい配列の長さである。
// 続き fruktoj.unshift('oranĝo'); console.log(fruktoj.length); // 5
要素の末尾を削除するには pop() メソッドを用いる。戻り値は削除された要素である。
// 続き fruktoj.pop(); // 'frago'
要素の先頭を削除するには shift() メソッドを用いる。戻り値は削除された要素である。
// 続き fruktoj.shift(); // 'oranĝo'
map メソッドと reduce メソッドの使い方を、多次元配列、特に 2 次元配列を例にとって練習する。 これは、2 次元配列を数学の行列のように扱う場合に有効な方法である。
const a = [[1,2,3],[4,5,6],[7,8,9]];
b[i]=a[i][0]+a[i][1]+a[i][2] となる配列 b[i] (i=0, 1, 2)を作る
const a=[[1,2,3],[4,5,6],[7,8,9]]
const b = a.map(h => h.reduce((is, as) => is + as, 0) ) // is = prev, as = current
console.log(b) // => [6, 15, 24]
c[i]=a[0][i] となる配列 c[i] (i=0, 1, 2)を作る
const a=[[1,2,3],[4,5,6],[7,8,9]]
const c = a.map(v => v[0])
console.log(c) // => [1, 4, 7]
上記は 1 列目を取り出す方法だ。同様に 2 列目、 3 列目もできる。これがわかると、行列の転置の方法もわかるはずだ。
d[i]=a[0][i]+a[1][i]+a[2][i] となる配列 d[i] (i=0, 1, 2)を作る
const a=[[1,2,3],[4,5,6],[7,8,9]]
const d = a.reduce((is, as) => is.map((v,i) => v+as[i]), a[0].slice().fill(0) );
console.log(d) // => [12, 15, 18]
この d を作る例は私には難しい。 [1,2,3]+[4,5,6] ができるようなコールバック関数を reduce に与えなければならないので、 このように map を使っている。reduce で使える引数を is, as で与えているが、 これはエスペラントの過去時制を表わす接尾辞 -is と現在時制を表わす接尾辞 -as である。 英語ならばそれぞれ previousValue, currentValue を使うだろう。 もう一つ注意すべき点は、reduce に与える初期値を a[0].slice().fill(0) としたことである。 最初 a[0].fill(0) としていたが、これではもとの a[0]の値を上書きしてしまうので、 誤った値 [11, 13, 15] になってしまう。そこで slice を使ってコピーを作るようにした。 2022-04-06
a[i][j] の転置行列を作る。 すなわち e[j][j]=a[i][j] となる配列 e[i] を作る。ここでは、i, j = 0, 1, 2 とする。なお、以下の方法では、行数と列数が異なる場合でも問題ない。
const a=[[1,2,3],[4,5,6],[7,8,9]]
const e = a[0].map((_, j) => a.map(h => h[j]))
console.log(e) // => [[1,4,7],[2,5,8],[3,6,9]]
この例も私には難しい。最初の map メソッドの引数 (_, j) は、第 1 の引数は捨て、 第2の引数 j のみを使うことを意味する。j は列が j 番目であることを表している。 第 j 列に対して再度 a.map で横(行)horizontalo ごとに h[j] を呼び出してベクトルを作る。 このベクトルを作ることについては、列の抜き出しも参照されたい。 なお、行列の転置を関数とすることもできる。仮に trans という名前をつけよう。
const a=[[1,2,3],[4,5,6],[7,8,9]]
const trans = m => m[0].map((_, v) => m.map(h => h[v]));
console.log(trans(e)) // => [[1,4,7],[2,5,8],[3,6,9]]
a が三角行列である場合の転置行列を作る。まず a が下三角行列である場合を考える。つまり、下三角から上三角への変換である。
const a=[[1],[2,3],[4,5,6]]
const e = a.map((_, i) => a.slice(i).map(h => h[i]))
console.log(e) // => [[1,2,4],[3,5],[6]
次に a が上三角行列である場合を考える。
const a=[[1,2,3],[4,5],[6]]
const e = a.map((_, i) => a.slice(0, i + 1).map((h, j) => h[i - j]))
console.log(e) // => [[1],[2,4],[3,5,6]
この上三角から下三角への変換は、その逆に比べて難しい。これらの変換は、a.slice(i) や a.slice(0, i + 1) が肝である。 前者は i が増えるにつれて a を上から削る量を多くしている。一方後者は i が増えるにつれて a を下から削る量を少なくしている。 また、前者では不要だった 内側の map の j が、後者では必要となっている。これについて細かな説明は省略する。
i と j を固定してa[i] と a[j] をそれぞれベクトルとみなしたときの内積を作る。次は i = 1, j = 2 とした場合だ。
const a=[[1,2,3],[4,5,6],[7,8,9]]
const f = a[1].reduce((is, as, i) => is + as * a[2][i], 0)
console.log(f) // => 122 (=28+40+54)
内積を作る関数に ip (エス : interna produto) という名前をつけよう。
const a=[[1,2,3],[4,5,6],[7,8,9]]
const ip = (v, w) => v.reduce((is, as, i) => is + as * w[i], 0);
console.log(a[1], a[2]) // => 122 (=28+40+54)
次に、2 次元配列と内積を発展させる。2 次元配列 a に対し、1 次元配列 a[0] と同じく配列 a[0], a[1], a[2] の内積をそれぞれ求める。
const a=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
const g = a.map(v => ip(v, a[0]));
console.log(g) // => [30, 70, 110] ([1*1+2*2+3*3+4*4,1*5+2*6+3*7+4*8,1*9+2*10+3*11+4*12])
さらに発展させて、行列の積を作ることができる。
const a=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
const b=[[3,4],[5,6],[7,8],[9,10]];
const h = a.map(v => trans(b).map(w => ip(v, w)));
console.log(h) // => [[70, 80],[166,192],[262,304]]
特に、行列が 1 つの場合、a を行列として a と trans(a) の積を作る。
const a=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
const k = a.map(v => a.map(w => ip(v, w)));
console.log(k) // => [[30, 70, 110],[70, 174, 278],[110, 270, 446]]
この結果を見るとわかる通り、計算には無駄がある。積は対称行列だから、計算は上三角部分だけでいい。
a[i]とa[j]の内積を a[i]*a[j] のように書くとき、次のような二次元配列が作れるか。
[[a[0]*a[0],a[0]*a[1],a[0]*a[2]],[a[1]*a[1],a[1]*a[2]],[a[2]*a[2]]]
妥協して次の書き方にした。
const a=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
const l = a.map(v, i) => a.map((w, j) => (i <= j ? ip(v, w) : undefined));
console.log(l) // => [[30, 70, 110],[, 174, 278],[,, 446]]
このプログラムを書いたとき「もう少しスマートな書き方があるはずだ」と記した後1年以上考えた。
その結果、上三角行列を得るには次のように書けばいいことがわかった。ここで ip(w, u) は配列 w と配列 u の内積をとる関数である。
(
const a=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
const l = a.map((u, i, v) => v.slice(i).map(w => ip(w, u)))
console.log(l) // => [[30, 70, 110],[174, 278],[446]]
下三角行列を得るには次のように書けばいい。(
const a=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
const l = a.map((u, i, v) => v.slice(0, i + 1).map(w => ip(w, u)))
console.log(l) // => [[30],[70, 174],[110, 278, 446]]
二つの与えられた行列の列を連結させて得られる行列を拡大行列という。 拡大行列は、二つの行列に対し同じ行基本変形を施すことを目的として構成される。列の数を合わせるために、 下記の例では b = [4,3,1] ではないことに注意。
const a=[[1,3,2],[2,0,1],[5,2,2]], b = [[4],[3],[1]]; const c = a.map((v, i) => v.concat(b[i])); console.log(c); // [[1,3,2,4],[2,0,1,3],[5,2,2,1]]
ただ、この場合は、b = [4,3,1] でも同じ結果が得られる。
const a=[[1,3,2],[2,0,1],[5,2,2]], b = [4,3,1]; const c = a.map((v, i) => v.concat(b[i])); console.log(c); // [[1,3,2,4],[2,0,1,3],[5,2,2,1]]
行方向に連結させるときは注意が必要である。次はうまくいかない。
const a=[[1,3,2],[2,0,1],[5,2,2]], b = [4,3,1]; const c = a.concat(b); console.log(c); // [[1, 3, 2], [2, 0, 1], [5, 2, 2], 4, 3, 1]
次もうまくいかない。
const a=[[1,3,2],[2,0,1],[5,2,2]], b = [[4],[3],[1]]; const c = a.concat(b); console.log(c); // [[1, 3, 2], [2, 0, 1], [5, 2, 2], 4, 3, 1]
次はうまくいく。
const a=[[1,3,2],[2,0,1],[5,2,2]], b = [[4,3,1]]; const c = a.concat(b); console.log(c); // [[1, 3, 2], [2, 0, 1], [5, 2, 2], [4,3,1]]
次のようにする手もある。
const a=[[1,3,2],[2,0,1],[5,2,2]], b = [4,3,1]; const c = a.concat(Array.of(b)); console.log(c); // [[1, 3, 2], [2, 0, 1], [5, 2, 2], [4,3,1]]
連立一次方程式を解くときに、行列を LU 分解したりコレスキー分解したあとで解を求める方法が前進代入と後退代入である。 奥村「C 言語による標準アルゴリズム事典」のp.384 「LU 分解」の例を挙げる。次の例は A = LU となっている。
`A = [[2,5,7],[4,13,20],[8,29,50]]`
`L = [[1,0,0],[2,1,0],[4,3,1]], U = [[2,5,7],[0,3,6],[0,0,4]]`
Ax = b の形の連立一次方程式を解くには、まず (LU)x = b から L(Ux) = b と変形し、Ux = y とおく。 第1段階では、Ly = b から y を求める。これは、前進代入と呼ばれる方法を使う。
`L[0][0]*y[0] = b[0], L[1][0]*y[0]+L[1][1]*y[1] = b[1], L[2][0]*y[0]+L[2][1]*y[1]+L[2][2]*y[2]=b[2]` であるから、`L[i][i]=1`と合わせて、次の式が得られる。
`y[0] = b[0], y[1] = b[1] - L[1][0]*y[0], y[2] = b[2] - L[2][0]y[0] - L[2][1]y[1]`
プログラムは次のようになるだろう。
const n = b.length;
for (let i = 0; i < n; i++) {
y[i] = b[i];
for (let j = 0; j < i; j++) {
y[i] -= L[i][j] * y[j];
}
}
`b = [23,58,132]` として計算してみると、 が得られる。
この方法は確実だが、インデックスを使ってループを回している。他の方法はどうだろうか。j のループは reduce が使えそうだ。 i のループはひとまず手で展開すれば、次のようなプログラムが考えられる。
y = b.slice(); y[0] = L[0].slice(0, 0-n).reduce((acc, val, j) => acc - val * y[j], b[0]) y[1] = L[1].slice(0, 1-n).reduce((acc, val, j) => acc - val * y[j], b[1]) y[2] = L[2].slice(0, 2-n).reduce((acc, val, j) => acc - val * y[j], b[2])
この方法でも y=[23,12,4] が得られる。それでは、i のループのかわりに map を使ってみよう。
let y = b.slice(); y = L.map((l, i) => l.slice(0, i - n).reduce((acc, val, j) => acc - val * y[j], b[i]) );
この結果は y=[23, 12, -134] となり、誤った値となっている。y の扱い方に問題があるのだろう。map のかわりに forEach を使ってみた。次はどうだろうか。
let y = b.slice(); L.forEach((l, i) => { y[i] = l.slice(0, i - n).reduce((acc, val, j) => acc - val * y[j], b[i]) })
for ループを使うのとほとんど変わりないが、y=[23,12,4] という正しい値が得られている。
HTML を扱う場合、一見すると配列でありながら、実は配列でない構造がある。 たとえば、getElementsByTagName で取得した DOMエレメントのオブジェクト HTMLCollection や、 querySelectorAll で取得した DOM エレメントのオブジェクト NodeList などの配列風オブジェクト(配列もどき)は、 配列によるアクセスができるので配列と思いがちだが、 実は配列ではない。というのも、配列のクラスに備わっているソートや検索、列挙などが使えないからだ。
// link 要素列を取得(HTMLCollection) const elements = document.getElementsByTagName('link'); // 列挙したいがエラー elements.forEach(element => console.log(element));
このような場合は、Array.from で配列そのものに変換できる。
// link 要素列(HTMLCollection)を取得して配列に変換する const elements = Array.from(document.getElementsByTagName('link')); // 列挙する elements.forEach(element => console.log(element));
Array.from() (developer.mozilla.org) 参照。Array.from() は配列もどきの他にも、Set や Map のような反復可能オブジェクトを配列に変換するときにも使える。
別法として、ArrayObject = Array.prototype.slice.call(HTMLCollectionObject) のようにすれば、HTMLCollection が Array に変換できる。 Array.prototype.slice() (developer.mozilla.org) 参照。
まりんきょ学問所 > JavaScript 手習い > 配列