実数の扱い・レポート-No.1
プログラムの制御構造と実数の取扱いの確認として、以下のレポートを次回講義までに提出せよ。
No.1-1 制御構造
以下の3つ(No.1-1-1,No.1-1-2,No1-1-3)の問題から、No.1-1-「(自分の出席番号 % 3)+1」について、プログラムのフローチャートを描き、その実行順序を20ステップめまで答えよ。
レポートには、
- 元プログラム
- フローチャート
- 実行順序
- 変数の変化がわかる内容
を明記すること。
No.1-1-1
No.1-1-2
switch-case 文は説明していませんが、挙動をよく調べて回答してください。
No.1-1-3
No.1-2 実数の扱い
自分の携帯電話番号(もしくは家の電話番号)の末尾4桁のうち、2桁を整数部、末尾2桁を小数部として、その値を2進数に変換した結果を、計算方法が判るように答えよ。ただし、結果の2進数の小数部は8桁まで答えよ。
例えば、0778628278 ならば、82.78 )10を、xxxxxxx.xxxxxxxx )2 のように答えること。
実数の注意点・回答編
回答がすぐに見えないように、別記事に分ける
数値の精度に注意
// 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 ;
}
このプログラムの問題点は、終了条件が th != PI で書かれている点。
コンピュータの中では、データはすべて2進数で扱い、データを保存する桁数も有限である。
このため、0.0314159265 を 100 回 加えても、桁末尾の誤差のため、3.14159265 != 3.1415926535となってしまう。(ただしこの説明は10進数で説明しているけど、実際は2進数で同じような現象が発生している。)
int型とdouble型での暗黙の型変換
// 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 ;
}
このプログラムの問題点は、th / 180 。これがゼロになる原因。
コンピュータの中では、整数型 int と、実数 double 型では、計算の仕方が異なる。特に、実数型は、小数点の位置や指数を考えながら計算を行う必要があるため、処理に時間がかかる。このため、大量のデータを扱う場合にはコンピュータにとって簡単な計算となるように書く必要がある。
整数型は、小数点以下の値を持たない。このためコンピュータの中では、th = 5 の時、th / 180 を計算すると、5/180 = 0.0277 ではなく、5/180 = 0 で計算を行う(小数点以下は切り捨て)。
C言語の原則: 暗黙の型変換
同じ精度で計算できるのなら同じ精度の型で計算する。
精度が異なる場合は、精度の高い型に変換してから計算する。
int 演算子 int = int
int 演算子 double = double
このようなことが発生するので、y=sin(th…)の行は、以下のように書くべきである。
y = sin( th / 180.0 * 3.1415926535 ) ; // 180.0 は double 型 y = sin( (double)th / 180 * 3.1415926535 ) ; // 型キャストで double 型 y = sin( double( th ) / 180 * 3.1415926535 ) ; // C++の型変換関数 y = sin( (double)th / 180.0 * 3.1415926535 ) ; // 暗黙の型変換を排除
数値の範囲に注意
// 16bit コンピュータの時代に、 // 画面上のマウスとターゲットの距離を計算したかった int distance( int x0 , int y0 , int x1 , int y1 ) { int dx = x1 - x0 ; int dy = y1 - y0 ; return sqrt( dx * dx + dy * dy ) ; }
例えば、このプログラムで (x0,y0)=(0,0) , (x1,y1)=(200,200) 出会った場合、sqrt( 40000 + 40000 ) という計算が出て来る。
ところで、16bit コンピュータにおける int 型は、どうやって覚えているのか?
符号あり整数型
コンピュータの中では、負の数を扱う必要から、2の補数表現が用いられる。
x = 0000,0000,0000,1010(2) = 10(10) ~x = 1111,1111,1111,0101 1の補数 ~x + 1 = 1111,1111,1111,0110 1の補数に+1 ⇒ -10(10) x + ~x = 1111,1111,1111,1111 x + ~x + 1 = 1,0000,0000,0000,0000 ≒ 0 // 16bit目ははみ出るからzero
このため、16bit コンピュータの int 型で扱える数は、
max = 0111,1111,1111,1111(2) = 32767(10) min = 1000,0000,0000,0000(2) = -32768(10)
40000 は、16bit コンピュータでは、扱える範囲を越えている。
ということで、前述のプログラムは、
// 16bit コンピュータなら、long int 型は 32bit int distance( int x0 , int y0 , int x1 , int y1 ) { long int dx = x1 - x0 ; long int dy = y1 - y0 ; return (int)sqrt( dx * dx + dy * dy ) ; } // スピードを気にしないのなら(sqrtがdouble型だし...) double distance( double x0 , double y0 , double x1 , double y1 ) { double dx = x1 - x0 ; double dy = y1 - y0 ; return sqrt( dx * dx + dy * dy ) ; }
メモリを効率よく使うには
メモリ利用の効率
次にメモリの利用効率の話について解説する。
例えば、1クラスの名前のデータを覚えるなら、以下のような宣言が一般的であろう。
#define MEMBER_SIZE 50 #define NAME_LENGTH 20 char name[ MEMBER_SIZE ][ NAME_LENGTH ] ;
しかしながら、クラスに寿限無とか銀魂の「ビチグソ丸」のような名前の人がいたら、20文字では足りない。 また、クラスの人数も、巨大大学の学生全員を覚えたいとい話であれば、 10000人分を用意する必要がある。 ただし、10000人の寿限無ありを考慮して、5Mbyte の配列を準備したのに、与えられたデータ量が100件で終わってしまうなら、その際のメモリの利用効率は極めて低い。
このため、最も簡単な方法は、以下のように巨大な文字配列に先頭から名前を入れていき、 文字ポインタ配列に、各名前の先頭の場所を入れる方式であれば、 途中に寿限無がいたとしても、問題はない。
char array[2000] = "ayuka¥0mitsuki¥0t-saitoh¥0tomoko¥0....." ; char *name[ 50 ] = { array+0 , array+6 , array+14 , array+23 , ... } ;
この方式であれば、2000byte + 4byte(32bitポインタ)×50 のメモリがあれば、 無駄なメモリ空間も必要最低限とすることができる。
参考:
寿限無(文字数:全角103文字)
ビチクソ丸(文字数:全角210文字)
引用Wikipedia
複素数とクラス・隠蔽化の演習
コンストラクタやメソッドの書き方
前回のプログラムを、もう少しC++のオブジェクト指向っぽい書き方を導入してみる。この際に、分かりやすく記述するため、行数が長くなってきた時のための処理を考慮して、記述してみる。
#include <stdio.h> #include <math.h> class Complex { private: double re ; double im ; public: // 絶対座標系でも極座標系でも、実部・虚部・絶対値・偏角を扱うため inline double get_re() const { return re ; } inline double get_im() const { return im ; } inline double get_r() const { return sqrt( re*re + im*im ) ; } inline double get_th() const { return atan2( im , re ) ; } // 極座標系なら、以下のような getterメソッド を書いておけば、 // 座標系を気にせず処理がかける。 // inline double get_re() const { return r * cos( th ) ; } // inline double get_r() const { return r ; } // inline は、開いたサブルーチンで実装してくれるので処理速度が遅くならない // foo() const { ... } は、オブジェクトに副作用が無いことを示す。 // コンストラクタのメンバー変数の初期化の書き方 // Complex( ... ) // : 要素1( 値1 ) , 要素2( 値2 ) ... // { その他の初期化 // } Complex() // コンストラクタ : re( 0.0 ) , im( 0.0 ) {} Complex( double r , double i ) // コンストラクタ : re( r ) , im( i ) {} // メソッドのプロトタイプ宣言 // メソッドの中身が長くなる場合は、 // 名前と引数だけを宣言しておく void print() ; void add( Complex ) ; void mul( Complex ) ; } ; // ←しつこいけどセミコロン忘れないでね。 // クラス宣言の外にメソッドの定義を書く // メソッドの中身の記述 「::」をクラス限定子と呼ぶ void Complex::print() { printf( "%lf + j%lf¥n" , get_re() , get_im() ) ; } void Complex::add( Complex z ) { re = re + z.re ; im = im + z.im ; } void Complex::mul( Complex z ) { double r = re * z.re - im * z.im ; double i = re * z.im + im * z.re ; re = r ; im = i ; } int main() { Complex a( 1.0 , 2.0 ) ; Complex b( 2.0 , 4.0 ) ; a.add( b ) ; a.print() ; return 0 ; }
参考資料
レポート1(複素数の加減乗除)
授業中に示した上記のプログラムをベースに、 記載されていない減算・除算のプログラムを作成し、レポートを作成する。 レポートには、下記のものを記載すること。
- プログラムリスト
- プログラムへの説明
- 動作確認の結果
- プログラムより理解できること。
実際にプログラムを書いてみて分かった問題点など…
マージソートのオーダー
マージソートの分析
マージソートは、与えられたデータを2分割し、 その2つの山をそれぞれマージソートを行う。 この結果の2つの山の頂上から、大きい方を取り出す…という処理を繰り返すことで、 ソートを行う。
このことから、再帰方程式は、以下のようになる。
この再帰方程式を、N=1,2,4,8…と代入を繰り返していくと、 最終的に処理時間のオーダが、 となる。
:
よって、
選択法とクイックソートの処理時間の比較
データ数 N = 20 件でソート処理の時間を計測したら、選択法で 10msec 、クイックソートで 20msec であった。
- データ件数 N= 100 件では、選択法,クイックソートは、それぞれどの程度の時間がかかるか答えよ。
- データ件数何件以上なら、クイックソートの方が高速になるか答えよ。
設問2 は、通常の関数電卓では求まらないので、数値的に方程式を解く機能を持った電卓が必要。
xenial の snapd いるの?
以前から発生していた、Ubuntu のサーバで snapd がうまくインストールできないトラブル。 install しようとすると古いバージョンを削除するときの prerm script でエラーとかいうメッセージがでるので、/var/lib/dpkg/info/snapd.prerm を編集して、エラーの出る処理の前に exit 0 を入れたりしながら、無理やり削除して、インストールを試みるけど、こんどは、postinst でエラー。
原因がつかめずに、四苦八苦していたけど、「バージョンアップして trusty を使っているなら、/etc/apt/sources.list などの中にある xenial 関係を消せ」という記事を見つける。
まさに、xenial を upgrade した trusty だったので、/etc/apt/ を探すと、php 関係のパッケージで、
deb http://ppa.launchpad.net/ondrej/php/ubuntu xenial main
というのが見つかったので、早々に消してみた。その後、”aptitude update ; aptitude safe-upgrade” を実行したけど、特にパッケージが消されたりもしなかったし、これで OK かな。
コンストラクタと複素数クラスと隠蔽化
コンストラクタ
プログラミングでは、データの初期化忘れによる間違いもよく発生する。これを防ぐために、C++ のクラスでは、コンストラクタ(構築子)がある。データ構造の初期化専用の関数。
// コンストラクタ #include <stdio.h> #include <string.h> class Person { private: char name[ 20 ] ; int phone ; public: void print() { printf( "%s %d¥n" , name , phone ) ; } Person() { // コンストラクタ(A) name[0] = '¥0' ; // 空文字列 phone = 0 ; } Person( const char str[] , int tel ) { // コンストラクタ(B) strcpy( name , str ) ; phone = tel ; } ~Person() { // デストラクタ print() ; // 内容の表示 } } ; int main() { Person saitoh( "t-saitoh" , 621111 ) ; // (A)で初期化 Person tomoko() ; // (B)で初期化 saitoh.print() ; // "t-saitoh 621111" の表示 tomoko.print() ; // " 0" の表示 return 0 ; // この時点で saitoh.~Person() // tomoko.~Person() が自動的に } // 呼び出される。
コンストラクタと反対に、デストラクタは、データが不要となった時に自動的に呼び出される関数。
複素数クラスの例
隠蔽化と基本的なオブジェクト指向の練習課題として、複素数クラスをあげる。ここでは、複素数の加算・乗算を例に説明をするので、減算・除算などの処理を記述することで、クラスの扱いに慣れてもらう。
直交座標系
#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 ;
}
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
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() ;
}
極座標系
上記の直交座標系の 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 add( Complex z ) {
; // 自分で考えて
}
void mul( Complex z ) { // 極座標系での乗算は
r = r * z.r ; // 絶対値の積
th = th + z.th ; // 偏角の和
}
} ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;
このように、プログラムを開発していると、当初は直交座標系でプログラムを記述していたが、途中で極座標系の方がプログラムが書きやすいという局面となるかもしれない。しかし、オブジェクト指向による隠蔽化を正しく行っていれば、利用者に影響なく「データ構造」や「その手続き(メソッド)」を書換えることも可能となる。
このように、プログラムをさらに良いものとなるべく書換えることは、オブジェクト指向ではリファクタリングと呼ぶ。
再帰処理の処理時間
再帰関数と再帰方程式
再帰関数は、自分自身の処理の中に「問題を小さくした」自分自身の呼び出しを含む関数。プログラムには問題が最小となった時の処理があることで、再帰の繰り返しが止まる。
// 階乗 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 文に最適化が可能である。つまり繰り返し処理に書き換えが可能である。このため、ループにすれば局所変数を消費しない。
最後に、再帰方程式の事例として、ハノイの塔の処理時間について説明し、 数学的帰納法での証明を示す。
ハノイの塔
ハノイの塔は、3本の塔にN枚のディスクを積み、ディスクの上により大きいディスクを積まずに、移動させるパズル。
一般解の予想
ハノイの塔の移動回数を とした場合、 少ない枚数での回数の考察から、
ということが予想ができる。
この予想が常に正しいことを証明するために、ハノイの塔の処理を、 最も下のディスク1枚と、その上の(N-1)枚のディスクに分けて考える。
再帰方程式
これより、
ということが言える。
ディスクが枚の時、予想が正しいのは明らか。
ディスクが 枚で、予想が正しいと仮定すると、
枚では、
となり、 枚でも、予想が正しいことが証明された。 よって数学的帰納法により、1枚以上で予想が常に成り立つことが証明できた。
再帰を含む一般的なプログラム例
ここまでの再帰方程式は、再帰の度に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 ; } // s(0,8) ← sum(a,0,8) // / \ // / \ // s(0,4) s(4,8) // / \ / \ // s(0,2) s(2,4) s(4,6) s(6,8) // / \ / \ / \ / \ // s(0,1) s(1,2) s(2,3) s(3,4) s(4,5) s(5,6) s(6,7) s(7,8)
このプログラムでは、配列の合計を計算しているが、引数の L,R は、合計範囲の 左端・右端を表している。そして、再帰のたびに2つに分割して解いている。
このような、処理を分割し、分割した処理を再帰で計算し、その分割した処理結果を組み合わせて結果を求めるような処理方法を、分割統治法と呼ぶ。
このことから、以下の再帰方程式が得られる。
これを、代入法により解いていくと、
ということで、このプログラムの処理時間は、 で表される。
創造工学演習・予備実験(SQLとPHP)
先週のPHPの予備実験に引き続き、データベースの説明とそのプログラムで演習を行う。
<?php // データの保存先 $dbfile = "/home/guests/guest0/public_data/tsaitoh.db" ; if ( !file_exists( $dbfile ) ) { // データベースの宣言と初期化 $db = new SQLite3( $dbfile ) ; $cmd = "create table nametel ( name char( 20 ) , phone char( 20 ) ) ;" ."insert into nametel ( name , phone ) values ( 't-saitoh' , '090-1111-1111' ) ;" ."insert into nametel ( name , phone ) values ( 'tomoko' , '090-2222-2222' ) ;" ."insert into nametel ( name , phone ) values ( 'mitsuki' , '090-3333-3333' ) ;" ."insert into nametel ( name , phone ) values ( 'ayuka' , '090-4444-4444' ) ;" ; if ( $db->exec( $cmd ) ) { print "Success.\n" ; } else { print "Fail\n" ; } $db = null ; } ?> <html> <head> <title></title> </head> <body> <?php // 警告表示の抑止 ini_set( 'error_reporting' , E_WARNING ) ; // データベース処理(検索処理) $db = new SQLite3( $dbfile ) ; $id = $_REQUEST[ "name" ] ; if ( $db !== FALSE && $id != "" ) { // SQL命令の生成(インジェクションの危険性指摘は別述) $cmd = "select * from nametel where name='$id' ;" ; print $cmd ; // 問い合わせの実行 $result = $db->query( $cmd ) ; print "<table border='1'>\n" ; print "<tr><td>name</td><td>phone</td></tr>" ; while( ($row = $result->fetchArray( SQLITE3_BOTH )) !== FALSE ) { print "<tr>" ."<td>".$row[0]."</td>" ."<td>".$row[1]."</td>" ."</tr>\n" ; } print "</table>\n" ; } ?> <form method="post" action="sql.php"> <input type="text" name="name" /> <input type="submit" value="exec" /> </form> <?php // データベース登録処理 $newname = $_REQUEST[ "newname" ] ; $newphone = $_REQUEST[ "newphone" ] ; if ( $newname != "" && $newphone != "" ) { // データ登録用SQL $cmd = "insert into nametel ( name , phone ) values ( '$newname' , '$newphone' ) ;" ; // データ登録 $db->exec( $cmd ) ; print "$newname さんの電話番号 $newphone を登録しました" ; } ?> <form method="post" action="sql.php"> <input type="text" name="newname" /> <input type="text" name="newphone" /> <input type="submit" value="exec" /> </form> </body> </html>
実数の注意点
C言語でプログラムを作成していて、簡単な数値計算のプログラムでも動かないと悩んだことはないだろうか?解らなくて友達のプログラムを真似したら動いたけど、なぜ自分のプログラムは動かなかったのか深く考えたことはあるだろうか?
まずは動く例
以下のプログラムは、見れば判るけど、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 ;」に書き換えたら動き出す。
では何が悪かったのか…
回答編
数値の範囲に注意
前節の回答編で示したが、数値の扱える値の範囲に注意すべきである。
私自身が自分で書いたプログラムで悩んだ例を以下に示す。
// 16bit コンピュータの時代に、 // 画面上のマウスとターゲットの距離を計算したかった int distance( int x0 , int y0 , int x1 , int y1 ) { int dx = x1 - x0 ; int dy = y1 - y0 ; return sqrt( dx * dx + dy * dy ) ; }
このプログラムを実行した時、通常はうまく動くのだけど、時々「sqrt は、負の値では計算できません」というエラーを表示する。
なぜだろうか?
回答編
2進数への変換(補助資料)
10進数で 123.45 は、1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 を意味する。(あたりまえ)
2進数に変換する場合、整数部と小数部に分けて考えると、
10進数なら、それぞれを 10 で割る、10 をかけると 123 / 10 = 12.3 小数部に3 が出てくる。 0.45 * 10 = 4.5 整数部に4 が出てくる。 2進数なら、それぞれを 2 で割る、2をかけると...
123.45 を 2進数に変換 2)123 )10 = 1111011 )2  ̄ ̄ ̄ ̄ 2) 61 ... 1 次々と2で割って、余りを求める  ̄ ̄ ̄ ̄ 2) 30 ... 1  ̄ ̄ ̄ ̄ 2) 15 ... 0  ̄ ̄ ̄ ̄ 2) 7 ... 1  ̄ ̄ ̄ ̄ 2) 3 ... 1  ̄ ̄ ̄ ̄ 2) 1 ... 1  ̄ ̄ ̄ ̄ 0 ... 1 ✕2)0.45 )10 = 0.01110011001100...)2  ̄ ̄ ̄ ̄ ̄ ✕2)0.9 ... 0 次々と2倍して、整数部を求める  ̄ ̄ ̄ ̄ ̄ ✕2)1.8 ... 1 ※  ̄ ̄ ̄ ̄ ̄ ✕2)1.6 ... 1  ̄ ̄ ̄ ̄ ̄ ✕2)1.2 ... 1  ̄ ̄ ̄ ̄ ̄ ✕2)0.4 ... 0  ̄ ̄ ̄ ̄ ̄ ✕2)0.8 ... 0 ※の繰り返し  ̄ ̄ ̄ ̄ ̄ ✕2)1.6 ... 1  ̄ ̄ ̄ ̄ ̄ : よって、123.45 )10 = 1111011 .011100110011...)2