【基本情報技術者試験】線形探索・二分探索で比べる C・Go・Kotlin ~「探し方」の違いが書き方にも表れる~

スポンサーリンク
C言語

はじめに

1回目の記事ではバブルソートのトレースを、2回目の記事では線形探索・二分探索のトレースを、それぞれC言語で確認した。3回目の記事では、1回目で書いたバブルソートをGo・Kotlinに移植して書き比べた。4回目となる今回は、2回目で書いた線形探索・二分探索を、GoとKotlinに移植した

バブルソートのときと同様、アルゴリズムのロジックは3言語ともまったく同じである。ただ今回は「探索」というテーマ上、配列の走査の仕方や分岐の書き方に、言語ごとの個性がよりはっきり出てくる。

3言語のコード

C言語版(おさらい)

int linear_search(int arr[], int size, int target) {
    int comparisons = 0;

    for (int i = 0; i < size; i++) {
        comparisons++;
        printf("  比較%d回目: arr[%d] = %d と %d を比較\n", comparisons, i, arr[i], target);

        if (arr[i] == target) {
            printf("  → 見つかりました(比較回数: %d回)\n", comparisons);
            return i;
        }
    }

    printf("  → 見つかりませんでした(比較回数: %d回)\n", comparisons);
    return -1;
}

int binary_search(int arr[], int size, int target) {
    int low = 0;
    int high = size - 1;
    int comparisons = 0;

    while (low <= high) {
        int mid = (low + high) / 2;
        comparisons++;

        printf("  比較%d回目: low=%d, high=%d, mid=%d (arr[mid]=%d) と %d を比較\n",
               comparisons, low, high, mid, arr[mid], target);

        if (arr[mid] == target) {
            printf("  → 見つかりました(比較回数: %d回)\n", comparisons);
            return mid;
        } else if (arr[mid] < target) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }

    printf("  → 見つかりませんでした(比較回数: %d回)\n", comparisons);
    return -1;
}

Go版

func linearSearch(arr []int, target int) int {
    comparisons := 0

    for i, v := range arr {
        comparisons++
        fmt.Printf("  比較%d回目: arr[%d] = %d と %d を比較\n", comparisons, i, v, target)

        if v == target {
            fmt.Printf("  → 見つかりました(比較回数: %d回)\n", comparisons)
            return i
        }
    }

    fmt.Printf("  → 見つかりませんでした(比較回数: %d回)\n", comparisons)
    return -1
}

func binarySearch(arr []int, target int) int {
    low := 0
    high := len(arr) - 1
    comparisons := 0

    for low <= high {
        mid := (low + high) / 2
        comparisons++

        fmt.Printf("  比較%d回目: low=%d, high=%d, mid=%d (arr[mid]=%d) と %d を比較\n",
            comparisons, low, high, mid, arr[mid], target)

        if arr[mid] == target {
            fmt.Printf("  → 見つかりました(比較回数: %d回)\n", comparisons)
            return mid
        } else if arr[mid] < target {
            low = mid + 1
        } else {
            high = mid - 1
        }
    }

    fmt.Printf("  → 見つかりませんでした(比較回数: %d回)\n", comparisons)
    return -1
}

Kotlin版

fun linearSearch(arr: IntArray, target: Int): Int {
    var comparisons = 0

    for (i in arr.indices) {
        comparisons++
        println("  比較${comparisons}回目: arr[$i] = ${arr[i]} と $target を比較")

        if (arr[i] == target) {
            println("  → 見つかりました(比較回数: ${comparisons}回)")
            return i
        }
    }

    println("  → 見つかりませんでした(比較回数: ${comparisons}回)")
    return -1
}

fun binarySearch(arr: IntArray, target: Int): Int {
    var low = 0
    var high = arr.size - 1
    var comparisons = 0

    while (low <= high) {
        val mid = (low + high) / 2
        comparisons++

        println("  比較${comparisons}回目: low=$low, high=$high, mid=$mid (arr[mid]=${arr[mid]}) と $target を比較")

        when {
            arr[mid] == target -> {
                println("  → 見つかりました(比較回数: ${comparisons}回)")
                return mid
            }
            arr[mid] < target -> low = mid + 1
            else -> high = mid - 1
        }
    }

    println("  → 見つかりませんでした(比較回数: ${comparisons}回)")
    return -1
}

