プログラミング通論 コード穴埋め問題集

期末試験範囲(第8回〜第14回)の実装を全部書けるようにするための問題集。コードの空欄 ___ を埋めて、解答コールアウトで答え合わせをする。

使い方

  1. 紙に(または頭の中で)空欄を埋めてみる
  2. [!note]- 解答 をクリックして答え合わせ
  3. [!tip]- 解説 で「なぜそう書くのか」を確認
  4. 書けなかった問題は翌日にもう一度

共通の型定義(全問共通)

以下の定義は授業ノートで毎回登場するもの。ここで確認しておく。

/* リストのノード */
struct node {
    elementtype element;   /* 保持するデータ */
    struct node *next;     /* 次のノードへのポインタ */
};
 
/* ソート対象のレコード */
typedef struct {
    keytype key;           /* ソートの基準となるキー */
    elementtype element;   /* 付随するデータ */
} recordtype;
 
/* 2つのレコードを入れ替える(ソートで多用) */
void swap( recordtype *x, recordtype *y ){
    recordtype tmp;
    tmp = *x;
    *x = *y;
    *y = tmp;
}

添字の約束

  • リストは head というデータを持たない先頭ノードを使う
  • ソートの配列は a[0] を使わず a[1]〜a[n] で扱う(選択・挿入・バブル・ヒープ・分布数え上げ)
  • ただしクイックソート2・マージソート・探索は a[0] から使う

Part 1: リスト構造とその応用(第8・9回)

問1: リストの初期化 initlist

空のリスト(head のみ)を作る関数。head はデータを持たないノード。

struct node *initlist ( void ) {
    struct node *n;
    n = ___①___malloc ( sizeof ( struct node ) );   /* ノード用メモリ確保 */
    n -> next = ___②___NULL;                      /* 次はまだ無い */
    return n;
}

問2: リストへの挿入 insert

ノード p の直後に、値 x を持つノードを挿入する。

void  insert ( struct node *p,  elementtype  x ) {
    struct node *n;
    n = ( struct node * ) malloc ( sizeof ( struct node ) );
    n -> element = x;
    n -> next = ___①___p->next;     /* 元のリストの続きを n につなぐ */
    ___②___p->next = n;             /* p の次を n に張り替える */
}

問3: リストからの削除 delete

p が指すノードののノードを削除する。

void delete ( struct node *p ) {
    if ( ___①___p->next != NULL ) {
        p -> next = ___②___p->next->next;
    }
}

問4: リストでスタック push / pop

リストの head をスタックのトップとして使う。typedef struct node *stack; とする。

void push ( stack s, elementtype x ) {
    ___①___insert ( s, x );        /* 先頭に挿入 = リストの insert を再利用 */
}
 
elementtype pop ( stack s ) {
    elementtype retval;
    if ( ___②___stackempty ( s ) ) {   /* 空チェック */
        printf ( "underflow\n" );
        exit(1);
    } else {
        retval = s->next->element___③___;    /* 先頭ノードのデータを取り出す */
        ___④___delete ( s );       /* 先頭ノードを削除 */
        return retval;
    }
}

問5: リストでキュー enqueue / dequeue

struct queue { link front, rear; } で、front は head、rear は末尾を指す。

void enqueue ( struct queue *q, elementtype x ) {
    ___①___insert ( q -> rear, x );   /* 末尾に挿入 */
    q -> rear = q->rear->next___②___;         /* rear を新しい末尾へ更新 */
}
 
elementtype dequeue ( struct queue *q ) {
    elementtype retval;
    if ( queueempty ( *q ) ) {
        printf ( "underflow\n" ); exit(1);
    } else {
        retval = q->front->next->element___③___;        /* 先頭ノードのデータ */
        ___④___delete ( q -> front );  /* 先頭を削除 */
        if ( queueempty ( *q ) ) {
            q -> rear = q -> front;   /* 空になったら rear も head に戻す */
        }
        return retval;
    }
}

問6: リストの逆転 reverse2(リンクつなぎ替え)

ノードを複製せず、next リンクを逆向きにつなぎ替えて逆順にする。

