/*
* 20200522-02-QQQQ.c
* 引数の文字列の長さに対するフィボナッチ数だけの文字 "*" を出力する関数
*
* 0 ( n = 0 の時 )
* fib(n) = { 1 ( n = 1 の時 )
* fib(n-1)+fib(n-2) ( その他 [n > 1 の時] )
*
fib(0) = 0
fib(1) = 1
fib(2) = fib(2-1) + fib(2-2)
= fib(1) + fib(0)
= 1 + 0
= 1
fib(3) = fib(3-1)+fib(3-2)
= fib(2) + fib(1)
= 1 + 1
= 2
fib(0), fib(1), fib(2), ...
0, 1, 1, 2, 3, 5, 8, 13, ...
[考え方のポイント]
フィボナッチ関数の定義にそって、
C 言語の関数定義を行えばよい
"" ( "" の時 )
f("123") = { "*" ( "*" <-> "*" + 1 = "" の時 )
f("123"+1)+fib("123"+2) ( その他 ["", "*" でもない時] )
*/
#include <stdio.h>
#include <string.h> /* strcmp を利用するために必要 */
/*
* void c_fibnacci ( char *n )
* n : 長さで、整数値を表す文字列
*/
void c_fibnacci ( char *n ) {
if ( !strcmp ( n, "" ) ) { /* n の長さが 0 */
/* 何もしなくて良い (関数は終了) */
} else if ( !strcmp ( n + 1, "" ) ) { /* n の長さが 1 */
printf ( "*" );
} else { /* その他 ( n の長さが 1 より長い ) */
/* 再帰呼出しを二度行う */
/* 一度目は、1 だけ長さを減らす */
c_fibnacci ( n + 1 );
/* ニ度目は、2 だけ長さを減らす */
c_fibnacci ( n + 2 );
}
}
/*
* main
*/
int main ( void ) {
/* fib(4) = 3 回だけ「*」を出す */
c_fibnacci ( "****" );
printf ( "\n" );
/* fib(7) = 13 回だけ「*」を出す */
/*
** この部分を完成させなさい
*/
printf ( "\n" );
return 0;
}
#include <stdio.h>
/*
関数定義と関数呼び出し
関数呼び出しによって、
関数定義の本体の部分が呼び出される
*/
void f(void) { /* 関数定義 */
printf ( "Hello, World\n" ); /* 他の関数を呼び出す */
}
void g(void) {
f();
f();
}
int main(void) {
f(); /* 関数呼び出し */
/* 関数の定義部分を実行しなさい */
/*
f()
関数 f の定義が探される
f()
=> printf ( "Hello, World\n" );
printf ( "Hello, World\n" );
*/
g();
/*
g() =>
f()
=> printf
f()
=> printf
g() は結局
printf 二回に置き換わる
*/
return 0;
}
/*
main()
+- f()
+- printf
+- g()
+- f()
+- printf
+- f()
+- printf
main()
+- printf
+- printf
+- printf
*/
#include <stdio.h>
void f(void) {
printf ( "Hello, World\n" );
}
void g(void) {
f();
f();
}
int main(void) {
f();
g();
return 0;
}
/*
main()
+- printf
+- printf
+- printf
=>
main()
+- f()
+- printf
+- f()
+- printf
+- f()
+- printf
=>
main()
+- f()
+- printf
+- g()
+- f()
+- printf
+- f()
+- printf
プログラムを作成する
基本命令を並べるだけ(順接の仕組み)
基本命令を組み合わせて関数を作り
関数呼び出しの組み合わせでもよい
もし、
目的とする関数が
いつも同じ振る舞いであれば、
単に基本命令を「順接」だけでよい
条件によって、異なる振る舞いをする
if 構文や再帰を利用必要がある
プログラムを一度に作るのは大変
例 : 100 以上、プリントする
=> 100 行の命令を書けばよい
<= 大変
関数を定義する
=> プログラムの記述を減らす事ができる
=> 大きな家をいきなり作るのは大変
まず、土台を作る
つぎに、柱をたて
屋根をあげたり、かべを作り
窓や、ドアを備え付ける
内装をしっかり
全体を、いくつかの部分に分割し、
分割したもの(もとより簡単になっている)を実現する
もし、分割したものも複雑なら、これをさらに分割する
問題を解決するために、
その問題をより簡単な問題に分割してから解くという考え方
分割統治法
関数を定義して利用する
プログラムを作成するときに「分割統治法」を適用している
*/
#include <stdio.h>
/*
再帰を無制限に利用すると、
無限ループになる
*/
void f(void) {
printf ( "Hello, World\n" );
f(); /* 再帰呼び出し */
}
int main(void) {
f();
return 0;
}
/*
main
+- f
+- printf
+- f
+- printf
+- f
+- printf
+- f
....
=>
main
+- printf
+- printf
+- printf
.... 無限に続く
*/
#include <stdio.h>
#include <string.h>
/*
再帰を無制限に利用すると、
無限ループになる
*/
void f(char *n) {
if ( !strcmp ( n, "" ) ) {
printf ( "End\n" ); /* 再帰呼び出しをしない場合 */
} else {
printf ( n );
f( n ); /* 再帰呼び出しをする場合 */
}
}
int main(void) {
f( "abc\n" );
return 0;
}
/*
main
+- f
+- printf
+- f
+- printf
+- f
+- printf
+- f
....
=>
main
+- printf
+- printf
+- printf
.... 無限に続く
*/
#include <stdio.h>
#include <string.h>
/*
再帰を無制限に利用すると、
無限ループになる
if 構文と、引数の処理の組み合わせで、
お行儀のよい ( 無限ループしない ) 再帰的定義関数が作れる
*/
void f(char *n) {
if ( !strcmp ( n, "" ) ) { /* 引数の文字列の長さが 0 */
printf ( "End\n" ); /* 再帰呼び出しをしない場合 */
} else {
printf ( n );
f( n + 1 ); /* 再帰呼び出しをする場合 */
/* 引数の文字列の長さが 1 減る */
}
}
int main(void) {
f( "abc\n" );
return 0;
}
/*
main
+- f ( "abc\n" );
+- printf ( "abc\n" );
+- f ( "bc\n" );
+- printf ( "bc\n" );
+- f ( "c\n" );
+- printf ( "c\n" );
+- f ( "\n" );
+- printf ( "\n" );
+- f ( "" );
+- printf ( "End\n" );
=>
main
+- printf ( "abc\n" );
+- printf ( "bc\n" );
+- printf ( "c\n" );
+- printf ( "\n" );
+- printf ( "End\n" );
*/
#include <stdio.h>
#include <string.h>
/*
繰り返しのイデオム(成形句:パータン)
関数 f() を、引数 n (文字列) の長さだけ繰り返す
関数 fn(n) は、次のような、再帰的定義関数として定義可能
void fn(char *n) {
if ( !strcmp ( n, "" ) ) {
} else {
f();
fn ( n + 1 );
}
}
*/
void f(void) {
printf ( "こんにちは\n" );
}
void fn(char *n) {
if ( !strcmp ( n, "" ) ) { /* 引数の文字列の長さが 0 */
/* なにもしない */
} else {
f(); /* 繰り返したい関数呼び出し */
fn( n + 1 ); /* 再帰呼び出し */
/* 引数の長さを一つ減らす */
}
}
int main(void) {
fn( "abc\n" );
return 0;
}
/*
main
+- f ( "abc\n" );
+- printf ( "abc\n" );
+- f ( "bc\n" );
+- printf ( "bc\n" );
+- f ( "c\n" );
+- printf ( "c\n" );
+- f ( "\n" );
+- printf ( "\n" );
+- f ( "" );
+- printf ( "End\n" );
=>
main
+- printf ( "abc\n" );
+- printf ( "bc\n" );
+- printf ( "c\n" );
+- printf ( "\n" );
+- printf ( "End\n" );
*/
前回(2020/05/22)の内容
再帰的に関数を定義する
関数定義の中で、自分自身を呼び出す事
f() {
f();
f();
}
f() =>
f()
=> f()
=> f()
f()
=> f()
=> f()
... f() が無限に増殖して、置き換えが終わらない
!!! 再帰呼び出しを無制限に行うと、無限の展開になり
!!! 意味が定まらない
!!! <= 再帰呼び出しの「マナー」が重要
再帰呼び出しのマナー
再帰呼び出しをする場合としない場合に分ける
=> if 構文が必須
引数 ( if 構文の中の条件式で利用する )
再帰的に定義されている関数を呼び出すとき
引数の値が同じであれば、振る舞いも同じになる
再帰呼びだしをするときに、引数の値が変換する必要がある
最終的に、その変換を繰り返すと、再帰しない条件を満たすようになる必要がある
例:
終了する条件(再帰しない条件)
=> 引数の文字列の長さが 0
=> 引数が空文字列("")になる
引数の変換規則
=> 文字列の長さを一つ減らす ( +1 )
!! 「文字列の長さ」は、0 以上
=>
任意の長さの文字列を与えても
その長さを減らしてゆけば、
必ず 0 になる
事が保証されている
ので、
引数の文字列の長さが 0 となり、
再帰呼び出しをしない場合が必ず起きる
事が、保証される。
一般的に
引数が有限の値をもち、
その値を減らす事が可能で
最低値を終了(再帰呼び出しをしない)条件とすれば、
無限ループをしない再帰的定義関数が作れる
<<現状>>
終了条件は、「引数の文字列が空である」
引数の操作は、「引数の文字列の長さを 1 減らす(+1 する)」
の一択
# ほかのパターンもあるが、これだけは、まず覚える
<<復習>>
関数定義と関数呼び出しの関係
=> 「関数呼び出し」の表現が
「関数定義の本体」の表現に置き換わる
関数定義の中で、
他の関数呼び出しが可能
=> 「置き換え」が何度も行われる
複数の関数定義と関数呼び出しを与わせて、
main 関数から、関数を呼び出すと
「置き換え」が何度もおきて、
最終的に、既存の関数(printf)呼び出しに置き換わる
up( "abc")
=> .. =>
printf ( "\n" );
printf ( "c\n" )
printf ( "bc\n" );
printf ( "abc\n" );
=>
c
bc
abc
何度か、置き換えが行われて、最終的に、
既存の命令(関数呼び出し)の列に代わる
# 暗黙の内に
# 「置き換えがいつかは終わる事」
# を仮定
# <= 置き換えが終わらなければ、どの関数をどれだけ実行してよいかわからない
# プログラムの意味(どの関数を実行するか?)が定まらない
# もし、関数の定義をするときに、
# 既存の関数 ( 基本の関数か、定義済の関数 )
# しか、本体で呼び出さなければ、
# 置き換えが有限である事はすぐにわかる
# # 置き換えに関する構造帰納法で示すことができる
プログラムを作成する
C 言語では、main 関数を定義する
基本命令の実行 ( printf しか知らない )
やりたいこと
基本命令の列として考える
基本命令をいくつか、組み合わせて関数を作り
最終的に main 関数が、組み合わせの組み合わせとして定義さればよい
「分割統治法」を利用して、
簡単な関数に分割してゆく
!! 分割をする事により、問題が簡単になる
分割 => それを統合する
<= 統合ができるように分割する
統合の仕方に従って分割すると便利
三つの基本構文
命令文を「組み合せ」て、新しい命令を作る仕組み
=> 命令の統合の仕方
順接 : 命令を並べる => 命令を、並べた順に実行する
条件分岐 : if 構文 => 条件によって二つの命令の何方か一方を実行する
繰返し : 再帰 => 条件が成立するまで命令を繰り返し実行する
!! 統合の仕方を逆を考える事によって、
!! 分割ができる
万能性
任意のプログラムは、三つの基本構文を (有限回) 適用して作れる
# 任意の問題が、この三つの統合の逆の分割によって、
# 最終的に、簡単に解ける問題に分割できる
# 事が、保証されている
[2020/05/29]
再帰的定義と数学的帰納法
再帰的定義
関数 f() を定義する本体に f() 自身を含める仕組み
「繰返し」を表現
最低一つの引数があり、
引数の値が「小く」なり、
「最小」で終了になるように定義
例 : 文字列を引数して、その「文字列の長さ」に着目
そうしないと、
「無限ループ」になってしまう
数学的帰納法
自然数に関する命題 P(n) (n は任意の自然数)
例: P(n) : n には、必ず n より大きい自然数が存在する
任意の自然数に関する命題の証明をどのように行うか ?
P(0), P(1), P(2), ... <= 無限個の命題を証明する必要がある
=> すべてを確かめる事はできない
=> 数学的帰納法
有限な証明(記述)で、無限個の命題の証明(とみなす)仕組み
# 数学的帰納法の原理
# 原理:これこれの形をした証明は、こうゆう証明とみなす
# 背理法 : 命題 P()の否定を仮定して、矛盾がみちびければ、
# P() が成立するとみなす原理
#
# 「普通の」式の変形の証明
# a^2+2ab+b^2 = (a+b)^2
# a^2+2ab+b^2 = a^2+ab+ab+b^2
# = a(a+b)+(a+b)b
# = a(a+b)+ b(a+b)
# = (a+b)(a+b)
# = (a+b)^2
任意の自然数 n に関する命題 P(n) を証明するための「枠組み」
# 帰納法に基づく証明
次の「(証明)手順」で、
「P(n) が、任意の自然数 n で成立する事」を示す
[Start Step]
「P(1) が成立」する事を示す
[Next Step]
「P(k) が成立」すれば、「P(k+1)が成立」する事を示す
上記の 2 つと
「数学的帰納法の原理」から、
「P(n) の成立」を示す
例
P(n) : n より大きな自然数が存在する
n=0)
1 は、0 より大きいので、0 より大きい自然数が存在する
n=k の場合を仮定して n=k+1 の場合を示す)
n=k の時に命題 P(n) = P(k) が成立するので、
k より大きな自然数 k' が存在する
k < k'
よって、
k + 1 < k' + 1
となる
ここで、k'+1 は自然数なので
k + 1 より大きな自然数 (k'+1)が存在する
P(k+1)
が成立する。
なので、
P(k)を仮定してP(k+1)を示す
事ができた。
n=0 の時成立し、n=k を仮定して n=k+1 を示す事ができたので、
数学的帰納法(の原理)により、
命題 P(n) は任意の自然数 n について成立する。
再帰的定義と数学的帰納法の関係
再帰的定義された関数の性質は、
数学的帰納法を適用して、証明しやすい
α (n=0 の時)
f(n) = {
f(n-1) の式 (n>0 の時:再帰的定義)
P(f(n)) : P は、関数 f の性質
任意の自然数 n に対してい
P(f(n))
を示したい場合
帰納法を利用して、
[Start Step] P(f(0)) = P(α) を示す
[Next Step] P(f(k+1)) = P(f(n-1) の式)を示す
をそれぞれ利用するだけ
再帰的定義された関数の性質は、数学的帰納法で証明しやすい
再帰的定義には、[Start Step] と [Next Step] がそのまま書いてある
数学的帰納法の証明をする時に、その記述がそのまま利用できる
P(f(n)) が 任意の自然数 n で成立する
f(n) の性質が P である
P(y) となる y を計算する y=f(n) を決めている
=> ある条件 P を示す数を計算する関数 f を定めている
数学的帰納法の証明があると、
その証明記述から、その性質をもったものを計算する関数が、
再帰的定義を利用して、定義可能
帰納法の証明の
[Start Step] と [Next Step]
を抜き出せばよい
[一進数]
n 進数 ( n > 1 )
自然数の表現 ( 大学の数学では、自然数は 0 から始める事が多い )
0,1,2,..,9,10,11,..,99,100,..
零、一、..九、十、...、百、千
I,II,III,IV,V,..X,..
n 個のものが集ったら桁上げをする仕組み
1,2,..,9 の次は 10 ( 十 : 1 と 0 )
桁(数字の位置)によって、意味を変えている
0 という数字(何もない)が必要なのは、
桁を示すために、必要
!! そろばんは、0 がない
!! 100 => 1
!! 10 => 1
!! 筆算では、桁の位置を表現するために 0 という数字が必要
例 1 : 十進数 => 普段使っている数の仕組み
例 2 : ニ進数 => コンピュータの世界で利用される数の仕組み
0, 1, 10, 11, 100, ..
0 => 何もない / 1 => 数(一つある)
0 ? n-1 を表す「n 個の『数字』」を利用する
最低 0 と 1 が必要なので、二進法は、数字が最小の表現(桁)
それ以外は「桁」を利用して表現
一進数 ( n 進数のノリではあるが、全然異る原理 )
数字の 1 のみを利用する
1 の個数で数を表現する
十進数 ニ進数 一進数
0 0 無
1 1 1
2 10 11
3 11 111
4 100 1111
課題プログラム内の「/*名前:ここ*/」の部分を書き換え「/*この部分を完成させなさい*/」の部分にプログラムを追加して、プログラムを完成させます。
Download : 20200529-01.c
/*
* 20200529-01-QQQQ.c
* 一進数のフィボナッチ数
*
* 0 (n=0)
* fib(n) = { 1 (n=1)
* fib(n-1) + fib(n-2) (n>1)
*/
#include <stdio.h>
#include <string.h>
/*
* 一進数のフィボナッチ数列
*/
void fib ( char *n ) {
if ( !strcmp ( n, "" ) ) { /* n = 0 */
/* 何もしなくてよい */ /* fib(0) = 0 */
} else if ( !strcmp ( n + 1, "" ) ) { /* n = 1 */
printf ( "0" ); /* fib(1) = 1 */
} else { /* n > 0 */
fib ( n + 1 ); /* fib(n-1) + fib(n-2) */
/*
** この部分を完成させなさい
*/
}
}
int main ( void ) {
/* fib 3 */
fib ( "000" );
printf ( "\n" );
/* fib 10 */
/*
** この部分を完成させなさい
*/
printf ( "\n" );
return 0;
}
$ ./20200529-01-QQQQ.exe 00 0000000000000000000000000000000000000000000000000000000 $
Download : 20200529-02.c
/*
* 20200529-02-QQQQ.c
* 一進数の階乗
*/
#include <string.h>
/*
* n_time_fac ( n, n1 )
* fac ( n1 ) を n 回行う
* => n * fac(n1)
*/
void n_time_fac ( char *n, char *n1 ) {
if ( !strcmp ( n, "" ) ) { /* n = 0 */
/* 何もしない */
} else { /* n > 0 */
/*
** この部分を完成させなさい
*/
n_time_fac ( n + 1, n1 ); /* n * fac ( n-1 ) */
}
}
/*
* 一進数の階乗
*/
void fac ( char *n ) {
if ( !strcmp ( n, "" ) ) { /* n = 0 */
printf ( "0" ); /* fac(0) = 1 */
} else { /* n > 0 */
n_time_fac ( n, n + 1 );
}
}
/*
* main
*/
int main ( void ) {
/* fac(3) */
fac ( "000" );
printf ( "\n" );
/* fac(5) */
/*
** この部分を完成させなさい
*/
printf ( "\n" );
return 0;
}
$ ./20200529-02-QQQQ.exe 000000 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 $