省メモリ超高速マージソート

クイックソートに匹敵する速さの不安定インプレースマージソート    追加のメモリを僅かだけ使った,クイックソートより速い安定マージソート

ランダムなデータに対して,クイックソートに匹敵する速さの不安定インプレースマージソート (クイックソートが最速という常識への挑戦)

一般のマージソートでは,データの記憶領域と同じ程度の追加の記憶領域が必要です。インプレースマージソート(in-place merge sort)とは,追加の記憶領域をほとんど使わずに行うマージソートです。
また,一般のマージソートでは,キーの値が同じ要素のソー ト前の順序が,ソート後も変わらずに維持されます。このようなソートを安定ソートと言います。
一般のマージソートは,最速と言われているクイックソートよりも遅いようです。ただし,クイックソートは,安定ソートではありません。(私が作ったマージソートはクイッ クソートよりも速いです。)

In-Place Sorting With Merge Sort に,平均計算時間と最悪計算時間が共に O(n log n)で,安定ソートではないインプレースマージソートがありますが,それは一般のマージソートより遅いものです。
私も,平均計算時間と最悪計算時間が共に O(n log n)で,安定ソートではないインプレースマージソートを作りました。再帰呼 び出しを用いていて,追加の記憶領域は O(log n) です。(2025年1月)
その後,改良を重ねたところ,大量のランダムなデータに対する平均計算時間はクイックソートに匹敵するまでになりました。
ここで示すインプレースマージソートは,In-Place Sorting With Merge Sort にあるインプレースマージソートに幾つかの改良を施した形になっていて,高速になっています。その1つは,部分列のソート法を次のように変えたことです。
    データ列のある部分をソートする → データ列のある部分をソートされたデータ列にする
ここでは,データ列のある部分について,そこに存在するデータをソートするのではなく,その部分を,他の部分にあるデータも用いてソートされたデータ列に したのです。

In-Place Sorting With Merge Sort では,整列されたm個のデータと整列されたk個のデータを,整列されていないk個のデータを上手く用いて,整列されたm+k個のデータにしていました。
    (整列されたm個,整列されていないk 個,整列されたk個) → (整列されたm+k個,整列されていないk個)
ここでのインプレースマージソートは,上記の操作を再帰的に繰り返しました。このことが,In-Place Sorting With Merge Sort のソート法より高速になった最大の理由と思われます。

さらに,ソートされていないデータ が少なくになったとき,ソートされていないデータをソートされたデータ列にマージするのに新たな手法を用い,クイックソートに匹敵する速度までになりまし た。
また,小さなデータブロックに対するソートに挿入ソートを用いることにより高速化しましたが,クイックソートもこの方法で同様に高速化されます。
このソートは,最悪計算時間において,クイックソートやイントロソートよりも優れています。


世界最速のインプレースソートアルゴリズム
ランダムなデータに対してクイックソートとほぼ同じ速度を実現したコードです。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef int d_type;// ソートするキーの型

//挿入ソート
#define INSERTION_SORT(m)\
    int i=m;\
    for (;i<n;i++){\
        d_type tmp=array[i];\
        int j=i-1;\
        for (;j>=0 && array[j]>tmp;j--) array[j+1]=array[j];\
        array[j+1]=tmp;\
    }

//値を交換する関数
void swap(d_type *x,d_type *y){
    d_type tmp=*x;
    *x=*y;
    *y=tmp;
}

// マージ (整列されたm個,整列されていないk 個,整列されたk個)
// → (整列されたm+k個,整列されていないk個) を行う関数
void mergeD(d_type a[], int m, int k){
    d_type *b=a+m-1,*c=a+m+k-1,*d=a+m+2*k-1;
    while (1) {
        if (*b>*d) {
            swap(b,c);
            b--;c--;
            if (b<a) {
                while(c>=a){
                    swap(c,d);
                    c--;d--;
                }
                break;
            }
        } else {
            swap(c,d);
            c--;d--;
            if (c<=b) break;
        }
    }
}

#define RATIO 0.2 //RATIO<=0.5
//整列されたn個の要素の配列を作る関数
void merge_sortD(d_type array[], int n){
    if (n<50) {
        INSERTION_SORT(1)
        return;
    }
    int k=n*RATIO,m=n-k;//k=n/2をk=n*0.2に変えたら,なぜか高速化
    merge_sortD(array,m);
    merge_sortD(array+n,k);
    mergeD(array,m,k);
}

#define CS(x,y) if (array[x]<array[y]) swap(array+x,array+y);

