joho1-2016の日記

情報処理実習1の解説ブログです.皆さんが課題を解く時の助けになれば幸いです.

第7回 配列と文字列

第7回の授業では,配列と文字列について勉強しました.

学生の皆さんは,これまでのプログラミングで,何度も変数を宣言し,それらに値を代入するのは面倒だと思ったことはありませんか?
そして今後は,それではソースコードがとても長く,読み難くなる場合が出てきます.
こういった事を解決するものに,「配列」というものがあります.

1次元配列
配列とは変数のリストであり,1度で複数の変数を宣言できます(ただし,型が違う変数はそれぞれ宣言しなければならない).
1次元の配列は,

型 変数名[要素の個数];
 例: int days_of_month[12];

と宣言し,これにより,宣言した型の変数が要素の個数分生成されます(例だと,int型のday_of_monthという変数が12個生成された).
そして,これらの変数に値を代入したり,判定処理をするためにアクセスする時は,

month[0] = 31;

のように,変数名と何番目の要素にアクセスするのかを指定しなければいけません.注意すべき事として,
1.配列の宣言時に要素の個数がNならば,アクセスする時は0~(N-1)まで

//例  
int month[12];  //宣言が12個なら
month[0] = 1;
.
.
.
month[11]=12;  //アクセスは0~11まで

2.for文やwhile文などでまとめて値を代入する際,要素数以上の繰り返し処理をしてもコンパイルエラーは起きない

//例
int month[12];               //宣言は12個
for(int i=0; i<100; i++){  //要素数より多いループ回数!でもコンパイルは通ってしまう!
    month[i] = i+1;
}

3.まとめてのコピーは不可

int num1[12], num2[12];
num1 = num2;                 //これはできない!エラーになる.

1は宣言時とアクセス時で要素の扱う範囲が異なるように見えるので混乱しがちだが,アクセス時の[ ]内の数字は,最初の要素からのオフセットを表すので,1番目はオフセット0だし,今回の例の最期はオフセット11になるので,0~(N-1)となります.
2は実行して初めてミスに気付くとういう,かなり厄介なバグになるので,注意しましょう.
3は配列の特徴であり,C言語では対応していません.1つ1つに代入する事を意識しましょう.

配列とポインタの互換性は,ポインタを習っていない今解説しても混乱するだけなので,ここではしません.

多次元配列
実は1次元配列だけでなく,多次元配列も存在します.例えば2次元配列(3行5列)を作りたい場合,

int matrix[3][5];

と宣言する事で作成できます.
アクセスの場合,例えば1行目の3列目にアクセスしたい場合は,

matrix[0][2] = 12;

のように書きます.
三次元の時も,

int a[2][3][4];

のように,[ ]の数を増やす事で表せる.

文字列
文字列も配列の1種です.char型で宣言する事で文字が並び,文字列となります.
文字列に関しては,文字数+ヌル文字が要素数になることに注意しましょう.
文字列操作関数については,講義ページやインターネットで調べてみましょう.

以上で講義の解説は終了です.以下,各クラスの課題の考え方です.


5組
1.摂氏温度を華氏温度に変換する関数c2fをつくりなさい. 引数として実数1個,戻り値も実数とする.
main関数を作成し,関数を呼び出して正しく動作するかチェックすること.
(摂氏=Celsius度,華氏=Fahrenheit度の意味や変換方法は,各自で調べること.)

#include...

??? c2f(...)
{
    ....
    return ???;
}

int main(void)
{
    float T;  /* 温度を格納 */
    printf(...);
    scanf(...);
    
    printf("摂氏 %f 度は,華氏 %f 度です.\n", T, c2f(T) );
    return 0;
}

この課題のポイントは
 ①関数の型と引数
です.関数内にreturn文があるということは,返り値があるということです.また,printf()を見ると,関数c2f()は%fに対して代入していて,引数にはTを代入していますね.ということは,型と引数はこれに合わせると良いわけです.今回の課題において,①は全てに共通したポイントになるので,課題2以降ではポイントには書きません.

2.1からnまでの整数の和 ∑n を計算する関数 Sum を作成しなさい.

#include...
	
??? Sum(...)
{
    ....
    return ???;
}
	
int main(void)
{
    int n;
    printf(...);
    scanf(...);
	
    printf("1から %d までの和は %d です.\n", n, Sum(n) );
    return 0;
}

この課題のポイントは,
 ①関数内での引数の使用方法
です.①については,今回の課題は和の計算なので,おそらく繰り返し計算をすると思います.そして,その繰り返しの終点を関数Sum()の引数で指定しているわけです.つまり,繰り返し計算の終了判定に使えば良いわけですね.

