Lecture3 (2018/10/11)
本日の目標
多くの人の頭を悩ますであろう「ポインタ」に入る前に,教科書P.112までの内容を総チェックします.
課題1〜7まで挑戦してみてください.8〜10は挑戦してみたい人だけで結構です.
解いた課題はITC-LMSの課題のページから提出をお願いします.
前回の問題の答えの例
回文チェッカー
#include <stdio.h>
#include <string.h>
int check_kaibun(char str[]);
int main(){
char str[] = "kayak";
if( check_kaibun(str) ){
printf("%s is palindrome.\n", str);
}else{
printf("%s is NOT palindrome.\n", str);
}
return 0;
}
int check_kaibun(char str[]){
int i;
for ( i=0; i < strlen(str) / 2; i++ ){
if (str[i] != str[ ( strlen(str) - 1 ) - i ] ){
return 0;
}
}
return 1;
}
プログラムの組み立て方
何でもゼロから作ろうとしない.過去に自分が作ったプログラムを活用すること.
鉄則
- まずは入出力の仕様を決めよ.
- 何をどこから入力して,何をどこに出力するのか.
- そしてデータ構造を決めよ
- 入力した文字は配列に溜め込むのか,即処理するのか等
- 定めたデータに対して,繰り返しによる操作(forやwhile)を行え.
- if文等は,落ちと重なりが無いよう,すべて列挙する癖をつけよ
制御文について
変数の種類と型
教科書
sizeof()関数
課題1
(昔流行した)「世界のナベアツ」っぽいカウントを行うプログラムを作れ
- 仕様
- 1から1000までの数字を1行に一つ表示するが,3の倍数と3が付く数字のときだけ数字の後に「!!!」を表示
1
2
3!!!
4
5
6!!!
7
8
9!!!
10
11
12!!!
13!!!
・・・
30!!!
31!!!
32!!!
乱数
rand()関数を使うと疑似乱数(ランダムな数)を生成できる。ゲームやシミュレーション、数値計算では必ず用いることになる便利な関数。
#include <stdlib.h>
void
srand(unsigned seed);
int
rand(void);
The rand() function computes a sequence of pseudo-random integers in the range of 0 to RAND_MAX (as defined by the header file <stdlib.h>).
The srand() function sets its argument seed as the seed for a new sequence of pseudo-random numbers to be returned by rand(). These sequences are repeatable by calling srand() with the same seed value.
If no seed value is provided, the functions are automatically seeded with a value of 1.
使い方:
1. seedとして適当な値を選び、srand(seed)を呼ぶことで乱数の系列が決まる。
2. rand()を呼ぶと0〜RANDMAXの値を持つ順次疑似乱数が返される。
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int i, seed;
for (seed = 0; seed < 5; seed++) {
srand(seed);
printf("seed = %d\n", seed);
/* 1~100の乱数を10個生成 */
for (i=0; i<10; i++) {
printf("%3d ",rand() % 100 + 1);
}
printf("\n");
}
return 0;
}
srand()を何度も呼ぶのは間違い。
http://www9.plala.or.jp/sgwr-t/lib/srand.html
課題2
じゃんけんをして程よく負けてくれるプログラムを作れ.
- 仕様
- 標準入力からg, c, p, qの3つのコマンドを受け取る.
- コマンドに対して「あなたはグーを出しました.私はチョキを出しました.あなたの勝ち!」と表示する.
- 80%の確率で負けてくれる、など確率に応じて「強さ」をコントロールできる.
- qを押したときは終了する.
- 小文字のgだけでなく大文字のGなどでも受け付けるようにしてみよう.
ヒント:
まず、フローチャートは書けるか?
以下の順を追って逐次拡張できるだろうか?
1. 標準入力からg, c, p, qの3つのコマンドを受け取って、対応する手(グー、チョキ、パー)を表示、qで終了するプログラム。
2. 乱数を生成して、特定の割合で、勝敗を決めてから、相手の手に応じた結果を表示する。
(コンピュータの手を決めてから勝敗を判定するのが自然だが、この場合は確率をコントロールできない。)
標準Cライブラリ
適切なヘッダファイルをインクルードすることで,色々な関数を使えるようになる.
一覧はこちら.
試しにコンパイルしたら,,,どうなる?
#include <stdio.h>
// #include <math.h>
int main()
{
printf("sin(0.5 * pi) = %f\n", sin(0.5 * 3.14));
return 0;
}
manを使うと必要なライブラリがわかる
%man sin
SIN(3) BSD Library Functions Manual SIN(3)
NAME
sin -- sine function
SYNOPSIS
#include <math.h>
double
sin(double x);
long double
sinl(long double x);
float
sinf(float x);
DESCRIPTION
The sin() function computes the sine of x (measured in radians).
SPECIAL VALUES
sin(+-0) returns +-0.
sin(+-infinity) returns a NaN and raises the "invalid" floating-point
exception.
VECTOR OPERATIONS
If you need to apply the sin() function to SIMD vectors or arrays, using
the following functions provided by the Accelerate.framework may give
significantly better performance:
#include <Accelerate/Accelerate.h>
vFloat vsinf(vFloat x);
vFloat vsincosf(vFloat x, vFloat *c);
void vvsinf(float *y, const float *x, const int *n);
void vvsin(double *y, const double *x, const int *n);
void vvsincosf(float *s, float *c, const float *x, const int *n);
void vvsincos(double *s, double *c, const double *x, const int *n);
課題3
-5π〜5πの間でπ/100刻みでsin(πx)/πxを以下のように表示するプログラムを書け。(デジタル信号処理の標本化関数)
*πx に-5π〜5π の値を代入するのではなく、x に-5π〜5π の値を代入する。
-15.700000 -0.016704
-15.668600 -0.017791
-15.637200 -0.018710
-15.605800 -0.019450
-15.574400 -0.020004
...(略)...
15.574400 -0.020004
15.605800 -0.019450
15.637200 -0.018710
15.668600 -0.017791
15.700000 -0.016704
さらに以下のようにして結果をプロットせよ
% ./a.out | gnuplot -e " plot '-'"
なお、3次元でグラフを描く際はこちら
% ./a.out | gnuplot -e " splot '-'"
多次元配列
配列は以下のようにして容易に次元を増やせる.
int a[2][3]={
{10, 20, 30},
{40, 50, 60}
};
int a[2][2][3];
int array[30][20];
for( i = 0 ; i < 30 ; i++ ) {
for( j = 0 ; j < 20 ; j++ ) {
array[i][j] = i*10 + j;
}
}
array, array[], array[数字] の使い分け
int array1;// OK.だけどarrayは配列でなく整数の変数
int array2[]; // NG.配列の要素が何個か決まらない
int array3[3]; // OK
int array4[] = {1,2,3}; // OK
int array5[3] = {1,2,3}; // OK
char strings1[] = "abc"; //OK
char strings2[4] = "abc"; //OK
char strings3[4] = {'a', 'b', 'c', '\0'}; //OK
char strings4[3] = ”abc”; // NG ヌル文字が入らなくなるのはダメ.C言語は終端をヌル文字で識別する.
char strings5[3][10] = {"pen", "apple", "pineapple"}; // OK 文字列配列
printf("%s",strings1); // OK
printf("%c",strings2[0]); //OK
復習
丸括弧 () ・・・if、for、whileなどの条件指定、関数の引数printf(), 型変換 (double) number
角括弧 [] ・・・ 配列のインデックス
波括弧 {} ・・・制御構文やら関数など、配列の初期化
ダブルクォート " ・・・文字列リテラル
シングルクォート ' ・・・文字
課題4
上記を参考に文字列配列
char strarray[3][10] = {"pen", "apple", "pineapple"}
の各要素を読み出して、
pen apple pineapple
と表示するプログラムを作れ。
変数のスコープ
- 自動変数 (ローカル変数)
- mainの中の変数はmainに固有のものであり局所的なものである.つまり,他の関数から直接アクセスできない.他の関数内にある変数も同様である.
- ルーチン内部の局所変数は関数が呼ばれた時のみ存在し,関数から制御が離れれば消える.このような変数を自動変数と呼ぶ.
- 自動変数はそれぞれの関数の入り口で値をはっきりとセットする必要がある.
- 外部変数(グローバル変数)(p.39)
- 関数の外側で定義した変数.
- これらの変数は,それを使用する関数の中でも宣言しなければならない.(extern宣言)
- ただ,省略できるケースがある.それは外部変数の定義が,特定の関数で使われる以前にそのソースファイルの中でなされている場合である.
- 手っ取り早くいえば,ソースの最初に全ての外部変数を定義してしまい,関数の中でのextern宣言を省略するのが普通.
- 他人が使ったソースコード中にexternが存在したら,「これはどこかで外部変数が定義されていて,そのことを明示的に指定しているのだ」と理解すること.
#include <stdio.h>
void setvalue(int);
int y; // Global
int z; // Global
int main()
{
int x; // Local
x = 10;
y = 10;
z = 10;
printf("(x,y,z) = (%d, %d, %d)\n", x, y, z);
setvalue(5); // 5になるのはどれ?
printf("(x,y,z) = (%d, %d, %d)\n", x, y, z);
return 0;
}
void setvalue(int val)
{
int x;
extern int y;
int z;
x = val; // Local
y = val; // Global
z = val; // Local? Global?
}
- static 宣言
- 通常,ローカル変数は関数の実行を開始する時に作られ,関数が実行を終了する際に破棄される.
- staticで宣言したローカル変数はプログラムの実行開始時に作られ,プログラムの終了時まで保持されたまま破棄されない.
#include <stdio.h>
void hanzawa(void); // xとyを倍にする関数
int main()
{
hanzawa();
hanzawa();
hanzawa();
return 0;
}
void hanzawa(void)
{
int x = 1;
static int y = 1;
x = x * 2;
y = y * 2;
printf("x = %d, y = %d\n", x, y);
return;
}
課題5
手元にあるN枚のコインを,当選金と当選確率が異なるK台のスロットマシンに賭けるとき,最も多くの当選金を得られるような賭け方をするプログラムを作成せよ.
本解答欄には,そのアルゴリズムの説明と,N=1,000,000を賭けた際の儲かり具合(定義:最終的に手にした当選金÷掛け金N)を答え,課題のページからソースコードを提出せよ.
ルール
- スロットマシンはK台あり,全て掛け金はコイン一枚で統一.当選金と当選確率はマシンにより異なるが,マシンkの期待値は終始一定である.
- プレーヤーは期待値を事前に知ることはできないが,一枚ずつ賭けるたびに当選金を受け取ることができ,その結果に応じて次に賭けるマシンを決めて良い.
- N枚のコインを賭け終わったらゲーム終了で,ゲーム中に得た当選金をさらに賭けることはない.
- 解答に際しては以下のひな形を用いよ.これはランダムに0〜K-1のK台の中からランダムに選ぶ例である.
- N >> Kであると考えてよい.
- 採点の際には,bet(k)の関数の構造は変更しないが,各マシンの期待値を変更する.NもKも変わる可能性があることに注意せよ.
ヒント:基本,何回か適当に賭けてみて,その結果次第で,残りの賭ける先を決めることになる.その選び方の工夫に応じて加点する.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define K 5 // スロットマシンの数
#define N 100 // コインの数.採点時には1,000,000などの大きな数にする.
int bet(int k); //変更不可
int main() { //自由に変更して良い
srand((unsigned) time(NULL));
int credit = N, reward=0, sum=0;
while(credit-- > 0){
reward = bet( rand() % 5 );
sum += reward;
}
printf("sum = %f\n",(double) sum / N );
return 0;
}
// 以下の関数は変更不可
int bet(int k){ // 入力:スロットマシンの番号 出力:当選金
// これは例であり,採点時には当選金と確率が変化する
switch(k){
case 0:
if(rand() % 2 == 0){ // 2/2
return 2;
}else{
return 0;
}
break;
case 1:
if(rand() % 7 == 0){ // 5/7
return 5;
}else{
return 0;
}
break;
case 2:
if(rand() % 8 == 0){ // 10/8
return 10;
}else{
return 0;
}
break;
case 3:
if(rand() % 40 == 0){ // 50/40
return 50;
}else{
return 0;
}
break;
case 4:
if(rand() % 200 == 0){ // 100/200
return 100;
}else{
return 0;
}
break;
default:
return 0;
}
}
課題6
- news.txt(アルファベットは全て小文字化されている)の各文字のヒストグラム(各文字の頻度情報)を求めよ。
- 'a' から 'z' まで,何回出現しているのかを求め,count[] に格納すれば良い。
- 例えば'd' の頻度は count['d'-'a'] に格納すれば良い。
- 「- 'a'」という操作をすることの意味はわかるでしょうか?
- 以下の穴あきソースを用いて良い。
#include <stdio.h>
#define NALPHA 26 //アルファベットの種類
int main()
{
int count[NALPHA];
int c, i;
for( i = 0 ; i < NALPHA ; ++i ) { //カウント結果を記録する配列を初期化する
count[i] = 0;
}
while( ( c = getchar() ) != EOF ) {
if( /* ????? */ ) { // cがaからzの間アルファベットであれば、、、
count[c-'a']++; // 該当するアルファベットのカウント結果を+1する。
}
}
for( i = 0 ; i < NALPHA ; i++ ) {
printf( "%c : %d\n", 'a'+i, count[i] );
}
return 0;
}
結果
a : 9377 b : 1626 c : 4056 d : 4562 e : 14010 f : 2754 g : 2079 h : 4352 i : 9027 j : 179 k : 712 l : 4933 m : 2865 n : 9197 o : 8779 p : 2764 q : 157 r : 8016 s : 7868 t : 10464 u : 3105 v : 1347 w : 1542 x : 430 y : 2015 z : 85
- 注意:「整数 n が -8 以上,かつ,-2 以下ならば」という条件は,
- if ( -8 <= n <= -2 )
- ではありませんので,注意して下さい。
- 上のように書いても,文法的には誤りはありませんのでそのままコンパイルされます。
- ただ「整数 n が -8 以上,かつ,-2 以下ならば」という意味のプログラムにはなり
- if ( ( -8 <= n ) <= -2 )
- と解釈されます。n == -5 であれば,-8 <= n という比較演算は「真」ですので,値として整数の 1 を返します。そして,1 <= -2 というのは「偽」ですから,結局,n == -5 の時,上記の if 文は「偽」で,if の本体は実行されません。
- 「-8 <= -5 が整数の 1 を返す」というのが分からなければ,以下を実行してみましょう。
- printf( "x = %d\n", -8 <= -5 );
- printf( "x = %d\n", -8 >= -5 );
- >, <, >=, <= は,関係演算子,です。演算子なので「値を返す」のです。
課題7
news.txt を文字単位で読み取り,アルファベット('a'から'z')が二回連続して出現する全ての箇所から,アルファベット2文字を単位とした出現回数を int 型二次元配列 count[][] に格納せよ。結果はこちら.
- this is a pen であれば,th, hi, is, is, pe, en がカウントの対象となる。
- be という二文字が出現した回数は count[1][4](count['b'-'a']['e'-'a'])に格納する。
#include <stdio.h>
#define NALPHA 26
int main()
{
int count[NALPHA][NALPHA];
int c, i, j;
int first, second;
for( i = 0 ; i < NALPHA ; i++ ) {
for( j = 0 ; j < NALPHA ; j++ ) {
count[i][j] = 0;
}
}
second = ' ';
while( ( c = getchar() ) != EOF ) {
first = /*最初の文字をセット*/;
second = /*次に来る文字をセット*/;
if( /* count[][]の値を一つ増やす条件 */ ) {
count[first-'a'][second-'a']++;
}
}
// 表示
for( i = 0 ; i < NALPHA ; i++ ) {
for( j = 0 ; j < NALPHA ; j++ ) {
printf ("[%c][%c] = %d\n", 'a'+i, 'a'+j, count[i][j] );
}
}
return 0;
}
結果
[a][a] = 0, [a][b] = 186, [a][c] = 356, [a][d] = 288, ... , [z][x] = 0, [z][y] = 1, [z][z] = 3
課題8 (発展:自信のある人向け)
上記で作成した count[][] を用いて,ある文字 x が出現した場合に,次に文字 y が出現する確率 P(y|x) を計算してみよう。x = 'a', 'b', ..., 'z' とした場合,P(y | x) が最も高い場合,(0.0以外で)最も低い場合の,y を,その確率値と共に出力なさい。
- 例えば,文字 'c' が出てきた場合,次にどんな文字が来やすいか,来にくいか,という問題である。
- なお,printf で1文字を出力するのは,%c である。int c = 'a'+3; printf( "char = %c.\n", c ); のようになる。
#include <stdio.h>
#define NALPHA 26
int main()
{
int count[NALPHA][NALPHA];
double prob[NALPHA][NALPHA];
int c, i, j, sum; int first, second, minidx, maxidx;
double min, max;
for( i = 0 ; i < NALPHA ; i++ ) {
for( j = 0 ; j < NALPHA ; j++ ) {
count[i][j] = 0;
}
}
second = ' ';
while( ( c = getchar() ) != EOF ) { // 現在読み込んだ文字はcに代入されている
first = /*?????*/;
second = /*?????*/;
if( /*?????*/ ) {
count[first-'a'][second-'a']++; // アルファベットが続く場合はカウントアップ
}
}
for( i = 0 ; i < NALPHA ; i++ ) { // 確率を計算するループ
sum = 0;
for( j = 0 ; j < NALPHA ; j++ ) {
/*?? iが特定のアルファベットの時の全ケースを求めてから、i->jとなる確率は、という計算なので、、、 ??*/
}
for( j = 0 ; j < NALPHA ; j++ ) {
prob[i][j] = (double)count[i][j]/(double)sum;
}
}
for( i = 0 ; i < NALPHA ; i++ ) { //表示
min = 2.0; // 初期化にあり得ない数字を入れておく
max = -1.0; // 初期化にあり得ない数字を入れておく
for( j = 0 ; j < NALPHA ; j++ ) {
if( /*?????*/ && prob[i][j] > 0.0 ) {
min = /*?????*/;
minidx = /*?????*/;
}
if( /*?????*/ ) { max = /*?????*/;
maxidx = /*?????*/;
}
}
printf( "[%c]: min = %c (%e), max = %c (%e)\n", /*?????*/ );
}
return 0;
}
[a]: min = o (3.469813e-04), max = n (2.217210e-01)
[b]: min = g (6.281407e-04), max = e (2.487437e-01)
[c]: min = f (5.108557e-04), max = o (2.676884e-01)
[d]: min = f (4.506534e-04), max = e (3.154574e-01)
[e]: min = j (2.957413e-04), max = r (1.727129e-01)
[f]: min = g (5.063291e-04), max = o (2.835443e-01)
[g]: min = b (7.189073e-04), max = e (2.523364e-01)
[h]: min = c (2.574665e-04), max = e (4.423275e-01)
[i]: min = i (1.115698e-04), max = n (2.754658e-01)
[j]: min = i (2.339181e-02), max = o (3.625731e-01)
[k]: min = b (2.136752e-03), max = e (5.619658e-01)
[l]: min = n (2.421894e-04), max = l (2.279002e-01)
[m]: min = g (3.927730e-04), max = e (2.529458e-01)
[n]: min = x (1.457089e-04), max = t (1.978727e-01)
[o]: min = x (7.669692e-04), max = n (2.283012e-01)
[p]: min = d (3.824092e-04), max = o (2.221797e-01)
[q]: min = u (1.000000e+00), max = u (1.000000e+00)
[r]: min = h (1.051841e-03), max = e (2.803907e-01)
[s]: min = q (6.702413e-04), max = t (2.229669e-01)
[t]: min = b (2.471577e-04), max = h (2.863322e-01)
[u]: min = k (3.291639e-04), max = r (1.688611e-01)
[v]: min = u (7.479432e-04), max = e (6.985789e-01)
[w]: min = b (7.278020e-04), max = e (2.139738e-01)
[x]: min = l (3.472222e-03), max = p (3.298611e-01)
[y]: min = r (2.222222e-03), max = e (3.066667e-01)
[z]: min = l (1.333333e-02), max = e (4.933333e-01)
課題9 (発展:自信のある人向け)
1 文字を単位とした頻度情報 count[] から,各文字の出現確率 P(x) を求め,英文におけるアルファベットの情報量(エントロピー)を求めなさい。
なお,事象 i の確率が P(i) である場合,-log2(P(i)) をその事象 i の自己情報量と呼びます。
また,この自己情報量の全事象に対する期待値 Σ P(i)*( -log2(P(i)) ) を,その事象全体の(平均)情報量(エントロピー)と呼びます。結果は
entropy = 4.167580e+00
- #include <math.h> を書くと下記の関数が使えるようになります。
- double log( double x ) 自然対数(ln)
- double log10( double x ) 常用対数
課題10 (発展:自信のある人向け)
あるアルファベット x が検出された時の,次のアルファベットに対する情報量についても検討してみよう。
課題9を,2 文字単位で計測した count[][] を使って検討しなさい,ということです。
一文字目のアルファベット別に,課題9を実行しなさい,ということです。
一文字目が既知となった場合,情報量はどのように変化するのだろうか?
- エントロピー(平均情報量)は予測のし難さを意味します。エントロピーが大きければ,予想し難い,ということです。どのアルファベットが現れると,次のアルファベットは予想し易い,し難いのか?
- なお,count[x][y] == 0 の場合,文字 x の後に文字 y が出現する回数が 0 なので,P(y|x) == 0.0 となります。当然その log の値は,計算できません。しかし,x -> 0.0 の時,xlog(x) は 0.0 になりますので,count[x][y] == 0 の場合の自己情報量も 0.0 としてよいことになります(if 文で場合分けする必要があることに注意)。
news.txtの場合の答え
[a]: ent = 3.563437e+00
[b]: ent = 3.162106e+00
[c]: ent = 3.220312e+00
[d]: ent = 2.994324e+00
[e]: ent = 3.773022e+00
[f]: ent = 2.785498e+00
[g]: ent = 3.104862e+00
[h]: ent = 2.418587e+00
[i]: ent = 3.551245e+00
[j]: ent = 2.058812e+00
[k]: ent = 2.257183e+00
[l]: ent = 3.162094e+00
[m]: ent = 2.959273e+00
[n]: ent = 3.465873e+00
[o]: ent = 3.592704e+00
[p]: ent = 3.031292e+00
[q]: ent = 0.000000e+00
[r]: ent = 3.541815e+00
[s]: ent = 3.300040e+00
[t]: ent = 3.132873e+00
[u]: ent = 3.569027e+00
[v]: ent = 1.328396e+00
[w]: ent = 2.744439e+00
[x]: ent = 2.607650e+00
[y]: ent = 2.979715e+00
[z]: ent = 2.066047e+00