私は最近、Floyd、Rivest Selectルーチンのクイック選択よりもパフォーマンスが優れていると報告されている有望なKth選択ルーチンに遭遇しました。 このWikipediaの記事 は、Cに翻訳しようとした疑似コードバージョンを提供します。
リンク ALGOLのコードを含む実際の論文へ。
これは私のコードの翻訳です
void select(float array[], int left, int right, int k)
{
#define sign(x) ((x > 0.0) ? 1 : ((x < 0.0) ? (-1) : 0))
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
int n, i, j, ll, rr;
int s, sd;
float z, t;
while (right > left)
{
// use select recursively to sample a smaller set of size s
// the arbitrary constants 600 and 0.5 are used in the original
// version to minimize execution time
if (right - left > 600)
{
n = right - left + 1;
i = k - left + 1;
z = log(n);
s = 0.5 * exp(2 * z/3);
sd = 0.5 * sqrt(z * s * (n - s)/n) * sign(i - n/2);
ll = max(left, k - i * s/n + sd);
rr = min(right, k + (n - i) * s/n + sd);
select(array, ll, rr, k);
}
// partition the elements between left and right around t
t = array[k];
i = left;
j = right;
F_SWAP(array[left],array[k]);
if (array[right] > t)
{
F_SWAP(array[right],array[left]);//swap array[right] and array[left]
}
while (i < j)
{
F_SWAP(array[i],array[j]);
i = i + 1;
j = j -1;
while (array[i] < t)
{
i = i + 1;
}
while (array[j] > t)
{
j = j - 1;
}
}
if (array[left] == t)
{
F_SWAP (array[left], array[j])
}
else
{
j = j + 1;
F_SWAP (array[j],array[right])
}
// adjust left and right towards the boundaries of the subset
// containing the (k - left + 1)th smallest element
if (j <= k)
{
left = j + 1;
}
if (k <= j)
{
right = j - 1;
}
}
#undef sign
#undef F_SWAP
}
うまくいくようですが、このルーチンについて私を困惑させる多くの質問があります。
まず、私が読んだことによると、このルーチンはHoareのQuickSelectおよびWirthの選択アルゴリズムよりも高速であると想定されています。ただし、私のコードでは遅くなります。
マジックナンバー600と.5はここで正確に何をしているのですか?疑似コードは、実行時間を最小化するための任意の定数であることを説明しています。これはALGOL言語に固有のものでしたか?
対数、指数、平方根、およびシグマが実際にK番目の位置を見つけるために実行していることは何ですか?
このルーチンの変換で発生しているパフォーマンスの問題は、おそらく指数と対数が原因であると思います。
コードを少し試してみたところ、より良い結果が得られました。私は元の論文に戻り、ウィキペディアのページを無視しました。アルゴリズムを他のクイックセレクトルーチンと比較して、いくつかの素晴らしい結果を得ました。
わかりました、これが私が遊んでいる方法です。 これらは浮動小数点用であり、メソッドをvoidから変更したことにも注意してください。
クイックセレクトver 1
float quickselect(float *arr, int length, int kTHvalue)
{
#define F_SWAP(a, b) { tmp = arr[a]; arr[a] = arr[b]; arr[b] = tmp; }
int i, st;
float tmp;
for (st = i = 0; i < length - 1; i++)
{
if (arr[i] > arr[length-1]) continue;
F_SWAP(i, st);
st++;
}
F_SWAP(length-1, st);
#undef F_SWAP
return kTHvalue == st ?arr[st]
:st > kTHvalue ? quickselect(arr, st, kTHvalue)
: quickselect(arr + st, length - st, kTHvalue - st);
}
元のソースはRosettaコードだと思います。それはかなり速く機能しますが、他のいくつかのコードを見た後、別のクイック選択関数が書かれました。
クイック選択ver 2
float quickselect2(float *arr, int length, int kthLargest)
{
#define F_SWAP(a, b) { tmp = a; a = b; b = tmp; }
int left = 0;
int right = length - 1;
int pos;
float tmp;
for (int j = left; j < right; j++)
{
float pivot = arr[kthLargest];
F_SWAP(arr[kthLargest], arr[right]);
for (int i = pos = left; i < right; i++)
{
if (arr[i] < pivot)
{
F_SWAP(arr[i], arr[pos]);
pos++;
}
}
F_SWAP(arr[right], arr[pos]);
#undef F_SWAP
if (pos == kthLargest) break;
if (pos < kthLargest) left = pos + 1;
else right = pos - 1;
}
return arr[kthLargest];
}
私は周りを見回して、いくつかの良い約束があったさらに別のクイックセレクトを見つけました。中央値3のクイックセレクトでした
float quickselect_MO3(float *arr, const int length, const int kTHvalue)
{
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
unsigned int low = 0;
unsigned int high = length - 1;
for (unsigned int j = low; j < high; j++)
{
if (high <= low) // One element only
return arr[kTHvalue];
if (high == low + 1)
{ // Two elements only
if (arr[low] > arr[high])
F_SWAP(arr[low], arr[high]);
return arr[kTHvalue];
}
//median of 3
int middle = (low + high) / 2;
if (arr[middle] > arr[high])
F_SWAP(arr[middle], arr[high]);
if (arr[low] > arr[high])
F_SWAP(arr[low], arr[high]);
if (arr[middle] > arr[low])
F_SWAP(arr[middle], arr[low]);
// Swap low item (now in position middle) into position (low+1)
F_SWAP(arr[middle], arr[low+1]);
// Nibble from each end towards middle, swapping items when stuck
unsigned int ll = low + 1;
unsigned int hh = high - 1;//unsigned int hh = high;
for (unsigned int k = ll; k < hh; k++)
{
do ll++; while (arr[low] > arr[ll]);
do hh--; while (arr[hh] > arr[low]);
if (hh < ll)
break;
F_SWAP(arr[ll], arr[hh]);
}
// Swap middle item (in position low) back into correct position
F_SWAP(arr[low], arr[hh]);
// Re-set active partition
if (hh <= kTHvalue)
low = ll;
if (hh >= kTHvalue)
high = hh - 1;
}
#undef F_SWAP
}
3の中央値は高速で、かなり印象的です。
次に、私の書き直した(紙に変換された元のALGOLから)Floyd Routine。
フロイド中央値関数ver 1
float select(float array[], int left, int right, int k)
{
#define sign(x) ((x > 0.0) ? 1 : ((x < 0.0) ? (-1) : 0))
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
int i;
right = right - 1;
while (right > left)
{
// use select recursively to sample a smaller set of size s
// the arbitrary constants 600 and 0.5 are used in the original
// version to minimize execution time
if (right - left > right)
{
int n = right - left + 1;
i = k - left + 1;
float z = logf(n);
float s = 0.5 * expf(2 * z/3);
float sd = 0.5 * sqrtf(z * s * (n - s) / n) * sign(i - n / 2);
int ll = max(left, k - 1 * s/n + sd);
int rr = min(right, k + (n - 1) * s/n + sd);
select(array, ll, rr, k);
}
// partition the elements between left and right around t
float t = array[k];
i = left;
int j = right;
F_SWAP(array[left],array[k]);
if (array[right] > t)
{
F_SWAP(array[right],array[left]);
}
while (i < j)
{
F_SWAP(array[i],array[j]);
i++;
j--;
while (array[i] < t)
{
i++;
}
while (array[j] > t)
{
j--;
}
}
if (array[left] == t)
{
F_SWAP (array[left], array[j])
}
else
{
j++;
F_SWAP (array[j],array[right])
}
// adjust left and right towards the boundaries of the subset
// containing the (k - left + 1)th smallest element
if (j <= k)
{
left = j + 1;
}
if (k <= j)
{
right = j - 1;
}
}
return array[k];
#undef sign
#undef F_SWAP
}
最後に、対数関数、指数関数、平方根関数を削除することを決定し、同じ出力とさらに良い時間を得ました。
フロイド中央値関数ver 2
float select1(float array[], int left, int right, int k)
{
#define sign(x) ((x > 0.0) ? 1 : ((x < 0.0) ? (-1) : 0))
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
int i;
right = right - 1;
while (right > left)
{
// use select recursively to sample a smaller set of size s
// the arbitrary constants 600 and 0.5 are used in the original
// version to minimize execution time
if (right - left > right)
{
int n = right - left + 1;
i = k - left + 1;
int s = (2 * n / 3);
int sd = (n * s * (n - s) / n) * sign(i - n / 2);
int ll = max(left, k - i * s / n + sd);
int rr = min(right, k + (n - i) * s / n + sd);
select1(array, ll, rr, k);
}
// partition the elements between left and right around t
float t = array[k];
i = left;
int j = right;
F_SWAP(array[left],array[k]);
if (array[right] > t)
{
F_SWAP(array[right],array[left]);
}
while (i < j)
{
F_SWAP(array[i],array[j]);
i++;
j--;
while (array[i] < t)
{
i++;
}
while (array[j] > t)
{
j--;
}
}
if (array[left] == t)
{
F_SWAP (array[left], array[j])
}
else
{
j++;
F_SWAP (array[j],array[right])
}
// adjust left and right towards the boundaries of the subset
// containing the (k - left + 1)th smallest element
if (j <= k)
{
left = j + 1;
}
if (k <= j)
{
right = j - 1;
}
}
return array[k];
#undef sign
#undef F_SWAP
}
これが結果のチャートです
編集私はフロイドルーチンを中央値3でコード化し、チャートを更新しました。中央値が3で、クイックセレクトとほぼ同じ速度で評価されます。それはもう少し探検する価値があります。
float select3(float array[], int left, int right, int k)
{
#define sign(x) ((x > 0.0) ? 1 : ((x < 0.0) ? (-1) : 0))
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
int i;
right = right - 1;
while (right > left)
{
if( array[k] < array[left] ) F_SWAP(array[left],array[k]);
if( array[right] < array[left] ) F_SWAP(array[left],array[right]);
if( array[right] < array[k] ) F_SWAP(array[k],array[right]);
if (right - left > right)
{
int n = right - left + 1;
i = k - left + 1;
int s = (2 * n / 3);
int sd = (n * s * (n - s) / n) * sign(i - n / 2);
int ll = max(left, k - i * s / n + sd);
int rr = min(right, k + (n - i) * s / n + sd);
select1(array, ll, rr, k);
}
// partition the elements between left and right around t
float t = array[k];
i = left;
int j = right;
F_SWAP(array[left],array[k]);
if (array[right] > t)
{
F_SWAP(array[right],array[left]);
}
while (i < j)
{
F_SWAP(array[i],array[j]);
i++;
j--;
while (array[i] < t)
{
i++;
}
while (array[j] > t)
{
j--;
}
}
if (array[left] == t)
{
F_SWAP (array[left], array[j])
}
else
{
j++;
F_SWAP (array[j],array[right])
}
// adjust left and right towards the boundaries of the subset
// containing the (k - left + 1)th smallest element
if (j <= k)
{
left = j + 1;
}
if (k <= j)
{
right = j - 1;
}
}
return array[k];
#undef sign
#undef F_SWAP
}
ご覧のとおり、フロイドルーチンは、中央値が3のバージョン以外のクイックセレクトよりも高速です。3の中央値をフロイドルーチンに追加できなかった理由はわかりません。次。 Floydルーチンに関しては、もちろん、浮動小数点ログ、expおよびsqrt関数を削除すると、ルーチンが高速化されます。
フロイドがそもそもなぜそれをそこに置いたのか、私はまだ分かりません。
編集6-7-15これは非再帰バージョンで、再帰バージョンと同じ速度で実行されます。
float select7MO3(float array[], const int length, const int kTHvalue)
{
#define sign(x) ((x > 0) ? 1 : ((x < 0) ? (-1) : 0))
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
int left = 0;
int i;
int right = length - 1;
int rr = right;
int ll = left;
while (right > left)
{
if( array[kTHvalue] < array[left] ) F_SWAP(array[left],array[kTHvalue]);
if( array[right] < array[left] ) F_SWAP(array[left],array[right]);
if( array[right] < array[kTHvalue] ) F_SWAP(array[kTHvalue],array[right]);
if ((right - left) > kTHvalue)
{
int n = right - left + 1;
i = kTHvalue - left + 1;
int s = (2 * n / 3);
int sd = (n * s * (n - s) / n) * sign(i - n);
ll = max(left, kTHvalue - i * s / n + sd);
rr = min(right, kTHvalue + (n - i) * s / n + sd);
}
// partition the elements between left and right around t
float t = array[kTHvalue];
i = left;
int j = right;
F_SWAP(array[left],array[kTHvalue]);
if (array[right] > t)
{
F_SWAP(array[right],array[left]);
}
while (i < j)
{
F_SWAP(array[i],array[j]);
i++;
j--;
while (array[i] < t)
{
i++;
}
while (array[j] > t)
{
j--;
}
}
if (array[left] == t)
{
i--;
F_SWAP (array[left], array[j])
}
else
{
j++;
F_SWAP (array[j],array[right])
}
// adjust left and right towards the boundaries of the subset
// containing the (k - left + 1)th smallest element
if (j <= kTHvalue)
{
left = j + 1;
}
else if (kTHvalue <= j)
{
right = j - 1;
}
}
return array[kTHvalue];
#undef sign
#undef F_SWAP
}
編集6-13-15
フロイドの選択ルーチンをさらに試し、彼のルーチンをN.ワースの選択ルーチンと比較し始めました。彼のルーティンをフロイドのルーティンと組み合わせるとどうなるかというアイデアが出くわしました。これが私が思いついたものです。
float FloydWirth_kth(float arr[], const int length, const int kTHvalue)
{
#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }
#define SIGNUM(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : (x)))
int left = 0;
int right = length - 1;
int left2 = 0;
int right2 = length - 1;
//while (left < right)
while (left < right)
{
if( arr[right2] < arr[left2] ) F_SWAP(arr[left2],arr[right2]);
if( arr[right2] < arr[kTHvalue] ) F_SWAP(arr[kTHvalue],arr[right2]);
if( arr[kTHvalue] < arr[left2] ) F_SWAP(arr[left2],arr[kTHvalue]);
int rightleft = right - left;
if (rightleft < kTHvalue)
{
int n = right - left + 1;
int ii = kTHvalue - left + 1;
int s = (n + n) / 3;
int sd = (n * s * (n - s) / n) * SIGNUM(ii - n / 2);
int left2 = max(left, kTHvalue - ii * s / n + sd);
int right2 = min(right, kTHvalue + (n - ii) * s / n + sd);
}
float x=arr[kTHvalue];
while ((right2 > kTHvalue) && (left2 < kTHvalue))
{
do
{
left2++;
}while (arr[left2] < x);
do
{
right2--;
}while (arr[right2] > x);
//F_SWAP(arr[left2],arr[right2]);
F_SWAP(arr[left2],arr[right2]);
}
left2++;
right2--;
if (right2 < kTHvalue)
{
while (arr[left2]<x)
{
left2++;
}
left = left2;
right2 = right;
}
if (kTHvalue < left2)
{
while (x < arr[right2])
{
right2--;
}
right = right2;
left2 = left;
}
if( arr[left] < arr[right] ) F_SWAP(arr[right],arr[left]);
}
#undef F_SWAP
#undef SIGNUM
return arr[kTHvalue];
}
速度の違いは驚くべきものです(少なくとも私にとっては)。以下は、さまざまなルーチンの速度を示すグラフです。