link reverse2 ( link p ) {
    link head, oldp, keep;
    head = p; oldp = ___①___;   /* 1つ前のノード(最初は無い) */
    p = p -> next;              /* 注目ノードへ */
    while ( p != NULL ) {
        keep = oldp;
        oldp = p;
        p = p -> next;
        oldp -> next = ___②___;   /* 注目ノードの次を1つ前へ向ける */
    }
    head -> next = ___③___;       /* head の次に最後尾を繋ぐ */
    return head;
}

Part 2: ソート(第11〜14回)

問7: 選択ソート selection_sort

「残りの中から最小値を探して、先頭に持ってくる」を繰り返す。配列は a[1]〜a[n]。

void selection_sort( recordtype a[], int n ){
    int i, j, minindex;
    for( i=1; i<n; i++ ){
        minindex = i;
        for( j=___①___1; j <= n; j++ ){        /* 未整列部分を走査 */
            if( a[j].key < a[minindex].key___②___ ){          /* より小さい値を見つけた */
                minindex = j;
            }
        }
        ___③___swap ( &a[minindex], &a[i] );       /* 最小値を先頭へ */
    }
}

問8: 挿入ソート insertion_sort

先頭から順に「整列済み部分の正しい位置に、1 枚ずつ挿入」していく。番兵に INT_MIN を使う。

void insertion_sort( recordtype a[], int n ){
    int i, j; recordtype v;
    a[0].key = INT_MIN___①___;        /* 番兵:key の最小値 */
    for( i=2; i<=n; i++ ){
        v = a[i]; j = i;
        while( ___②___a[j-1].key > v.key ){   /* 1つ前が自分より大きい間 */
            a[j] = ___③___a[j-1];         /* 1つ前を後ろにずらす */
            j--;
        }
        ___④___a[j] = v;                /* 空いた場所に挿入 */
    }
}

問9: バブルソート bubble_sort

右端から隣同士を比較し、小さい方を左へ運ぶ。泡(バブル)が浮かぶように見えるのが名前の由来。

void bubble_sort( recordtype a[], int n ){
    int i, j;
    for( i=1; i<n; i++ ){
        for( j=n; j>=1___①___; j-- ){        /* 右端から左へ */
            if( a[j].key < a[j-1].key___②___ ){        /* 隣同士を比較 */
                ___③___swap ( &a[j], &a[j-1] );  /* 小さい方を前へ */
            }
        }
    }
}

問10: シェルソート shell_sort

「間隔 h だけ離れた要素同士で挿入ソート」を、h を縮めながら繰り返す。h は 3h+1 で成長させる。

void shellsort( recordtype a[], int n ){
    int i, j, h; recordtype v;
    for( h=1; h*3+1 <= n/9; h = h*3+1 );   /* 間隔hの初期値を決める */
    for( ; h > 0; h /= 3 ){                /* hを3で割って狭める */
        for( i = h+1; i <= n; i++ ){
            v = a[i]; j = i;
            while( ___①___ && ___②___ > v.key ){  /* h個前と比較 */
                a[j] = a[j-h];             /* h個左の要素を右へずらす */
                j -= h;
            }
            a[j] = v;
        }
    }
}

問11: クイックソート quicksort + partition(Hoare の方法)

pivot を境に「小さいグループ」「大きいグループ」に分ける分割統治法。左端を pivot にし、i を左から、j を右から走査する。

void quicksort( recordtype a[], int l, int r ){
    int i;
    if ( l < r ){
        i = ___①___partition ( a, l, r );   /* pivot の位置を決める */
        quicksort( a, l, i-1 );     /* 左部分列を再帰 */
        quicksort( a, i+1, r );     /* 右部分列を再帰 */
    }
}
 
int partition( recordtype a[], int l, int r ){
    int i, j; recordtype v;
    v = a[l]; i = l; j = r+1___②___;    /* j は右端の1つ外から */
    do {
        do { i++; } while( a[i].key < v.key );   /* pivotより大きい値を探す */
        do { j--; } while( a[j].key > v.key );   /* pivotより小さい値を探す */
        if ( i < j ){
            ___③___swap ( &a[i], &a[j] );           /* 交差していなければ入れ替え */
        }
    } while ( ___④___j<i );                        /* 交差するまで繰り返す */
    a[l] = a[j]; a[j] = v;                       /* pivot を正しい位置へ */
    return ___⑤___j;
}

