0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

javaで記述した TimSortよりも 高速 かつ 安定・安全 な qsort (Sqsort)

Last updated at Posted at 2021-03-23

最近のソートは、クイックソートに代わってマージソートが主流のようです。
最大の理由は「安定」性にあると思われます。しかし、マージソートは
 (1)要素の移動回数が多い
 (2)同じキー値が複数現れる場合でも速度向上が十分には望めない
などの欠点があります。
#####このことから「安定」な qsort (Sqsort) (比較ソート) を作りました。
#####最大の特徴は、キー値に同じ値が現れる場合に極めて高速なことです。
Sqsortはjavaで記述してあり、同じくjavaで記述したTimSortより高速です。
大雑把にいうと、Sqsortは補助配列(bas2)と対象配列(base)で
以下のような移動を繰り返します。

   bas2:...............  →  bas2:5555......77787  →  bas2:..........77787
   base:357358257257237  A base:332223.........  B  base:3322235555.....

プログラム(約150行 classファイル 3164byte) は最後に記載します。
TimSort(約900行 classファイル 8801byte) よりかなりコンパクトです。
処理時間を測定したものを以下に記載します。

    ソート名   キーの種類   要素数   繰返し回数 処理時間(秒)
     TimSort    d=-3      e=10000     R=2000     C=8.21 
     Sqsort     d=-3      e=10000     R=2000     C=5.65 
                                                            
     TimSort    d=100     e=10000     R=2000     C=6.69 
     Sqsort     d=100     e=10000     R=2000     C=2.81 
                                                            
     TimSort    d=2       e=10000     R=6000     C=7.41 
     Sqsort     d=2       e=10000     R=6000     C=2.09 
 
      d=-3:キーの重複なし d=100:キーが100種類 d=2:キーが男女など         
      繰返し回数:処理時間の測定誤差を減らすためにソートを繰り返した回数   

すべての場合で、Sqsort は TimSort より高速です。
より詳細な測定結果・ベンチマークプログラム・TimSort/Sqsortのソースコードなどは
    http://ww51.tiki.ne.jp/~srr-cake/qsort/sqs18j/   にあります。

「安定」とは、キー値が同じ時、ソート前の順序をソート後も保持することです。
「安全」とは、「悪意のある配列により、異常に長い処理時間を発生させることができない」
       こととします。

Sqsort では「安全」を実現するために、ピボットの候補を
乱数(System.nanoTime())により決定しています。
これにより、最悪計算時間O(n^2)は変化しませんが、
元々その発生確率は非常に低いものです。
これまで長い間、C言語のnewlibライブラリのqsort()の実装において
最悪計算時間への配慮がなかったことが、その低さを示していると思います。
今回、最悪計算時間の発生確率は変わりませんが、
「悪意のある配列」を作ることは不可能になっていると思います。

以上3種類のソートを比較すると次の表のようになります。

     TimSort Sqsort  qsort  
速さ  
作業領域
安定性  ×
安全性  ×

また、TimSortでは配列に内在するソート済み部分列を有効利用しますが、
Sqsortにその機能はありません。
代わりに、Sqsortは同じキー値が複数回現れる場合に高速です。
TimSortにこの機能はありません。
すなわち、県名別にソートしたり、男女を分けるためにソートする場合に
極めて高速です。
 

Sqsortでは、最悪計算時間や安全性の問題を、
乱数による確率の問題として扱おうとしています。
これについては、以下の記事で解説します。
  「乱数でピボットを選択した qsort の最悪計算時間 について」
    https://qiita.com/t-kawa/items/3dfba10c407d1aede95d

 

以下が Sqsort のソースコードです。

/***************************************************/
/*         Sqsort   (stable qsort)                 */
/*                                                 */
/* by 河村 知行 (kawamura tomoyuki)    2021.2.27   */
/*             t-kawa@crux.ocn.ne.jp               */
/***************************************************/