void merge_sort(d_type array[], int n){
    int m=n*(1.0-RATIO);//mをできるだけ大きくした
    merge_sortD(array,m);//整列されたm個の要素の配列を作る
    while (1){//m個の要素の配列にk個の要素をマージすることを繰り返す
        int k=(n-m)/2;
        if (k<3) break;
        int i=m,end=m+k-3;
        for (;i<=end;i++) {int j=i+2;CS(i,j);CS(i+1,j);CS(j,i+k)}
        merge_sort(array+m+k,n-m-k);
        mergeD(array,m,k);
        m=m+k;
    }
    INSERTION_SORT(m) //残った要素を挿入ソートする
}

#undef CS
#undef RATIO
#undef INSERTION_SORT

int main(){
    int n=100000000;
    int i;
    clock_t start,end;
    d_type *array=malloc(n*sizeof(d_type));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++) array[i]=rand()*(RAND_MAX+1)+rand();
    start=clock();
    merge_sort(array,n);
    end=clock();
    printf("%f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    for (i=1;i<n;i++) if(array[i-1]>array[i]) printf("%d %d\n",i,array[i]);
    free(array);
    return 0;
}

上記の merge_sort() は,基本的なコードに次のコードを付加することにより,大きく高速化されています。
高速化のために付加した次のコードは,ソートされていない残ったデータのうちで,小さい値のデー タをなるべく早くマージするためのものです。無くても動作しますので,速度を比較してみてください。

        int i=m,end=m+k-3;
        for (;i<=end;i++) {int j=i+2;CS(i,j);CS(i+1,j);CS(j,i+k)}

なお,この2行は,高速化のために最初に考えた次の2行を,さらなる高速化のために改良したものです。

        int i=m,end=m+k;
        for (;i<end;i++) if (array[i]<array[i+k]) swap(array+i,array+i+k);

上記のプログラムがやっていることの概略は,次の様です。
1 全データの80%分のソートされた配列を作る。(この作業は,関数 merge_sortD() により高速に行われる)
2 残りの20%のうちの半分の10%をソートされた配列にし,それをソートされた80%の配列にマージして,ソートされた90%分の配列を作る。
3 残りの10%のうちの半分の5%をソートされた配列にし,それをソートされた90%の配列にマージして,ソートされた95%分の配列を作る。
4 残りの5%のうちの半分の2.5%をソートされた配列にし,それをソートされた95%の配列にマージして,ソートされた97.5%分の配列を作る。
          …………

最初のコードでは,できるだけデータ数がほぼ等しいデータ群をマージしようとしていました。すなわち,m≒k となる割合を多くすれば高速になると思い込んでいました。
ところが,コードの一部の「k=n/2」を「k=n*0.2」に変えたところ,不思議にも大きく速くなりました。
データ数比 1:1でマージしていたのを,4:1でマージするように変えると遅くなると思っていたのに,速くなるなんてびっくり。
私には理由は分かりませんが,「クイックソートのピボットは中央値でなく四分位数を選択したほうが高速」というブログを見て,もしかしたら分岐予測ミス確 率の関係かなと思っています。

注 RATIO の値は0.2としていますが,もっと細かく,0.19や0.18とすると,実行するコンピュータによっては少しだけ高速になるかもしれません。

最悪計算時間がO(n log n) となることは,次の様に分かります。
 ① ほぼ同じ大きさのデータをマージする操作の計算時間の合計はO(n log n) で抑えられる。
 ② 大きさが大きく異なるデータをマージする回数はO(log n)で抑えられるから,それらの計算時間の和はO(n log n) で抑えられる。
 ③ 高速化のテクニックは計算時間を減少させるだけである。

注 関数 merge_sort() にある「m=n*(1.0-RATIO);//mをできるだけ大きくした」は,関数 merge_sortD()を用いるに当たり,次の条件が満たされれば良いと考えたからです。
    m+m×RATIO+m×(RATIO)2+m×(RATIO)3+m×(RATIO)4+……≦n
ここで,無限等比級数の和の公式により
    左辺=m/(1-RATIO)
よって
    m/(1-RATIO)≦n
となるから
    m≦n×(1-RATIO)


クイックソートとの比較
クイックソートは「世界最速のソートアルゴリズム」と言われることがありますが,素朴なままのクイックソートのコードでは上記のインプレースマージソート より遅いです。
そこで,私ができるだけの高速化をしたクイックソートのコードを作り,世界最速のソートアルゴリズムを目指したインプレースマージソートのコードと計算時 間の比較をすることにしました。
私のパソコンで計測したところでは,このインプレースマージソートの1億個のランダムなデータに対する平均計算時間は,クイッ クソートと同じくらいです。
実際にコンパイルして実行してみて下さい。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef int d_type;// ソートするキーの型

#define INSERTION_SORT(m)\
    int i=m;\
    for (;i<n;i++){\
        d_type tmp=array[i];\
        int j=i-1;\
        for (;j>=0 && array[j]>tmp;j--) array[j+1]=array[j];\
        array[j+1]=tmp;\
    }

