Haskell と C言語でバブルソートを比較

Haskell

非常に簡潔に書けるが、再帰を用いており慣れないと理解しづらい。

-- リストの最小値を先頭に上げる
bubble [x] = [x] -- 要素1個ならそのまま
bubble (x:xs)
    | x > y     = y:x:ys -- 1個目が2個目以降の最小値より大きければ入れ替える
    | otherwise = x:y:ys -- そうでなければ入れ替えない
    where
        (y:ys) = bubble(xs) -- 2個目以降の最小値を先頭に

-- バブルソート
bsort [x] = [x]          -- 要素1個ならそのまま
bsort xs  = y : bsort ys -- 最小値を先頭に、残りをさらにバブルソート
    where
        (y:ys) = bubble(xs) -- 最小値を先頭に

main = do
    print $ bsort [4, 6, 9, 8, 0, 3, 5, 1, 7, 2]

C言語 (再帰)

同じことをC言語で書くと長くなる。

#include <stdio.h>
#define SWAP(x, y) {int _x = x; x = y; y = _x;}

// 配列の最小値を先頭に上げる
void bubble(int x[], int n)
{
    if (n == 1) {
        return; // 要素1個ならそのまま
    } else {
        // 2個目以降の最小値を先頭に
        bubble(&x[1], n - 1);
        // 1個目が2個目以降の最小値より大きければ入れ替える
        if (x[0] > x[1]) SWAP(x[0], x[1]);
    }
}

// バブルソート
void bsort(int x[], int n)
{
    if (n == 1) {
        return; // 要素1個ならそのまま
    } else {
        // 最小値を先頭に
        bubble(x, n);
        // 残りをバブルソート
        bsort(&x[1], n - 1);
    }
}

void print_array(int x[], int n)
{
    for (int i = 0; i < n; i++) printf("%d ", x[i]);
    printf("\n");
}

int main(void)
{
    int x[] = { 4, 6, 9, 8, 0, 3, 5, 1, 7, 2 };
    int n = sizeof(x) / sizeof(x[0]);
    print_array(x, n);

    bsort(x, n);

    print_array(x, n);
    return 0;
}

C言語 (ループ)

しかし、C言語であれば再帰など使わずループで書いた方がはるかに簡潔で理解しやすい。

#include <stdio.h>
#define SWAP(x, y) {int _x = x; x = y; y = _x;}

// バブルソート
void bsort(int x[], int n)
{
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (x[j] > x[j + 1]) SWAP(x[j], x[j + 1]);
        }
    }
}

void print_array(int x[], int n)
{
    for(int i = 0; i < n; i++) printf("%d ", x[i]);
    printf("\n");
}

int main(void)
{
    int x[] = { 4, 6, 9, 8, 0, 3, 5, 1, 7, 2 };
    int n = sizeof(x) / sizeof(x[0]);
    print_array(x, n);

    bsort(x, n);
    
    print_array(x, n);
    return 0;
}

再帰とループのパフォーマンス比較

上記のC言語バブルソートで1000個のデータをソートしたときの処理時間を筆者の環境で計測した。

  • 再帰:11.6msec
  • ループ: 1.0msec

またデータ数を5,000個にすると再帰ではスタックオーバーフロー(スタックサイズ=1MB)が発生した。当然ながら、ループではデータ数 100,000個でもスタックオーバーフローは生じなかった。(ただし両者ともデータはスタックに置かずグローバル変数とした。)

所感

ループは理解が容易なのに対し、再帰はプログラミング初心者がつまづくポイントの一つである。まして上記のバブルソートの例のように再帰が二重になっていると、筆者のポンコツな脳ミソでは直感的に実行順序が分からない。もちろん、再帰を使わないと難しい処理もある。ディレクトリ探索とかフラクタル図形描画とか。だから再帰は必須のテクニックではある。しかしループで簡単に実現できる処理ならループを使えばいいじゃないかと思う。また処理速度やメモリ使用量においてもループのほうが断然有利である。