• Tidak ada hasil yang ditemukan

計算機言語 第 14 回 ポインタへのポインタ ポインタの配列

N/A
N/A
Protected

Academic year: 2024

Membagikan "計算機言語 第 14 回 ポインタへのポインタ ポインタの配列"

Copied!
8
0
0

Teks penuh

(1)

計算機言語 I 14

ポインタへのポインタ , ポインタの配列

この資料: http://www.math.u-ryukyu.ac.jp/~suga/gengo/2022/14.pdf

レポートへのツッコミ

データの入れ替えを利用したバブルソートで,seiseki_t型の40個の配列を整列させるときに,「構造体 は,代入によるコピーが可能」ということを使うとソースはすっきりとします.

次のページのソースにあるように,関数swap(seiseki_t *, seiseki_t *)を用意しておくと, int 型の 値を入れ替える時と同様に,

swap(&x, &y);

で, seiseki_t型の変数, x, y の値が入れ替わります. (次のページのプログラムでは, ファイル名は, seiseki.dataとしてあります.)

ただし,構造体のコピーは, CPUの動作としては, 各メンバ単位でひとつひとつコピーしますので,動作が 遅くなります. したがって,上のようなプログラムは, できる限り書かないようにしますし,そのための工夫の 方法を述べるのが, 今回の授業の目的のひとつです.

注意: 以前,バブルソートを解説した際に述べましたが,バブルソートは対称群の性質が見えるという意味で数 学的には面白いのですが, 整列アルゴリズムとは効率が悪いことがわかっています. 実際にプログラムでバブ ルソートを利用することはありません.

(2)

#include <stdio.h>

typedef struct seiseki{

char name[10];

int score;

} seiseki_t;

void swap(seiseki_t *x, seiseki_t *y) {

seiseki_t temp;

temp = *x;

*x = *y;

*y = temp;

}

int main() {

FILE *fp;

int i, j;

seiseki_t data[40];

fp=fopen("seiseki.data","r");/* エラー処理は略 */

for (i=0; i< 40; i++){/* データ読み込み */

fscanf(fp, "%s%d", data[i].name, &(data[i].score));

} /* fclose は省略 */

for (j=39; j>0; j--){ /* バブルソート */

for (i=0; i<j; i++){

if (data[i].score<data[i+1].score){

swap(&(data[i]),&(data[i+1]));

} } }

for (i=0; i< 40; i++){

printf("%s %3d\n", data[i].name, data[i].score);

}

return 0;

}

(3)

ポインタへのポインタ

「ポインタ型変数」も識別子である. という認識は重要です. 識別子なら, C の言語規則から, プログラム実 行中にはそのアドレスが定まるのです.

表題のポインタへのポインタとは,ポインタ型変数を指すポインタのことです. 当然,この値を変数値として 持つ変数を使うことができ,ポインタ型変数のアドレスを保持する変数が考えられます. これは,現実のプログ ラムでは,「ポインタ型変数の配列」の先頭要素を指すポインタとしてよく利用されます. 実際, 教科書の例で は,行列を2重配列ではなく,「行ベクトルの並び」として実現する方法が書かれています.

このような変数を考える理由は,主に次の 2つです.

• 2重配列のような変数を動的に確保するのに都合が良い.

• ある種のプログラムの実行速度を改善する.

教科書には,上の例が書かれています. 例えば,m×n行列の計算をするプログラムを書くとして,「m, n(行 列のサイズ)が実行時にしか決まらない」とします. これをプログラムする一つの方法は,m×nの一次元配

列をmalloc()で確保して,それを行列のように扱うプログラムを書くことです. しかし, これはプログラム

が複雑になるとともに,下にある実行速度に影響するプログラムになったりします.

そこで,教科書にあるように,n次元の行ベクトルがm個並んだものとして, 行列を定めます. すなわち,ポ インタ型変数の配列 p[m]を準備し, p[i]は, 第i+ 1行の行ベクトルを指すポインタとするのです(配列の 場所を示すiは0から始まることに注意). この時,識別子pは配列変数p[m]の先頭要素p[0]へのポインタ を保持しますから, ポインタへのポインタとなるわけです.

