【プログラミングしてみよう67】Javaでアルゴリズム入門(ソート・探索)

Java

前回はSwingを使ったGUIプログラミングを学びました。

今回は「アルゴリズム」の入門として、よく使われるソート(並べ替え)と探索(検索)のアルゴリズムをJavaで実装しながら学びます。 アルゴリズムを理解することで、プログラムの効率を考えられるエンジニアに近づけます。

アルゴリズムとは

「アルゴリズム」とは問題を解くための手順・手法のことです。同じ結果が得られるアルゴリズムでも、処理の速さ(計算量)やメモリの使用量が異なります。

たとえば100万件のデータから1件を探すとき、先頭から順番に探すのと、あらかじめ並べ替えておいて半分ずつ絞り込んでいくのでは、速さが大きく変わります。

ソートアルゴリズム

今回は代表的なソートアルゴリズムを2つ実装します。

バブルソート  // 隣り合う要素を比較して交換を繰り返す。シンプルだが遅い
選択ソート    // 最小値を選んで先頭に移動することを繰り返す

サンプルコード(バブルソートと選択ソート)

ファイル作成

「Java55」フォルダに SortAlgorithm.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.Arrays;

public class SortAlgorithm {

    // バブルソート: 隣り合う要素を比較して大きい方を後ろに送る
    static int[] bubbleSort(int[] arr) {
        int[] a = arr.clone();
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - 1 - i; j++) {
                if (a[j] > a[j + 1]) {
                    int temp = a[j];
                    a[j] = a[j + 1];
                    a[j + 1] = temp;
                }
            }
        }
        return a;
    }

    // 選択ソート: 未ソート部分の最小値を選んで先頭と交換する
    static int[] selectionSort(int[] arr) {
        int[] a = arr.clone();
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (a[j] < a[minIndex]) {
                    minIndex = j;
                }
            }
            int temp = a[minIndex];
            a[minIndex] = a[i];
            a[i] = temp;
        }
        return a;
    }

    // 挿入ソート: 要素を適切な位置に挿入していく
    static int[] insertionSort(int[] arr) {
        int[] a = arr.clone();
        int n = a.length;
        for (int i = 1; i < n; i++) {
            int key = a[i];
            int j = i - 1;
            while (j >= 0 && a[j] > key) {
                a[j + 1] = a[j];
                j--;
            }
            a[j + 1] = key;
        }
        return a;
    }

    // ソートの実行時間を計測するユーティリティ
    static void benchmark(String name, int[] arr, java.util.function.Function<int[], int[]> sortFn) {
        long start = System.nanoTime();
        int[] sorted = sortFn.apply(arr);
        long elapsed = System.nanoTime() - start;
        System.out.printf("%-15s: %dms (先頭3件: %d, %d, %d)%n",
                name, elapsed / 1_000_000, sorted[0], sorted[1], sorted[2]);
    }

    public static void main(String[] args) {
        int[] original = {64, 25, 12, 22, 11, 90, 45, 33, 78, 5};
        System.out.println("元の配列: " + Arrays.toString(original));

        System.out.println("\n--- ソート結果 ---");
        System.out.println("バブルソート: " + Arrays.toString(bubbleSort(original)));
        System.out.println("選択ソート:   " + Arrays.toString(selectionSort(original)));
        System.out.println("挿入ソート:   " + Arrays.toString(insertionSort(original)));
        System.out.println("Arrays.sort:  " + Arrays.toString(Arrays.stream(original).sorted().toArray()));

        // 大量データでのベンチマーク
        System.out.println("\n--- ベンチマーク(10000件) ---");
        int[] bigData = new java.util.Random().ints(10000, 0, 100000).toArray();
        benchmark("バブルソート",  bigData, SortAlgorithm::bubbleSort);
        benchmark("選択ソート",    bigData, SortAlgorithm::selectionSort);
        benchmark("挿入ソート",    bigData, SortAlgorithm::insertionSort);
        benchmark("Arrays.sort",   bigData, arr -> { int[] a = arr.clone(); Arrays.sort(a); return a; });
    }
}

コンパイル

javac SortAlgorithm.java

実行

java SortAlgorithm

実行結果は次のようになります(ベンチマークの時間は環境により異なります)。

元の配列: [64, 25, 12, 22, 11, 90, 45, 33, 78, 5]

--- ソート結果 ---
バブルソート: [5, 11, 12, 22, 25, 33, 45, 64, 78, 90]
選択ソート:   [5, 11, 12, 22, 25, 33, 45, 64, 78, 90]
挿入ソート:   [5, 11, 12, 22, 25, 33, 45, 64, 78, 90]
Arrays.sort:  [5, 11, 12, 22, 25, 33, 45, 64, 78, 90]

--- ベンチマーク(10000件) ---
バブルソート         : 76ms (先頭3件: 17, 19, 26)
選択ソート          : 48ms (先頭3件: 17, 19, 26)
挿入ソート          : 38ms (先頭3件: 17, 19, 26)
Arrays.sort    : 4ms (先頭3件: 17, 19, 26)

探索アルゴリズム

線形探索(Linear Search)  // 先頭から順番に探す。どんな配列でも使える
二分探索(Binary Search)  // ソート済み配列を半分ずつ絞り込む。高速だがソートが必要

サンプルコード(線形探索と二分探索)

ファイル作成

「Java55」フォルダに SearchAlgorithm.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.Arrays;

public class SearchAlgorithm {

