大筋については Rubyによる本編 を参照されたい。
本編とおおむね同様に解くことができる。
"GAKKOU".split(’’).permutation.sort.uniq[100-1]
//
を引数として渡そうとするとエラーになる。
/(?:)/
あるいは
new Regexp('')
と書いてもいいが(正規表現にこだわるメリットはないね)、//
は、JSではコメント文字なので、この形で空正規表現は作れない (このページで議論している)。重複要素を削除するメソッド uniq は もとは Unixのuniqコマンド(重複行の削除) に相当する機能を提供するためのもので、隣り合った要素の比較だけを行うことになっている (一般に sort してから使うことを想定しているため)。
たとえば [1,2,1].uniq() は、(2つの1は隣接していないので)何も削除しない。
いくつかの考え方でコーディングできるので列挙して示す。
reduce を使う
function uniq(a) { //
return(
a.length < 1 ? a :
a.slice(1).reduce((r,e)=>
r.slice(-1)[0]==e ? r : r.concat([e]),
[a[0]])
)
}
このメソッドは最初の要素を取り出して、残りの配列に対して
.reduce
を呼び出す作り方。なので、長さ0の配列を渡した時だけは違う処理が必要になる
(ここでは引数として渡された a をそのまま返しているが、[]
を返しても同じ)。
JSの Array.prototype.reduce は、第1引数にリレーするための無名関数、 第2引数として(必要ならば)初期値を渡す。
a.slice(0,1)
でもいい)
を初期値として、重複しない要素をその後ろに追加しながらリレーしていく。配列 a の、最初の要素を取り除いた残り、を表すために a.slice(1) を使っている。
a.slice(1,a.length)
でもいい(が長くなる)。無名関数の中で、配列 r の末尾の要素を取り出して e
と比較している。 末尾の要素は r[r.length-1]
でも取り出せるが、ここでは r.slice(-1)[0]
とした。
r[-1]
でいいのだが、ここはJSの弱い所でもある。配列 r の末尾に要素 e を追加した新たな配列を作る(つまり push のような破壊的 メソッドを使わない)方法として、
(ruby なら 演算子 +
を使って r+[e]
と書けるが) メソッド Array.prototpye.concat
を使って、r.concat([e])
と書くことになる。
再帰で書く
function uniq2(a) { // 再帰バージョン
if(a.length < 1 ) return []
else {
let top=a[0],
r=uniq2(a.slice(1)) // slice(1,a.length) でも同じ
return top==r[0] ? r : [top].concat(r)
}
}
先頭要素(top)を取り除いた残りの配列に対して uniq2 を再帰呼び出ししてから、
その結果(r)の先頭と top を比較し、違う値ならば先頭に挿入する、という手順。
ただし上記のプログラムは若干ごまかしつつ実装していて、末端の呼び出しが
[]
を返してきてそれを変数 r
で受け取ったあと、r[0]
(存在しない要素)を評価している。JSでは、このアクセスは
undefined
を返すため ==
による比較式は false
となる筈なので、 問題なく動作はするが、
同じロジックを他の言語で実装すると失敗する可能性はある。
if(a.length <= 1 )
return a
とするのが無難かも知れない。末尾再帰で書く
function uniq3(a) { // 末尾再帰バージョン
if(a.length < 1 ) return [] // 長さ0の場合の特別扱い
function uniq3_(h,t) { // 内部関数を定義してこれを呼び出す
// console.log(h,t)
if(t.length < 1) return h
else {
let top=t[0], rest=t.slice(1)
return(uniq3_(h.slice(-1)[0]==top ? h : h.concat([top]),rest))
}
}
return(uniq3_(a.slice(0,1),a.slice(1)))
}
そのため、もとの関数に、引数の数が1つ(返すべき値)加えられたものを 内部関数として再帰的に使うことが多い
(もとの関数にデフォルト値のある引数を予め準備しておいて、内部関数なしで作る、 というアプローチもあるが)。
a.slice(0,1)
)。逐次バージョン
function uniq4(a) { // 逐次バージョン
let n=a.length, b=[...a], p=0
for(let i=0; i<n-1 ; i++)
if(a[i]!=a[i+1]) b[++p]=a[i+1]
return(b.slice(0,p+1))
}
これは関数型言語らしくない逐次型のコード(性能的には優れているが、 この書き方しかできないプログラマにはならないようにしよう)。
結果を格納するための配列を(b として)用意する。長さは予測できないが、 a より長くなることはないので、単純に a をコピーすればいいだろう。
ここではスプレッド構文(...
を使う)でコピーしているが、
a.concat()
、a.slice(0)
、Array.from(a)
などいくつかの書き方がある。
for によるループで一時的変数 i を変化させるが、その範囲は(nではなく) n-1 の手前までとしている(添字 i と i+1 が指す要素を比較するためこれでいい)。
b[0] にはすでに(a が 空配列でない限り)a[0]
と同じものが格納されているので、 a から b
に転写する(その直前の要素と違う値ならば)ものは a[i+1]
でいい。
転写先の(bの中での)添字は、それまでに転写されている最大添字(を p が保持している) に1を加えた値(引用直前に `++p` でインクリメントするので)。
b の(格納場所としての)長さは(a と同じく)n
だが、最後に、格納済の範囲だけを .slice
で切り出して返す(そのため長さは p+1
になる)。
なお、もとの配列 a が空配列だった場合は、forでは何も行われず、p は 0のまま。 最後に slice して返すときに、長さとして p+1 つまり 1 が渡されるので、 想定しない動作をしそうだが、
実は JSでは Array.prototype.slice の第2第1引数に
もとに配列の長さより大きい値を与えても、長過ぎる分は無視されるので、
uniq4([])
は []
を返すことになる
(これはちょっと技巧的な手抜き方法だと言えるので、真似しない方がいいかも知れない)。
メソッドにする
Array.prototype.uniq = function () { // メソッドにする
return(
this.length < 1 ? this :
this.slice(1).reduce((r,e)=>{
// console.log(r,e)
return(r.slice(-1)[0]==e ? r : r.concat([e]))
},[this[0]])
)
}
このコードは、先に示した関数 uniq(a)
(reduce版)を、
Arrayに対するメソッドとして定義する書き方を示したもの。
他のバージョンをメソッド化する時も考え方は同じ(なので省略した)。
引数はとらない。代わりに、対象となる配列は this として書くことになる。
先に示した例では、無名関数の本体は、単一の式だったので ブレースを省略し、return も省略して書いていたが、
関数本体が複数の文から成る場合(といってもここではデバッグのための出力文は コメントアウトされていて結果的には文は1つになっているが)は 上記のようにやや長い記述をする必要がある。
ここでも本編 と同様に、複数の考え方での書き方を示す。
配列を前後に分解して再帰呼び出し(全要素を並び替える)
配列 a の
i番目の要素を取り出すのは簡単(a[i]
)だが、i番目を除いた配列を作るのは少し面倒。
a.slice(0,i).concat(a.slice(i+1))
(でもこれまでの説明を見ていればわかりますね?)
これを各添字について生成してリストにする。[...a.keys()]
で添字の範囲となる配列が生成できる のはこれまでにも見てきたとおり。
[...a.keys()].map(i=>a.slice(0,i).concat(a.slice(i+1)))
関数自体は、以下のような外枠で定義することを前提とする。
function perm(a){
// ... 中身はあとで作るとして
}
配列aから要素を1つ減らしたものに対する順列生成は、こう呼ぶことになる。
perm(a.slice(0,i).concat(a.slice(i+1)))
上記の生成物の先頭に、取り置いておいた1つの要素を付加する。perm が生成するのが順列(配列)の配列なので、 それぞれについて同じことをさせるため、map を使う。
perm(a.slice(0,i).concat(a.slice(i+1))).map(l=>[a[i]].concat(l))
配列 a の各添字について上記を行う。
[...a.keys()].map(i=>
perm(a.slice(0,i).concat(a.slice(i+1))).map(l=>[a[i]].concat(l))
)
再帰呼び出しの関数なので、再帰呼び出し1レベルごとに引数となる配列の長さは漸次短くなる。
その末端で、空配列(長さゼロ)が渡されたら、それ以上の呼び出しをせず、末端の値を返す。
末端の値として [[]]
がふさわしいことは、本編で述べたとおり。
function perm(a) {
return a.length<=0 ? [[]] :
[...a.keys()].map(i=>
perm(a.slice(0,i).concat(a.slice(i+1))).map(l=>[a[i]].concat(l))
).flat()
}
本編で(rubyで)解説したのと同様、配列の配列になっているものを、1段階、平滑化するため、 ここでは Array.prototype.flat を呼んでいる。
なお、RubyのArray#flattenを引数なしで呼ぶと完全に平滑化して1本の配列にするが、 JSではそれとは違い、flat を引数なしで呼んだときには1段だけ平滑化する仕様。
これで完成したので、実験してみて下さい。
perm([1,2])
perm([1,2,3])
perm([1,2,3,4]).length // あまり長くなるようなら 長さ(順列の数は n! ね)だけ確認しよう
長さを指定して nPm に相当する生成する。
function perm(a, lvl=a.length) { // 長さを指定する(省略値は配列
return lvl<=0 || a.length<=0 ? [[]] :
[...a.keys()].map(i=>
perm(a.slice(0,i).concat(a.slice(i+1)),lvl-1).map(l=>[a[i]].concat(l))
).flat()
}
変更箇所は3つ。
Arrayのメソッドにする
Array.prototype.perm = function (lvl=this.length) { // Array に対するメソッドとして定義
return lvl<=0 || this.length<=0 ? [[]] :
[...this.keys()].map(i=>
this.slice(0,i).concat(this.slice(i+1)).perm(lvl-1).map(l=>[this[i]].concat(l))
).flat()
}
Stringのメソッドにすることもできる
String.prototype.perm = function (lvl=this.length) { // Array に対するメソッドとして定義
return lvl<=0 || this.length<=0 ? [""] :
[...this.keys()].map(i=>
(this.slice(0,i)+this.slice(i+1)).perm(lvl-1).map(l=>[this[i]]+l)
).flat()
}
Array版との差異はそんなに多くない(ArrayとStringで共通の関数や機能が多いので)。
+
で大丈夫(concat 不要)。メソッド版は、以下のように動作確認できるだろう。
[1,2].perm()
[1,2,3].perm()
[1,2,3,4].perm().length
"abc".perm()
"abcd".perm()
ここでは、添字を使って配列要素を切り出すことをせず、代わりに [要素1つ, それ以外の要素からなる配列] をタプルにしたもの(実体は配列だが)を、もとの配列の各要素について生成する所から出発する。
関数型言語での順列生成ではこれが標準的なアプローチのようだ。
function pickEach(a) {
// console.log(a)
if(a.length<=0) return []
else {
let top=a[0], rest=a.slice(1)
return [[top,rest]].concat(pickEach(rest).map(([t,r])=>[t,[top].concat(r)]))
}
}
この関数の動作は本編で解説したのとほぼ同じ、ただ、以下の点には注意が必要だろう。
.map{|a,b|...}
のように2引数で受け取ればいいが、.map((t,r)=> ...)
とはできず、 .map(([t,r])=> ...)
のように配列として受け取る必要がある。この pickEach を使うと、permは、比較的楽に書ける。
function perm(a) {
return a.length<=0 ? [[]] :
pickEach(a).map(([top,rest])=>perm(rest).map(l=>[top].concat(l))).flat()
}
sort | uniq
もしくは sort -u
に相当することをJSで行うとき、Set(集合)データ型が使える。
new Set("GAKKOU".perm())
これにより重複が除去されている。[...new
Set("GOKAKU".perm())].sort()[99]
new Set("GAKKOU".split(’’).perm())
重複が除去されないので、期待した回答は得られない。
===
演算子で比較していて、 たとえば [1,2]===[1,2]
が false
となる (同値の配列を === で比較しても等価とならない)ことによる。大筋は本編と同じ。
配列の一部(先頭から、wid個分の幅)を cnt回分転回 (先頭要素を該当範囲の末尾にまわす)させる機能を まず実現する。
# 全体を転回するなら
a.slice(cnt).concat(a.slice(0,cnt))
# 部分を転回するなら
a.slice(cnt,wid).concat(a.slice(0,cnt)).concat(a.slice(wid))
# これを関数にしておこう
function p_rot(a,wid,cnt) {
return a.slice(cnt,wid).
concat(a.slice(0,cnt)).
concat(a.slice(wid))
}
再帰的に呼び出すための内部関数を用意する。
function perm_(a,wid){
# console.log(a,wid)
return(wid>a.length?[a]:
[...Array(wid).keys()].
map(cnt=>p_rot(a,wid,cnt)).
map(l=>perm_(l,wid+1)).
flat()
)}
それを呼び出すメインの関数
function perm(a) {
return(perm_(a,1))
}