問12: クイックソート2 quicksort2(Lomuto の方法)

a[0] からデータが入っている配列用。pivot を乱数で選び、pivot より小さいものを左に詰めていく。

void quicksort2( recordtype a[], int n ){
    int i, last;
    if( n <= 1 ) return;                        /* 終端条件 */
    ___①___swap ( &a[0], &a[ rand() % n ] );       /* pivot を乱数で選び先頭へ */
    last = 0;                                   /* pivotより小さい値の右端 */
    for( i = 1; i < n; i++ ){
        if ( a[i].key < a[0].key ){
            ___②___swap ( &a[++last], &a[i] );     /* last の次に詰めて入れ替え */
        }
    }
    ___①___swap ( &a[0], &a[last] );               /* pivot を正しい位置へ */
    quicksort2( a, last );                      /* 左部分列 */
    quicksort2( ___③___a+last+1 , n - last - 1 );      /* 右部分列 */
}

問13: ヒープソート pushdown + heapsort

ヒープ(親 ≥ 子 の完全二分木)を作り、根(最大値)を取り出して末尾に置くのを繰り返す。

void pushdown( recordtype a[], int first, int last ){
    int r = first, k = 2 * r;        /* r=親, k=左の子 */
    while( k <= last ){
        if( k < last && ___①___ ){  /* 右の子の方が大きいなら */
            k++;                     /* 大きい方の子を選択 */
        }
        if( a[r].key >= a[k].key ){
            break;                   /* ヒープ条件OK → 終了 */
        }
        ___②___ ( &a[r], &a[k] );   /* 親 < 子 → 入れ替え */
        r = k; k = ___③___;         /* さらに下のノードへ */
    }
}
 
void heapsort( recordtype a[], int n ){
    int i;
    for( i = n/2; i >= 1; i-- ){     /* ボトムアップにヒープ化 */
        ___④___ ( a, i, n );
    }
    for( i = n; i >= 2; i-- ){
        ___②___ ( &a[1], &a[i] );   /* 最大値(根)を末尾へ */
        ___④___ ( a, 1, i-1 );      /* 縮んだヒープを再構築 */
    }
}

問14: 分布数え上げソート distribution_counting

比較を一切せず、キーの「度数分布」と「累積度数分布」を使って整列する。キーの範囲 m が既知であることが前提。

void distribution_counting( recordtype a[], int n ){
    int i, j, count[m];
    recordtype b[n+1];
    for( j = 0; j < m; j++ ){ ___①___; }        /* 度数分布を初期化 */
    for( i = 1; i <= n; i++ ){ ___②___; }        /* 度数分布を作成 */
    for( j = 1; j < m; j++ ){ ___③___; }        /* 累積度数分布に変換 */
    for( i = n; i >= 1; i-- ){
        b[ ___④___ ] = a[i];                    /* 累積数が格納場所 */
        count[a[i].key]--;                       /* 同じ値対策で減らす */
    }
    for( i = 1; i <= n; i++){ a[i] = b[i]; }     /* 元の配列に戻す */
}

問15: 基数整列法 radix sort

キーをビット列とみなし、上位ビットから「0 グループ」「1 グループ」に分けて再帰的に整列する(基数交換法)。

/* x を k ビット右へシフトし、その左 j ビットを取り出す */
int bits( int x, int k, int j ){
    return ( x >> k ) & ___①___;
}
 
void radixsort( recordtype a[], int l, int r, int b ){
    int i, j;
    if ( l < r && b >= 0 ){
        i = l; j = r;
        do {
            while ( ___②___ == 0 && i < j ){ i++; }  /* 左から0を探す */
            while ( ___③___ == 1 && i < j ){ j--; }  /* 右から1を探す */
            if ( i != j ){ swap( &a[i], &a[j] ); }
        } while ( j != i );
        if ( bits(a[r].key, b, 1) == 0 ){ j++; }
        radixsort( a, l, j-1, b-1 );   /* 0の部分を再帰 */
        radixsort( a, j, r, b-1 );     /* 1の部分を再帰 */
    }
}

