サブリストがかなりのメモリを占有しているのに、なぜクイックソートが「インプレース」と記述されるのですか?確かにバブルソートのようなものだけが配置されていますか?


7

クイックソートは「インプレース」と説明されていますが、次のような実装を使用しています。

def sort(array):
    less = []
    equal = []
    greater = []
    if len(array) > 1:
        pivot = array[0]
        for x in array:
            if x < pivot:
                less.append(x)
            if x == pivot:
                equal.append(x)
            if x > pivot:
                greater.append(x)
        return sort(less) + equal + sort(greater)
    else:
        return array

再帰ごとにリストのコピーを作成する必要があります。最初の戻りまでに、メモリには次のようになります。

  • アレイ
  • 大きい+等しい+小さい

次に、すべてのサブリストにわたる2回目の再帰によって、次のようになります。

  • アレイ
  • 大きい、等しい、最初の再帰から少ない
  • 大きい1から大きい+等しい+小さい大きい1から大きい+等しい+小さい

等...

これは単にひどく書かれたコードですか、それとも、大きなリストの場合、実際にそれらを格納するために相当量の余分なスペースが必要だと思いますか?

「インプレース」で何かを考えるとき、バブルソートを考えます。これは、http//en.wikipedia.org/wiki/File:Bubble-sort-example-300pxのようなリスト内の要素を単純に交換します。 gif

BubbleSortは、スワップされる可能性のある要素を格納するために1つの追加の変数のみを必要とします。


1
あなたが何を求めているのか分かりません。あなたの質問が何であるかを明確にするために質問を編集してみませんか?QuickSortをインプレースで実装できるかどうかを尋ねているだけですか?もしそうなら、それは通常の場所での標準的な回答を伴う標準的な質問です-あなたは十分な研究を行っていません。PSまた、あなたはあなたの言葉遣いをより正確にしようとするかもしれません。たとえば、最初の文が何を言っているかわかりません(動詞が欠けているようです)。2番目の文では、「あなた」が誰であるかわかりません。
DW

1
インプレースは、外部データ領域の使用を回避できるが、ソートする元の配列のみを使用できることを意味します。この実装はそれを行いません。
–ThorbjørnRavn Andersen 2014

回答:


21

このクイックソートの特定の実装は、インプレースではありません。これは、データ構造を一方向にしか拡大できないリストとして扱います(この場合、マージソートはより単純で高速になります)。ただし、クイックソートのインプレース実装を作成することは可能であり、これが通常の方法です。

インプレース実装では、代わりに、新しく構築された配列でソート関数が再帰的に呼び出されるのではなく、次第に小さくなります。

def sort(array, start, end):
    if end >= start: return
    pivot = array[start]
    middle = start + 1
    for i in range(start+1, end):
        if array[i] >= array[middle]:
            tmp = array[i]
            array[i] = array[middle]
            array[middle] = tmp
            middle += 1
    sort(array, start, middle)
    sort(array, middle, end)

(このコードに注意してください、私はそれをタイプしただけで、証明しませんでした。オフバイワンエラーは修正するのはあなたです。実際の実装では、小さいサイズに対して異なるアルゴリズムを使用しますが、これは漸近的な動作には影響しません。 -worldの実装ではより適切なピボット選択されますが、実際にはこの質問には対応していないため、ここでは説明しません。)

Wikipediaのページを提示非インプレースおよびアルゴリズムのインプレースバージョンの両方。

ここで記述されているクイックソートには、追加のストレージが必要です。ここで、は再帰の深さ(ピボットの品質によって異なります)、は要素のサイズです。スタックサイズの要件を改善できます。2つの再帰呼び出しがあり、2番目の呼び出しを末尾呼び出し(スタックを消費しない)にすることができます。小さい方から常に再帰呼び出しを最初に行う場合、までの配列長の最大スタックサイズは、を満たします。なので、です。したがって、ピボットの選択に関係なく、追加のストレージ要件を達成でき。O(d)+sdsnS^(n)S^(m)mn/2S^(n/2)S^(n)lg2(n)S^(1)O(logn)+s

挿入ソート、選択ソート、ヒープソートなど、インプレースで実装できる他のソートアルゴリズムはたくさんあります。マージソートの単純な形式はインプレースではありませんが、より洗練されたバリアントがあります。

