私は配列を基本的に任意のサイズ(ほとんどがlog2(size(array))
よりも大きい)の並べ替えられたサブシーケンスの連続である状態に置くインプレース並べ替えアルゴリズムを開発しています。その後、所定のサブシーケンスをマージします。記述された状態に到達すると、現在の形式のアルゴリズムは最初の2つのサブシーケンスをマージし、その後、結果を次のサブシーケンスとマージします。マージ時に、ソートされたサブシーケンスがどこから始まるかがわかります。それらを再度見つける必要はありません。
うまくいきますが、このマージスキームは最適ではないと思います。よりスマートなマージスキームを使用することは可能であるはずだと思います。私が考えることができる最良のアルゴリズムは、最小の連続するソートされたサブシーケンスを探してそれらをマージし、すべてがマージされるまで繰り返すアルゴリズムです。アイデアは、最初に小さいシーケンスをマージする方が安価であるため、最後に最大のシーケンスをマージする必要があるということです。
N個の連続するサブシーケンスを適切にマージするためのより効率的なアルゴリズムはありますか?
要求通りに、次の配列をソートしたいとしましょう:
_10 11 12 13 14 9 8 7 6 5 0 1 2 3 4
_
私のアルゴリズムは、質問にはまったく関係のないことを行いますが、配列は次の状態のままにします。
_10 11 12 13 14 0 5 6 7 8 9 1 2 3 4
^ ^ ^
_
キャレットは、配列内の十分に大きいソートされたサブシーケンスが始まる場所を示しています。実際のコードでは、使用する抽象化に応じて、イテレータまたはインデックスに対応します。次のステップは、これらのサブシーケンスをマージして配列をソートすることです(重要な場合は、すべてがlog2(size(array))
よりも大きいが、サイズが異なる場合があることに注意してください)。この配列のさまざまな部分をマージするための最も賢い方法は、最後のサブシーケンスを中央のサブシーケンスとマージして、配列を次の状態のままにすることです。
_10 11 12 13 14 0 1 2 3 4 5 6 7 8 9
^ ^
_
...次に、残りの2つのサブシーケンスを2つマージして、配列が実際にソートされるようにします。先ほど述べたように、インプレースマージステップの前に、このようなサブシーケンスを最大log2(size(array))
まで含めることができます。
マージ手順の現在の解決策には少し間接的な方法が含まれます。キャレットが指すイテレータはリストに格納されます。次に、すべての要素がリストイテレータの1つである最小ヒープを作成し、比較関数はすべてのイテレータにその間の距離を関連付けます隣人。 2つのサブシーケンスがマージされると、ヒープから値をポップして、対応するイテレーターをリストから削除します。これが基本的に私のC++アルゴリズムが行うことです:
_template<typename Iterator, typename Compare=std::less<>>
auto sort(Iterator first, Iterator last, Compare compare={})
-> void
{
// Code irrelevant to the question here
// ...
//
// Multi-way merge starts here
std::list<Iterator> separators = { first, /* beginning of ordered subsequences */, last };
std::vector<typename std::list<Iterator>::iterator> heap;
for (auto it = std::next(separators.begin()) ; it != std::prev(separators.end()) ; ++it)
{
heap.Push_back(it);
}
auto cmp = [&](auto a, auto b) { return std::distance(*std::prev(a), *std::next(a)) < std::distance(*std::prev(b), *std::next(b)); };
std::make_heap(heap.begin(), heap.end(), cmp);
while (not heap.empty())
{
std::pop_heap(heap.begin(), heap.end(), cmp);
typename std::list<Iterator>::iterator it = heap.back();
std::inplace_merge(*std::prev(it), *it, *std::next(it), compare);
separators.erase(it);
heap.pop_back();
}
}
_
反復子について推論する方が簡単だと思うので、C++でアルゴリズムを記述しましたが、一般的なアルゴリズムの回答を歓迎します。
最初の2つのシーケンスを繰り返しマージする場合、2つ(またはそれ以上)をマージすると、最初の要素を何度も比較するため(n log(n)^ 2 ???)、マージソートよりも実行時間が長くなります。 )マージソートの効率に近づくたびに最小(隣接)シーケンス。
最小の隣接シーケンスを見つけるには、各ブランチのシーケンスの約半分を持つツリーを再帰的に構築し、次に最も低いブランチを最初にマージします。
----編集
最初のバージョン:
区切り記号がマージソートアルゴリズムの暗黙のパーティションである場合の必須のマージソート。
Order separators according to their index in the array
MergeIt(first, last) {
if (only one or zero separator)
return first;
split = separator containing the middle separator (first, last)
return inplace_merge(MergeIt(first, split), MergeIt(split, end));
}
これにより、最小数のマージのみが行われ、比較の最小数は行われないことが保証されます。これは、より大きなシーケンスが現在の最小シーケンスとマージされる可能性があるためです。
バージョン2:
基本的にはマージソートですが、シーケンスの長さを考慮に入れています。
Order separators according to their index in the array
MergeIt(first, last) {
if (only one or zero separator)
return first;
split = separator containing the middle element of array(first, last) // not the middle separator
return inplace_merge(MergeIt(first, split), MergeIt(split, end));
}
分割により、コールツリーで上位のストップが上位に移動すると、小さなシーケンスが最初にマージされます。大きなシーケンスが現在の最小長とマージされる可能性があるため、これは比較の最小数を保証しませんが、ここでは大きなシーケンスが小さいため、バージョン1よりも優れています。
バージョン3:
最小数の要素にわたる隣接するシーケンスをマージします
Make heap of pairs of adjacent sequences, sort after minimum length of the pairs, for each sequence only add the shortest of its prev and next.
// each sequence will appear max twice except the first and last.
while (heap.size() > 1) {
min = heap.pop
remove the possible other occurrence of min.first and min.second
sequence = inplace_merge(min.first, min.second)
insert the minimum of pair(prev(sequence), sequence) and pair(sequence, next(sequence)) in heap.
}
オーバーヘッドにより、これは2番目のバージョンよりも遅くなる可能性がありますが、inplace_mergeによる比較は最小になっているはずです。