// Sqsort は安定な比較ソート。TimSortより高速で,作業領域は2n(TimSortはn),安全性はほぼ同じ
/*
対象配列(base)と補助配列(bas2)(baseと同じ大きさ)を用いて安定なソートを行う。
以下のように A,B 2段階で要素を移動する。ここで「5」はピボット(分割要素)を表している。

   bas2:...............  →  bas2:5555......77787  →  bas2:..........77787
   base:357358257257237  A  base:332223.........  B  base:3322235555.....

移動Aで、ピボットと同じ要素をbas2の左端に集め、5より小さい要素をbaseの左端に集め、
5より大きい要素をbas2の右端に集める。このとき77787は初期順序(元順)の逆順になっている。
移動Bの完了時点で5555はすでにソート完成状態の位置に収まっている。
B直後、5555以外の要素は、base上では元順に、bas2上では逆順に並んでいる。
次に332223の{所属フラグ(base/bas2)・先頭位置・末尾位置}をスタックに保存する。
これ以降、77787に対しbaseとbas2の意味を入れ替えて同じ処理を繰り返す。
部分配列が長さ15以下になれば挿入ソートを実施して部分配列のソートを完了し、次に
スタックから{フラグ(base/bas2)・先頭位置・末尾位置}を取り出して処理を続ける。
スタックが空になればソートを終了する。
*/
#define C(x,y)  {           x=(y);       }  /* ソート対象配列の要素のコピーに用いる */
#define S(x,y)  {T tmp=(x); x=(y); y=tmp;}  /* ソート対象配列の要素のスワップに用いる */

#define CMP(x,y)       cmp.compare((x),(y))
#define med3p(x,y,z)  (CMP(x,y)<=0 ? (CMP(y,z)<=0 ? y : (CMP(x,z)<=0 ? z : x)) : \
                                     (CMP(y,z)>=0 ? y : (CMP(x,z)<=0 ? x : z)) )

#define I(x) {x++;} /* 配列base,bas2の添字を増やす */
#define D(x) {x--;} /* 配列base,bas2の添字を減らす */

import java.util.Comparator;