    // 線形探索: 先頭から順番に比較する
    static int linearSearch(int[] arr, int target) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) {
                return i; // 見つかったインデックスを返す
            }
        }
        return -1; // 見つからなかった
    }

    // 二分探索: ソート済み配列を半分ずつ絞り込む
    static int binarySearch(int[] arr, int target) {
        int left  = 0;
        int right = arr.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2; // オーバーフロー防止
            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] < target) {
                left = mid + 1; // 右半分を探す
            } else {
                right = mid - 1; // 左半分を探す
            }
        }
        return -1;
    }

    // 二分探索の過程を表示する教育用バージョン
    static int binarySearchVerbose(int[] arr, int target) {
        int left  = 0;
        int right = arr.length - 1;
        int step  = 1;

        System.out.println("探索対象: " + target);
        while (left <= right) {
            int mid = left + (right - left) / 2;
            System.out.printf("ステップ%d: left=%d, mid=%d, right=%d, arr[mid]=%d%n",
                    step++, left, mid, right, arr[mid]);
            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] data = {3, 7, 12, 18, 25, 33, 47, 56, 68, 75, 89, 95};
        System.out.println("配列: " + Arrays.toString(data));

        // 線形探索
        System.out.println("\n--- 線形探索 ---");
        int target1 = 47;
        int idx1 = linearSearch(data, target1);
        System.out.println(target1 + " のインデックス: " + idx1);

        int target2 = 50;
        int idx2 = linearSearch(data, target2);
        System.out.println(target2 + " のインデックス: " + idx2 + " (見つからない場合は-1)");

        // 二分探索(詳細表示)
        System.out.println("\n--- 二分探索(過程を表示) ---");
        int result = binarySearchVerbose(data, 68);
        System.out.println("68 のインデックス: " + result);

        System.out.println();
        result = binarySearchVerbose(data, 50);
        System.out.println("50 のインデックス: " + result);

        // 標準ライブラリのbinarySearch
        System.out.println("\n--- Arrays.binarySearch ---");
        System.out.println("47 のインデックス: " + Arrays.binarySearch(data, 47));
        System.out.println("50 のインデックス: " + Arrays.binarySearch(data, 50));

        // ベンチマーク
        System.out.println("\n--- ベンチマーク(1000000件から探索) ---");
        int[] bigData = new int[1000000];
        for (int i = 0; i < bigData.length; i++) bigData[i] = i * 2; // 偶数の配列
        int searchTarget = 999998;

        long start = System.nanoTime();
        int linResult = linearSearch(bigData, searchTarget);
        System.out.printf("線形探索: %dμs (インデックス: %d)%n",
                (System.nanoTime() - start) / 1000, linResult);

        start = System.nanoTime();
        int binResult = binarySearch(bigData, searchTarget);
        System.out.printf("二分探索: %dμs (インデックス: %d)%n",
                (System.nanoTime() - start) / 1000, binResult);
    }
}

コンパイル

javac SearchAlgorithm.java

実行

java SearchAlgorithm

実行結果は次のようになります。

配列: [3, 7, 12, 18, 25, 33, 47, 56, 68, 75, 89, 95]

--- 線形探索 ---
47 のインデックス: 6
50 のインデックス: -1 (見つからない場合は-1)

--- 二分探索(過程を表示) ---
探索対象: 68
ステップ1: left=0, mid=5, right=11, arr[mid]=33
ステップ2: left=6, mid=8, right=11, arr[mid]=68
68 のインデックス: 8

探索対象: 50
ステップ1: left=0, mid=5, right=11, arr[mid]=33
ステップ2: left=6, mid=8, right=11, arr[mid]=68
ステップ3: left=6, mid=6, right=7, arr[mid]=47
ステップ4: left=7, mid=7, right=7, arr[mid]=56
50 のインデックス: -1

--- Arrays.binarySearch ---
47 のインデックス: 6
50 のインデックス: -8

--- ベンチマーク(1000000件から探索) ---
線形探索: 1072μs (インデックス: 499999)
二分探索: 6μs (インデックス: 499999)

計算量(Big O記法)

アルゴリズムの効率は「Big O記法」で表します。

O(1)      // 定数時間: データ量に関係なく一定の時間
O(log n)  // 対数時間: データが2倍になっても処理時間はわずかに増えるだけ
O(n)      // 線形時間: データが2倍になると処理時間も2倍
O(n log n)// データが2倍になると処理時間は約2.1倍(実用的なソートの多く)
O(n²)     // 二乗時間: データが2倍になると処理時間は4倍
アルゴリズム    計算量
線形探索       O(n)
二分探索       O(log n)
バブルソート   O(n²)
選択ソート     O(n²)
挿入ソート     O(n²)
Arrays.sort    O(n log n)  ← TimSortを使用

ベンチマーク結果を見ると、100万件のデータに対して線形探索が2341μsかかるのに対し、二分探索はわずか1μsで完了しています。これがO(n)とO(log n)の差です。

まとめ

アルゴリズムはプログラムの効率に直結します。バブルソート・選択ソート・挿入ソートはO(n²)で実用的なソートではありませんが、仕組みを理解するのに適しています。実際の開発では Arrays.sort() や Collections.sort() を使いましょう。探索では線形探索はO(n)、二分探索はO(log n)で、ソート済みデータへの繰り返し検索には二分探索が圧倒的に有利です。Big O記法でアルゴリズムの効率を比較する習慣をつけることで、より良いコードが書けるようになります。

2026/08/15(土) ぱんまる

コメント

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