void swap(d_type *x,d_type *y){
    d_type tmp=*x;
    *x=*y;
    *y=tmp;
}

void mergeD(d_type a[], int m, int k){
    d_type *b=a+m-1,*c=a+m+k-1,*d=a+m+2*k-1;
    while (1) {
        if (*b>*d) {
            swap(b,c);
            b--;c--;
            if (b<a) {
                while(c>=a){
                    swap(c,d);
                    c--;d--;
                }
                break;
            }
        } else {
            swap(c,d);
            c--;d--;
            if (c<=b) break;
        }
    }
}

#define RATIO 0.2 //RATIO<=0.5
void merge_sortD(d_type array[], int n){
    if (n<50) {
        INSERTION_SORT(1)
        return;
    }
    int k=n*RATIO,m=n-k;//k=n/2をk=n*0.2に変えたら,なぜか高速化
    merge_sortD(array,m);
    merge_sortD(array+n,k);
    mergeD(array,m,k);
}

#define CS(x,y) if (array[x]<array[y]) swap(array+x,array+y);

void merge_sort(d_type array[], int n){
    int m=n*(1.0-RATIO);//mをできるだけ大きくした
    merge_sortD(array,m);//整列されたm個の要素の配列を作る
    while (1){//m個の要素の配列にk個の要素をマージすることを繰り返す
        int k=(n-m)/2;
        if (k<3) break;
        int i=m,end=m+k-3;
        for (;i<=end;i++) {int j=i+2;CS(i,j);CS(i+1,j);CS(j,i+k)}
        merge_sort(array+m+k,n-m-k);
        mergeD(array,m,k);
        m=m+k;
    }
    INSERTION_SORT(m) //残った要素を挿入ソートする
}
#undef CS
#undef RATIO

void insertion_sort(d_type array[],int n){
    INSERTION_SORT(1)
}
#undef INSERTION_SORT

d_type med3(d_type x,d_type y,d_type z){
    if (x<y){
        if (y<z)      return y;
        else if (z<x) return x;
    }else{
        if (z<y)      return y;
        else if (x<z) return x;
    }
    return z;
}

void quick_sort(d_type array[], int begin, int end){
    int n=end-begin;
    if (n<50){
        insertion_sort(array+begin,n+1);
        return;
    }
    d_type pivot=med3(array[begin],array[begin+n/2],array[end]);
    int l=begin,r=end;
    while(1){
        while(array[l]<pivot) l++;
        while(array[r]>pivot) r--;
        if(l>=r) break;
        swap(array+l,array+r);
        l++;r--;
    }
    quick_sort(array,begin,r);//l=r+1
    quick_sort(array,r+1,end);
}

int main(){
    int n=100000000;
    int i;
    clock_t start,end,end2;
    d_type *array=malloc(n*sizeof(d_type));
    d_type *array2=malloc(n*sizeof(d_type));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++){
        array[i]=rand()*(RAND_MAX+1)+rand();
        array2[i]=array[i];
    }
    start=clock();
    merge_sort(array,n);
    end=clock();
    quick_sort(array2,0,n-1);
    end2=clock();
    printf("merge_sort %f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    printf("quick_sort %f秒  \n",(double)(end2-end)/CLOCKS_PER_SEC);
    for (i=0;i<n;i++) if(array[i]!=array2[i]) printf("%d %d\n",array[i],array2[i]);
    free(array);
    free(array2);
    return 0;
}

実行例
2台のパソコンで実行してみましたが,CPUやコンパイラの設定によって,このインプレースマージソートとクイックソートでどちらが速いかが逆転したの で,皆さんのパソコンで試してみてください。

       実行例           実行例2



追加のメ モリをデータ量の数%だけ使った,ランダム なデータに対してクイックソートより速くて安定なマージソート

インプレースなマージソートでクイックソートに匹敵する速度がでるのなら,追加のメモリを少しだけ使えば,クイックソートより速くて安定なマージ ソートができるのではと考えました。
目標は,以下の条件の全てを満たすソートを作ることです。

    マージソート系の安定ソート
    平均計算時間と最悪計算時間は共にO(n log n)
    ランダムなデータに対して、クイックソートと同等以上の実行速度であること
    ソートするデータが多いときに追加メモリの使用量を元のデータのメモリ量の1%程度まで抑えたい
    コードは既存の省メモリ高速ソートよりも短くてシンプルにしたい


そこで,インプレースマージソートのコードを元に,追加メモリ使用量を元のデータのメモリ量の15%程度だけ使って安定なマージソートのコードを書いたと ころ, ク イックソートよりも速くなりました。
課題は,「速度がクイックソートと同等以上という条件を保って,追加メモリ使用量を元のデータのメモリ量の1%程度まで減らす」,ということです。

