0
1

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.

qsort クイックソートを改造した高速かつ安定なソート ss18

Last updated at Posted at 2020-09-11

####◎旧来の qsort の不便さ
C言語の著名なライブラリに、newlib と glibc があります。
その中の qsort 関数は20年以上使用されていると思うのですが、
いくつか不便な点があります。

1.newlib
newlib の qsort は、直接ソートのみで間接ソートへの自動切り替えがありません。
そのために、元配列の要素が大きいとき、低速となります。
(間接ソート:各要素へのポインタ配列を作り、これをソートする。最後に元配列の要素を移動する。)

2.glibc
glibc の qsortは、実体はマージソートです。
そのために、キーが(社員番号のような)重複がないときは高速ですが、
キーに(男・女のような)重複があるときは、低速です。
また、マージソートなので安定ソートです。
しかし、作業領域の確保(malloc)に失敗したときは、
普通のクイックソートを行います。そのため、「安定」を保証できません。

今回公開する ss18 は、高速な安定ソートです。
「不安定で良いから、とにかく高速なソート」を望むなら、
ss15 (http://ww51.tiki.ne.jp/~srr-cake/qsort/qs15/) をお薦めします。

newlib・glibc 意外はあまり調べていません。より高速な比較ソートがあれば教えて下さい。
 

####◎作業領域について
ss18 は作業領域(オーダー$O(n)$)(=補助配列)を必要とします。
glibc の qsort は安定性が保証されないのに同じ作業領域$O(n)$を使用します。
筆者としては、主メモリが安価な時代ではこれでよいと考えています。
 

####◎計算時間について
qsortでは、最悪計算時間が$O(n^2)$となります。
しかし、ss18 はピボットの選定を丁寧に行っているので、
(偶然または運悪く発生する)相性の悪いソート対象配列はほぼないと
考えています。

ただし、悪意を持って作られたソート対象配列では計算時間が$O(n^2)$
となり得ます。この対策としては、「ピボットの選定をより丁寧に行う」
ことがあります。(ただ、現在のバージョンでは実装していません)
具体的には、「プロセス内での最初の ss18 の呼び出し時に、time()関数などで
再現性のない乱数を発生させて、この値でピボットを変動させる」などがあります。
この方法は安定ソートである ssort で特に有効です。ピボットが変動しても、
ソート後に対象配列は常に同じになるからです。qsort では異なる可能性があります。

#クイックソートを改造して安定な比較ソート stable sort (ssort)(ss18) を作成しました
以前に公開した ss14 とは全く別のアルゴリズムです。
よりコンパクトで、より高速です。大雑把にいうと、
ss18は補助配列(ptr2)と対象配列(ptr)で以下のような移動を繰り返します。

   ptr2:...............  →  ptr2:5555......77787  →  ptr2:..........77787
   ptr :357358257257237  A  ptr:332223.........  B  ptr:3322235555.....

アルゴリズム解説のための簡易版(要素サイズを8バイトに固定)(ss18c4d)の
処理時間を測定したものを以下に記載します。
簡易版のプログラムは最後に記載します。

              キーの種類  要素数   サイズ  繰返し回数 処理時間(秒) 
    qs_glibc    d=-3      e=10000   s=8      R2463      T=5.74
    ss18c4d     d=-3      e=10000   s=8      R2463      T=4.73
                                                              
    qs_glibc    d=100     e=10000   s=8      R7035      T=15.47
    ss18c4d     d=100     e=10000   s=8      R7035      T=6.25
                                                              
    qs_glibc    d=2       e=10000   s=8      R24630     T=35.19
    ss18c4d     d=2       e=10000   s=8      R24630     T=2.35
                                                              
      d=-3:キーの重複なし d=100:キーが100種類 d=2:キーが男女など
      繰返し回数:処理時間の測定誤差を減らすためにソートを繰り返した回数

qs_glibc は、Cのライブラリ "glibc" のqsort関数を性能測定用に改造したものです。
qs_glibc は、はmalloc()に失敗しない限りマージソートとして動作します。
マージソートは安定なソート(https://ja.wikipedia.org/wiki/ソート) です。
 
すべての場合で、ss18c4d は qs_glibc より高速です。
 
 
正規版(ss18e8b)は、 http://ww51.tiki.ne.jp/~srr-cake/qsort/ss18/ にあります。

readme2.txt に正規版のベンチマークテストの方法を載せました。
正規版では、qsnewlibの処理時間も測定しています。
( http://ww51.tiki.ne.jp/~srr-cake/qsort/ss18/bench-sample.txt を参照 )

qsnewlib は、Cのライブラリ "newlib" のqsort関数です。安定でないソートです。
なので、qsnewlib が最速になって当然なのですが、間接ソートへの
自動切り替えがないため要素サイズが大きい場合に qsnewlib は遅くなります。

qsnewlib(非安定) qs_glibc(安定) ss18e8b(安定) を比べると
64bitシステムでは、概ね ss18e8b が最速です。

 
 

/**********   ssort (stable sort)   ss18    *******/
/* 
ss18 の簡易版  (ss18は安定な比較ソート。マージソートやss14より高速。)

対象配列(ptr)と補助配列(ptr2)(ptrと同じ大きさ)を用いて安定なソートを行う。
以下のようにAB2段階で要素を移動する。ここで「5」はピボット(分割要素)を表している。

   ptr2:...............  →  ptr2:5555......77787  →  ptr2:..........77787
   ptr :357358257257237  A  ptr:332223.........  B  ptr:3322235555.....

移動Aで、ピボットをptr2の左端に集め、5より小さい要素をptrの左端に集め、
5より大きい要素をptr2の右端に集める。このとき77787は初期順序の逆順になっている。
移動Bのとき、もしも5555が逆順ならば、B直後の5555が正順になるように移動する。
この時点で5555はすでにソート完成状態の位置に収まっている。
次に332223の{先頭位置・末尾位置・正順逆順}をスタックに保存する。
これ以降、77787に対して同じ処理を繰り返す。部分配列が長さ2以下になれば部分ソートを終了して
スタックから{先頭位置・末尾位置・正順逆順}を取り出して処理を続ける。
スタックが空になれば処理を終了する。
(以上は簡易版なので、ピボットの収納位置が正規版とは異なっている)
*/

#include <stdio.h>
//typedef long unsigned int size_t;
void *malloc(size_t); void free(void*); void exit(int);

static void assert(int x, char *s)  //必要な場合は、下の行のコメントを取り去る
{ /* if (x) return; fprintf(stderr, "++++ %s ++++\n", s); exit(1); */ }

#define  F(x) (*((long long int *)(x)))
#define  COPY(x,y) {                      F(x)=F(y);        }  /*配列の要素のコピー処理*/
#define  SWAP(x,y) {long long int v=F(x); F(x)=F(y); F(y)=v;}  /*配列の要素のスワップ処理*/
//上の3行は要素の移動に関するもの  別なものに書き換え可能です


//ここから下は、ss18によるソートプログラムです
typedef struct { char *LLss, *RRss; int Rv; } stack_node;   /*L,Rを積むスタックの構造体*/
#define PUSH(llss,rrss,rv) {top->LLss = (llss); top->RRss = (rrss); top->Rv = (rv); ++top;} /*積む*/
#define POP(llss,rrss)     {--top; llss = top->LLss; rrss = top->RRss; rev = top->Rv;}      /*戻す*/

int ssort( void *base, size_t nel, size_t size,  int (*cmp)(void *a, void *b) )
              /* base : ソートしようとする配列へのポインタ    */
              /* nel  : 配列baseの要素数                      */
              /* size : 配列baseの要素の大きさ(バイト単位)  */
              /* cmp  : 要素の大小比較をする関数へのポインタ  */
{
 char *l,*l0,*r,*r0,*L,*R; // l0,r0,L,Rは配列の左右の起点。 L~R間の要素をl,r,mへ振り分ける
 char *ptr,*ptrE,*ptr2;    // ptr,ptr2 は L~R,l0~r0 配列の先頭  送受は毎回入れ替わる
 char *m,*m0,*p;           // pはL~R間を移動 *mにピボットを貯める
 int  t, rev;              // tは作業用  rev==0のときL~R間は正順
 long long int Z;          // Zは配列間の距離(正負ありうる)
 stack_node stack[32], *top = stack;     /*現在のところは32で十分*/

 assert(sizeof(          int) == 4, "sizeof(          int) == 4");
 assert(sizeof(long long int) == 8, "sizeof(long long int) == 8");
 assert(size == 8 , "簡易版のなので、sizeは8に固定とする"       );
 //簡易版なので、baseのアライメントはsizeof(char*)になっているとする

 ptr2 = malloc( nel * size );      //ptr2はソートのための補助配列
 if (ptr2 == NULL) {assert(0,"mallocに失敗した"); return 1;}
 ptr = base;
 Z = ptr2 - ptr;  //Zは2個の配列・補助配列間の距離、正負両方あり
 ptrE = ptr + nel * size;  // ptrE はL~Rが対象配列・補助配列いずれにあるか、判定に使用する
 L=ptr; R=&ptr[size*(nel-1)]; rev=0;
 goto LOOP;
 //ここまでは、ssortの1回の呼び出して1度だけ実行される


nxt:
 if (stack == top) {free(ptr2); return 0;}  /*スタックが空になったとき終了する*/
 POP(L,R);                                  /*スタックに積んである値を取り出す*/

LOOP:
 if (R<L) {                                           /*要素なし*/
   goto nxt;
 }
 if (L==R) {                                          /*要素数1*/
   if (ptr<=L && L<ptrE) {           }  // L<ptr2 の判定が普通だが、ptr2<base のシステムも存在する
   else                  {COPY(L-Z,L)}
   goto nxt;
 }
 if (L+size==R) {                                     /*要素数2*/
   if ((t=cmp(L,R))<0) if (ptr<=L && L<ptrE) {                       }
                       else                  {COPY(L-Z,L) COPY(R-Z,R)}
   else       if (t>0) if (ptr<=L && L<ptrE) {SWAP(L,R)              }
                       else                  {COPY(L-Z,R) COPY(R-Z,L)}
   else                if (ptr<=L && L<ptrE) if (rev==0) {                       }
                                             else        {SWAP(L,R)              }
                       else                  if (rev==0) {COPY(L-Z,L) COPY(R-Z,R)}
                                             else        {COPY(L-Z,R) COPY(R-Z,L)}
   goto nxt;      
 }
 
 if (ptr<=L && L<ptrE) {l=l0=L  ; r=r0=R+Z; m=m0=L+Z;}  // l : ピボットより小さい要素を次に置く場所
 else                  {l=l0=L-Z; r=r0=R-Z; m=m0=L;  }  // r : ピボットより大きい要素を次に置く場所
                                                        // m : ピボットと同じ    要素を次に置く場所

 COPY(m,L) m+=size;  //簡易版のなので、単純に先頭の要素をピボットとする
 for (p=L+size; p<=R; p+=size) {
   if ((t=cmp(p,m0))<0) {COPY(l,p) l+=size;}
   else        if (t>0) {COPY(r,p) r-=size;}
   else                 {COPY(m,p) m+=size;}
 }
 assert((l-l0)+(m-m0)+(r0-r)==(R-L+size),"(l-l0)+(m-m0)+(r0-r)がおかしい");

 if (rev==0) {
   p=l; do {COPY(p,m0) m0+=size; p+=size;} while (m0<m);  //m0~mには少なくとも1個ある
   if (l-l0 < r0-r) {PUSH(r+size,r0,1) L=l0; R=l-size; rev=0;} /*左から先にソートする*/
   else             {PUSH(l0,l-size,0) L=r+size; R=r0; rev=1;} /*右から先にソートする*/
 }else{
   p=l; do {m-=size;   COPY(p,m) p+=size;} while (m0<m);  //m0~mには少なくとも1個ある
   if (l-l0 < r0-r) {PUSH(r+size,r0,0) L=l0; R=l-size; rev=1;} /*左から先にソートする*/
   else             {PUSH(l0,l-size,1) L=r+size; R=r0; rev=0;} /*右から先にソートする*/
 }
 goto LOOP;
}
0
1
1

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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?