HOWTO · Java

Java で sort()メソッドを使わずに配列をソートする方法

sort()メソッドを使用せずに Java で配列をソートする方法を学ぶ

このページの内容

Java の配列は、Arrays.sort() を呼び出さなくても、要素を比較して自分で順番を入れ替えればソートできます。ソートの仕組みを学ぶときや、課題で手動実装が求められるときに役立ちます。Java の Arrays.toString() は配列を表示できますが、ソートはしません。

以下の例では、整数配列を昇順に並べ替えます。バブルソート、選択ソート、挿入ソートは動きを追いやすく、マージソートとクイックソートでは再帰的な分割統治の方法を紹介します。各コード例は単体で実行できる Java プログラムです。提供された実行証拠によると、5つの例はいずれも [11, 12, 22, 25, 34, 64, 90] という同じ結果になります。

Java で Arrays.sort() を使わずに配列をソートする

ソートアルゴリズムは、配列の要素が目的の順序になるまで、その位置を変更します。整数を昇順に並べる基本的な方法は、値を比較し、小さい値を大きい値より前に移動することです。以下のアルゴリズムは、それぞれ異なる方法でこの処理を行います。マージソートの実装ではマージ時に一時配列を使いますが、それ以外は入力配列をその場で変更します。

例として、{64, 34, 25, 12, 22, 11, 90} を {11, 12, 22, 25, 34, 64, 90} に並べ替えることを考えます。各実装では Arrays.sort() を意図的に使用していません。Arrays.toString() は配列を表示用の形式に整えるためだけに使います。

バブルソート

バブルソートは、隣り合う要素の組を繰り返し走査します。左側の値が右側より大きければ、両者を入れ替えます。1回の走査が終わると、未整列部分に残っている最大値がその末尾へ移動します。走査中に一度も入れ替えがなければ、swapped フラグによってループを早く終了します。最悪の場合の計算量は O(n²) なので、この実装は小さな入力やアルゴリズムの学習に向いています。

各コード例では Main というクラスを宣言しています。例を1つずつ Main.java として保存して実行してください。この例を実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

import java.util.Arrays;

public class Main {
  public static void main(String[] args) {
    int[] values = {64, 34, 25, 12, 22, 11, 90};
    bubbleSort(values);
    System.out.println(Arrays.toString(values));
  }

  static void bubbleSort(int[] values) {
    for (int end = values.length - 1; end > 0; end--) {
      boolean swapped = false;
      for (int i = 0; i < end; i++) {
        if (values[i] > values[i + 1]) {
          int temp = values[i];
          values[i] = values[i + 1];
          values[i + 1] = temp;
          swapped = true;
        }
      }
      if (!swapped) {
        break;
      }
    }
  }
}
[11, 12, 22, 25, 34, 64, 90]

選択ソート

選択ソートでは、配列を概念上、整列済みの先頭部分と残りの未整列部分に分けます。各走査で未整列部分から最小値を探し、それを未整列部分の先頭にある値と入れ替えます。たとえば、最初の走査では 11 を見つけてインデックス 0 に移し、次の走査では 12 を見つけてインデックス 1 に移します。初期状態にかかわらず比較回数は O(n²) ですが、入れ替えの回数は線形です。

このセクションのコードと表示されるラベルや出力は英語のままです。

import java.util.Arrays;

public class Main {
  public static void main(String[] args) {
    int[] values = {64, 34, 25, 12, 22, 11, 90};
    selectionSort(values);
    System.out.println(Arrays.toString(values));
  }

  static void selectionSort(int[] values) {
    for (int i = 0; i < values.length - 1; i++) {
      int minIndex = i;
      for (int j = i + 1; j < values.length; j++) {
        if (values[j] < values[minIndex]) {
          minIndex = j;
        }
      }
      int temp = values[i];
      values[i] = values[minIndex];
      values[minIndex] = temp;
    }
  }
}
[11, 12, 22, 25, 34, 64, 90]

挿入ソート

挿入ソートは、左から右へ整列済みの先頭部分を広げていきます。次の値を key に保存し、先頭部分にあるそれより大きな値を1つ右へずらしてから、空いた位置に key を入れます。各走査の開始時点で、i より前の要素はすでに整列済みです。動きを追いやすく、小さな配列やほぼ整列済みの配列では有効な場合がありますが、最悪の場合の計算量は O(n²) です。

このセクションのコードと表示されるラベルや出力は英語のままです。

import java.util.Arrays;

public class Main {
  public static void main(String[] args) {
    int[] values = {64, 34, 25, 12, 22, 11, 90};
    insertionSort(values);
    System.out.println(Arrays.toString(values));
  }