比較ポイント①:配列を「なめる」書き方

線形探索は配列の先頭から1つずつ見ていくだけの単純な処理だが、その「1つずつ見ていく」ループの書き方に言語ごとの個性が出る。

言語書き方特徴
Cfor (int i = 0; i < size; i++) + arr[i]添字を自分で管理し、都度 arr[i] で値を取り出す
Gofor i, v := range arr添字 i と値 v を同時に取得できる。arr[i] の書き直しが不要
Kotlinfor (i in arr.indices)arr.indices で「有効な添字の範囲」だけを表現できる

バブルソート編でも触れたとおり、Cのように自分で添字を管理する書き方は基本に忠実な分、記述量は増える。Go/Kotlinはどちらも「添字管理をどう楽にするか」という方向性は同じだが、Goは値まで一緒に取り出せる range、Kotlinは添字の範囲だけを取り出す indices と、アプローチが少し異なるのが面白いところである。

比較ポイント②:二分探索の分岐の書き方

二分探索は「一致」「対象より小さい」「対象より大きい」の3パターンに分岐する処理である。ここは3言語で最も書き味の違いが出る部分だ。

  • Cif (arr[mid] == target) { ... } else if (arr[mid] < target) { ... } else { ... } という素直な if-else の連なり
  • Go:Cとほぼ同じ if-else if-else の形。Cが読めればそのまま読める
  • Kotlinwhen { } という式で3パターンをまとめて書ける。if-else の連なりよりも「複数条件のうちどれか」という意図が伝わりやすい

二分探索のように分岐条件が3つ以上並ぶ処理では、Kotlinの when のような「多分岐専用の構文」があるとコードの見通しがよくなる、という一つの例になっている。

比較ポイント③:探索の様子を表示する書き方

今回のコードは、比較回数や探索範囲(lowhighmid)を毎回出力して、探索の過程を目に見える形にしている。この「値を文章に埋め込んで表示する」処理も、言語ごとに書き方が異なる。

  • Cprintf("...%d...", comparisons, i, arr[i], target) のように、フォーマット指定子と変数の対応を自分で数えながら書く
  • Gofmt.Printf("...%d...", ...) とCとほぼ同じ書き方。関数名が Printf(先頭大文字)になる点だけ違う
  • Kotlin" 比較${comparisons}回目: ..." のように、文字列の中に直接変数を埋め込む文字列テンプレートを使う

C・Goはどちらも「フォーマット文字列+変数リスト」という同じ発想だが、Kotlinは変数を文字列の中に直接書けるため、「どの値がどこに入るか」を目で追いやすいという違いがある。特に今回のように出力する値の数が多い処理では、この違いが体感しやすいはずだ。

まとめ

  • アルゴリズムのロジック(線形探索・二分探索とも)は3言語で完全に同じ
  • 配列を舐めるループの書き方は、Go(range)・Kotlin(indices)でそれぞれ異なる楽さがある
  • 多分岐の処理は、Kotlinの when のような専用構文があると見通しがよくなる
  • 値の出力方法も、C/Goの「フォーマット文字列型」とKotlinの「文字列テンプレート型」で発想が異なる

バブルソート編・線形探索/二分探索編と2つのアルゴリズムを3言語で書き比べてみて、「ロジックは同じでも、言語が用意している道具(構文)によって書き味が変わる」という感覚がよりはっきりとしてきた。今後も、Cで学んだ他のアルゴリズムの多言語比較を続けていく予定である。


この記事で扱ったコードは、GitHubの c-language-studies リポジトリ(FE-algorithm/02_search/go/FE-algorithm/02_search/kotlin/)で公開している。

コメント

タイトルとURLをコピーしました