アルゴリズムとデータ構造
2012 年 6 月 7 日
担当:酒居敬一@A468 ([email protected])
http://www.info.kochi-tech.ac.jp/k1sakai/Lecture/ALG/2012/index.html
ü テキスト
『アルゴリズムとデータ構造』,
石畑清 著 ( 岩波書店 )
ü 参考書
『アルゴリズムとデータ構造』,
平田富夫 著(森北出版).
『アルゴリズムとデータ構造入門』,
東野勝治,臼田昭司 著(森北出版).
『ハッカーのたのしみ』,
H.S. ウォーレン Jr 著,滝沢徹,鈴木貢,
赤池英夫,葛毅,藤波順久,玉井浩訳(星雲社)
『プログラミング言語C』,
B.W. カーニハン, D.M. リッチー 著,
講義計画
1.
アルゴリズムと計算量 (6月7日5時限)2.
線形探索・2分探索 (6月11日5時限)3.
2分探索木 (6月14日5時限)4.
平衡木・B
木 (6月18日5時限)5.
ハッシュ法 (6月21日5時限)6.
シェルソート (6月25日5時限)7.
クイックソート (6月28日5時限)8.
ヒープソート (7月2日5時限)9.
マージソート・ビンソート (7月5日5時限)10.
グラフの表現法・探索 (7月9日5時限)11.
連結性の判定 (7月12日5時限)12.
最短路の問題 (7月17日2時限)※13.
文字列のアルゴリズム (7月19日5時限)14.
バックトラック・幅優先探索・ゲームの木の探索 (7月23日5時限)15. NP
完全問題・近似アルゴリズム (7月26日5時限)16.
[クォータ末試験] (7月30日5時限)成績評価
• クオータ末試験および演習を総合的に 評価する.
• 試験や演習で持ち込めるもの
– 教科書・ノート・配布資料
• 再試験はしない.
本講義の位置づけ
1. プログラムの勉強 ( 技術的な知識 )
ü 計算機言語、情報学群実験1
ü 背景は、表現方法としてのプログラミング言語
2. アルゴリズムとデータ構造 ( 抽象的な知識 )
ü 計算機システムの基礎
ü 数学と計算法(計算機は Σ や∫を知らない)
3. 計算機のしくみの勉強 ( 低水準の知識 )
ü 情報学群実験 2
ü コーディング対象を知る
4. システム設計の勉強 ( 高水準の知識 )
ü ソフトウェア工学、オペレーティングシステム 5
アルゴリズム+データ構造=プログラム
( このように書くのは簡単 )
アルゴリズムとデータ構造
(Java, C, )
抽象化 具体化
この間があまりにも遠いのが現実 間を埋めるもの→想像力
想像力を増やす→経験を積む
経験を積むには→楽しさが必要 楽しさって何?
書いたとおり動くのが救い
プログラムとは?
• アルゴリズムとデータ構造を表現したもの
– 表現方法にはいっぱいある
• プログラミング言語の数だけ
– 構造体やレコードといったデータ構造
– 連接や条件文や繰り返し文といった制御構造
• 計算機に仕事をさせる指示・手続き
– 計算機は指示通りに動くように作られている
• 動かない場合も稀にある …
7
なぜ学習するか?
• すばやくコーディングするため
• 美しいコードを書くため
• わかりやすいコードにするため
• どのように表現するか、どのように処理し
目的を達成するか、を理解する
すばやくコーディングする
• よく知られたものを使う
– 探せばどこかに実装が存在する
– 既存のものを使えばデバグの手間が省ける – 定番と呼ばれる書籍の存在
• 全体をよく考えて、既存のものが使えるよ うにする
– そのために勉強する!
美しいコード
• 洗練されたコードは美しい
– 適切なアルゴリズム – 適切なデータ構造
• コーディング規則の外側に美しさがある
– 人間が読み書きするものであることを肝に銘じて
– きちゃないコードは読む気がするか?
わかりやすいコード
• 構造がわかりやすい
– よくしられたデータ構造 – よくしられたアルゴリズム
– これらの再帰的な組み合わせ
• 構造がプログラミング言語の自然なデータ 型や制御文に合っている
• プログラムの共有ができる
– 3 日後の自分は他人と同じ
• 記憶力のいい人は 1 ヶ月くらいは平気?
抽象的 vs. 具体的
• ptr で指される領域から value を線形探索
• for(i = n; i; i--, ptr++)
if(*ptr == value) break;
• mov eax,value mov edx,ptr
mov ecx,n
0: cmp eax,[edx]
je 1f
lea edx,[edx+4]
loop 0b
Euclidの互除法 (2ページ 1.1 )
1. mをnで割って、余りをrとする。
2. r=0であれば、アルゴリズムは終了する。
このとき、nが最大公約数である。
3. m←nとする(nの値をmに代入する)。
次にn←rとして1に戻る。
ここでは、次の処理が使われている。
• 除算
• 0との比較・分岐処理
• 変数への代入
• 繰り返し(ループ)
/* C言語によるgcdの例1 */
int gcd(int m, int n) {
int r;
1: r = m % n;
if (r == 0) goto 2;
m = n;
n = r;
goto 1;
2: return n;
}
/* C言語によるgcdの例2 */
int gcd(int m, int n) {
int r;
while((r = m % n) != 0){
m = n;
n = r;
}
return n;
}
/* Java とほとんど同じ */
プログラムは、連接(文の並び順による評価)・条件分岐(たとえば
if
文)・繰り返し(例えばwhile
文)だけで構成できるとされている。そもそも、
goto
を使わないで書いたほうがわかりやすいことも多い。そのような背景で、
Java
のようにgoto
を使えないプログラミング言 語がある。; アセンブリ言語によるgcd関数の例
.text
gcd: mov.w @(2,sp),r1 ; 引数 m
mov.w @(4,sp),r0 ; 引数 n 1: divxu.b r0l,r1
xor.b r2h,r2h
mov.b r1h,r2l ; r = m % n
beq 2f ; if(r == 0) goto 2 mov.w r0,r1 ; m = n
mov.w r2,r0 ; n = r bra 1b ; goto 1 2: rts ; return n .end
戻りアドレス 引数n 引数m
スタックフレーム sp+4
sp+2 sp
簡単なアルゴリズムであればアセンブリ言語でも記述できる。
ただし、アルゴリズムが必要とする処理をプロセッサが知っ ていれば
…
ちなみに、スタックというデータ構造は、C言語では例のよう に、さりげなく使われている。
; アセンブリ言語によるgcd関数の例
.text
gcd: mov.w @(2,sp),r0 ; m
mov.w @(4,sp),r1 ; n
beq 1f ; if (n == 0) divxu.b r1l,r0
mov.b r0h,r0l
xor r0h,r0h ; m % n push r0
push r1 bsr gcd adds.w #2,sp adds.w #2,sp
1: rts ; return m .end
戻りアドレス 引数n 引数m
スタックフレーム sp+4
sp+2 sp
レジスタ変数
r2(
変数r)
が、不要になっ ている。再帰呼び出しでは、引数は新しい領域 に確保される。新しい領域としては、ス タックが使われる。
bsr直後のスタック
sp+10 sp+8 sp+6 sp+4 sp+2 sp
戻りアドレス 引数n 引数m
戻りアドレス 引数n 引数m
アルゴリズム
• アルゴリズムは必ず問題を解決するもの
– いつかは停止しないといけない
• ひとつまたは複数のデータを操作し目的の 結果を得るための一連の処理手順
– ループ不変条件
• 繰り返し開始直前にこの条件が成立。
• この条件が成立しているときに、
繰り返しを1回すすめると、再びこの条件が成立。
計算量の概念 ( 7 ページ 1.2 節)
• アルゴリズムの性能を示す指標
– 時間計算量
• ( 文字通り ) 計算に要する時間
–
最悪時間計算量・平均時間計算量– 空間計算量(領域計算量)
• どれくらいの作業領域を必要とするかを表す
• 大きな問題が少ない計算量で解ければ優秀
– 漸近的に表現したものが O 記法
• 計算量を定式化したとき、計算量に最も大きな影響
を及ぼす項をとりだしたもの。
O 記法
• 漸近的な振る舞いを表す
– 定数項は無視 – 係数は1
– 一般には最も影響力の強い項のみで表す
• 項の形で大きく2つに分けられる(問題 : n )
– 多項式 n
k– 指数関数 k n
O( n ) O( n log n )
O( n 2 ) O(e n )
2500 100
25 10
10 10
基本的データ構造
• スカラ
– 基本型として限定的に記述可能
• ベクトル
– 1次元の配列として表現可能なことがある – 実は、普通の計算機では演算できない
• グラフ
– 実は、普通の計算機では簡単に表現できない
• 集合
– 実は、普通の計算機では簡単に表現できない
– もちろん、集合演算はできない 21
メモリと配列
• 計算機のメモリは一種の1次元配列である
– プロセッサが扱える最小単位を要素としている
• 普通の計算機では byte を最小の単位としている
– 有限の大きさを持つ
• ただし、仮想記憶管理機構により伸長できる場合もある
– 配列のインデックスに相当するものがアドレス
• メモリへのアクセスはアドレッシングと呼ばれる
– 普通の計算機では、プロセスにはこの配列が 1 個
• プログラミング言語による多彩なデータ構造
• プログラミング言語のコンパイラが変換します
1 2 3 4
n
… …
配列(27ページ)
• 添え字とデータが 1 対 1 で対応
• 添え字は連続
– 添え字が1から始まるとは限らない
• データの挿入や削除は面倒
添え字を用いてアクセスする(例では3)
1 2 3 ・・・ m
・・・・・・・・・・・・・・・・
・・・・・・・
・・・・・・・
二次元配列
• 行と列それぞれをインデックスで指し示す
(3,2)
添え字を
用いて
データに
アクセス
・・・・・・・・・
・・・・・
1 2 3 ・・・・ m
三次元配列
(3,1,1)
添え字を 用いて データに
アクセス
連結リスト(29ページ)
• データをそれぞれの要素に格納
• 要素どおし、つながってリストを形成
先頭 データ 次 データ 次 データ 次 次
データ データ 次
メモリ上に置かれた構造型データをアドレスで指す
先頭
次
データ データ 次 データ 次 データ 次 データ 次 次
データ データ 次
次 データ
次 データ
次 データ
先頭 前
挿入対象 前
削除対象
例:メモリ割り当て (37ページ)
空きの領域 最も簡単なメモリ割り当てアルゴリズム
・リストで管理
・データ領域を分断し、あらたなリスト要素として、挿入
・リストのヘッダには空きか使用中かのフラグがある
・割当てるデータ領域はリストのヘッダとヘッダの間
使用中 空き 使用中
空き
使用状況を示すフラグ 次のヘッダを指すポインタ
ヘッダは左のような構造
要素としては、フラグとポインタしかない
メモリの割り付け・開放を繰り返していくとメモリが分断するようになる
・フラグメンテーションといい、大きな領域を割り当てできなくなる
・そこで、ときおり、空き領域にはさまれたヘッダを削除する
使用中 空き
空き 空き 空き 空き 空き
使用中
メモリ割付け技法
•
連続割付け•
単一連続割付け•
分割割付け(
ゾーン割り付け)
•
固定区画割付け•
可変区画割付け•
非連続割付け•
ページング•
セグメンテーション 管理手法•
線形リスト•
ハッシュ表スタック( LIFO )
スタックポインタ
ポップ プッシュ
スタックポインタ
スタックポインタ
連結リスト操作(挿入)
リストを使ったスタック( push )
単純な連結リスト(線形リスト)データ挿入
先頭 次 次 次
データ データ データ
次
データ • データをそれぞれの要素に格納
• 要素どおし、つながってリストを形成
31
先頭 次 次 次 データ データ データ
連結リスト操作(削除)
リストを使ったスタック( pop )
Postscript
• プログラミング言語ではなくて、ページ記述言語
• オペランドスタック、辞書スタックを持つ
• オブジェクトはリテラル、エグゼキュータブル
• A-WS で gs というコマンドで実行できる
• push/pop 以外のスタック処理がある
• index 指定位置の内容をコピーして push
• rotate 指定位置までのスタックを配列とみて回転
• [] , {}, () スタックから配列オブジェクトを切り出せる
RPN (逆ポーランド記法)
普通は
1 + 2
と入力して3
という答えを得ます同じ答えを得るために、
RPN
で書くと1 2 +
となります。普通の書き方の
(1 + 2) × (3 + 4)
をRPN
にすると1 2 + 3 4 + ×
となります。ありがちなプログラミング言語では規則をもとに構文解析を行っている 演算子には優先順位がある
括弧は優先度を変える
変わった言語、変わったプロセッサというものがありまして
Forth, PostScript
といった言語、インテルx87
数値演算プロセッサこれらはスタックを基本的なデータ構造としている
GS>1 2 add 3 4 add mul =
21 GS>1 2 add 3 4 add mul 5 6 mul 7 8 mul sub div = -0.807692
GS>30 sin = 0.5 GS>45 cos = 0.707107 (1 + 2) × (3 + 4)
RPN
では1 2 + 3 4 + (1 + 2) × (3 + 4) ÷(5×6 – 7×8)
RPN
では1 2 + 3 4 + × 5 6 × 7 8 × - ÷
PostScript
では、三角関数まで定義されている。RPN で記述するとき、日本語で数式の動作を
読み上げることにかなり近い順序になる
/*
可変長引数を持つ関数 */
int printf(const char *fmt,…);
/*
それの呼び出し */
printf(“%d %f \n”, int_number, dbl_number);
C
言語では引数はスタックに積まれて、関数に渡される。呼び出し側の責任で
引数を積んだり降ろしたりする。
呼ばれた関数の スタックフレーム
PC fmt
dbl_number int_number
呼んだ関数の
←
TOS
呼ばれた関数にわかっていること
1.
呼ばれた関数のスタックフレームの大きさ2.
関数の戻り先(
呼び出し元PC)
3.
第1
引数がconst char *fmt
であることつまり
fmt
が読める→以降の引数がわかる/*
可変長引数を持つ関数 */
int execl(const char *path, const char *arg, ...);
/*
それの呼び出し */
execl(“/bin/ls”, “ls”, “-l”, “/home/sakai”, NULL);
呼ばれた関数の スタックフレーム
PC
“ /bin/ls ”
“ -l ”
“ ls ”
呼んだ関数の スタックフレーム
←
TOS
呼ばれた関数にわかっていること
1.
呼ばれた関数のスタックフレームの大きさ2.
関数の戻り先(
呼び出し元PC)
3.
第1
引数が“/bin/ls ”
であること4.
第2
引数が”ls ”
であること5.
最後の引数は必ずNULL
であることNULL
“ /home/sakai ”
スタックに何らかのマークを
push
し、TOS
からマークまでをリストとして渡す37
% PostScript
における配列の切り出し% [1 2 3 4 5 6 7 8 9 10]
という配列を定義GS>[
GS<1>1 GS<2>2 GS<3>3 GS<4>4 GS<5>5 GS<6>6 GS<7>7 GS<8>8 GS<9>9 GS<10>10 GS<11>]
GS<1>==
[1 2 3 4 5 6 7 8 9 10]
GS>
PostScript
では、配列が定義できる。スタック上の「マーク」と「マークまでを配列とする」
オペレータを組み合わせて使う。
このオペレータはマークまでをすべて
pop
し、配列オブジェクトとしてふたたび
push
する[
オブジェクトの並び]
は通常の配列{
オブジェクトの並び}
は実行可能な配列(
手続き)
(
文字の並び)
は文字の配列(文字列)待ち行列( FIFO ) (44ページ)
待ち行列(データの挿入・取得)
データ挿入 データ取得
39
待ち行列の配列による実現
データ挿入 データ取得
新しい要素を入れようとすると入らない
→右へコピーして移動
→隙間を空ける
リングバッファ (46 ページ )
Ø 配列の最初と最後を接続して環にしたもの Ø 2つのポインタでデータの出し入れを管理
Ø データの先頭を指すポインタ
Ø head, front
Ø データの最後尾を指すポインタ
Ø tail, rear
Ø 2つのポインタが重なったらデータは空
Ø 領域の大きさを n としたらポインタの位置は n とおり Ø データの数が 0 から n まで n+1 とおりある
Ø ポインタが重なったら、空か満杯の状態が重なる … 41
リングバッファ
挿入口 リア
取り出し口
•
環状のデータ格納領域•
データの存在を示すポインタフロント
リア リア
満杯になったとき、
リアとフロントのポインタが 重なってしまって
空と区別が付かない
配列を使用したリングバッファ
ü 配列には始まりと終わりがある
ü ポインタが終わりまで移動したら先頭へ戻る ü (フロント‐リア)の演算でも境界を考慮
ü ラップラウンド処理
ü ラップラウンド処理
ü 条件文で判定
ü 配列の大きさを 2 のべき乗にする
ü 配列のインデックスをビットごとの AND 処理
public class Queue { private Queue() {
}
public Queue(int aMaxSize) {
int realSize = aMaxSize + 1;
this.maxSize = realSize;
this.queueArray = new Object[realSize];
this.front = 0;
this.rear = 0;
}
private int front;
private int maxSize;
private Object[] queueArray;
private int rear;
}
• データのおき場所は配列
• front, rear というポインタで管理
• キューの容量は maxSize で管理
public Object dequeue() {
if(this.isEmpty()){
System.err.println("
待ち行列は空です");
return null;
}
Object dequedObject = this.queueArray[this.front];
this.queueArray[this.front] = null;
++this.front;
if(this.maxSize == this.front){
this.front = 0;
}
return dequedObject;
}
public boolean isEmpty() {
return (this.rear == this.front);
}
• front の指すところがキューの先頭
• 先頭と最後尾が同じ場合は空
• 条件文でラップラウンド処理
public boolean enqueue(Object aTarget) {
if(this.isFull()){
System.err.println("
待ち行列はいっぱいです");
return false;
}
this.queueArray[this.rear] = aTarget;
++this.rear;
if(this.maxSize == this.rear){
this.rear = 0;
}
return true;
}
public boolean isFull() {
return ((this.rear + 1) == this.front);
}
• rear の指すところがキューの最後尾
• 先頭と最後尾が同じ場合は空
• そうならないようにする判定が必須
• 条件文でラップラウンド処理
public void printAll() {
System.out.println("
待ち行列の内容");
if(this.isEmpty()){
System.out.println();
return;
}
int count = 1;
int position = this.front;
int limit = (this.front > this.rear)? this.maxSize: this.rear;
while(position < limit){
System.out.println(count +"\t" + this.queueArray[position]);
++count;
++position;
}
position = 0;
limit = (this.front > this.rear)? this.rear: 0;
while(position < limit){
System.out.println(count +"\t" + this.queueArray[position]);
++count;
++position;
}
• 場合分けしてラップラウンド処理
• front から配列終わりまでの表示
• 配列先頭から rear までの表示
//
リストのデータ構造// Java
言語でアクセッサありpublic class Element1
{ public Element1(Object aData) {
this.data = aData;
}
public Object getData() {
return this.data;
}
public Element1 getNextElement() {
return this.next;
}
public void setNextElement(Element1 anNextElement) {
this.next = anNextElement;
}
private Object data; //
参照型private Element1 next;
}
/*
リストのデータ構造 */ /* C
言語版その1 */ union Object {
int Integer;
double Double;
}; struct Element1 {
union Object data;
struct Element1 *next;
};
//
リストのデータ構造// Java
言語でアクセッサなしpublic class Element2
{ public Element2() {
}
public Object data;
public Element2 next;
}
/*
リストのデータ構造 */ /* C
言語版その2 */ struct Element2 { void *data;
struct Element2 *next;
}; 49
//
リストのデータ構造// Java
言語でアクセッサあり// Element1 next_element1; //
どこかで与えられているElement1 new_element1;
new_element1 = new Element1(new Integer(100));
new_element1. setNextElement(next_element1);
/*
リストのデータ構造 *//* C
言語版その1 */
/* struct Element1 *next_element1: /*
どこかで与えられている */ struct Element1 *new_element1;
new_element1 = malloc(sizeof (struct Element1));
new_element1->data.Integer = 100;
new_element1->next = next_element1;
/*
リストのデータ構造 *//* C
言語版その2 */
/* struct Element2 *next_element2: /*
どこかで与えられている */ struct Element2 *new_element2;
new_element2 = malloc(sizeof (struct Element2));
new_element2->data = malloc(sizeof (int));
*((int *)new_element2->data) = 100; /* cast as lvalue
で行儀が悪い */ //
リストのデータ構造// Java
言語でアクセッサなし// Element2 next_element2; //
どこかで与えられているElement2 new_element2;
new_element2 = new Element2();
new_element2.data = new Integer(100);
new_element2. next = next_element2;
Element1
のインスタンスgetData() getNextElement() setNextElement()
data next
Element2
のインスタンスdata next
Integer
のインスタンス100
Element1
のインスタンスgetData() getNextElement() setNextElement()
data next new_element1
next_element1
new_element2
Element2
のインスタンスdata 100
data next next_element1
next 100
next_element1
next data data
next
//
リストのデータ構造// Java
言語でアクセッサありpublic class Element1
{ public Element1(int aData) {
this.data = aData;
}
public int getData() {
return this.data;
}
public Element1 getNextElement() {
return this.next;
}
public void setNextElement(Element1 anNextElement) {
this.next = anNextElement;
}
private int data; //
値型private Element1 next;
}
/*
リストのデータ構造 */ /* C
言語版その1 */ struct Element1 { int data;
struct Element1 *next;
};
//
リストのデータ構造// Java
言語でアクセッサなしpublic class Element2
{ public Element2() {
}
public int data;
public Element2 next;
}
/*
リストのデータ構造 */ /* C
言語版その2 */ struct Element2 { int *data;
struct Element2 *next;
};
//
リストのデータ構造// Java
言語でアクセッサあり// Element1 next_element1; //
どこかで与えられているElement1 new_element1;
new_element1 = new Element1(100);
new_element1. setNextElement(next_element1);
/*
リストのデータ構造 *//* C
言語版その1 */
/* struct Element1 *next_element1: /*
どこかで与えられている */ struct Element1 *new_element1;
new_element1 = malloc(sizeof (struct Element1));
new_element1->data.Integer = 100;
new_element1->next = next_element1;
/*
リストのデータ構造 *//* C
言語版その2 */
/* struct Element2 *next_element2: /*
どこかで与えられている */ struct Element2 *new_element2;
new_element2 = malloc(sizeof (struct Element2));
new_element2->data = malloc(sizeof (int));
*((int *)new_element2->data) = 100; /* cast as lvalue
で行儀が悪い */ //
リストのデータ構造// Java
言語でアクセッサなし// Element2 next_element2; //
どこかで与えられているElement2 new_element2;
new_element2 = new Element2();
new_element2.data = 100;
new_element2. next = next_element2;
Element2
のインスタンス100
next
Element1
のインスタンスgetData() getNextElement() setNextElement()
data next new_element1
next_element1
new_element2
Element2
のインスタンスdata 100
data next
Element1
のインスタンスgetData() getNextElement() setNextElement()
100
next
next 100
next data data
next
ルートノード
エッジ ノード
木構造
ルートとそれ以外の ノードにちょうど1つだけ の経路しか存在しない