配列と乱数
数を記録(記憶)するためにrpnが用意している仕組みに、スタックとレジスタがありました。スタックはLIFO(Last In First Out:後入れ先出し)型の記憶装置で、数値の取り出し方に制約があります(読み出しが保存した順番に依存する)。
対してレジスタにはそのような制限はなく、自由に数値の保存と取り出しができます。実際に複雑なプログラムをする際に多用するのがレジスタでしょう。制約といえばアルファベット記号の分しか保存場所がないことくらいです。
そして、スタックでもレジスタでもない便利な第三の記憶方法が配列です。配列は数値を記憶できる場所が連続して、たくさん並んでいるイメージです。
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
| | | | | | | | ... | | | | | | | |
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
この配列にrpnで扱う数値を格納することができます。
配列の仕組みは大抵のプログラム言語に実装されています。多次元配列、動的配列、連想配列など様々な配列が選べますが、rpnでは単純な1次元配列が用意されているだけです。ただ、原理的には1次元配列があれば各種配列はシミュレートできるので、小さな電卓ソフトとしては十分でしょう。
配列へのアクセスはインデックスで
配列にアクセス(数値を格納したり取出したり)するには、配列に付いている番号(インデックス)を指定します。1番目の配列に数値を保存したり、5番目の配列から数値を読み出したりするわけです。
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
| | | | | | | | ... | | | | | | | |
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
0 1 2 3 4 5 6 1017 1019 1021 1023
ちなみに、rpnの配列インデックスは上図にあるように0から1023までになります。つまり、rpnの配列には記憶できる場所が1024個あることになります。数が細かくて覚えるのが面倒なら、rpnの配列は1000個までと覚えておいてもいいでしょう。レジスタと配列をあわせれば相当数の記憶領域を利用できます。
rpnの配列操作
さて、rpnで配列にアクセスする方法ですが、配列操作記号の「@」と「#」を使います。@が数値を取り出すための記号で、#が数値を格納するための記号です。レジスタの使い方と少し似ていますね。
具体的な@と#の使い方ですが、rpn式では「数値 インデックス 操作記号(#)」の順番で記述することで数値を配列に格納できます。例を示すと以下のようになります。
このrpn式では1番目の配列に数値の10を格納しています。先の数字が格納する数字で次の数字が配列のインデックスです。rpnの実行結果としては何も出てきませんが、実行直後の配列のイメージは以下のようになっています(DOSのプロンプトが出るまで)。
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
| | 10| | | | | | ... | | | | | | | |
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
0 1 2 3 4 5 6 1017 1019 1021 1023
次に1番目の配列から数値を取り出す場合ですが、こちらは「インデックス 操作記号(@)」の順に記述することで配列から数値を取り出せます。具体的には以下のように書きます。
10
最初に1番目の配列に10を格納してから、改めて1番目の配列から数値を取り出しています。取り出した値はスタックに積まれるので、rpnの実行結果としては、10が出力されます。
では、もうちょっと複雑な例を示しましょう。2番目の配列から数値を取り出して、1を加えてから2番目の配列に戻すにはどうしたらいいでしょうか。答えは以下のとおりです。
「2 @」の段階で2番目の配列にあった数値がスタックに積まれます。この場合、配列の初期値は0なのでスタックには0が格納されます。次にスタックの数値に「1 +」で1つ加算してスタックに戻しています。最後にスタックの数値を配列インデックスの2番に格納しています。配列のイメージを示しますね。
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
| | | 1| | | | | ... | | | | | | | |
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
0 1 2 3 4 5 6 1017 1019 1021 1023
レジスタを使った間接参照
ところで、この配列へのアクセスですが、直接に配列番号を記述しなくてもレジスタを使って間接的に参照することもできます。以下は一旦インデックスの2をiレジスタに格納してから、そのiレジスタを使って配列にアクセスしています。
「2 #i」で配列番号をiレジスタに保存しておいて、「@i @ 1 + @i #」とすることでiレジスタをインデックスとして配列にアクセスするために使います。配列番号を3に変えるとするなら、以下のようにiレジスタにセットする値を変えるだけです。
1行しかないrpn式ではインデックスのレジスタ化にあまり意味はありませんが、長いプログラムを作る段階になってくると必要な機能になってきます。
配列アクセスの簡便法
配列のアクセスに関して、最後に簡便法を示しましょう。配列のインデックスが固定値の場合は簡略して記述することができます。例えば、以下のrpn式を簡略化できるのです。
1番目の配列に10を格納してから取り出し、1を増やしてから、2番目の配列に格納しています。実行直後のイメージは以下のとおりです。
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
| | 10| 11| | | | | ... | | | | | | | |
+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+
0 1 2 3 4 5 6 1017 1019 1021 1023
このrpn式を以下のように記述できます。
「#1」を見ると分かるように「1 #」のように指定しなくてもよいことがわかります。「@1」や「#2」も然りです。大した違いはないように思いますが、しばらく使っているとコンパクトに分かりやすく書けることに気付きます。
ランダムにサイコロを振る
このように配列は便利な記憶装置とも言えるのですが、レジスタと比べると指定が面倒な感じがします。しかし、レジスタを使うにはあらかじめ決めておいたアルファベットの記号が必要なので、柔軟性に乏しい面があります。
例えば、サイコロを10回振って出た目の数をカウントするプログラムを考えてみましょう。数値が1だったらaレジスタをカウントアップ、数値が2だったらbレジスタを、…、そして数値が6だったらfレジスタをカウントアップするためには、以下のようにプログラムしておく必要があります。
aレジスタ = aレジスタ + 1
}
if (数値 = 2) {
bレジスタ = bレジスタ + 1
}
:
(中略)
:
if (数値 = 6) {
fレジスタ = fレジスタ + 1
}
のようにです。仮に正32面体などのサイコロだとすると、とても長いプログラムになります。もうちょっと簡単にプログラムできないものでしょうか。
配列を使ったサイコロのシミュレーション
そこで、実際にサイコロのシミュレーションを行なうプログラムを作りながら、配列の便利さを確認してみましょう。
まず、サイコロの動きをシミュレートするには、1から6までの数字をランダムに作り出す必要があります。rpnにはランダムな数字を発生させる乱数生成の記号として「?」があります。以下のように指定することで、1から6までの数字のうちどれか1つをランダムに生成します。
3
使い方は簡単ですね。今回は3が出ました。連続して3回サイコロを振ると以下のようになります。
3 5 4
では、この乱数を使ってサイコロ振りのシミュレーションをしてみましょう。
rpnのサイコロシミュレーション
配列を使えばこの手のカウントプログラムは簡単にできます。先にrpnを使ったプログラムを示してしまいますね。10回サイコロを振って出た目をカウントするプログラムは以下のとおりです。
たった、これだけです。1行でシミュレーションできることになります。rpn式は難しそうに見えますが、部分に分けて理解すれば簡単です。以下で①から④までの動きを解説しておきます。
------ ------------- ----- --------------------
① ② ③ ④
①の「6 ? #r」でサイコロを振って、rレジスタに保存しています。 ②の部分でrレジスタをインデックスにして配列から数値を取り出し、1を加えて再度保存しています。③で①と②の動作を10回繰り返すことを指示しています。④ではrpnの処理が終わった後、配列のアクセス簡便法で1から6までの配列をスタックに積んでいます。
では、実際に実行してみましょう。
3 2 1 2 1 1
1の目が3回、2の目が2回、3の目が1回、4の目が2回、5の目が1回、6の目が1回の合計10回です。たったの1行プログラムですぐにシミュレーションできましたね。ついでに100回、1000回、10000回とサイコロを振ったときの出る目のカウントを実行してみましょう。-rオプションの数値を変えるだけで実験できます。
20 19 14 16 14 17
>rpn 6 ? #r @r @ 1 + @r # -r 1000 -e @1 @2 @3 @4 @5 @6
157 177 157 161 165 183
>rpn 6 ? #r @r @ 1 + @r # -r 10000 -e @1 @2 @3 @4 @5 @6
1560 1706 1684 1679 1653 1718
100回程度の試行(サイコロ振り)だとばらつきはある程度予測できますが、1000回、10000回と試行しても結構、ばらついているように見えます。
確率的には10000回試行してサイコロの目が均等に出るとすれば、それぞれ1666.7回のはず(10000回×1/6)なのですが、実際には一番多く出た目の数と一番少なく出た目の数の差は158もあります。なかなか確率どおりには行かないものです。
実際、10000回試行の結果でも、統計的な有意差は確認できませんでした。このあたりの話題に関しては、ビジネス統計講座(検定編)の適合度検定に詳しくあります。