3.キーボードから和暦(平成??年)の整数の数値を渡すと,西暦に変換して返す関数wa2seiを作りなさい.
キーボードからの入力および結果の画面への表示は全て main 関数内で行うこと.

実行例: 
和暦を入力してください(平成):28
平成 28 年は,西暦2016年です.

この課題のポイントは,
 ①和暦から西暦への変換
です.このぐらいの問題であれば,ググッた方が早いと思います.そして,returnには西暦を返せば良いわけです.

4.整数を引数として,引数が素数かどうかを判定する関数 IsPrime() 関数を作成しなさい.
関数 IsPrime 内では,素数の判定のみ行い,画面表示などは全て main 関数内で行うこと.
戻り値の意味などは各自で設定して良いが,コメントとしてソースコード中に記述しておくこと.
(例えば,素数の場合の関数の戻り値は0,それ以外は1とする,など.)

#include...
	
??? IsPrime(...)
{
    ....
    return ???;
}
	
int main()
{
    int n;       /* 整数を格納 */
    printf(...);
    scanf(...);
	
    ....
}

この課題のポイントは,
 ①素数の判定
です.素数とは,条件「1とその数自身以外の数では割り切れない数」を満たす数のことですね.と言うことは,入力された数nを1からnまで割っていき,余りが0になる数をカウントしていき,上述の条件を満たす時,素数であることを示す返り値を与えれば良いわけです.

5.三つの実数を引数に取り,それらの中央値(メディアンという)を返す関数 median を作成せよ.
main関数を用いて確認すること.

#include...
	
??? median(...)
{
	....
	return ???;
}
	
int main()
{
    printf(...); scanf(...);	
    ....
}
実行例:
a= 10.5
b= -3.9
c= 0.3
中央値は 0.3 です.

この課題のポイントは,
 ①中央値の判定
です.今までの条件処理を思い出せばさほど難しい判定ではないと思います.今回のように実数が3つと決まっているのならば,全ての大小関係を比較して,中央値を見つけるのが1番楽かと思います.

以上で5組の課題の考え方については終了です.

6組の課題についてです.
金,土と寝込んでいまして,更新が遅くなってしまい申し訳ありません.

1. 入力した5つの英単語を,辞書順に並べ替えるプログラムを作成せよ. プログラム名を,学籍番号10桁-07-1.cppとする.
//実行例
単語を入力 : everything
単語を入力 : evening
単語を入力 : eve
単語を入力 : EVA
単語を入力 : evacuation
everything, evening, eve, EVA, evacuation
辞書順に並べ替え

  • > EVA, evacuation, eve, evening, everything

この問題を解くポイントは,

  1. 複数の文字列をどのように扱うか?
  2. どのように並び替えるのか?

という2点かと思います.
まず,1.についてですが,これは二次元配列を利用することで解決を図ります.
二次元配列とは,

char words[5][1024];

のように定義するものです.これによって,words[1]とwords[0]を並び替える,というような操作が可能となり,非常に処理が書きやすくなります.
次に,2番の並び替えについてですが,こちらは一番後ろとその1つ前のものを比べて,入れ替える動作を行います.そうすることによって,最も小さいもの(辞書的に一番早いもの)が一番先頭にくるため,その次に小さいものを見つける時は,先頭から2番目のものから最後までを比べるだけで良くなります.この処理をプログラムにすると(今回の場合は単語数は5個と決まっているので),

for(int k=0;k<5;k++){
    for(int i=5-1;i>k;i--){
        単語の辞書順を比べて入れ替える処理;
    }
}

のようになります.
また,文字列における順番の入れ替えは,二次元配列words[5][1024]と設定していることから,string.hを利用して,

#include <string.h>

words[5][1024];
temp[1024];

strcpy(temp,words[i-1]);
strcpy(words[i-1], words[i]);
strcpy(words[i], temp);

ここで,tempは入れ替える前の文字を一時的に保存しておく変数です.
あとは,書く単語の,書く文字通しで大きさを比較することで,辞書順に並び替えることが出来ます.

単語の文字数が違う場合でも,文字を入力した際の配列の最後には,NULL文字(0)が入っていることを考慮しておけば,うまく順番を入れ替えることが出来るかと思います.

