さらに2進数の活用ネタの紹介「複数選択の別視点」
2進数とビットマスクを組み合わせると、入門書に載ってないようなテクニックに使えたりします
このweb内のどこかに書いてる隠しコラムと重複する内容です
隠しコラムを発見して知っている人は読む必要はないかと思います。
また、今回はプログラムのネタなので、プログラムをしない人には特に面白いと思えない内容かと思いますのでさようなら。
多分どんなプログラムの入門書にも載ってないような内容かと思いますが、お題は「for文中のif文の条件判定をショートコードするためのビット演算テクニック」の紹介です。
for(i=0;i<9;i++){
【何かの処理】
if(i===1||i===2)【条件判定で特別な処理】
【何かの処理】
}
例のようなプログラムのif文の条件判定部分を汎用的な感じで別の書き方をする、というものです。
※汎用的な感じ、の意味は、i===1||i===2 だけでなく、
i===1||i===2||i===4 や i===1||i===4||i===5 の場合でも書ける、ということです。
奇数偶数判定の場合なら…
例えば、iが 0,2,4,6,8 の時だけ、iが1,3,5,7の時だけ、特別な処理をする、つまり奇数か偶数でグループになっている場合なら、教科書通りに if(i%2===0) や if(i%2)、前回の記事で紹介したビット演算を活用した if(i&1) で真なら奇数・偽なら偶数などの書き方ができるのですが、お題は、奇数偶数で判定できないiが1と2の時や1と2と4の時に特別な処理をする、というものです。
お題の条件だけを見ると…
for(i=0;i<9;i++){
【何かの処理】
if(i===1||i===2)【条件判定で特別な処理】
【何かの処理】
}
例だけの数値を見て、もしi=0が含まれていれば、 if(i<3)なので、
0を含まない、と組み合わせて、 if( !i && i<3)
でいいんじゃない?と考えるかもしれませんが、わざわざそう書くぐらいなら、i===1||i===2 のままの方が単純で良い、
また、i===1 || i===2 || i===4 の場合には使えない、ということになってしまいます。
汎用的に使えるアイデアを考える
i===1 || i===2 では最大2回の判定、i===1 || i===2 || i===4 なら最大3回の判定処理になる点を、ifの条件文は1回だけ真か偽かを判定するためのアイデアを考えるのが目的です。
例えば、switch文で書けば、case 1: case 2: case 4:…;break; や case 1: case 4: case 5:…;break; と書くような感じでひとまとめにiの値を指定できるアイデアを考えます。といってもswitch文を使って下記のように書くということではありません。
for(i=0;i<9;i++){
【何かの処理】
switch(i){case 1:case2:【条件判定で特別な処理】break;}
【何かの処理】
}
お題としては、case 1: case 2: case 4: のように場合場合で判定値を書くのではなく、条件の値も変数1個に数値を1個指定するだけ、すなわち、1,2 や 1,2,4 1,4,5 の部分も変数に1個の数値を指定するだけ、というものを考えます。
つまり『 if( n &【for文のiを使って何らかの式】) 』と書くのが目的です。…ホントにそんなことできるのか?と思われるかもしれませんが…
複数選択状態は1個の10進数で表現できるというネタを思い出す
>すなわち、1,2 や 1,2,4 1,4,5 の部分も変数に1個の数値を指定するだけ
と書きましたが、それは、前に書いた記事で、アンケートの複数選択状態を1個の10進数で表現した時のように、2進数に変換した時の1が選択状態、という意味を与えることで、例えば7なら、選択肢の1と2と3が選択されている事を表現できる、というネタで紹介しています。
ということで、ある数値を2進数に変換したら、1と2、や 1と2と4、1と4と5、が選択されている、すなわち 2進数にしたら条件に合う部分のみ 1 になっている数値を発見し、それとループ変数の i = 0 から 8までの値を使って、うまく & してみると、真や偽に判定できる、ような式を考えるというのが目的になります。
とりあえず条件の値を2進数にして眺めてみる
if(i===1||i===2)の時。
1 = 0001、2 = 0010、なので1と2を1つの10進数で表現すると、0011 = 3…。
ですが、1と2を3という数値で表現して、
iと&しようとしても、for文ループの iが 0 1 2 3 4 5 6 7 8 となり、3も出てきますので、1の時又は2の時だけという条件判定には使えない、ということになってしまいます。
また、if(i===1||i===2||i===4)の時。
1 = 0001、2 = 0010、4 = 0100、なので1と2と4を1つの10進数で表現すると、0111 = 7 で重複しないのですが、
if(i===1||i===4||i===5)の時は、
1 = 0001、4 = 0100、5 = 0101、なので1と4と5を単純に2進数に変換するだけでは、1つの10進数で表現できません。
なぜなら 5 という数値が1と4を表現しているからです。
複数選択状態の時のように2進数を並べてみる
条件[i=1 又はi=2]で、iは0から8までの値になります。
とりあえず、ループで変化するiの値を2進数にして並べてみます。
i=0 = 0000
i=1 = 0001 ←条件
i=2 = 0010 ←条件
i=3 = 0011
i=4 = 0100
i=5 = 0101
i=6 = 0110
i=7 = 0111
i=8 = 1000
上記数値でフラグが立ってるイメージで考えてみると、(縦の数値の並びの中で1の部分だけを眺める)
i=3 が i=1 と i=2 を含む形になるので、そのまま i をビットマスクにすることはできない、ということに気づくかと思います。
※iの値によって、前々回のお話のように、1が1個だけ各桁にある数値(ビットマスク)が並んでいる状態が必要なのです。
アイデアのポイントはシフト演算
アイデアのポイントはシフト演算です。
webプログラマの場合、ビット演算と同様に、シフト演算なんて使うことあるのか?と考え、スルーしている人もいるかもしれませんので、簡単に紹介しますと、シフト演算は2進数のビットを左や右にシフトする(ずらす)というもので、2進数の仕様から、右に1だけシフトさせるのは2で割ることと同じ、左に1だけシフトさせるのは2を掛けるのと同じ、という計算処理です。
2の階乗計算で、2の3乗(2 * 2 * 2)はjavascriptではMath.Pow(2,3)などと書きますが、これは1<<3と同じ結果なので、階乗計算よりシフトするの方が処理が軽いため、単純に2の階乗計算のためだけに使っている人もいるかもしれません。
※分かりやすくするために2進数8桁で紹介しています。
右シフト[>>]の例
4>>1 = 00000100>>1 = 00000010 = 2
7>>1 = 00000111>>1 = 00000011 = 3
4>>2 = 00000100>>2 = 00000001 = 1
左シフト[<<]の例
4<<1 = 00000100<<1 = 00001000 = 8
7<<1 = 00000111<<1 = 00001110 = 14
4<<2 = 00000100<<2 = 00010000 = 16
今回のアイデアでは左シフトを活用します。
アイデアのイメージのための1を左シフトした結果を並べてみる
アイデアのイメージは、for文のiの値で左シフトした値です。
i=0 → 1<<0 = 000000001 = 1(10進数)
i=1 → 1<<1 = 000000010 = 2(10進数)
i=2 → 1<<2 = 000000100 = 4(10進数)
i=3 → 1<<3 = 000001000 = 8(10進数)
i=4 → 1<<4 = 000010000 = 16(10進数)
i=5 → 1<<5 = 000100000 = 32(10進数)
i=6 → 1<<6 = 001000000 = 64(10進数)
i=7 → 1<<7 = 010000000 = 128(10進数)
i=8 → 1<<8 = 100000000 = 256(10進数)
iの値を2進数にした時とは違い、こちらの場合は 1のある桁が重複してない状態になっています。
「重複してない」は、それらを足し算しても、個別に抽出して判定に使うことができる、ということです。
001 + 010 = 011 = 3(10進数) 10進数の 3、すなわち 011 の各桁が1の数値を抽出→ 010 と 001 ということです。
また、この値は、1 を i の値で順に左シフトした結果はこれまで使っていたビットマスクの一覧になっています。
ちなみに「シフト演算なんて何に使うんだ?」という質問には、ビットマスクの値を用意する時に左シフトを使うと便利、と回答することができます。
お題の場合の条件文を考える
i===1||i===2 の場合
i=0 → 1<<0 = 000000001 ←偽:計算結果が0
i=1 → 1<<1 = 000000010 ←真:計算結果が0以外
i=2 → 1<<2 = 000000100 ←真:計算結果が0以外
i=3 → 1<<3 = 000001000 ←偽:計算結果が0
i=4 → 1<<4 = 000010000 ←偽:計算結果が0
i=5 → 1<<5 = 000100000 ←偽:計算結果が0
i=6 → 1<<6 = 001000000 ←偽:計算結果が0
i=7 → 1<<7 = 010000000 ←偽:計算結果が0
i=8 → 1<<8 = 100000000 ←偽:計算結果が0
真の値は、000000010 + 000000100 = 2 + 4 = 6
なので 6 が i===1||i===2 を1つの数値で表現した値です。
今回のお題(i===1||i===2)の場合は
if(6&1<<i)
と表現できる、ということです。これを汎用的に書くと…
var i,n=6;
for(i=0;i<9;i++){
【何かの処理】
if(n&1<<i)【条件判定で特別な処理】
【何かの処理】
}
i===1||i===2 の場合の数値を計算する方法で、000000010 + 000000100 = 2 + 4 = 6 と書きましたが、 普通に計算する場合の式を書いてみると、1<<1 + 1<<2 は、 2^1(2の1乗) + 2^2(2の2乗) = 2 + 4 = 6 ということになります。
ということで、
if(i===1||i===2||i===4)の時は、
n = 2^1 + 2^2 + 2^4 = 2 + 4 + 16 = 22
if(i===1||i===4||i===5)の時は、
n = 2^1 + 2^4 + 2^5 = 2 + 16 + 32 = 50
となります。
動作テスト
n=6でチェック n=22でチェック n=50でチェック
ここに
結果表示
今回紹介したアイデア [n&1<<i] は、2記事目の「複数の選択状態を示す10進数値から情報を取り出す時はビットマスクで…」の時にビットマスクを作っている部分を簡単に【 a=6; …略… f=Math.pow(2,i); …略… a&f 】と、2の階乗計算で書いてましたが、判定部分は結局 [n&Math.pow(2,i)] となっています。実はこの2の階乗計算をシフト演算にしただけのネタを別視点(シフト演算の活用)で紹介してみた、というものなのです。
ということで、for文中のif文判定で複数の条件を1つの10進数で表現し、計算したビットマスクの値と&演算すると真・偽の判定ができるというアイデアを紹介してみました
今回紹介したアイデア [n&1<<i] を使うと、switch文で大量に記述したりする10~20個程度のボタンや要素の表示非表示の(10個のボタンなら case が最低1024行になるような)全パターンを1文で書くことができるようになる、などと活用方法を思いつくかもしれません。(そのネタの詳細はjavascript研究ページの記事に書いてます)
プログラムが苦手だったり面白くないと感じていたりする人は、これまで入門書やネットにある一般的な記述をコピペするだけでプログラムを作っていたりして、深く考えずに「●○したい時には★☆と書く」という英語や国語のような暗記・辞書を引くタイプの思考でプログラムしてたりするのではないかと思ったりします。
辞書引いて誰かが作ったパーツを組み立てるだけよりも、数学的な内容をよく調べて自分の発見や発想で作ってみると、案外プログラムが楽しくなるのではないかと思ったりします。
2進数とビット演算の&、シフト演算、ビットマスク、等を組み合わせて活用するネタを4回の記事で書いてきましたが、紹介したネタはjavascriptだけではなく、どんなプログラムでも同じ考え方で同じ結果を得ることができる、という内容なので、
よければ、ぜひ、使ってみてください(朝の料理人もこみち風)
ということで、とりあえず、お疲れさまでした。