問16: マージソート mergesort(配列版)

列を半分に分割 → 各部分を再帰ソート → 併合(マージ)する分割統治法。右半分を逆順にコピーすることで番兵を不要にするのがこの実装の工夫。

void mergesort ( recordtype a[], int l, int r ){
    int i, j, k, m; recordtype b[LIMIT];
    if ( l < r ){
        m = ___①___;                  /* 中央を求める */
        mergesort ( a, l, m );         /* 左半分を再帰 */
        mergesort ( a, ___②___m+1, r );  /* 右半分を再帰 */
        for ( i = m; i >= l; i-- ){ b[i] = a[i]; }   /* 左半分をそのままコピー */
        i = l;
        for ( j = m+1; j <= r; j++ ){ b[___③___r-j] = a[j]; }  /* 右半分を逆順にコピー */
        j = r;
        for ( k = l; k <= r; k++ ){                    /* 両端からマージ */
            if ( ___④___ ){ a[k] = b[i++]; }          /* 左が小さければ左から */
            else { a[k] = b[j--]; }                    /* 右が小さければ右から */
        }
    }
}

Part 3: 探索(第14回)

問17: 線形探索(番兵あり)

先頭から順に探す線形探索。末尾に番兵 x を置いて、ループ内の比較を 1 つに減らす。

int search( elementtype x, elementtype a[], int N ){
    int i;
    a[N] = x;                     /* 番兵:末尾に x をセット */
    for ( i = 0; ___①___; i++ ) ;  /* 必ず見つかる(番兵で停止) */
    return ___②___;              /* 番兵で止まったら見つからなかった */
}

整列済みの配列が前提。中央と比較して、探索範囲を半分ずつに絞る。

int search( elementtype x, elementtype a[], int N ){
    int l, r, m;
    l = 0; r = N-1;
    while ( l < r ){
        m = ___①___;              /* 中央を求める */
        if ( x < a[m] ){
            r = ___②___;          /* 前半側を探索 */
        } else if ( x > a[m] ){
            l = ___③___;          /* 後半側を探索 */
        } else {
            l = r = m;             /* 見つかった → 範囲を収束 */
        }
    }
    return ( l == r && a[l] == x );
}

問19: 内挿探索 interpolation search(挑戦)

二分探索の「中央」を、値の分布から推定した位置に変える。データが一様分布していると効率が良い。

int search( elementtype x, elementtype a[], int N ){
    int l, r, m;
    l = 0; r = N-1;
    while ( l < r ){
        m = ___①___;   /* 内挿:値の比から位置を推定 */
        if ( x < a[m] ){
            r = m - 1;
        } else if ( x > a[m] ){
            l = m + 1;
        } else {
            l = r = m;
        }
    }
    return ( l == r && a[l] == x );
}

付録: ソート・探索の計算量まとめ

アルゴリズム最良平均最悪安定備考
選択ソートO(n²)O(n²)O(n²)不安定移動が少ない
挿入ソートO(n)O(n²)O(n²)安定ほぼ整列済みで最強
バブルソートO(n)O(n²)O(n²)安定交換回数が初期状態依存
シェルソート-O(n^3/2)O(n^3/2)不安定挿入ソートの改良
クイックソートO(n log n)O(n log n)O(n²)不安定pivot 次第
ヒープソートO(n log n)O(n log n)O(n log n)不安定in-place
分布数え上げO(n)O(n)O(n)安定キーの範囲が必要
基数整列O(n)O(n)O(n)安定ビット列で分割
マージソートO(n log n)O(n log n)O(n log n)安定作業配列が必要
線形探索O(1)O(n)O(n)-整列不要
二分探索O(1)O(log n)O(log n)-整列済みが前提
内挿探索O(1)O(log log n)O(n)-一様分布を仮定