追加のメモリ量を元のデータのメモリ量の15%程度に削減した バージョン
通常のマージソートに対して,以下のような工夫をしました。ここで,整列された2つのデータをAとBとし,Bを作業記憶領域Wを用いてマージするとしま す。
1 通常はAとBの両方をWにコピーするが,AだけをWにコピーするようにした。
2 AとBのデータ数の比率は通常は1:1だが,これを 0.15:0.85 すなわち 3:17 とした。
3 AとBをマージする関数merge()はポインタを用いて記述した。
4 小さなデータブロックに対するソートは挿入ソートを用いた。
5 マージのたびに作業記憶領域Wを確保せずに,W用に最初に一度だけ n*RATIO 個の容量の配列を確保して使い回した。

2により,追加のメモリ量は元のデータのメモリ量の15%程度に設定していますが,これは,メモリと速度の両方にメリットがあります。
実際,プログラムにある定数 RATIO を0.5に変えると,AとBのデータ数の比率は1:1になりますが,このように変更すると,かなり低速になってしまいます。
よって,ランダムなデータをマージするときのデータ数の比率がほぼ1:1 のソートは,このソートより遅いと言えます。
このソートのコードを示します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef int d_type;// ソートするキーの型

void insertion_sort(d_type arr[],int n){
    int i=1;
    for (;i<n;i++){
        d_type tmp=arr[i];
        int j=i-1;
        for (;j>=0 && arr[j]>tmp;j--) arr[j+1]=arr[j];
        arr[j+1]=tmp;
    }
}

void merge(d_type a[],int m,int n,d_type w[]){
    int i=0;
    for (;i<m;i++) w[i]=a[i];
    d_type *b=a+m,*end=a+n;
    while (1) {
        if (*b<*w) {
            *a=*b;
            a++;b++;
            if (b>=end) {
                while(a<end){
                    *a=*w;
                    a++;w++;
                }
                break;
            }
        } else {
            *a=*w;
            a++;w++;
            if (a>=b) break;
        }
    }
}

#define RATIO 0.15
void merge_sort(d_type arr[],int n,d_type work[]) {
    if (n<50) {
        insertion_sort(arr,n);
        return;
    }
    int m=n*RATIO;
    merge_sort(arr,m,work);
    merge_sort(arr+m,n-m,work);
    if (arr[m-1]<=arr[m]) return;
    merge(arr,m,n,work);
}

void MergeSort(d_type arr[], int n) {
    d_type *work=malloc((int)(n*RATIO)*sizeof(d_type));
    merge_sort(arr,n,work);
    free(work);
}
#undef RATIO

int main(){
    int n=100000000;
    int i;
    clock_t start,end;
    d_type *arr=malloc(n*sizeof(d_type));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++) arr[i]=rand()*(RAND_MAX+1)+rand();
    start=clock();
    MergeSort(arr,n);
    end=clock();
    printf("%f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    for (i=1;i<n;i++) if(arr[i-1]>arr[i]) printf("%d %d\n",i,arr[i]);
    free(arr);
    return 0;
}

上記のコードの平均計算時間と最悪計算時間はO(n log n)ですが,最良計算時間はO(n) で,データが既にソートされている場合は非常に高速に処理できます。
そのために,関数merge_sort()のコードに次のif文を加えてあります。

    if (arr[m-1]<=arr[m]) return;


追加のメモリ量を元のデータのメモリ量の5%程度に削減したバージョン
追加のメモリ量を5%程度に削減したバージョンは,上記の基本のコードより僅かに速度が低下しますが,クイックソートより速いようです。
また,追加のメモリ量を2%程度に削減するには,下記のコードの関数 MergeSort() にある wm=n*0.05+6 を wm=n*0.02+6 と変更するだけです。そうすると遅くなってしまいますが,それでもクイックソートと同じくらいの速さです。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef int d_type;// ソートするキーの型

void insertion_sort(d_type arr[],int n){
    int i=1;
    for (;i<n;i++){
        d_type tmp=arr[i];
        int j=i-1;
        for (;j>=0 && arr[j]>tmp;j--) arr[j+1]=arr[j];
        arr[j+1]=tmp;
    }
}

void merge(d_type a[],int m,int n,d_type w[]){
    int i=0;
    for (;i<m;i++) w[i]=a[i];
    d_type *b=a+m,*end=a+n;
    while (1) {
        if (*b<*w) {
            *a=*b;
            a++;b++;
            if (b>=end) {
                while(a<end){
                    *a=*w;
                    a++;w++;
                }
                break;
            }
        } else {
            *a=*w;
            a++;w++;
            if (a>=b) break;
        }
    }
}