このようにして書かれたのが, 教科書,プログラム 10.5です. プログラムの中で,sizeof(double *)の部 分は,double 型ポインタの大きさの意味になります. double型の大きさではありません. 教科書,プログラ ム10.5で注目してほしいのは値の入出力の部分で,ポインタのポインタとして宣言した変数aに対して, 2次 元配列のようにアクセスができるところです. 以前「配列とポインタの関係」で述べたないようで,ポインタ 変数の値をずらす演算が,配列変数と同じになることを利用しています. この特徴は,配列がコピーできないに つながりましたが,「不便なこと」ではないのです. むしろ, PCを効率よく利用するに役立つことです.

このことを踏まえると,ポインタへのポインタ変数の宣言として double **a;

double *a[];

は同じ意味になります.

教科書,プログラム10.6は,あまり意味のない工夫なので, 飛ばしてください. 教科書,プログラム 10.7は, 実行してみてください. ポインタ配列のアドレス増の様子がわかると思います.

(4)

さて,上で挙げたポインタ配列を使うふたつめの理由について述べます.

「ある種のプログラムの実行速度を上げる」例として,「要素の入れ替え」があります. 例えば,連立一次方程 式を解くプログラムを書くとします(工学系ではよくある話). 連立一次方程式の解法は, 皆さんが1年次の時 に線型代数学で学んだ「掃き出し法」をプログラムします. 解の公式であるクラメルの公式はプログラミング では全く使えません. 掃き出し法のひとつの操作に「行を入れ替える」というものがあります. これを2次元 配列で実装すると, とても遅いプログラムになりますが,上のようなポインタ配列で実装すると,コンピュータ の速度は劇的に速くなります.

前回のレポート問題について : 「ポインタのポインタ」の応用

上の事を前回のレポート問題で実装してみます. seiseki_t型の構造体を入れ替えるのではなく, それらへ のポインタの指す値を入れ替えることにより,整列を実行するわけです.

プログラムでは, data[40]はseiseki_t型へのポインタを保持するポインタ変数の配列にします. 従っ て,各個人のデータを読むたびに, seiseki_t型のためのメモリをmalloc()で確保し,そこへのアドレスを

data[i]に代入していきます. 整列アルゴリズムは,単純なバブルソートにしました.

注意して欲しいのは,構造体へのポインタに対する矢印演算子 -> です. aが何らかの構造体へのポインタ なら, a -> memberは,aが指す構造体内のmemberという変数値を参照します.

また, 要素を入れ替えるswap()では, まさにポインタへのポインタが引数となります. これにより,正しい 間接参照が可能であることを理解して下さい.

(5)

#include <stdio.h>

#include <stdlib.h>

typedef struct seiseki{

char name[10];

int score;

} seiseki_t;

void swap(seiseki_t **x, seiseki_t **y) {

seiseki_t *temp;

temp = *x;

*x = *y;

*y = temp;

}

