ホーム » 2019 (ページ 10)
年別アーカイブ: 2019
Excel で計算式の基礎(part1)
情報制御基礎では、プログラムで計算する所を、Excel のような表計算ソフトを用いて検証してもらったりする予定なので、Excel で計算式を使う方法を説明する。
セルの場所と簡単な式
簡単な、品名・単価・個数・価格の表を考える。以下の表のように、列の名前と、品名・単価・個数まで入力した後、単価と個数をかけた価格を求めるとする。
Excel では、表の列には左から、A,B,C,D… , 表の行には上から1,2,3,4,5 と番号が振られていて、特定の列・特定の行のデータを表す時には、列行を組み合わせ、A1に品名、B3に¥80、C5に4 が入っている。
例えば、D2 に、ノート単価120円、ノート個数3個をかけた値を入れたい場合は、D2の場所に、
=B2*C2
を書き込めば、その場所には360が表示される。
D3には、”=B3*C3″を入力すれば、160 が表示される。しかし、この様な式を何度も入力するのは面倒である。
この場合、セル・カーソルを、D2 に合わせ、[右ボタン]-[コピー]を行い、D3 で[右ボタン]-[貼り付けオプション]-[貼り付け]を行えば、”=B3*C3″が入力される。
ここで注意しないといけないのが、式を張り付ける場合には、貼り付け先のセルの場所が一つ下の行なので、行番号を表す2の部分が1つ下の行番号3に書き換えられて、貼り付けが行われる。(相対参照)
関数式
例えば、下左図のような、数字とその平方根の表を作る場合、A2 に 1、B2に =sqrt( A2 ) を入力、A3 に =A2+1 を入力したあと、B2の式をB3にコピー&ペーストし、A3,B3 を A4~A6にペーストすればいい。
B2に入力したような、sqrt( A2 ) のようなものは、関数式と呼ばれる。
また、A3,B3 といった複数の行・列をまとめた範囲を示す時は、A3:B3 といった表記方法であらわす。
絶対参照と相対参照
最初の例に戻って、単価と個数の積で今度は税率を加えて計算する例を考える。また、税率は後で変化するかもしれないので、B1 のセルに税率を記入しておく場合を考える。
この場合、D3 には、” =B3*C3*(1+B1) ” を入力すればいい。
ただ、このように式を入力すると、D3 の計算式を、D4,D5,D6 にコピーすると、セル D4 には =B4*C4*(1+B2) が入力されてしまい、B2 には単価という文字が記載されているため、正しい結果が求まらない。
こういった場合には、絶対参照を用いる。D3 に記入する式を
=B3*C3*(1+$B$2)
とし、この D3 の式を D4 にコピー&ペーストすると、列記号、行番号の前に$がついた部分の式は、貼り付け場所に応じて変化しない。
このような、$B$2 といったセルの参照は、絶対参照と呼ぶ。これに対し、B2 といったセル参照は、貼り付け場所に応じて書き換えられるので、相対参照と呼ぶ。
絶対参照と相対参照が混ざった、$B2, B$2 といった書き方もある。
式の入力時にF4ボタンを押す度に、B2→$B$2→B$2→$B2→B2 と変化する
$B2 は、式をコピーすると列部分はBのまま、行部分は場所に合わせて変化する。
B$2 は、式をコピーすると列部分は場所に合わせて変化し、行部分は2のままとなる。
レポート課題(2-2)
Excel で、xを0〜180度まで変化させたときのsin(x),位相をyとした時のsin(x+y)の値の表を作り、グラフ機能で表示せよ。
レポートには、セルにどのような式を入力したかの説明と、結果のグラフ(解りやすいように表示を工夫すること)をつけること。
派生と継承
隠ぺい化の次のステップとして、派生・継承を説明する。
派生を使わずに書くと…
元となるデータ構造(例えばPersonが名前と年齢)でプログラムを作っていて、 途中でその特殊パターンとして、所属と学年を加えた学生(Student)という データ構造を作るとする。
// 元となる構造体(Person) struct Person { char name[ 20 ] ; // 名前 int age ; // 年齢 } ; // 初期化関数 void set_Person( struct Person* p , char s[] , int x ) { strcpy( p->name , s ) ; p->age = x ; } // 表示関数 void print_Person( struct Person* p ) { printf( "%s %d\n" , p->name , p->age ) ; } void main() { struct Person saitoh ; set_Person( &saitoh , "t-saitoh" , 50 ) ; print_Person( &saitoh ) ; }
パターン1(そのまんま…)
上記のPersonに、所属と学年を加えるのであれば、以下の方法がある。 しかし以下パターン1は、要素名がname,ageという共通な部分があるようにみえるが、 プログラム上は、PersonとPersonStudent1は、まるっきり関係のない別の型にすぎない。
このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。
// 元のデータに追加要素(パターン1) struct PersonStudent1 { // Personと同じ部分 char name[ 20 ] ; // 名前 int age ; // 年齢 // 追加部分 char dep[ 20 ] ; // 所属 int grade ; // 学年 } ; void set_PersonStudent1( struct PersonStudent1* p , char s[] , int x , char d[] , int g ) { // set_Personと同じ処理を書いている。 strcpy( p->name , s ) ; p->age = x ; // 追加された処理 strcpy( p->dep , d ) ; p->grade = g ; } // 名前と年齢だけ表示 void print_PersonStudent1( struct PersonStudent1* p ) { // print_Personと同じ処理を書いている。 printf( "%s %d\n" , p->name , p->age ) ; } void main() { struct PersonStudent1 yama1 ; set_PersonStudent1( &yama1 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent1( &yama1 ) ; }
パターン2(元データの処理を少し使って…)
パターン1では、同じような処理を何度も書くことになり、面倒なので、 元データ用の関数をうまく使うように書いてみる。
// 元のデータに追加要素(パターン2) struct PersonStudent2 { // 元のデータPerson struct Person person ; // 追加部分 char dep[ 20 ] ; int grade ; } ; void set_PersonStudent2( struct PersonStudent2* p , char s[] , int x , char d[] , int g ) { // Personの関数を部分的に使う set_Person( &(p->person) , s , x ) ; // 追加分はしかたない strcpy( p->dep , d ) ; p->grade = g ; } void print_PersonStudent2( struct PersonStudent2* p ) { // Personの関数を使う。 print_Person( &p->person ) ; } void main() { struct PersonStudent2 yama2 ; set_PersonStudent2( &yama2 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent2( &yama2 ) ; }
このパターン2であれば、元データ Person の処理をうまく使っているので、 プログラムの記述量を減らすことはできるようになった。
しかし、print_PersonStudent2() のような処理は、元データ構造が同じなのに、 いちいちプログラムを記述するのは面倒ではないか?
そこで、元データの処理を拡張し、処理の流用ができないであろうか?
基底クラスから派生クラスを作る
オブジェクト指向では、元データ(基底クラス)に新たな要素を加えたクラス(派生クラス)を 作ることを「派生」と呼ぶ。派生クラスを定義するときは、クラス名の後ろに、 「:」「public/protected/private」基底クラス名を書く。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: // 追加部分 char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } } ; void main() { Person saitoh( "t-saitoh" , 50 ) ; saitoh.print() ; Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; }
ここで注目すべき点は、main()の中で、Studentクラス”yama”に対し、yama.print() を呼び出しているが、パターン2であれば、print_PersonStudent2()に相当するプログラムを 記述していない。 しかし、この派生を使うと Person の print() が自動的に流用することができる。 これは、基底クラスのメソッドを「継承」しているから、 このように書け、名前と年齢「yama 22」が表示される。
さらに、Student の中に、以下のような Student 専用の新しい print()を記述してもよい。
class Student ...略... { ...略... // Student クラス専用の print() void print() { // 親クラス Person の print() を呼び出す Person::print() ; // Student クラス用の処理 printf( "%s %d\n" , dep , grade ) ; } } ; void main() { ...略... Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; }
この場合は、継承ではなく機能が上書き(オーバーライト)されるので、 「yama 22 / PS 2」が表示される。
派生クラスを作る際の後ろに記述した、public は、他にも protected , private を 記述できる。
public だれもがアクセス可能。 protected であれば、派生クラスからアクセスが可能。 派生クラスであれば、通常は protected で使うのが一般的。 private 派生クラスでもアクセス不可。
仮想関数への伏線
上記のような派生したプログラムを記述した場合、以下のようなプログラムでは何が起こるであろうか?
class Student ... { : void print() { Person::print() ; // 名前と年齢を表示 printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示 } } ; void main() { Person saitoh( "t-saitoh" , 55 ) ; saitoh.print() ; // t-saitoh 55 名前と年齢を表示 Student mitsu( "mitsuki" , 19 , "E" , 4 ) ; Student ayuka( "ayuka" , 17 , "EI" , 2 ) ; mitsu.print() ; // mitsuki 19 / E 4 名前,年齢,所属,学年を表示 ayuka.print() ; // ayuka 17 / EI 2 名前,年齢,所属,学年を表示 Person* family[] = { &saitoh , &mitsu , &ayuka , // 配列の中に、Personへのポインタと } ; // Studentへのポインタが混在している // 派生クラスのポインタは、 // 基底クラスのポインタとしても扱える for( int i = 0 ; i < 3 ; i++ ) family[ i ]->print() ; // t-saitoh 53/mitsuki 18/ayuka 16 } // が表示される。
ソートアルゴリズム
前回の授業のハノイの塔は、単純な再帰方程式で処理時間のオーダーが巨大となる一例として示した。そこで、プログラムの中でよく利用されるデータの並び替え(ソート)で処理時間の分析を行ってみる。
様々なソートアルゴリズム
データの有名な並び替えアルゴリズムとその処理時間のオーダーを示す。
- バブルソート O(N2)
- 選択法 O(N2/2)
- クイックソート O( N log N )
- マージソート O( N log N )
マージソートの分析
マージソートは、与えられたデータを2分割し、 その2つの山をそれぞれマージソートを行う。 この結果の2つの山の頂上から、大きい方を取り出す…という処理を繰り返すことで、 ソートを行う。
このことから、再帰方程式は、以下のようになる。
この再帰方程式を、N=1,2,4,8…と代入を繰り返していくと、 最終的に処理時間のオーダが、 となる。
:
よって、
選択法とクイックソートの処理時間の比較
データ数 N = 20 件でソート処理の時間を計測したら、選択法で 10msec 、クイックソートで 20msec であった。
- データ件数 N = 100 件では、選択法,クイックソートは、それぞれどの程度の時間がかかるか答えよ。
- データ件数何件以上なら、クイックソートの方が高速になるか答えよ。
設問2 は、通常の関数電卓では求まらないので、数値的に方程式を解く機能を持った電卓が必要。
実数型と取り扱いの注意
実数型(float / double)
実数型は、単精度実数(float型)と、倍精度実数(double型)があり、それぞれ32bit,64bitでデータを扱う。
単精度型(float)では、符号1bit,指数部8bit,仮数部23bitで値を覚え、数値としては、以下の値を意味する。(精度が低いので普通のコンピュータではあまり使われることはない)
符号✕ 1.仮数部 ✕ 2指数部-127
倍精度型(double)では、符号1bit,指数部11bit,仮数部52bitで値を覚え、数値としては、以下の意味を持つ。
符号✕ 1.仮数部 ✕ 2指数部-1023
倍精度型を使えば、正しく計算できるようになるかもしれないが、実数型はただの加算でも仮数部の小数点の位置を合わせたりする処理が必要で、浮動小数点専用の計算機能を持っていないような、ワンチップコンピュータでは整数型にくらべると10倍以上遅い場合もある。
実数の注意点
C言語でプログラムを作成していて、簡単な数値計算のプログラムでも動かないと悩んだことはないだろうか?解らなくて友達のプログラムを真似したら動いたけど、なぜ自分のプログラムは動かなかったのか深く考えたことはあるだろうか?
単純な合計と平均
整数を入力し、最後に合計と平均を出力するプログラムを以下に示す。
しかし、C言語でこのプログラムを動かすと、10,10,20,-1 と入力すると、合計(sum)40,件数(cnt)3で、平均は13と表示され、13.33333 とはならない。
小数点以下も正しく表示するには、どうすればいいだろうか?
ただし、変数の型宣言を “double data,sum,cnt ;” に変更しないものとする。
// 入力値の合計と平均を求める。 #include <stdio.h> int main() { int data ; int sum = 0 ; int cnt = 0 ; for(;;) { printf( "数字を入力せよ。-1で終了¥n" ) ; scanf( "%d" , &data ) ; if ( data < 0 ) break ; cnt = cnt + 1 ; sum = sum + data ; } printf( "合計 %d¥n" , sum ) ; printf( "平均 %d¥n" , sum / cnt ) ; }
C言語では、int型のsum / int型のcnt の計算は、int 型で計算を行う。このため、割り算だけ実数で行いたい場合は、以下のように書かないといけない。
printf( "平均 %lf¥n" , (double)sum / (double)cnt ) ;
// (double)式 は、sum を一時的に実数型にするための型キャスト
まずは動く例
以下のプログラムは、見れば判るけど、th を 0度〜360度まで5度刻みで変化させながら、y = sin(th) の値を表示するプログラム。
// sin の値を出力 #include <stdio.h> #include <math.h> int main() { double th , y ; for( th = 0.0 ; th <= 360.0 ; th += 5.0 ) { y = sin( th / 180.0 * 3.1415926535 ) ; printf( "%lf %lf¥n" , th , y ) ; } return 0 ; }
動かないプログラム
では、以下のプログラムはどうだろうか?
// case-1 ---- プログラムが止まらない #define PI 3.1415926535 int main() { double th , y ; // 0〜πまで100分割でsinを求める for( th = 0.0 ; th != PI ; th += PI / 100.0 ) { y = sin( th ) ; printf( "%lf %lf¥n" , th , y ) ; } return 0 ; }
// case-2 ---- y の値が全てゼロ int main() { int th ; double y ; for( th = 0 ; th <= 360 ; th += 5 ) { y = sin( th / 180 * 3.1415926535 ) ; printf( "%d %lf¥n" , th , y ) ; } return 0 ; }
どちらも、何気なく読んでいると、動かない理由が判らないと思う。そして、元のプログラムと見比べながら、case-1 では、「!=」を「<=」に書き換えたり、case-2 では、「int th ;」を「double th ;」に書き換えたら動き出す。
では何が悪かったのか…
回答編
C言語の基礎(part1)
学際科目の情報制御基礎において、学科間でプログラミングの初歩の理解差があるので、簡単なC言語プログラミングの基礎の説明。
Hello World
“Hello World”と表示するだけのC言語プログラムは以下のようになる。
// コメントの書き方1 // "//"で始まる行は、プログラムの説明(コメント) /* コメントの書き方2 */ // "/*"から"*/"で囲まれる範囲もコメント #include <stdio.h> // #で始まる行はプリプロセッサ行 // stdio.h には、入出力関数の説明が書いてある int main() { // 一連の処理の塊を関数と呼ぶ。 // C言語では main() 関数を最初に実行する。 printf( "Hello World\n" ) ; // printf() は、以下の物を表示する関数。 // "\n"は、文字を出力して改行するための特殊文字 return 0 ; // main() 関数が、正常終了したことを意味する } // 0 を返り値として返す。
“#include <>”のプリプロセッサ行は、最初のうちは解りにくいので、これを書かないとダメ…と思っていればいい。
#include <stdio.h> は、別ファイル(ヘッダファイル) stdio.h に記載されているプログラムを読み込む機能。
stdio.h には、printf() とか scanf() とかの関数や定数などの情報が記載されている。
C言語の基本的な命令(文)は、”;”で終わる。(単文)
複数の処理をまとめる場合には、”{“から”}”の中に、複数の文を書き並べる。(複文)
関数とは、複数の処理をひとまとめにした、処理の「かたまり」と思えばいい。
関数の型 関数名( 仮引数 ... ) { 処理1 ... ; 処理2 ... ; }printf() の 文字列中の”\n”(あるいは”¥n”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。
変数と代入
#include <stdio.h> #include <math.h> // 数学関数を使う 平方根 sqrt() を使っている int main() { // 変数の宣言 int i ; // 符号付き32bit変数 i の宣言 int a = 123 , j ; // a を 123 で初期化 , j も整数型 float x ; // 単精度実数の x を宣言 double y = 1.234 , z ; // 倍精度実数の y を宣言し 1.234 で初期化, // z も倍精度実数 // 変数への代入 i = 1 ; // i に 1 を代入 i = 12 + 2 * a ; // 12+2*a を代入 a は123なので、 // iには、258 が入る。 x = sqrt( 2.0 ) ; // x に 2.0 の平方根(1.4142)を代入 z = y * 2.0 + x * 3.0 ; // y*2+x*3をzに代入 // 変数の内容の表示 printf( "%d\n" , i ) ; // 整数型(%d)で、 i の値を表示 printf( "%f\n" , x ) ; // 単精度実数(%f) で、x の値を表示 printf( "%lf\n" , z ) ; // 倍精度実数(%lf)で、z の値を表示 printf( "iの値は%d,xの値は%lfです。\n" , i , x ) ; return 0 ; // 正常終了 0 を返す }
変数(計算結果を格納する入れ物)を使う場合は、変数を宣言する。
変数名には、何が入っているのか理解しやすいように、名前をつければいい。(英字で始まり、英数字が続くもの,_が入ってもいい)
変数に値を記憶する時は、”変数名=式 ;”の様に書くと、代入演算子”=” の右辺を計算し、その計算結果が左辺の変数に保存される。
変数の内容を表示する時には、printf() の文字列の中に、%d,%f,%lf などの表示したい式の型に応じたものを書いておく。
式の値が、その %.. の部分に書き込まれて、出力される。
繰り返しの制御命令
最も基礎的な繰り返し命令として、for() 文を説明。
#include <stdio.h> int main() { int i ; for( i = 1 ; i <= 10 ; i++ ) { // iを1から10まで変化させる。 printf( "%d %d\n" , i , i*i ) ; // i と iの二乗を表示 } return 0 ; }
for文の意味を説明するために、対応するフローチャートを示す。
先のプログラムをフローチャートで示し、その命令の実行順序と、その変数の変化を下図に示す。
理解度確認
以下のプログラムの実行順序と、最終結果で表示される内容を答えよ。
ポインタを使った処理
この後の授業で、ポインタを使ったプログラムが増えるので、ポインタの理解の確認
ポインタと引数
値渡し
// 値渡しのプログラム void foo( int x ) { // x は局所変数(仮引数は呼出時に // 対応する実引数で初期化される。 x++ ; printf( "%d¥n" , x ) ; } void main() { int a = 123 ; foo( a ) ; // 124 // 処理後も main::a は 123 のまま。 foo( a ) ; // 124 }
このプログラムでは、aの値は変化せずに、124,124 が表示される。
でも、プログラムによっては、124,125 と変化して欲しい場合もある。
どのように記述すべきだろうか?
// 大域変数を使う場合 int x ; void foo() { x++ ; printf( "%d¥n" , x ) ; } void main() { x = 123 ; foo() ; // 124 foo() ; // 125 }
しかし、このプログラムは大域変数を使うために、間違いを引き起こしやすい。
// 大域変数が原因で予想外の挙動をしめす簡単な例 int i ; void foo() { for( i = 0 ; i < 2 ; i++ ) printf( "A" ) ; } void main() { for( i = 0 ; i < 3 ; i++ ) // このプログラムでは、AA AA AA と foo() ; // 表示されない。 }
静的局所変数
大域変数を使わない方法としては、静的局所変数( static )を使う方法もある。
// 静的局所変数を使う場合 void foo() { static int x = 123 ; // x は foo の内部でのみ使用可 // x はプログラム起動時に作られ 123 で初期化される。 x++ ; printf( "%d¥n" , x ) ; } void main() { foo() ; // 124 foo() ; // 125 // ここで x = 321 といった代入はできない }
静的局所変数は、変数の寿命が大域変数と同じように、プログラム起動から終了までの間だが、参照できるスコープは所属するブロック内に限られる。
ポインタ渡し
C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。
// ポインタ渡しのプログラム void foo( int* p ) { // p はポインタ (*p)++ ; printf( "%d¥n" , *p ) ; } void main() { int a = 123 ; foo( &a ) ; // 124 // 処理後 main::a は 124 に増えている。 foo( &a ) ; // 124 } // さらに125と増える。
ポインタを利用して引数に副作用を与える方法は、ポインタを正しく理解していないプログラマーでは、危険な操作となる。C++では、ポインタ渡しを使わないようにするために、参照渡しを利用する。
参照渡し
// ポインタ渡しのプログラム void foo( int& x ) { // xは参照 x++ ; printf( "%d¥n" , x ) ; } void main() { int a = 123 ; foo( a ) ; // 124 // 処理後 main::a は 124 に増えている。 foo( a ) ; // 124 } // さらに125と増える。
ポインタ渡しを使う処理
// ポインタ渡しのプログラム void swap( int* pa , int* pb ) { int t = *pa ; *pa = *pb ; *pb = t ; } void main() { int a = 123 ; int b = 111 ; swap( &a , &b ) ; printf( "%d %d¥n" , a , b ) ; }
高専プロコン連携シンポジウム2019
〜セキュリティの観点からプロコンに期待すること〜
高専プロコンの応募に合わせ、以下のような講演会が開催されます。
今年度は、課題部門で「ICT を活用した地域活性化」,自由部門で「クラウドコンピューティング」「オープンデータ」「サイバーセキュリティ」「人工知能」がキーワードに挙げられており、高専生に現場のニーズや動向を学習する機会として、開催されます。
日 時: | 5月20日(月) 16:20〜16:50 Skype for businessを用いた動画配信 |
学内場所: | 電子情報工学科3F 情報処理演習室 |
講 師: | 株式会社日立製作所 セキュリティ事業統括本部サイバーセキュリティ技術本部 セキュリティ人材統括センタセンタ長千葉寛之様 |
講演テーマ: | セキュリティの観点からプロコンに期待すること |
補 足: | 発言・質問は、Twitter(ハッシュタグ #procon30 にて)受付 |
再帰呼び出しと再帰方程式
再帰関数と再帰方程式
再帰関数は、自分自身の処理の中に「問題を小さくした」自分自身の呼び出しを含む関数。プログラムには問題が最小となった時の処理があることで、再帰の繰り返しが止まる。
// 階乗 (末尾再帰) int fact( int x ) { if ( x <= 1 ) return 1 ; else return x * fact( x-1 ) ; } // ピラミッド体積 (末尾再帰) int pyra( int x ) { if ( x <= 1 ) return 1 ; else return x*x + pyra( x-1 ) ; } // フィボナッチ数列 (非末尾再帰) int fib( int x ) { if ( x <= 2 ) return 1 ; else return fib( x-1 ) + fib( x-2 ) ; }
これらの関数の結果について考えるとともに、この計算の処理時間を説明する。 最初のfact(),pyra()については、 x=1の時は、関数呼び出し,x<=1,return といった一定の処理時間を要し、 で表せる。 x>1の時は、関数呼び出し,x<=1,*,x-1,returnの処理に加え、x-1の値で再帰の処理時間がかかる。 このことから、
で表せる。
} 再帰方程式
このような再帰を使って表した式は再帰方程式と呼ばれる。これを代入によって解けば、一般式 が得られる。
fact() や pyra() のような関数は、プログラムの末端に再帰が行われている。(fib()は、再帰の一方が末尾ではない)
このような再帰は、末尾再帰と呼ばれ、関数呼び出しの return を、再帰処理の先頭への goto 文に最適化が可能である。つまり繰り返し処理に書き換えが可能である。このため、ループにすれば局所変数を消費しない。
再帰を含む一般的なプログラム例
ここまでの再帰方程式は、再帰の度にNの値が1減るものばかりであった。もう少し一般的なプログラムで再帰方程式を使って処理時間を考えてみよう。
以下のプログラムを実行したらどんな値になるであろうか?それを踏まえ、処理時間はどのように表現できるであろうか?
int array[ 8 ] = { 3 , 6 , 9 , 1 , 8 , 2 , 4 , 5 , } ; int sum( int a[] , int L , int R ) { // 非末尾再帰 if ( R - L == 1 ) { return a[ L ] ; } else { int M = (L + R) / 2 ; return sum( a , L , M ) + sum( a , M , R ) ; } } int main() { printf( "%d¥n" , sum( array , 0 , 8 ) ) ; return 0 ; }
このプログラムでは、配列の合計を計算しているが、引数の L,R は、合計範囲の 左端・右端を表している。そして、再帰のたびに2つに分割して解いている。
このような、処理を分割し、分割した処理を再帰で計算し、その分割した処理結果を組み合わせて結果を求めるような処理方法を、分割統治法と呼ぶ。
このことから、以下の再帰方程式が得られる。
これを、代入法により解いていくと、
ということで、このプログラムの処理時間は、 で表される。
最後に、再帰方程式の事例として、ハノイの塔の処理時間について説明し、 数学的帰納法での証明を示す。
ハノイの塔
ハノイの塔は、3本の塔にN枚のディスクを積み、ディスクの上により大きいディスクを積まずに、移動させるパズル。
一般解の予想
ハノイの塔の移動回数を とした場合、 少ない枚数での回数の考察から、
… ① ということが予想ができる。
この予想が常に正しいことを証明するために、ハノイの塔の処理を、 最も下のディスク1枚と、その上の(N-1)枚のディスクに分けて考える。
再帰方程式
上記右の図より、N枚の移動をするためには、上に重なるN-1枚を移動させる必要があるので、
… ②
… ③
ということが言える。(ハノイの塔の移動回数の再帰方程式)
ディスクが枚の時、予想が正しいのは明らか①,②。
ディスクが 枚で、予想が正しいと仮定すると、
枚では、
… ③
… ①
となり、 枚でも、予想が正しいことが証明された。 よって数学的帰納法により、1枚以上で予想が常に成り立つことが証明できた。
理解度確認
- 前再帰の「ピラミッドの体積」pyra() を、ループにより計算するプログラムを記述せよ。
- 前講義での2分探索法のプログラムを、再帰によって記述せよ。(以下のプログラムを参考に)
- 再帰のフィボナッチ関数 fib() の処理時間にふさわしい再帰方程式を示せ
int a[ 10 ] = { 7 , 12 , 22 , 34 , 41 , 56 , 62 , 78 , 81 , 98 } ; int find( int array[] , int L , int R , int key ) { // 末尾再帰 // 目的のデータが見つかったら 1,見つからなかったら 0 を返す。 if ( __________ ) { return ____ ; // 見つからなかった } else { int M = _________ ; if ( array[ M ] == key ) return ____ ; else if ( array[ M ] > key ) return find( array , ___ , ___ , key ) ; else return find( _____ , ___ , ___ , ___ ) ; } } int main() { if ( find( a , 0 , 10 , 56 ) ) printf( "みつけた¥n" ) ; }
コンストラクタと複素数クラス
コンストラクタ
プログラミングでは、データの初期化忘れによる間違いもよく発生する。これを防ぐために、C++ のクラスでは、コンストラクタ(構築子)がある。データ構造の初期化専用の関数。
// コンストラクタ #include <stdio.h> #include <string.h> class Person { private: char name[ 20 ] ; int age ; public: void print() { printf( "%s %d¥n" , name , age ) ; } Person() { // コンストラクタ(A) name[0] = '¥0' ; // 空文字列 age = 0 ; } Person( const char str[] , int ag ) { // コンストラクタ(B) strcpy( name , str ) ; age = ag ; } ~Person() { // デストラクタ print() ; // 内容の表示 } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; // (A)で初期化 Person tomoko() ; // (B)で初期化 saitoh.print() ; // "t-saitoh 55" の表示 tomoko.print() ; // " 0" の表示 return 0 ; // この時点で saitoh.~Person() // tomoko.~Person() が自動的に } // 呼び出される。
コンストラクタと反対に、デストラクタは、データが不要となった時に自動的に呼び出される関数。
このクラスの中には、引数無しのコンストラクタと、引数ありのコンストラクタが出てくる。C++では、同じ名前の関数でも引数の数や型に応じて呼出す関数を適切に選んでくれる。(関数のオーバーロード)
デストラクタは、データが不要となった時に自動的に呼び出してくれる関数で、一般的にはC言語でのファイルの fopen() , fclose() のようなものを使う処理で、コンストラクタで fopen() , デストラクタで fclose() を呼出すように使うことが多いだろう。同じように、コンストラクタで malloc() を呼出し、デストラクタで free() を呼出すというのが定番の使い方だろう。
複素数クラスの例
隠蔽化と基本的なオブジェクト指向の練習課題として、複素数クラスをあげる。ここでは、複素数の加算・乗算を例に説明をするので、減算・除算などの処理を記述することで、クラスの扱いに慣れてもらう。
直交座標系
#include <stdio.h> #include <math.h> // 直交座標系の複素数クラス class Complex { private: double re ; // 実部 double im ; // 虚部 public: void print() { printf( "%lf + j%lf¥n" , re , im ) ; } Complex( double r , double i ) { re = r ; im = i ; } Complex() { // デフォルトコンストラクタ re = im = 0.0 ; } void add( Complex z ) { // 加算は、直交座標系だと極めてシンプル re = re + z.re ; im = im + z.im ; } void mul( Complex z ) { // 乗算は、直交座標系だと、ちょっと煩雑 double r = re * z.re - im * z.im ; double i = re * z.im + im * z.re ; re = r ; im = i ; } double get_re() { return re ; } double get_im() { return im ; } double get_abs() { // 絶対値 return sqrt( re*re + im*im ) ; } double get_arg() { // 偏角 return atan2( im , re ) ; } } ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね int main() { // 複素数を作る Complex a( 1.0 , 2.0 ) ; Complex b( 2.0 , 3.0 ) ; // 複素数の計算 a.print() ; a.add( b ) ; a.print() ; a.mul( b ) ; a.print() ; return 0 ; }
極座標系
上記の直交座標系の Complex クラスは、加減算の関数は単純だけど、乗除算の関数を書く時には面倒になってくる。この場合、極座標系でプログラムを書いたほうが判りやすいかもしれない。
// 局座標系の複素数クラス class Complex { private: double r ; // 絶対値 r double th ; // 偏角 θ public: void print() { printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ; } Complex() { r = th = 0.0 ; } // 表面的には、同じ使い方ができるように // 直交座標系でのコンストラクタ Complex( double x , double y ) { r = sqrt( x*x + y*y ) ; th = atan2( y , x ) ; // 象限を考慮したatan() } // 極座標系だと、わかりやすい処理 void mul( Complex z ) { // 極座標系での乗算は r = r * z.r ; // 絶対値の積 th = th + z.th ; // 偏角の和 } // 反対に、加算は面倒な処理になってしまう。 void add( Complex z ) { ; // 自分で考えて } // double get_abs() { return r ; } double get_arg() { return th ; } double get_re() { return r * cos( th ) ; } double get_im() { return r * sin( th ) ; } } ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;
このように、プログラムを開発していると、当初は直交座標系でプログラムを記述していたが、途中で極座標系の方がプログラムが書きやすいという局面となるかもしれない。しかし、オブジェクト指向による隠蔽化を正しく行っていれば、利用者に影響なく「データ構造」や「その手続き(メソッド)」を書換えることも可能となる。
このように、プログラムをさらに良いものとなるべく書換えることは、オブジェクト指向ではリファクタリングと呼ぶ。
正しくクラスを作っていれば、クラス利用者への影響が最小にしながらリファクタリングが可能となる。
メソッドのプロトタイプ宣言
class 構文では、{} の中に、要素の定義や、メソッドの記述を行うと説明してきたが、メソッド内の処理が長い場合もある。
この時に、すべてを {} の中に書こうとすると、全体像が見渡せない。こういう時には、以下のように、メソッドのプロトタイプ宣言と、メソッドの実体の記述を別に記載する。
class Complex { private: double re , im ; public: Complex( double r , double i ) ; // メソッドのプロトタイプ宣言 void print() ; } ; // メソッドの実体 Complex::Complex( double r , double i ) { re = r ; im = i ; } void Complex::print() { printf( "%lf + j %lf" , re , im ) ; }
ゲッター/セッター (経験者向け解説)
それぞれのクラス宣言では、get_re() , get_im() , get_abs() , get_arg() というメソッドを記載した。このように記述しておくと、クラス外で re , im といったメンバを public 指定をせずに、リファクタリングしやすいクラスにすることができる。(re, im を public にしてしまうと、クラス外でオブジェクトへの代入が可能となる。)
PHPなどの private や public の機能のないオブジェクト指向言語では、get_xx() といった要素の参照しかできないメソッドを作ったうえで、クラス外でメンバ参照をしないというマナーを徹底させることで、public 機能の代用し、隠蔽化を徹底させることも多い。
この場合、参照専用の get_xx() と同じように、要素に値を設定するためのメソッド set_xx( 値… ) を作るとプログラムの意味が分かりやすくなる。こういったクラスの参照や代入のメソッドは、getter(ゲッター),setter(セッター)と呼ぶ。
ゲッターやセッターメソッドでは、要素を参照(あるいは代入)するだけといった極めて単純な関数を作ることになる。この場合、関数呼び出しの処理時間が無駄になる。この対処として、C++ には inline 関数機能がある。関数(メソッド)の前に、inline を指定すると、コンパイラは関数呼び出し用の命令を生成せず、それと同じ処理となる命令を埋め込んでくれる。(開いたサブルーチン)
class Complex { private: double re , im ; public: : inline void get_re() { return re ; } } ;
const 指定 (経験者向け解説)
C++ では、間違って値を書き換えるような処理を書けないようにするための、const 指定の機能がある。
void foo( const int x ) { x++ ; // 定数を書き換えることはできない。 printf( "%d\n" , x ) ; } int main() { const double pi = 3.141592 ; // C言語で #define PI 3.141592 と同等 int a = 123 ; foo( a ) ; return 0 ; }
前に説明した、getter メソッドは要素を参照するだけで、オブジェクトの中身が変化しない。逆に言えば、getter のメソッド内にはオブジェクトに副作用のある処理を書いてはいけない。こういった用途に、オブジェクトを変化させないメソッド宣言がある。先の、get_re() は、
class ... { : inline double get_re() const { re = 0 ; // 文法エラー return re ; } } ;
高専ワイヤレスIoT技術実証コンテスト
総務省主催の、地域における若手人材を活用した電波有効利用に資するIoT技術実証の円滑な実施のため、有効策を取りまとめていくことを目的とした事業の一環としてのコンテストが開催されます。
全国の高等専門学校(学生)を対象に、第5世代移動通信システム(5G)の特性を生かした技術及びワイヤレスIoT技術を活用することにより、地域における諸課題について、様々な分野や業種、自治体、地域等を巻き込んだ解決、あるいは新たなサービスの実現に繋がるようなアイデアを公募します。