void merge_sort_wm(d_type arr[], int n, d_type work[], int wm) {
    if (n<50){
        insertion_sort(arr,n);
        return;
    }
    int m=n*0.15;
    merge_sort_wm(arr,m,work,wm);
    merge_sort_wm(arr+m,n-m,work,wm);
    if (arr[m-1]<=arr[m]) return;
    while (m>0) {
        if (m<wm) wm=m;
        m=m-wm;
        merge(arr+m,wm,n-m,work);
    }
}

void MergeSort(d_type arr[], int n) {
    int wm=n*0.05+6;
    d_type *work=malloc(wm*sizeof(d_type));
    merge_sort_wm(arr,n,work,wm);
    free(work);
}

int main(){
    int n=100000000;
    int i;
    clock_t start,end;
    d_type *arr=malloc(n*sizeof(d_type));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++) arr[i]=rand()*(RAND_MAX+1)+rand();
    start=clock();
    MergeSort(arr,n);
    end=clock();
    printf("%f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    for (i=1;i<n;i++) if(arr[i-1]>arr[i]) printf("%d %d\n",i,arr[i]);
    free(arr);
    return 0;
}

追加のメモリ量を元のデータのメモリ量の1%程度に削減したバージョン
追加のメモリ量を1%程度に削減するには wm=n*0.01+6 と変更すればできますが,クイックソートより遅くなってしまいます。そこで,追加のメモリ量が1%程度でもクイックソートと同じくらいの速さとなるよう, さらなる改良をしてみました。
ウィキペディアの「ソート」のページでは,安定な In-place マージソートの平均計算時間と最悪計算時間は O(n(log n)2) となっています。
そのようなコードは,Google検索やyahoo検索で「インプレースマージソート  回転と再帰を用いた超高速なC言語のコード」などと上手く検索すれば,AIが提供してくれます。(AIによるコードには,バグがあり動作しない可能性があ ります。)
そのインプレースマージソートに上記の省メモリマージソートをうまく組み込めば,僅かなメモリでも実用に耐える速度の,安定マージソートが得られるので はないかと考えました。
まずは,検索でAIより得た動作不良のインプレースマージソートのコードを動作するように直し,少しだけ変更を加えた,計算時間が O(n(log n)2) の基本のインプレースマージソートのコードを示します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 挿入ソート
void insertion_sort(int arr[],int i,int n){
    for (;i<n;i++){
        int tmp=arr[i];
        int j=i-1;
        for (;j>=0 && arr[j]>tmp;j--) arr[j+1]=arr[j];
        arr[j+1]=tmp;
    }
}

// 2つの変数の値を交換する
void swap(int *x,int *y){
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

// 配列の範囲 [l, r] を反転させる
void reverse(int arr[], int l, int r) {
    while (l < r) {
        swap(arr + l, arr + r);
        l++; r--;
    }
}

// 範囲 [l, r] 内で、[l, m-1] と[m, r] の要素の前後を入れ替える
void rotate(int arr[], int l, int m, int r) {
    if (m <= l || r < m) return;
    reverse(arr, l, m - 1);
    reverse(arr, m, r);
    reverse(arr, l, r);
}

// キー以上の最初の位置
int lower_bound(int arr[], int low, int high, int key) {
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (arr[mid] >= key)
            high = mid;
        else
            low = mid + 1;
    }
    return low;
}

// キーより大きい最初の位置
int upper_bound(int arr[], int low, int high, int key) {
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (arr[mid] > key)
            high = mid;
        else
            low = mid + 1;
    }
    return low;
}

// 再帰的In-place マージ関数
void inPlaceMerge(int arr[], int l, int m, int r) {
    // マージ不要な場合
    if (m <= l || r < m) return;

    // 前半の最大値が後半の最小値以下の場合
    if (arr[m-1] <= arr[m]) return;

    // マージするデータ数が少なくなったとき
    if (r-l<=32){
        insertion_sort(arr+l, m-l, r-l+1);
        return;
    }

    // 分割点(回転させる境界)を見つける
    int i, j;
    if (m - l >= r - m + 1) {
        i = l + (m - l) / 2;
        // 範囲 [m, r+1) で arr[i] 以上に大きい最初の要素を探す
        j=lower_bound(arr, m, r+1, arr[i]);
    } else {
        j = m + (r - m + 1) / 2;
        // 範囲 [l, m) で arr[j] より大きい最初の要素を探す
        i=upper_bound(arr, l, m, arr[j]);
    }

    // 回転して領域を融合 (iからm-1とmからj-1を交換)
    rotate(arr, i, m, j-1);
    // 新しい分割点
    m = i + (j - m);

    // 再帰的にマージ
    inPlaceMerge(arr, l, i, m - 1);
    inPlaceMerge(arr, m , j, r);
}

