web-dev-qa-db-ja.com

並べ替えられたn個の連続した配列をマージする効率的なアルゴリズム

私は配列を基本的に任意のサイズ(ほとんどが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++でアルゴリズムを記述しましたが、一般的なアルゴリズムの回答を歓迎します。

7
Morwenn

最初の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による比較は最小になっているはずです。

1
Surt