  static void insertionSort(int[] values) {
    for (int i = 1; i < values.length; i++) {
      int key = values[i];
      int j = i - 1;
      while (j >= 0 && values[j] > key) {
        values[j + 1] = values[j];
        j--;
      }
      values[j + 1] = key;
    }
  }
}
[11, 12, 22, 25, 34, 64, 90]

マージソート

マージソートは配列を再帰的に半分ずつ分割し、各部分が1つの値になるまで続けた後、整列済みの部分配列をマージします。マージ中は2つのインデックスで各半分の次の未使用値を追跡し、小さい方の値を先に結果へコピーします。値が等しい場合に左側の値を選ぶため、元の相対順序が保たれます。この実装の計算量は O(n log n) で、再帰中に一時配列を割り当てるため、入力サイズに比例する追加メモリを使います。

このセクションのコードと表示されるラベルや出力は英語のままです。

import java.util.Arrays;

public class Main {
  public static void main(String[] args) {
    int[] values = {64, 34, 25, 12, 22, 11, 90};
    mergeSort(values);
    System.out.println(Arrays.toString(values));
  }

  static void mergeSort(int[] values) {
    if (values.length < 2) {
      return;
    }
    int middle = values.length / 2;
    int[] left = Arrays.copyOfRange(values, 0, middle);
    int[] right = Arrays.copyOfRange(values, middle, values.length);
    mergeSort(left);
    mergeSort(right);
    merge(values, left, right);
  }

  static void merge(int[] values, int[] left, int[] right) {
    int i = 0, j = 0, k = 0;
    while (i < left.length && j < right.length) {
      if (left[i] <= right[j]) {
        values[k++] = left[i++];
      } else {
        values[k++] = right[j++];
      }
    }
    while (i < left.length) values[k++] = left[i++];
    while (j < right.length) values[k++] = right[j++];
  }
}
[11, 12, 22, 25, 34, 64, 90]

クイックソート

クイックソートはピボットを選び、現在の範囲を分割して、ピボットより小さい値を前に、それ以外の値を後ろに配置します。この実装では範囲の最後の値をピボットにします。分割処理はピボットの最終インデックスを返し、再帰呼び出しでその左右の範囲をそれぞれソートします。クイックソートの計算量は通常 O(n log n) ですが、すでに整列済みのデータなど、入力によってはこのピボット選択で O(n²) になることがあります。再帰呼び出しにはスタック領域も必要です。

このセクションのコードと表示されるラベルや出力は英語のままです。

import java.util.Arrays;

public class Main {
  public static void main(String[] args) {
    int[] values = {64, 34, 25, 12, 22, 11, 90};
    quickSort(values, 0, values.length - 1);
    System.out.println(Arrays.toString(values));
  }

  static void quickSort(int[] values, int low, int high) {
    if (low >= high) {
      return;
    }
    int pivotIndex = partition(values, low, high);
    quickSort(values, low, pivotIndex - 1);
    quickSort(values, pivotIndex + 1, high);
  }

  static int partition(int[] values, int low, int high) {
    int pivot = values[high];
    int smallerEnd = low - 1;
    for (int current = low; current < high; current++) {
      if (values[current] < pivot) {
        smallerEnd++;
        int temp = values[smallerEnd];
        values[smallerEnd] = values[current];
        values[current] = temp;
      }
    }
    int pivotIndex = smallerEnd + 1;
    int temp = values[pivotIndex];
    values[pivotIndex] = values[high];
    values[high] = temp;
    return pivotIndex;
  }
}
[11, 12, 22, 25, 34, 64, 90]

アルゴリズムの選び方とトレードオフ

学習や小さな配列のソートでは、バブルソート、選択ソート、挿入ソートを使うと比較や値の移動を確認しやすくなります。ただし、計算量が二乗で増えるため、大きな入力には適しません。挿入ソートは、配列が小さい場合や、ほぼ整列済みの場合に向いています。選択ソートは、各走査で最大1回しか値を入れ替えないため、書き込み回数を抑えたい場合に使えることがあります。ただし、毎回残りの値を走査する必要があります。

マージソートは計算量が O(n log n) と予測しやすく、安定した順序が必要な場合に適していますが、この実装では一時配列を割り当てます。クイックソートは一般に高速で、元の配列内でソートします。一方、この末尾要素をピボットにする実装には、最悪時の二乗の計算量と再帰によるスタック使用があります。本番コードでは、手動アルゴリズムが明示的に必要でない限り、Java の標準ライブラリのソートを使うのが適切です。通常のプリミティブ配列のソートには Arrays.sort() を使い、手動アルゴリズムの動作や制約が課題となる場合に、その仕組みを学んだり実装したりしてください。