// ソート関数
void inPlaceMergeSort(int arr[], int l, int r) {
    //if (r - l <= 0) return;
    int n = r - l + 1;
    if (n<50) { // 小さいところは挿入ソートで高速化
        insertion_sort(arr+l,1, n);
        return;
    }
       
    int m = l + n / 2;
    inPlaceMergeSort(arr, l, m - 1);
    inPlaceMergeSort(arr, m , r);
    inPlaceMerge(arr, l, m , r);
}

int main(){
    int n=100000000;
    int i;
    clock_t start,end;
    int *arr=malloc(n*sizeof(int));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++) arr[i]=rand()*(RAND_MAX+1)+rand();
    start=clock();
    inPlaceMergeSort(arr,0,n-1);
    end=clock();
    printf("%f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    for (i=1;i<n;i++) if(arr[i-1]>arr[i]) printf("%d %d\n",i,arr[i]);
    free(arr);
    return 0;
}

次に,基本のインプレースマージソートの挿入ソートの部分を省メモリマージソートに変え,追加のメモリ量を元のデータのメモリ量の1%程度にしたコードを 示します。
上記のコードの関数 rotate() は回転を用いて移動を行うという素晴らしいアイデアですが,回転を用いない関数 exchange() をせっかく考えたので,次のコードでは,それに変えてあります。
速度は,高速化の工夫がされたクイックソートと同じくらいです。
なお,wm=n*0.001+6 とすれば追加のメモリ量を0.1%程度に削減できます。
また,例えば,wm=100 とすればインプレースマージソートになり,#include <math.h> を付け加えて wm=sqrt(n) とすれば,ほぼインプレースマージソートになります。
これらの場合でも極端な速度低下は起きません。

基本のインプレースマージソートに対して,次のような変更を行いました。
1 ソート関数 inPlaceMergeSort() において,n<50 で挿入ソートに切り換えているが,n<=wm*20 で省メモリマージソートに切り換えるようにした。
2 ソート関数 inPlaceMergeSort() において,m = l + n / 2 と2分割して再帰しているが,m = l + n/4 と 1:4 に分割して再帰した。
3 コードを短くシンプルにするために,関数 upper_bound() は使わないようにした。
4 マージ関数 inPlaceMerge() において,マージ不要な m <= l || r < m となるまで再帰を進めているが,m-l<=wm*3.5 という条件で省メモリマージソートのマージに切り換えるようにした。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef int d_type;// ソートするキーの型

void insertion_sort(d_type arr[],int n){
    int i=1;
    for (;i<n;i++){
        d_type tmp=arr[i];
        int j=i-1;
        for (;j>=0 && arr[j]>tmp;j--) arr[j+1]=arr[j];
        arr[j+1]=tmp;
    }
}

void merge(d_type a[],int m,int n,d_type w[]){
    int i=0;
    for (;i<m;i++) w[i]=a[i];
    d_type *b=a+m,*end=a+n;
    while (1) {
        if (*b<*w) {
            *a=*b;
            a++;b++;
            if (b>=end) {
                while(a<end){
                    *a=*w;
                    a++;w++;
                }
                break;
            }
        } else {
            *a=*w;
            a++;w++;
            if (a>=b) break;
        }
    }
}

void merge_wm(d_type arr[], int m, int n, d_type work[], int wm){
    while (m>0) {
        if (m<wm) wm=m;
        m=m-wm;
        merge(arr+m,wm,n-m,work);
    }
}

void merge_sort_wm(d_type arr[], int n, d_type work[], int wm) {
    if (n<50){
        insertion_sort(arr,n);
        return;
    }
    int m=n*0.15;
    merge_sort_wm(arr,m,work,wm);
    merge_sort_wm(arr+m,n-m,work,wm);
    if (arr[m-1]<=arr[m]) return;
    merge_wm(arr,m,n,work,wm);
}

// 2つの変数の値を交換する
void swap(d_type *x, d_type *y){
    d_type tmp = *x;
    *x = *y;
    *y = tmp;
}

// 範囲 [0, m+n) 内で、[0, m) と[m, m+n) の要素の前後を入れ替える
void exchange(d_type arr[], int m, int n) {
    if (n <= 0) return;
    int i = m + n - 1;
    while (m > 0) {
        for (; i >= n; i--) swap(arr + i, arr + i - n);
        while (m >= n) m = m - n; // m = m % n
        n = n - m;
    }
}

// キー以上の最初の位置
int lower_bound(d_type arr[], int low, int high, d_type key) {
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (arr[mid] >= key)
            high = mid;
        else
            low = mid + 1;
    }
    return low;
}