Quicksortは常にスタックで実行できること、および追加ストレージとランタイムの両方を持つマージソートのバリアントがあることを指摘してくれたAryabhataに感謝します。lg(n)O(1)O(nlog(n))


1
@ギルズ:いいえ。ピボット選択アルゴリズムが常に最小値を選択したとしても、スペースを使用するようにクイックソートを実装でき。(ただし、それはクイックソートの変形であると主張するかもしれません)。重要なのは、短いパーティションでは常に再帰し、長いパーティションではテールを再帰することです。O(logn)
アリヤバタ2014年

1
ところで、をしたインプレースマージソートも達成されたと思います。akira.ruc.dk/~keld/teaching/algoritmedesign_f04/Artikler/04/…は、線形時間インプレースマージを主張しています。O(nlogn)
アリヤバタ2014年

3

Gillesの回答に加えて、配列の代わりにリンクリストを使用する場合は、コードでQuicksortをインプレースにすることもできます。小さいリストの1つに要素を追加するときは、元のリストから要素を削除してください。

次の疑似コードは、想定/保証します:

  • 各リストの最初のエントリ(以下ではヘッドと呼ばれます)は要素を凝視せず、変更に対するリストへのポインタをそのまま維持することができます。次のポインタnew listを持つヘッドで構成されるリストを作成しNILます。
  • sortヘッドへのポインタを受け取り、ソートされたリストの最後の要素へのポインタを返します。引数として指定されたポインタは、ソートされたリストの先頭でもあります。
  • 各要素とリストヘッドのメモリ位置は変わりません。

sort(list):
    less_cur = less = new list
    equal_cur = equal = new list
    greater_cur = greater = new list
    if list.next != NIL:
        cur = list.next
        pivot = cur.value
        while cur != NIL:
            list.next = cur.next
            if cur.value < pivot:
                less_cur.next = cur
                less_cur = less_cur.next
            if cur.value == pivot:
                equal_cur.next = cur
                equal_cur = equal_cur.next
            if cur.value > pivot:
                equal_cur.next = cur
                equal_cur = equal_cur.next
            cur = cur.next
        less_cur.next = greater_cur.next = NIL
        less_last = sort(less) 
        list.next = less.next
        less_last.next = equal
        greater_last = sort(greater)
        equal_cur.next = greater.next
        return greater_last
    else:
        return list

上記のコードから可能な最適化がいくつかありますが、この実装はGillesの回答の配列ベースの実装よりもメモリオーバーヘッドが大きくなります。さらに、実際には、リンクリストはローカライズされていないため、同じデータを配列に保持するよりも多くのキャッシュミスを引き起こすという問題があります。

ただし、並べ替えによって要素へのポインタを維持する必要がある場合、この実装は有利です。並べ替えとは関係のない理由でリンクリストにデータを格納している場合は、この点にも注意してください。(ソートのインプレースネスが懸念される場合、リストと配列間の変換はおそらく問題になります。)


これは、「インプレース」を「要素をコピーする必要がない」と見なしますが、すべてのリストが要素を指しているため、多くの作業を行います。私は、「インプレース」に完全ではないと(すべてのゴミを収集する必要があるものとして終わる)、これらすべてのauxillaryのデータ構造の使用を検討する
するThorbjörnRavnアンデルセン

@Thorbjørn2つのポインター(二重リンクリストの場合は4つ)を変更することで、元のリストから削除し、小さいリストに追加できます。これがゴミを生成する場所はわかりません。
FrankW 2014年

おそらく、私たちが同じことについて議論していることを確認するために、提案したことの実装を示す必要があります。
するThorbjörnRavnアンデルセン

@ThorbjørnRavnAndersenCommon Lispの1つは、The Pitmanualのシープトリックです。リストの最初の開始点であったリストノードは後ではない可能性があるため、「所定の場所」にはありませんが、追加のストレージは必要ありません。パーティション分割により、リスト構造が破壊的に変更され、サブリストが元のリストと同じリストノードから作成されます。明らかに、これにはリストの構造を変更する機能が必要ですが、一部の実装(JavaのListインターフェースなど)では提供されていません。
Joshua Taylor

@FrankWこれを「追加のストレージなし」ほど「インプレース」と呼ぶのは正しいとは思いません。リストがソートされる前の位置iのリストノードは、後で位置iにない場合があります。
ジョシュアテイラー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.