DOBON.NET DOBON.NETプログラミング掲示板過去ログ

クイックソートの末尾再帰をループに置換する手段

環境/言語:[使用言語(C#) .NET Frameworkのバージョン(3.5)]
分類:[.NET]

Array.Sortよりも速いクイックソートを実装したいです。

ソート済みであれば処理を抜けるようにして、配列の最初の要素と中央の要素と
最後の要素をソートして、中央の要素を枢軸に選ぶようにして、最初から2番目
の要素と最後から2番目の要素から比較するようにしました。

static Int32[] QuicksortSort(Int32[] a, Int32 low, Int32 hig)
{
    if (QuicksortIsSorted(a, low, hig)) return a;
    var v = QuicksortPivot(a, low, hig);
    var l = low + 1;
    var h = hig - 1;
    do
    {
        while (a[l] < v) l++;
        while (a[h] > v) h--;
        if (l > h) break;
        Swap(a, l++, h--);
    } while (true);
    if (low < h) QuicksortSort(a, low, h);
    if (l < hig) QuicksortSort(a, l, hig);
    return a;
}

ソート済みの配列やソート済みのものを逆順にした配列をソートするばあいには
Array.Sortより速くなりました。しかし、要素の値が一意でランダムにかき混ぜ
られた配列ではまだArray.Sortより遅いです。

10000の要素をもつInt32のランダムな配列をソートしたばあいの結果は以下でした。

[Array.Sort]
time(ns) :      20001
sorted : True

[Quicksort]
time(ns) :      50003
sorted : True

末尾再帰をループに置換すれば速くなると思い以下のようにしました。

static Int32[] QuicksortSort(Int32[] a, Int32 low, Int32 hig)
{
    var l = low;
    var h = hig;
    do
    {
        if (QuicksortIsSorted(a, l, h)) return a;
        var v = QuicksortPivot(a, l, h);
        l++;
        h--;
        do
        {
            while (a[l] < v) l++;
            while (a[h] > v) h--;
            if (l > h) break;
            Swap(a, l++, h--);
        } while (true);
        if (low < h) QuicksortSort(a, low, h);
        h = hig;
    } while (l < h);
    return a;
}

結果は以下でした。約300倍の時間が掛かるようになりました。解せません。
そしてわかりません。クイックソートの末尾再帰をループに置換する手段を教え
てください。あと、他にクイックソートを高速にする方法がありましたらお教え
いただけるとありがたいです。よろしくお願いします。

[Array.Sort]
time(ns) :          0
sorted : True

[Quicksort]
time(ns) :   15420034
sorted : True
添付ファイル: Program.zip (1 KB)
2011/10/24(Mon) 10:16:11 編集(投稿者)
2011/10/23(Sun) 22:56:26 編集(投稿者)
2011/10/23(Sun) 22:43:24 編集(投稿者)
2011/10/23(Sun) 22:38:16 編集(投稿者)

■No29284に返信(もりおさんの記事)
> Array.Sortよりも速いクイックソートを実装したいです。
>
> ソート済みであれば処理を抜けるようにして、配列の最初の要素と中央の要素と
> 最後の要素をソートして、中央の要素を枢軸に選ぶようにして、最初から2番目
> の要素と最後から2番目の要素から比較するようにしました。
>

とりあえず気付いた点を

QuicksortIsSortedを再帰毎に呼び出すのはまずいのでは?
配列の分割が進むにつれ処理の足を引っ張るような気がします。
(上記訂正: QuicksortPivot(a, l, h)は消さないほうが速いです。こちらで余計な処理を入れておりその分計測上遅くなってしまっていたようです。失礼しました。)

それと高速化するには関数呼び出しを極力抑えたほうがいいです。
例えば
Swap(a, l++, h--);

t = a[l];//(tはループ外でInt32として宣言済み)
a[l] = a[h];
a[h] = t;
l++; h--;
と変更するとやや速くなります。


しかしながら、整数配列の並びかえはArray.Sortで十分に高速です。下手に自作するより安全でもあります。本当に時間をかけてソートを自作しなければならないか、自作するメリットが十分にあるかどうかもう一度確認したほうがいいかもしれません。

まぁ実験というなら別ですが...。
2011/10/25(Tue) 22:02:40 編集(投稿者)

補足です。
ポインタを利用したクイックソートを実行すると配列約10000以上ならArray.Sortより
こちらでは高速になりました。ポインタを利用したソートを軸に改良していけばかなりの高速化を図れるかもしれません。
以下ソースです。
static unsafe void SampleSort(int[] array)
{
fixed (int* pointer = array)
{
pQuickSort(pointer, pointer + (array.Length - 1));
}
}

static unsafe void pQuickSort(int* left, int* right)
{
int* pl = left;
int* pr = right;
int pivot = *(pl + ((int)(pr - pl) / 2));
int temp;

do
{
while (*pl < pivot) pl++;
while (*pr > pivot) pr--;

if (pl <= pr)
{
temp = *pl;
*pl = *pr;
*pr = temp;
pl++;
pr--;
}
} while (pl <= pr);

if (pl < right) pQuickSort(pl, right);
if (left < pr) pQuickSort(left, pr);
}

それと
>クイックソートの末尾再帰をループに置換する手段を教えてください。

とのことですので教科書に載っている代表的な非再帰クイックソートものっけときます。少しいじればあなたの意図するプログラムに変更できるはずです。
以下ソース
public void quickSort(int[] array)
{
int left = 0;
int right = array.Length - 1;
Stack<int> leftStack = new Stack<int>(right - left + 1);
Stack<int> rightStack = new Stack<int>(right - left + 1);

leftStack.Push(left);
rightStack.Push(right);

int pl;
int pr;
int pivot;
int temp;

while (leftStack.Count != 0)
{
pl = left = leftStack.Pop();
pr = right = rightStack.Pop();
pivot = array[(left + right) / 2];

do
{
while (array[pl] < pivot) pl++;
while (array[pr] > pivot) pr--;
if (pl <= pr)
{
temp = array[pl];
array[pl] = array[pr];
array[pr] = temp;
pl++;
pr--;
}
} while (pl <= pr);

if (left < pr)
{
leftStack.Push(left);
rightStack.Push(pr);
}
if (pl < right)
{
leftStack.Push(pl);
rightStack.Push(right);
}
}
}
※スタックが遅い場合は配列で代用すると早くなるかもしれません。そのあたりは検索すればすぐに見つかるとおもいます。

以上です。高速化がんばってください。
■No29286に返信(itiさんの記事)
おっしゃるとおり実験です。理論の検証ではなくて実際に経験するという意味の実験です。
関数Swapを呼び出さないようにしたところかなり効果がありました。25%ほど速くなりました。
枢軸を選択する関数QuicksortPivotも挿入ソートで並び替えるようにしてSwap関数を
呼ばないようにしました。

static Int32 QuicksortPivot(Int32[] a, Int32 low, Int32 hig) {
  var mid = ((hig - low) >> 1) + low;
  var t = 0;
  if (a[mid] < a[low]) {
    t = a[mid];
    a[mid] = a[low];
    a[low] = t;
  }
  if (a[hig] < a[mid]) {
    if (a[hig] < a[low]) {
      t = a[hig];
      a[hig] = a[mid];
      a[mid] = a[low];
      a[low] = t;
    } else {
      t = a[hig];
      a[hig] = a[mid];
      a[mid] = t;
    }
  }
  return a[mid];
}

■No29288に返信(itiさんの記事)
unsafeのコードは*がポインタの宣言なのか、掛け算なのか、値なのか、アドレスなのか
私にはよくわかりません。難しそうなので今回は使用を避けようと思いますが、お教え
いただいたコードは後学のために保存しておきたいと思います。ありがとうございます。

全体をループで囲って引数に値を代入することで末尾再帰をループに置き換える
ことができました。処理速度はほとんど変わりませんでした。並び替えを行う配列の
内容によってはスタックオーバーフローが起こる可能性があるのでStackクラスを使う
やり方の方がよさそうですね。もうすこし検証してみます。

static Int32[] QuicksortSort(Int32[] a, Int32 low, Int32 hig) {
  var t = 0;
  while (low < hig) {
    if (QuicksortIsSorted(a, low, hig)) return a;
    var v = QuicksortPivot(a, low, hig);
    var l = low + 1;
    var h = hig - 1;
    do {
      while (a[l] < v) l++;
      while (a[h] > v) h--;
      if (l > h) break;
      t = a[l];
      a[l] = a[h];
      a[h] = t;
      l++; h--;
    } while (true);
    if (low < h) QuicksortSort(a, low, h);
    low = l;
  }
  return a;
}
2011/11/03(Thu) 18:54:38 編集(投稿者)

3つの要素の中央値を枢軸に選ぶクイックソートにはキラーシーケンスがあるようで
キラーシーケンスをソートするばあいにマージソートよりも遅くなりました。なので
9つの要素の中央値を枢軸に選ぶようにしました。

要素数が少ないばあいに挿入ソートを行うようにしました。10%ほど速くなりました。

スタックオーバーフローを避けるために要素数が少ない方を再帰で処理して、要素数が
多い方をループで処理するようにしました。

static Int32[] QuicksortSort(Int32[] a, Int32 low, Int32 hig) {
  var t = 0;
  while (low < hig) {
    if (QuicksortIsSorted(a, low, hig)) {
      return a;
    }
    if (hig - low < 10) {
      Insertionsort(a, low, hig);
      return a;
    }
    var v = 0;
    var l = 0;
    var h = 0;
    if (hig - low < 32) {
      v = QuicksortPivot(a, low, hig);
      l = low + 1;
      h = hig - 1;
    } else {
      var step = (hig - low) >> 3;
      var mid = ((hig - low) >> 1) + low;
      v = Median(
        Median(a[low], a[low + step], a[low + step + step]),
        Median(a[mid - step], a[mid], a[mid + step]),
        Median(a[hig - step - step], a[hig - step], a[hig]));
      l = low;
      h = hig;
    }
    while (l < h) {
      while (a[l] < v) {
        l++;
      }
      while (a[h] > v) {
        h--;
      }
      if (l > h) {
        break;
      }
      t = a[l];
      a[l] = a[h];
      a[h] = t;
      l++; 
      h--;
    }
    if (h - low < hig - l) {
      if (low < h) {
        QuicksortSort(a, low, h);
      }
      low = l;
    } else {
      if (l < hig) {
        QuicksortSort(a, l, hig);
      }
      hig = h;
    }
  }
  return a;
}
添付ファイル: 1320314035.zip (2 KB)
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板