// In-place マージ関数
void inPlaceMerge(d_type arr[], int l, int m, int r, d_type work[], int wm) {
    if (r < m) return;
    if (arr[m-1] <= arr[m]) return;

    // マージするデータ数が少なくなったとき
    if (m-l<=wm*3.5){
        merge_wm(arr+l, m-l, r-l+1, work, wm);
        return;
    }

    int i = l + (m - l) / 2;
    int j=lower_bound(arr, m, r+1, arr[i]);
    exchange(arr + i, m - i, j - m);
    m = i + (j - m);
    inPlaceMerge(arr, l, i, m - 1,work,wm);
    inPlaceMerge(arr, m , j, r,work,wm);
}

// ソート関数
void inPlaceMergeSort(d_type arr[], int l, int r, d_type work[], int wm) {
    int n=r-l+1;
    if (n/20<=wm){
        merge_sort_wm(arr+l,n,work,wm);
        return;
    }
    int m = l + n/4;
    inPlaceMergeSort(arr, l, m-1,work,wm);
    inPlaceMergeSort(arr, m , r,work,wm);
    inPlaceMerge(arr, l, m , r,work,wm);
}

void MergeSort(d_type arr[], int n) {
    int wm=n*0.01+6; // wn>=1 で動作
    d_type *work=malloc(wm*sizeof(d_type));
    inPlaceMergeSort(arr,0,n-1,work,wm);
    free(work);
}

int main(){
    int n=100000000;
    int i;
    clock_t start,end;
    d_type *arr=malloc(n*sizeof(d_type));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++) arr[i]=rand()*(RAND_MAX+1)+rand();
    start=clock();
    MergeSort(arr,n);
    end=clock();
    printf("%f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    for (i=1;i<n;i++) if(arr[i-1]>arr[i]) printf("%d %d\n",i,arr[i]);
    free(arr);
    return 0;
}


追加のメモリ量を,データ数の15%程度にしたコードと,5%程度にしたコードと,1%程度にしたコードと,0.1%程度にしたコードと,Wikipediaにある基本的なマージソー トのコードとの比較
上記のコードとWikipediaにある高速化の工夫をしていないマージソートのコードの速度を比較しようと思いました。
しかし,挿入ソートを用いていないWikipediaのマージソートでは比較にならないので,Wikipediaのコードに挿入ソートを書き足して高速化 しました。
これに加え,midの式を mid = (left + right) / 2 → mid = left + (right - left) / 4 と変えて,さらに高速にしました。
Wikipediaのコードにこれらの高速化を施した「merge_sortWiki」と比較してみました。これにより,上記のコードが実用的な性能を持 つことが示されました。


       実行例


関数merge()をポインタを用いて記述したことについて
関数merge()は,ポインタを使わずに次の様に書くことができます。こちらの方が平易なのですが,残念ながら遅くなってしまいます。

void merge(d_type arr[],int m,int n,d_type work[]) {
    int i=0,j=0,k=0;
    for (;i<m;i++) work[i]=arr[i];
    while (1) {
        if (arr[i]<work[j]){
            arr[k++]=arr[i++];
            if (i>=n){
                while (j<m) arr[k++]=work[j++];
                break;
            }
        }else{
            arr[k++]=work[j++];
            if (j>=m) break;
        }
    }
}


追加のメモリ量を元のデータのメモリ量の1%程度に削減したバージョンにおいて,追加のメモリを自動的に変えて最速にする工夫
このバージョンでは,配列 work[] が利用するメモリ数 wm は1以上で動作してwn が大きいほど速くなり,wm がnの15%以上のときに最速になります。
したがって,関数 MergeSort() を次の様に変えると,work[] が使用できるメモリに応じて自動的に最速でソートします。

void MergeSort(d_type arr[], int n) {
    int wm=n*0.15+6; // wn>=1 で動作
    do {
        d_type *work=malloc(wm*sizeof(d_type));
        if (work!=NULL){
            inPlaceMergeSort(arr,0,n-1,work,wm);
            free(work);
            return;
        }
        wm=wm/2;
    } while (wm>0);
    d_type work[1];
    inPlaceMergeSort(arr,0,n-1,work,1);
}


構造体を用いて安定ソートであることを確認するコード
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef struct{
    int value;    // ソートする値
    int index;    // 元の順序(インデックス)
} d_type;

int CMP(d_type a,d_type b){
    if(a.value>b.value) return 1;
    if(a.value<b.value) return -1;
    return 0;
}

void insertion_sort(d_type arr[],int n){
    int i=1;
    for (;i<n;i++){
        d_type tmp=arr[i];
        int j=i-1;
        for (;j>=0 && CMP(arr[j],tmp)>0;j--) arr[j+1]=arr[j];
        arr[j+1]=tmp;
    }
}