これらをふまえると,この辞書順の入れ替えは,3重ループの以下のような形になるでしょう,

    for(int k=0;k<5;k++){
        for(int i=5-1;i>k;i--){//一番最後の単語から1つ前の単語と辞書順を比べるためのループ
            for(int j=0;j<max_strlen;j++){//それぞれの単語の文字数だけループをまわす
                if(words[i][j]<words[i-1][j]){//直前の単語の対応する文字と辞書順を比べる
                    strcpy(temp,words[i-1]);
                    strcpy(words[i-1], words[i]);
                    strcpy(words[i], temp);
                    break;//どっちが辞書的に早いのか分かったらその単語に関する大きさ比べを終了することで,各単語の文字数の違いにも対応
                }
            }
        }
    }

2.複数の単語からなる文を入力すると,各単語の先頭文字を大文字に変えて表示するプログラムを作成せよ.
//実行例
> here is meiji university.
Here Is Meiji University.

この問題を解く上でのポイントは,

  1. どのようにスペースありで文字列を入力するか?
  2. どのように単語の先頭の文字を判別するか?
  3. どのように小文字を大文字に変換するか?

の3点かと思います.

まず,1.に関して,皆さんはキーボードからの何かを入力して処理を行う時には,「scanf」関数を利用していると思います.実際に試してみた人はお気づきかと思いますが,今まで通り,

char str[1024];
scanf(%s,str);

のように書くと,スペースありの文字列を入力するとその手前までで切れてしまうと思います.
これを回避するためには,

char str[1024];
scanf(%[^¥n],str);

とすることで,スペースありでもきちんと文字列が入力できます.

次に,2.ですが,これに関しては,私は,「スペースの次の文字が単語の先頭である(文字列全体に一番最初の文字は必ず単語の先頭である).」という条件でどの文字が単語の先頭かを判別しました.

これをプログラムにすると,入力された文字列はstrという変数に格納されているので,

for(int i =0;str[i]!=0;i++){
    
}
>||
この処理でstr内の文字1つ1つについて考えることができ,
>|cpp|
for(int i =0;str[i]!=0;i++){
    if(i==0){
        文字列の最初だから,単語の最初
    }
    else{
        if(str[i-1]==' '){
            str[i]はスペースの次の文字だから単語の先頭である
        }
    }
}

というような処理で,単語の先頭の文字を見つけることが出来ます.

最後に,3.の小文字を大文字に変換することについてですが,これはアスキーコードをもとに考えることですぐに解決することが出来ます.
アスキーコードでは,大文字と小文字の間には「32」の数の違いがあるので,これを考慮すると,

char komoji_to_omoji(char c){
    return (c - 32);
}

という関数で変換することが可能です.
これらを組み合わせれば,課題2は上手く動くプログラムが作成できると思います.

3.int num_list[] = {1, 2, 3, 4, 5, 6, 7} の数字の並びを, 任意の回数ローテーションさせるプログラムを作成せよ.
//実行例
ローテーションさせる回数:3
もとの並び
1 2 3 4 5 6 7
ローテーション後
5 6 7 1 2 3 4

ローテーションさせる回数:-3
もとの並び
1 2 3 4 5 6 7
ローテーション後
4 5 6 7 1 2 3

この問題では,

  • どのようにローテーションさせるか?

というのがポイントとなってきます.

この問題では,num_listと同じ大きさのnum_list_sortedという配列を用意した場合を考えます.
今回のような,値などをローテーションさせ(1ずつずらすなど),端まで来たら先頭に戻る(例えば,今回の場合だと,0〜6までいったら,0に戻ってくる)というような状態を「サイクリック」な状態と言います.このサイクリックで重要となるのが,「mod」という考え方です.(つまり,「あまり」について).
num_listの要素数をnum_list_dim,ローテーションする量をnとすると,このサイクリックなローテーションを行うには,

for(int i=0;i<num_list_dim;i++){
    num_list_sorted[(num_list_dim+i+n)%num_list_dim] = num_list[i];
}

によって実現できます.この処理をよく眺めてもらうと分かるかと思いますが,for文を利用することで,num_list内の1つ1つの数字について取り扱っており,「(num_list_dim+i+n)%num_list_dim」によってサイクリックにローテーションさせた先の値に上手く格納出来ていることが分かります.(例えば,今回の場合は,num_list_dim = 7,n=3とすると,num_list[0]の値は,num_list_sorted[3]にくることが望まれます.これをふまえて,「(num_list_dim+i+n)%num_list_dim」を考えると,num_list[0]とはi=0番目なので,(num_list_dim+i+n)%num_list_dim = (7+0+3)%7=3となり,きちんと条件を満たしていることが分かります.)

このサイクリックという考え方は,コンピュータ関係では,時折出てくる考え方なので,ぜひ覚えておいてください.


更新が遅くなり大変申し訳ありませんでした,課題提出がんばってください.