クイックソートと、それを再帰的方法と反復的方法の両方で実装する方法について学びました。
反復法の場合:
そして、再帰バージョンはwikiで定義された通常のバージョンです。
再帰的なアルゴリズムは、反復するアルゴリズムより常に遅いことを学びました。
では、時間の複雑さの観点からどの方法が推奨されますか(メモリは問題ではありません)?
どれがプログラミングコンテストで使用するのに十分な速さですか?
c ++ STL sort()は再帰的アプローチを使用していますか?
(漸近的)時間の複雑さ-両方とも同じです
「再帰は低速で反復する」-このステートメントの背後にある合理的な理由は、再帰スタックのオーバーヘッド(呼び出し間の環境の保存と復元)が原因です。
ただし、これらは「反復」の数を変更せずに、一定数の操作です。
再帰的なクイックソートと反復的なクイックソートはどちらもO(nlogn)
average caseとO(n^2)
worst caseです。
編集:
それを楽しむために、投稿に(Java)コードを添付してベンチマークを実行し、次に wilcoxon statistic test を実行して、実行時間が実際に異なっている確率を確認しました
結果は決定的なものです(P_VALUE = 2.6e-34、つまり、結果が同じである確率は2.6 * 10 ^ -34です-ありそうもありません)。しかし、答えはあなたが期待したものではありません。
反復解の平均は408.86ミリ秒でしたが、再帰解の平均は236.81ミリ秒でした
(注-recursiveQsort()
への引数としてInteger
ではなくint
を使用しました-それ以外の場合は、多くのボックス化を行う必要がないため、再帰の方がはるかに優れています整数。これも時間のかかる作業です。反復的な解決策には他に選択肢がないので、私はそうしました。
したがって、あなたの仮定は真実ではありません、再帰的解法は(私のマシンではJava少なくとも))、P_VALUE = 2.6e-34の反復解法よりも高速です。
public static void recursiveQsort(int[] arr,Integer start, Integer end) {
if (end - start < 2) return; //stop clause
int p = start + ((end-start)/2);
p = partition(arr,p,start,end);
recursiveQsort(arr, start, p);
recursiveQsort(arr, p+1, end);
}
public static void iterativeQsort(int[] arr) {
Stack<Integer> stack = new Stack<Integer>();
stack.Push(0);
stack.Push(arr.length);
while (!stack.isEmpty()) {
int end = stack.pop();
int start = stack.pop();
if (end - start < 2) continue;
int p = start + ((end-start)/2);
p = partition(arr,p,start,end);
stack.Push(p+1);
stack.Push(end);
stack.Push(start);
stack.Push(p);
}
}
private static int partition(int[] arr, int p, int start, int end) {
int l = start;
int h = end - 2;
int piv = arr[p];
swap(arr,p,end-1);
while (l < h) {
if (arr[l] < piv) {
l++;
} else if (arr[h] >= piv) {
h--;
} else {
swap(arr,l,h);
}
}
int idx = h;
if (arr[h] < piv) idx++;
swap(arr,end-1,idx);
return idx;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String... args) throws Exception {
Random r = new Random(1);
int SIZE = 1000000;
int N = 100;
int[] arr = new int[SIZE];
int[] millisRecursive = new int[N];
int[] millisIterative = new int[N];
for (int t = 0; t < N; t++) {
for (int i = 0; i < SIZE; i++) {
arr[i] = r.nextInt(SIZE);
}
int[] tempArr = Arrays.copyOf(arr, arr.length);
long start = System.currentTimeMillis();
iterativeQsort(tempArr);
millisIterative[t] = (int)(System.currentTimeMillis()-start);
tempArr = Arrays.copyOf(arr, arr.length);
start = System.currentTimeMillis();
recursvieQsort(tempArr,0,arr.length);
millisRecursive[t] = (int)(System.currentTimeMillis()-start);
}
int sum = 0;
for (int x : millisRecursive) {
System.out.println(x);
sum += x;
}
System.out.println("end of recursive. AVG = " + ((double)sum)/millisRecursive.length);
sum = 0;
for (int x : millisIterative) {
System.out.println(x);
sum += x;
}
System.out.println("end of iterative. AVG = " + ((double)sum)/millisIterative.length);
}
再帰は常に反復より遅いとは限りません。クイックソートはその完璧な例です。これを繰り返し行う唯一の方法は、スタック構造を作成することです。したがって、他の方法では、再帰を使用する場合にコンパイラーが行うのと同じことを行います。おそらく、これはコンパイラーよりも悪いことになります。また、再帰を使用しない場合(ジャンプして値をスタックにプッシュするため)にジャンプが増えます。
それがJavascriptで思いついたソリューションです。うまくいくと思います。
function qs_iter(items) {
if (!items || items.length <= 1) {
return items
}
var stack = []
var low = 0
var high = items.length - 1
stack.Push([low, high])
while (stack.length) {
var range = stack.pop()
low = range[0]
high = range[1]
if (low < high) {
var pivot = Math.floor((low + high) / 2)
stack.Push([low, pivot])
stack.Push([pivot+1, high])
while (low < high) {
while (low < pivot && items[low] <= items[pivot]) low++
while (high > pivot && items[high] > items[pivot]) high--
if (low < high) {
var tmp = items[low]
items[low] = items[high]
items[high] = tmp
}
}
}
}
return items
}
</ code>
間違いを見つけた場合はお知らせください:)