【Java】シェルソート
シェルソートとは
シェルソートとは、挿入ソートの改良版であり、大きな配列を小さな間隔列に分割し、それぞれの間隔列を挿入ソートでソートします。その後、間隔列の間隔を縮小しながら間隔が1になるまで再度ソートを行います。
シェルソートの性能は間隔列に大きく依存し、間隔列の選択によってソートの速度が大きく変わります。しかし、間隔列の最適解は未解決です。
シェルソートは、挿入ソート・選択ソート・バブルソートよりも効率的なソートアルゴリズムであり、比較的に実装が簡単であり、数千から数万の要素を持つ中程度のサイズのデータセットに対して効果的です。
問題の内容
- 問題
- サイズ
n
の数列a
をサイズk
の間隔列h
でシェルソートせよ。
n = 10 a = 7 6 10 2 5 4 8 3 9 1 k = 2 h = 4 1
- サイズ
言語によるシェルソートアルゴリズム
数列a
と間隔列h
は下記の通りです。
a: 7 6 10 2 5 4 8 3 9 1
h: 4 1
最初の間隔である4
を使って、数列a
を部分的にソートします。つまり、4個おきに要素をグループ分けして、それぞれのグループ内で挿入ソートを行います。
グループ1: 7, 5, 9
グループ2: 6, 4, 1
グループ3: 10, 8
グループ4: 2, 3
これらのグループ内で挿入ソートを行うと、以下のようになります。
グループ1: 5, 7, 9
グループ2: 1, 4, 6
グループ3: 8, 10
グループ4: 2, 3
それぞれのグループを元の位置に戻すと、数列は以下のようになります。
a = 5, 1, 8, 2, 7, 4, 10, 3, 9, 6
次に、間隔1
を使って、数列a
を部分的にソートします。これは通常の挿入ソートになります。
a = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
これで、数列a
は完全にソートされました。
間隔列h
に基づいて、部分的にソートされた配列が生成され、最後のステップでは通常の挿入ソートが適用されました。
コードによるシェルソートアルゴリズム
- 解答方針
- 間隔
h
は、h[0]
からh[k-1]
まで -
h
からn-1
までのインデックスi
について、a[i]
を挿入 - アルゴリズムにより
a[i]
が消えてしまうので、a[i]
を変数x
に保存 - 変数
x
を入れるインデックスを探すための変数j
をi-h
で初期化 -
a[j] > x
である間、a[j]
をh
だけ右にずらす -
a[j + h]
にx
を代入
- 間隔
シェルソートのコード例
import java.util.*;
public class Main {
static void insertionSort(int[] a, int n, int h) {
int numOfMove = 0;
// インデックス i が h から n - 1 までのループ
for (int i = h; i < n; i++) {
// 要素 a[i] を x に保存する
int x = a[i];
// 変数 x を入れるインデックスを探すための変数 j = i-h で初期化
int j = i - h;
// インデックス j が 0 以上で、要素 a[j] が x より大きい間
while (j >= 0 && a[j] > x) {
// 要素 a[j] を h だけ右にずらす
a[j + h] = a[j];
// インデックス j を h だけ減らす
j -= h;
numOfMove++;
}
// 要素 a[j + h] に x を代入する
a[j + h] = x;
}
System.out.println(numOfMove);
}
static void shellSort(int[] a, int n, int[] h, int k) {
for (int i = 0; i < k; i++) {
// 間隔 h の挿入ソートを呼び出す
insertionSort(a, n, h[i]);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
int k = sc.nextInt();
int[] h = new int[k];
for (int i = 0; i < k; i++) {
h[i] = sc.nextInt();
}
shellSort(a, n, h, k);
}
}
シェルソートアルゴリズムの解説
シェルソートアルゴリズムに該当する下記の部分のみを解説します。
static void insertionSort(int[] a, int n, int h) {
int numOfMove = 0;
for (int i = h; i < n; i++) {
int x = a[i];
int j = i - h;
while (j >= 0 && a[j] > x) {
a[j + h] = a[j];
j -= h;
numOfMove++;
}
a[j + h] = x;
}
System.out.println(numOfMove);
}
static void shellSort(int[] a, int n, int[] h, int k) {
for (int i = 0; i < k; i++) {
insertionSort(a, n, h[i]);
}
}
仮引数a
、n
、h
、k
は下記の通りになります。
a = [7, 6, 10, 2, 5, 4, 8, 3, 9, 1]
n = 10
h = [4, 1]
k = 2
shellSortメソッド
まずは、shellSort
メソッドについて解説します。
-
for
文の条件式i=0 < k=2
を満たすので、 -
insertionSort
メソッドを呼び出し、実引数としてa
、n
、h[0]=4
を渡します。 -
i
を1
増やして、for
分の条件式i=1 < k=2
を満たすので、 -
insertionSort
メソッドを呼び出し、実引数としてa
、n
、h[1]=1
を渡します。 -
i
を1
増やして、for
分の条件式i=2 < k=2
を満たさないので、shellSort
メソッドを終了します。
要は、ここでは、間隔4
でinsertionSort
メソッドを呼び出し、次に、間隔1
でinsertionSort
メソッドを呼び出しています。
insertionSortメソッド
上記の通りinsertionSort
メソッドは2回呼び出されますので、2回に分けて解説します。
1回目のinsertionSortメソッド呼び出し
1回目のinsertionSortメソッド呼び出しは、シェルソートそのものと言っても過言ではありません。
仮引数は下記の通りです。
a = [7, 6, 10, 2, 5, 4, 8, 3, 9, 1]
n = 10
h = 4
- 変数
numOfMove
を0
で初期化します。 -
for
文の条件式i=4 < n=10
を満たすので、 - 変数
x
にa[4]
の5
を代入し保存します。 - 変数
j
にi=4 - h=4
の0
を代入します。 -
while
文の条件式j=0 >= 0
かつa[0]=7 > 5
を満たすので、 -
a[4]
にa[0]
の7
を代入します。 - 変数
j
にj=0 - h=4
の-4
を代入し更新します。 -
numOfMove
を1
に更新します。 -
while
文の条件式j=-4 >= 0
を満たさないので、このループを抜けます。 -
a[0]
に変数x
の5
を代入します。
この時の配列a
は下記の通りです。a = [5, 6, 10, 2, 7, 4, 8, 3, 9, 1]
-
i
に1
を加えて、for
文の条件式i=5 < n=10
を満たすので、 - 変数
x
にa[5]
の4
を代入し保存します。 - 変数
j
にi=5 - h=4
の1
を代入します。 -
while
文の条件式j=1 >= 0
かつa[1]=6 > 4
を満たすので、 -
a[5]
にa[1]
の6
を代入します。 - 変数
j
にj=1 - h=4
の-3
を代入し更新します。 -
numOfMove
を2
に更新します。 -
while
文の条件式j=-3 >= 0
を満たさないので、このループを抜けます。 -
a[1]
に変数x
の4
を代入します。
この時の配列a
は下記の通りです。a = [5, 4, 10, 2, 7, 6, 8, 3, 9, 1]
-
i
に1
を加えて、for
文の条件式i=6 < n=10
を満たすので、 - 変数
x
にa[6]
の8
を代入し保存します。 - 変数
j
にi=6 - h=4
の2
を代入します。 -
while
文の条件式j=2 >= 0
かつa[2]=10 > 8
を満たすので、 -
a[6]
にa[2]
の10
を代入します。 - 変数
j
にj=2 - h=4
の-2
を代入し更新します。 -
numOfMove
を3
に更新します。 -
while
文の条件式j=-2 >= 0
を満たさないので、このループを抜けます。 -
a[2]
に変数x
の8
を代入します。
この時の配列a
は下記の通りです。a = [5, 4, 8, 2, 7, 6, 10, 3, 9, 1]
-
i
に1
を加えて、for
文の条件式i=7 < n=10
を満たすので、 - 変数
x
にa[7]
の3
を代入し保存します。 - 変数
j
にi=7 - h=4
の3
を代入します。 -
while
文の条件式j=3 >= 0
かつa[3]=2 > 3
を満たさないので、このループを抜けます。 -
a[7]
に変数x
の3
を代入します。(元々、a[7]=3
です。)
この時の配列a
に変更はありません。 -
i
に1
を加えて、for
文の条件式i=8 < n=10
を満たすので、 - 変数
x
にa[8]
の9
を代入し保存します。 - 変数
j
にi=8 - h=4
の4
を代入します。 -
while
文の条件式j=4 >= 0
かつa[4]=7 > 9
を満たさないので、このループを抜けます。 -
a[8]
に変数x
の9
を代入します。(元々、a[8]=9
です。)
この時の配列a
に変更はありません。 -
i
に1
を加えて、for
文の条件式i=9 < n=10
を満たすので、 - 変数
x
にa[9]
の1
を代入し保存します。 - 変数
j
にi=9 - h=4
の5
を代入します。 -
while
文の条件式j=5 >= 0
かつa[5]=6 > 1
を満たすので、 -
a[9]
にa[5]
の6
を代入します。 - 変数
j
にj=5 - h=4
の1
を代入し更新します。 -
numOfMove
を4
に更新します。 -
while
文の条件式j=1 >= 0
かつa[1]=4 > 1
を満たすので、 -
a[5]
にa[1]
の4
を代入します。 - 変数
j
にj=1 - h=4
の-3
を代入し更新します。 -
numOfMove
を5
に更新します。 -
while
文の条件式j=-3 >= 0
を満たさないので、このループを抜けます。 -
a[1]
に変数x
の1
を代入します。
この時の配列a
は下記の通りです。a = [5, 1, 8, 2, 7, 4, 10, 3, 9, 6]
これで、言語によるシェルソートアルゴリズムのグループ内で挿入ソートを行い、元の位置に戻した結果と同じになりました。
2回目のinsertionSortメソッド呼び出し
2回目のinsertionSortメソッド呼び出しは、挿入ソートと全く同じですので、少しだけ解説をして、残りの解説については割愛します。
仮引数は下記の通りです。
a = [5, 1, 8, 2, 7, 4, 10, 3, 9, 6]
n = 10
h = 1
- 変数
numOfMove
を0
で初期化します。 -
for
文の条件式i=1 < n=10
を満たすので、 - 変数
x
にa[1]
の1
を代入し保存します。 - 変数
j
にi=1 - h=1
の0
を代入します。 -
while
文の条件式j=0 >= 0
かつa[0]=5 > 1
を満たすので、 -
a[1]
にa[0]
の5
を代入します。 - 変数
j
にj=0 - h=4
の-4
を代入し更新します。 -
numOfMove
を1
に更新します。 -
while
文の条件式j=-4 >= 0
を満たさないので、このループを抜けます。 -
a[0]
に変数x
の1
を代入します。
この時の配列a
は下記の通りです。1回目のinsertionSortメソッド呼び出しと同じように、このような作業を繰り返して、最終的に配列a = [1, 5, 8, 2, 7, 4, 10, 3, 9, 6]
a
は下記のようにソートされます。a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Discussion