int main() {

FILE *fp;

int i, j;

seiseki_t *data[40];

fp=fopen("seiseki.data","r");/* エラー処理は略 */

for (i=0; i< 40; i++){/* データ読み込み */

data[i] = (seiseki_t *)malloc(sizeof(seiseki_t));/* エラー処理は略 */

fscanf(fp, "%s%d", data[i]->name, &(data[i]->score));

}

for (j=39; j>0; j--){ /* バブルソート */

for (i=0; i<j; i++){

if (data[i]->score<data[i+1]->score){

swap(&(data[i]), &(data[i+1]));

} } }

for (i=0; i< 40; i++){/* 結果を出力 */

printf("%s %3d\n", data[i]->name, data[i]->score);

(6)

ライブラリ関数qsort()

今の形のコンピュータが実用に使われるようになった当初から, データの整列処理に多く利用されることが 判明しました. そのため,様々な整列アルゴリズムが開発されています.

整列アルゴリズムは,次の観点で比較されています.

• 整列のための計算量(整列の速さ)

• 整列に必要なメモリ.

• 安定性(同じ値であったとき,元のデータの並びを変化させる可能性があるか否か.)

これらすべで良い結果が得られるアルゴリズムは,いまだに発見されていません(おそらく無いと思います が,無い事を証明できるかは知らない.). しかし,概ね効率がいいと知られているアルゴリズムがいくつかあり, その中でもHoare(ホア)が1960年に開発したquicksortというものがあります. 特別に速い整列方法(デー タの形によっては, 特別な方法がある)がない場合とか,データサイズが大きすぎて 1次記憶に収まりきらな いなどを除けば,これを利用するのが普通です. quicksort の特徴は,次です.

• データは 1次記憶上に全てあることを想定し,データサイズ程度の記憶容量しか必要としない.

• 安定ではない.

• 平均的な計算量はO(nlogn)である.(最悪はO(n2). バブルソートは,これが常にO(n2))

• 再帰的なプログラムのため,データ量が少ないと,再帰呼び出しのオーバーヘッドで却って遅くなる. C では, 標準ライブラリ関数として, qsort()というものが用意されています. その内容は, 必ずしも

quicksort の実装ではなく処理系依存ですが, 多くの場合, quicksortの最後の欠点を少し改善したプログラム

が実装されているようです. (ソート数が少なくなった時点で,再帰をやめて別のアルゴリズムを使うなど).

qsort()の使い方は,マニュアルコマンドでわかりますが,その読み方を少し解説します.

Bash4-4$ man 3 qsort

次のプログラムは,qsort()を用いて,seiseki_t data[40];をscore の大きい順に並べ替えた例です.

比較関数comp()については,別途解説します. 変数のキャストの仕方,ポインタを利用した間接参照の方法,

構造体ポインタを用いた構造体メンバーの参照方法がわかっていれば,次の式の意味はわかるはずです. (*(seiseki_t **)x)->score

comp()にカウンタを入れて, 私のデータで計測したところ, 166回のデータ比較で並び替えが終了しまし

た. バブルソートだと, 40×39

2 = 780回のデータ比較が必要ですから, qsort()の方が効率よく整列を実行

している事がわかります. 40個くらいでこの差ですが, 10000 = 105くらいだと, n2nlognの差は,数千 倍になります.

(7)

#include <stdio.h>

#include <stdlib.h>

typedef struct seiseki{

char name[10];

int score;

} seiseki_t;

int comp(const void *x, const void *y) {

return (*(seiseki_t **)y)->score-(*(seiseki_t **)x)->score;

}

int main() {

FILE *fp;

int i, j;

seiseki_t *data[40];

fp=fopen("seiseki.data","r");/* エラー処理は略 */

for (i=0; i< 40; i++){/* データ読み込み */

data[i] = (seiseki_t *)malloc(sizeof(seiseki_t));

fscanf(fp, "%s%d", data[i]->name, &(data[i]->score));

}

qsort(data,sizeof(data)/sizeof(data[0]),sizeof(data[0]),comp);

for (i=0; i< 40; i++){

printf("%s %3d\n", data[i]->name, data[i]->score);

}

return 0;

}

(8)

整列アルゴリズムについて

上で述べたように,qsort は適用範囲が割と広い整列アルゴリズムです. しかし,万能ではありません. 例えば, 今日の題材でも,「単純に試験結果を成績順に出力させるだけ」を目的とするなら, 並び替えをしな いバケットソート(Bucket sort,ビンソートともいう)が利用でき,処理手順はもっと少なくできます. この場 合,順序を決める評価値が0から100の101種類しかないという,データの特性を利用できるからです.

整列アルゴリズムについては,多くの方法があり,本来なら

• 主要なアルゴリズムの特徴

• それらの処理手順の量の評価

• データに対して最適な方法を選択する方法

を講義すべきですが,時間と私の能力の両方が足りていません. また,今回の「ポインタを利用した整列」でも わかるとおり,アルゴリズムを動かすコンピュータの仕組みも,計算効率に影響を与えます. 「多量のデータの 整列」は,データの特性と望む結果を検討して, 適切な方法を選ぶ必要があることは理解してください.

レポート問題 : 締め切り , 8 1 ( ) 10:00 AM(JST)

行列のサイズm, nを入力から受け取って, m×n行列を作成するプログラムを書け. プログラムは, 次の仕 様にすること.

• 行列のサイズm, nは標準入力から受け取る.

• 行列の成分は,int型にする.

• 行列のサイズが決まれば,各行単位で,行ベクトルの入力をするようにし,それをメモリ内に保持する.

• 最後に上で入力した行列を出力する.

• 出力は,行単位でする. 例えば,行列

(1 2 3

4 5 6

)

の出力は

1 2 3 4 5 6

となるようにする. 件名: gengo2022-1 report14.

Referensi

Dokumen terkait

生成規則・生成文法 生成規則を与えることでも 言語を定めることが出来る −→ 生成文法 generative grammar 生成規則による“文法に適っている”語の生成 • 初期変数を書く • 今ある文字列中の或る変数を 生成規則のどれかで書換える •

関数を呼び出すことによって2つの変数の値を交換するためには、関数の引数として何を渡 せば良いか。 このようなことを考える為に、「変数」が計算機上で実際にどう扱われているかを改めて見てみよう。 12–1 ビットとバイト よく「計算機は0と1しか判らない」という言い方をするが、これは電気的には「OnかOffか」正確には HighかLowか

4–2 代入 代入: assignment 宣言した変数へ値を代入書き込みするには = を用いる。代入文の書式は次の通り。 数学の“等式”と見た目には同じだが、イ メージとしては「変数名 ← 式の値」と いう感じ。 右辺の式を計算評価した値が、左辺の 変数名又は記憶領域を指し示す式が指 し示す記憶領域に書き込まれる。右辺の 式は単独の定数値や変数でも良い。

2.10 1 次分数変換の鏡像の原理 1 鏡像の位置 Cb の円は1次分数変換でCbの円に写ることを示した。 円上にない点についても、注目すべき性質が成り立つ。 定義 14.1 円に関して鏡像の位置 C はCb の円であり、z,z′ ∈Cb とする。zとz′がC に関して互いに鏡 像の位置にあるz とz′ がC に関して対称である とは,次のa, b

自分用なら構わないが,このようなファイルを人に送るべきではない.受け取った相手はpreamble.tex を持っていないだろうから. 6.5 数式環境 数式は$で挟んで書く.例えば,次のように書いてプレビューしてみよ. 1+1 と $1+1$ では出力が異なる. 初心者に多いエラーは,開く$のみ書いて,閉じる

いま, 位相空間X,Oの2点x, y ∈Xに対し, 関係 x∼yを,Xのある連結部分集合Aが存在してx, y ∈Aとなるときとして定義する と, これはXにおける同値関係となり命題6.2.7, Xの∼による商集合X/∼の 各同値類をX,Oの連結成分と呼ぶのであった定義6.2.8.. このと き,

合成函数の微分 以下fx, yは必要な回数だけ微分できる(十分なめらかである)とする. x, yがtの微分可能な函数x=xt,y=ytであるとき,合成して得られるtの函 数t 7!f xt, yt をtで微分することを考えよう.公式は次の定理の通り. 定理

め、リープフロッグ法が使われることも多い。式(12)~(15)をリープフロッグ法 の時間差分に書きかえると、 u+= u− 16 w+= w−− 2g∆t 17 x+= x−+ 2u0∆t 18 z+= z−+ 2w0∆t 19 となる。リープフロッグ法では、時刻t+Δtにおける物理量の値を求めるために、時刻