void merge(d_type a[],int m,int n,d_type w[]){
    int i=0;
    for (;i<m;i++) w[i]=a[i];
    d_type *b=a+m,*end=a+n;
    while (1) {
        if (CMP(*b,*w)<0) {
            *a=*b;
            a++;b++;
            if (b>=end) {
                while(a<end){
                    *a=*w;
                    a++;w++;
                }
                break;
            }
        } else {
            *a=*w;
            a++;w++;
            if (a>=b) break;
        }
    }
}

void merge_wm(d_type arr[], int m, int n, d_type work[], int wm){
    while (m>0) {
        if (m<wm) wm=m;
        m=m-wm;
        merge(arr+m,wm,n-m,work);
    }
}

void merge_sort_wm(d_type arr[], int n, d_type work[], int wm) {
    if (n<50){
        insertion_sort(arr,n);
        return;
    }
    int m=n*0.15;
    merge_sort_wm(arr,m,work,wm);
    merge_sort_wm(arr+m,n-m,work,wm);
    if (CMP(arr[m-1],arr[m])<=0) return;
    merge_wm(arr,m,n,work,wm);
}

// 2つの変数の値を交換する
void swap(d_type *x, d_type *y){
    d_type tmp = *x;
    *x = *y;
    *y = tmp;
}

void exchange(d_type arr[], int m, int n) {
    if (n <= 0) return;
    int i = m + n - 1;
    while (m > 0) {
        for (; i >= n; i--) swap(arr + i, arr + i - n);
        while (m >= n) m = m - n; // m = m % n
        n = n - m;
    }
}

// キー以上の最初の位置
int lower_bound(d_type arr[], int low, int high, d_type key) {
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (CMP(arr[mid],key)>=0)
            high = mid;
        else
            low = mid + 1;
    }
    return low;
}

// In-place マージ関数
void inPlaceMerge(d_type arr[], int l, int m, int r, d_type work[], int wm) {
    if (r < m) return;
    if (CMP(arr[m-1],arr[m])<=0) return;

    // マージするデータ数が少なくなったとき
    if (m-l<= wm*3.5){
        merge_wm(arr+l, m-l, r-l+1, work, wm);
        return;
    }

    int i, j;
    i = l + (m - l) / 2;
    j=lower_bound(arr, m, r+1, arr[i]);
    exchange(arr + i, m - i, j - m);
    m = i + (j - m);
    inPlaceMerge(arr, l, i, m - 1,work,wm);
    inPlaceMerge(arr, m , j, r,work,wm);
}

// ソート関数
void inPlaceMergeSort(d_type arr[], int l, int r, d_type work[], int wm) {
    int n=r-l+1;
    if (n/20<=wm){
        merge_sort_wm(arr+l,n,work,wm);
        return;
    }
    int m = l + n/4;
    inPlaceMergeSort(arr, l, m-1,work,wm);
    inPlaceMergeSort(arr, m , r,work,wm);
    inPlaceMerge(arr, l, m , r,work,wm);
}

void MergeSort(d_type arr[], int n) {
    int wm=n*0.15+6; // wn>=1 で動作
    do {
        d_type *work=malloc(wm*sizeof(d_type));
        if (work!=NULL){
            inPlaceMergeSort(arr,0,n-1,work,wm);
            free(work);
            return;
        }
        wm=wm/2;
    } while (wm>0);
    d_type work[1];
    inPlaceMergeSort(arr,0,n-1,work,1);
}

// 安定性をチェックする関数
int check_stability(d_type arr[], int n) {
    int i;
    for (i = 0; i < n - 1; i++) {
        if (arr[i].value==arr[i+1].value) {
            if (arr[i].index > arr[i + 1].index)
                return 0; // 不安定ソート
        }
    }
    return 1; // 安定ソート
}

int main() {
    int n=100000000;
    int i;
    clock_t start,end;
    d_type *arr=malloc(n*sizeof(d_type));
    srand((unsigned) time(NULL));
    for (i=0;i<n;i++){
        arr[i].value=rand();
        arr[i].index=i;
    }
    start=clock();
    MergeSort(arr, n);
    end=clock();
    printf("%f秒  \n",(double)(end-start)/CLOCKS_PER_SEC);
    for (i=1;i<n;i++) if(CMP(arr[i-1],arr[i])>0) printf("%d %d\n",i,arr[i].value);

    // 結果判定
    if (check_stability(arr, n))
        printf("安定の様です \n");
    else
        printf("不安定です \n");
    return 0;
}


理由が大して分からずマジックのように速くなったソートですが,マー ジソートの考案者のフォン・ノイマンもマジマジと見ているかもしれません。
最初のページ に戻る