効率的なquicksort
アルゴを作成しようとしています。それは大丈夫ですが、要素の数が膨大で、配列の特定のセクションが事前にソートされている場合、実行に長い時間がかかります。 quicksort
に関するWikipediaの記事を調べていたところ、次のように書かれていることがわかりました。
最大でO(log N)スペースが使用されていることを確認するには、最初に配列の小さい方の半分に再帰し、末尾呼び出しを使用してもう一方に再帰します。
このような小さな配列での呼び出し(つまり、長さが実験的に決定されたしきい値t未満の場合)には、定数配列が小さく、したがって小さな配列でより高速な挿入ソートを使用します。挿入ソートはソートされた配列を効率的に処理するため、このような配列はソートされないままにし、最後に単一の挿入ソートパスを実行することで実装できます。識別された各小さなセグメントの個別の挿入ソートは、多くの小さなソートの開始と停止のオーバーヘッドを追加しますが、クイックソートプロセスの働きにより、多くのセグメントの境界を越えてキーを比較する無駄な労力を回避します。また、キャッシュの使用も改善されます。
現在、両方のパーティションで再帰しています。最初のヒントを実装する方法はありますか? recurseは配列の小さい方の半分に最初に再帰し、末尾呼び出しを使用して他のに再帰することを意味しますか?次に、クイックソート内にinsertion-sort
を実装するにはどうすればよいですか?それは常に効率を改善しますか、それともアレイの特定のセクションが事前にソートされている場合のみですか?それが2番目のケースである場合、もちろん、それがいつ発生するかを知る方法はありません。では、insertion-sort
はいつ含める必要がありますか?
クイックソートでは、配列を2つの半分に区切るランダムなピボットを選択します。ほとんどの場合、1つは小さい可能性があります。
例えば配列サイズ100、ピボットは配列を40/60に区切ります。40が小さいサイズです。
挿入ソートを使用して10になるようにしきい値サイズを決定すると、半分のいずれかが10以下になるたびに、配列をピボットによって再帰的に分割し続ける必要があるとします。次のように動作する挿入ソートを使用できます。 O(n)小さなサイズの配列。
配列が逆にソートされている場合(最悪の場合)、挿入ソートは正しく動作しないことを考慮してください。
再帰に関しては、クイックソート再帰のストップケースを変更するだけです->配列サイズ<= 10再帰を停止し、挿入ソートを使用してすべての配列(この再帰ステップでははるかに小さい)をソートします。
末尾再帰とは、前半で必要なすべてを実行し、最後のメソッドとして小さい方の挿入ソートを呼び出すことを意味します。これは、スペースを節約するために使用されます。
Quick-sort()
choose a pivot
move the smaller elements from left
move the bigger elements from right
quick-sort on the bigger half of the array
if half is less then X
only then do an insertion sort on the other half <- this is a tail recursion insertion sort
else
quick sort on this half also
私が見る限り、2番目の最適化は、すべての再帰ステップで挿入ソートを使用しないことをお勧めしますが、制約が作成されたインデックスを覚えておいてから、すべてのスライスからのアイテムを連結する1つのバッチで挿入ソートを呼び出すと、これにより確実になりますキャッシュの使用を改善しますが、実装が少し難しいです。
標準のクイックソートをより効率的にする方法はいくつかあります。投稿の最初のヒントを実装するには、次のように記述します。
void quicksort(int * tab, int l, int r)
{
int q;
while(l < r)
{
q = partition(tab, l, r);
if(q - l < r - q) //recurse into the smaller half
{
quicksort(tab, l, q - 1);
l = q + 1;
} else
{
quicksort(tab, q + 1, r);
r = q - 1;
}
}
}
それで十分だと思います。次のステップは、再帰呼び出しを使用する代わりに、独自のスタックを実装することです(または、使用している言語の組み込み関数を使用します)。 (疑似)コードの例:
void quicksort2(int * tab, int l, int r)
{
int le, ri, q;
init stack;
Push(l, r, stack);
while(!empty(stack))
{
//take the top pair of values from the stack and set them to le and ri
pop(le, ri, stack);
if(le >= ri)
continue;
q = partition(tab, le, ri);
if(q - le < ri - q) //smaller half goes first
{
Push(le, q - 1, stack);
Push(q + 1, ri, stack);
} else
{
Push(q + 1, ri, stack);
Push(le, q - 1, stack);
}
}
delete stack;
}
次に、投稿から他のヒントを実装します。これを行うには、任意の定数を設定する必要があります。これをCUT_OFFと呼び、約20にします。これにより、挿入ソートに切り替える必要がある場合にアルゴリズムに通知されます。 CUT_OFFポイントに達した後に挿入ソートに切り替わるように前の例を変更するのは(ifステートメントを1つ追加するだけで)かなり簡単なはずです。
パーティション方式については、Hoareの代わりにLomutoパーティションを使用することをお勧めします。
ただし、データがすでに事前に並べ替えられている場合は、別のアルゴリズムを完全に使用することを検討できます。私の経験から、データが事前にソートされている場合、リンクリストに実装されたnatural series merge sortは非常に良い選択です。
私は少し前にそこにあるクイックソートベースのアルゴリズムを書きました(実際には選択アルゴリズムですが、ソートアルゴリズムも使用できます)。
この経験から学んだ教訓は次のとおりです。
これがお役に立てば幸いです。
私は最近 この最適化 を見つけました。 std :: sortよりも速く動作します。小さな配列では選択ソートを使用し、パーティション化要素として3の中央値を使用します。
これは私のC++実装です:
const int CUTOFF = 8;
template<typename T>
bool less (T &v, T &w)
{
return (v < w);
}
template<typename T>
bool eq (T &v, T &w)
{
return w == v;
}
template <typename T>
void swap (T *a, T *b)
{
T t = *a;
*a = *b;
*b = t;
}
template<typename T>
void insertionSort (vector<T>& input, int lo, int hi)
{
for (int i = lo; i <= hi; ++i)
{
for (int j = i; j > lo && less(input[j], input[j-1]); --j)
{
swap(&input[j], &input[j-1]);
}
}
}
template<typename T>
int median3 (vector<T>& input, int indI, int indJ, int indK)
{
return (less(input[indI], input[indJ]) ?
(less(input[indJ], input[indK]) ? indJ : less(input[indI], input[indK]) ? indK : indI) :
(less(input[indK], input[indJ]) ? indJ : less(input[indK], input[indI]) ? indK : indI));
}
template <typename T>
void sort(vector<T>& input, int lo, int hi)
{
int lenN = hi - lo + 1;
// cutoff to insertion sort
if (lenN <= CUTOFF)
{
insertionSort(input, lo, hi);
return;
}
// use median-of-3 as partitioning element
else if (lenN <= 40)
{
int median = median3(input, lo, lo + lenN / 2, hi);
swap(&input[median], &input[lo]);
}
// use Tukey ninther as partitioning element
else
{
int eps = lenN / 8;
int mid = lo + lenN / 2;
int mFirst = median3(input, lo, lo + eps, lo + eps + eps);
int mMid = median3(input, mid - eps, mid, mid + eps);
int mLast = median3(input, hi - eps - eps, hi - eps, hi);
int ninther = median3(input, mFirst, mMid, mLast);
swap(&input[ninther], &input[lo]);
}
// Bentley-McIlroy 3-way partitioning
int iterI = lo, iterJ = hi + 1;
int iterP = lo, iterQ = hi + 1;
for (;; )
{
T v = input[lo];
while (less(input[++iterI], v))
{
if (iterI == hi)
break;
}
while (less(v, input[--iterJ]))
{
if (iterJ == lo)
break;
}
if (iterI >= iterJ)
break;
swap(&input[iterI], &input[iterJ]);
if (eq(input[iterI], v))
swap(&input[++iterP], &input[iterI]);
if (eq(input[iterJ], v))
swap(&input[--iterQ], &input[iterJ]);
}
swap(&input[lo], &input[iterJ]);
iterI = iterJ + 1;
iterJ = iterJ - 1;
for (int k = lo + 1; k <= iterP; ++k)
{
swap(&input[k], &input[iterJ--]);
}
for (int k = hi ; k >= iterQ; --k)
{
swap(&input[k], &input[iterI++]);
}
sort(input, lo, iterJ);
sort(input, iterI, hi);
}
TimSortを見てください。完全にランダムではないデータの場合、クイックソートよりもパフォーマンスが優れています(これらは同じ漸近的な複雑さを持っていますが、TimSortの定数は低くなっています)。
末尾再帰は、再帰呼び出しをループに変更することです。 QuickSortの場合、次のようになります。
QuickSort(SortVar)
Granularity = 10
SortMax = Max(SortVar)
/* Put an element after the last with a higher key than all other elements
to avoid that the inner loop goes on forever */
SetMaxKey(SortVar, SortMax+1)
/* Push the whole interval to sort on stack */
Push 1 SortMax
while StackSize() > 0
/* Pop an interval to sort from stack */
Pop SortFrom SortTo
/* Tail recursion loop */
while SortTo - SortFrom >= Granularity
/* Find the pivot element using median of 3 */
Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)
/* Put the pivot element in front */
if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)
/* Place elements <=Key to the left and elements >Key to the right */
Key = GetKey(SortVar, SortFrom)
i = SortFrom + 1
j = SortTo
while i < j
while GetKey(SortVar, i) <= Key; i = i + 1; end
while GetKey(SortVar, j) > Key; j = j - 1; end
if i < j then Swap(SortVar, i, j)
end
/* Put the pivot element back */
if GetKey(SortVar, j) < Key then Swap(SortVar, SortFrom, j)
if j - SortFrom < SortTo - j then
/* The left part is smallest - put it on stack */
if j - SortFrom > Granularity then Push SortFrom j-1
/* and do tail recursion on the right part */
SortFrom = j + 1
end
else
/* The right part is smallest - put it on stack */
if SortTo - j > Granularity then Push j+1 SortTo
/* and do tail recursion on the left part */
SortTo = j - 1
end
end
end
/* Run insertionsort on the whole array to sort the small intervals */
InsertionSort(SortVar)
return
さらに、QuickSortが完了すると配列はおおまかにソートされるため、短い間隔でInsertionSortを呼び出す理由はありません。ソートする間隔が少なくなるためです。そして、これはInsertionSortの完璧なケースです。
スタックがない場合は、代わりに再帰を使用できますが、末尾再帰を維持します。
QuickSort(SortVar, SortFrom, SortTo)
Granularity = 10
/* Tail recursion loop */
while SortTo - SortFrom >= Granularity
/* Find the pivot element using median of 3 */
Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)
/* Put the pivot element in front */
if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)
/* Place elements <=Key to the left and elements >Key to the right */
Key = GetKey(SortVar, SortFrom)
i = SortFrom + 1
j = SortTo
while i < j
while GetKey(SortVar, i) <= Key; i = i + 1; end
while GetKey(SortVar, j) > Key; j = j - 1; end
if i < j then Swap(SortVar, i, j)
end
/* Put the pivot element back */
if GetKey(j) < Key then Swap(SortVar, SortFrom, j)
if j - SortFrom < SortTo - j then
/* The left part is smallest - recursive call */
if j - SortFrom > Granularity then QuickSort(SortVar, SortFrom, j-1)
/* and do tail recursion on the right part */
SortFrom = j + 1
end
else
/* The right part is smallest - recursive call */
if SortTo - j > Granularity then QuickSort(SortVar, j+1, SortTo)
/* and do tail recursion on the left part */
SortTo = j - 1
end
end
/* Run insertionsort on the whole array to sort the small intervals */
InsertionSort(SortVar)
return