class Sqsort {

static int L,R;        // L,Rは、baseまたはbas2上のソート対象となる領域[L~R]の左端と右端
static boolean flagB;  // [L~R]領域がbase上にあるときtrue、bas2上にあるときfalse
// popメソッド内で L,R,flagB へ代入できるように、sortメソッドのローカル変数からここへ移動した

//#define  base0  base  /*これを有効にすると、配列base0を複写しない。 32bitJVMでは有効の方が速い*/

@SuppressWarnings({"unchecked"})
public static <T> void sort( T[] base0, Comparator<? super T> cmp ) {//base0:ソート対象である配列
                                                                     //  cmp:要素の比較をする関数
 int l,r,m; //ptrB[L~R]間の要素をptrB[L~l],ptrR[r~R],ptrR[L~m]へ振り分ける。l領域 r領域 m領域
 T[] ptrB, ptrR; //flagBのとき ptrB==base ptrR==bas2、!flagBのとき ptrB==bas2 ptrR==base。
 int n,t,p;      // nは、現在ソート中の[L~R]領域の要素数。tは作業用。pはL~R間を移動する添字
 final long rndm = (System.nanoTime() & 0xFFFFFFFFFFFFL); //nanoTime()を乱数発生器として使用

 class StkElt {             //スタックをローカルクラスで実装
   boolean sB; int sL,sR;
   void push(boolean b, int l, int r) {      sB=b;     sL=l;     sR=r;}
   void pop ()                        {flagB=sB;     L=sL;     R=sR;  }
 }
 final int STK_LEN=32;      // base0.lengthの最大値は2^31-1なので32で十分
 StkElt stk[] = new StkElt[STK_LEN];
 for (int i=0; i<STK_LEN; i++) stk[i] = new StkElt();
 int top = 0;               // スタックポインタの初期値
   
 int Z = base0.length;      // このsortメソッドに渡されたソート対象配列の要素数
#ifndef base0               /* 64bit JVM では、コピーした配列をソートした方が速いので次行を使用 */
 T[] base = (T[]) new Object[Z];   System.arraycopy(base0, 0, base, 0, Z);
#endif
 T[] bas2 = (T[]) new Object[Z];  //baseと対になって使用。ただし、m領域のコピー先は常にbase

 flagB=true; L=0; R=Z-1;

 //ここまでは、sortの1回の呼び出して1度だけ実行される

 for(;;) {
   for(;;) {//LOOP:
     if (R<L)  {                                 break;/*goto nxt;*/} //要素なし
     if (L==R) {{if (!flagB) C(base[L],bas2[L])} break;/*goto nxt;*/} //要素数1
     
     n = (R - L + 1);      /*要素数 n>=2 */

     if (n <= 15) { //挿入ソート   結果はbase上に作る   n==2でも動く  「15」は実験で得られた値
       if (flagB) { //ソート領域base[L~R]は 元順 になっている
         if (CMP(base[L],base[L+1])>0) S(base[L],base[L+1])
         for (r = L+2; r <= R; r++) {
           if (CMP(base[r-1],base[r])<=0) continue;
           T tmp=base[r]; C(base[r],base[r-1])
           for (l=r-2; L<=l && CMP(base[l],tmp)>0; l--) C(base[l+1],base[l])
           base[l+1]=tmp;
         }
       }else{       //ソート領域bas2[L~R]は 逆順 になっている
         if (CMP(bas2[R-1],bas2[R])<0) {C(base[L],bas2[R-1]) C(base[L+1],bas2[R])  }
         else                          {C(base[L],bas2[R])   C(base[L+1],bas2[R-1])}
         for (r = R-2; L <= r; r--) {
           t=L+(R-r);
           if (CMP(base[t-1],bas2[r])<=0) {C(base[t],bas2[r]) continue;}
           T tmp=bas2[r]; C(base[t],base[t-1])
           for (l=t-2; L<=l && CMP(base[l],tmp)>0; l--) C(base[l+1],base[l])
           base[l+1]=tmp;
         }
       }
       break; /*goto nxt;*/
     }

     if (flagB) {ptrB=base; ptrR=bas2;} else {ptrB=bas2; ptrR=base;}

     {//3点処理(3要素からピボットを選択 高速化のため9点処理や27点処理の版もある)
       int rnd = (int)(rndm % ((n >> 1) - 2)); //nanoTime()を乱数発生器として使用
       T ll=ptrB[L+rnd], rr=ptrB[R-rnd],       //悪意ある攻撃を無効化するため、要素を乱数で選択
         mm=ptrB[L + (n >> 1)]; //中央の要素   //3点の内、定点が2点では悪意ある配列が作れてしまう

       T mmm=med3p(ll,mm,rr);                  //3点を比較して、mmmにピボットをセットする

     // l:ピボットより小さい要素(小要素)を次に置く所  r:大要素を次に置く所  m:同要素を次に置く所
     // ptrR[L~m]:左端からm領域を作る  ptrR[r~R]:の右端からr領域を作る(bas2上のr領域は逆順となる)
     // ptrB[L~l]:左端からl領域を作る  ptrB[L~R]:この全要素を左端からl領域,r領域,m領域へ振分ける
       l=m=p=L; r=R; 
       do {T pp=ptrB[p]; if ((t=CMP(pp,mmm)) < 0) {C(ptrB[l],pp) I(l)}
                         else if          (t > 0) {C(ptrR[r],pp) D(r)}
                         else                     {C(ptrR[m],pp) I(m)} I(p)} while(p<=R);
     }

     assert L<m;                    //m領域には少なくとも要素1個が配置される
     assert (l-L)+(m-L)+(R-r)==n;   //l領域,r領域,m領域の要素数の合計は ptrB[L~R]の要素数に等しい

     if (m-L == 1) {        //m領域をbase上のあるべき位置に移動する
       C(base[l],ptrR[L])   //要素数が1のときは、単純に代入(これは高速化のため。省略可能)
     }else{
       if (flagB) {
         System.arraycopy(bas2, L, base, l, m-L);                 //bas2上のm領域は常に元順
       }else{
         if (m<=l) {
           p=l; do {D(m) C(base[p],base[m]) I(p)} while (L<m);    //base上のm領域は常に逆順
         }else{
           for (t=m-1, p=l; p<t;) {S(base[p],base[t]) I(p) D(t)}  //m領域とその移動先が重なるとき
           for (p=m, m=l; L<m;) {D(m) C(base[p],base[m]) I(p)}
         }
       }
     }

     if (l-L < R-r) {stk[top++].push(!flagB,r+1,R); /*flagB=flagB;*/ R=l-1;} /*左を先にソート*/
     else           {stk[top++].push( flagB,L,l-1); flagB=(!flagB);  L=r+1;} /*右を先にソート*/
   }//goto LOOP;

   //nxt:                   /*スタックが空になったとき終了する*/
   if (top == 0) {
#ifndef base0               /* 64bit JVM では、コピーした配列をソートした方が速いので次行を使用 */
     System.arraycopy(base, 0, base0, 0, Z);
#endif
     return;
   }
   stk[--top].pop();   /*スタックに積んである値を取り出して、処理を繰り返す*/
 }
} // sort メソッドの終了
} // Sqsort クラスの終了
0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?