web-dev-qa-db-ja.com

リンクリストをnlogn時間でシャッフルするためのアルゴリズム

リンクリストを線形(n log n)時間と対数(log n)の余分なスペースでランダムにシャッフルする分割統治アルゴリズムを使用して、リンクリストをシャッフルしようとしています。

単純な値の配列で使用できるのと同様のクヌースシャッフルを実行できることは知っていますが、分割統治法でこれをどのように実行するかはわかりません。私が言いたいのは、私が実際に何を分割しているのかということです。リスト内の個々のノードに分割してから、ランダムな値を使用してリストをランダムに組み立て直しますか?

または、各ノードに乱数を与えてから、乱数に基づいてノードでマージソートを実行しますか?

20
5StringRyan

次はどうですか?マージソートと同じ手順を実行します。マージするときは、2つのリストから要素を1つずつ選択する代わりに、コインを投げます。コイントスの結果に基づいて、最初のリストから要素を選択するか、2番目のリストから要素を選択するかを選択します。

アルゴリズム

_shuffle(list):
    if list contains a single element
        return list

    list1,list2 = [],[]
    while list not empty:
        move front element from list to list1
        if list not empty: move front element from list to list2

    shuffle(list1)
    shuffle(list2)

    if length(list2) < length(list1):
        i = pick a number uniformly at random in [0..length(list2)]             
        insert a dummy node into list2 at location i 

    # merge
    while list1 and list2 are not empty:
        if coin flip is Heads:
            move front element from list1 to list
        else:
            move front element from list2 to list

    if list1 not empty: append list1 to list
    if list2 not empty: append list2 to list

    remove the dummy node from list
_

スペースの重要なポイントは、リストを2つに分割しても、余分なスペースは必要ないということです。必要な追加のスペースは、再帰中にスタック上のlogn要素を維持することだけです。

ダミーノードのポイントは、ダミー要素を挿入および削除すると、要素の分布が均一に保たれることを理解することです。

分析なぜ分布が均一なのですか?最終的なマージ後、任意の数値がiの位置で終わる確率P_i(n)は次のようになります。どちらかでした:

  • 独自のリストのi番目の場所で、リストが最初のi回コイントスに勝った場合、この確率は_1/2^i_です。
  • _i-1_-自身のリストの1位で、リストがコイントスを_i-1_回勝ちました最後のものを含むそして一度負けた場合、これの確率は_(i-1) choose 1_回_1/2^i_;
  • _i-2_-自身のリストの2位で、リストがコイントスを_i-2_回勝ちました最後のものを含むそして2回負けた場合、この確率は_(i-1) choose 2_回_1/2^i_;
  • 等々。

だから確率

_P_i(n) = \sum_{j=0}^{i-1} (i-1 choose j) * 1/2^i * P_j(n/2).
_

帰納的に、あなたはそのP_i(n) = 1/nを示すことができます。基本ケースを検証し、P_j(n/2) = 2/nと仮定します。 \sum_{j=0}^{i-1} (i-1 choose j)という用語は、正確には_i-1_ビットの2進数、つまり_2^{i-1}_の数です。だから私たちは得る

_P_i(n) = \sum_{j=0}^{i-1} (i-1 choose j) * 1/2^i * 2/n
       = 2/n * 1/2^i * \sum_{j=0}^{i-1} (i-1 choose j)
       = 1/n * 1/2^{i-1} * 2^{i-1}
       = 1/n
_

これが理にかなっていることを願っています。必要な唯一の仮定は、nが偶数であり、2つのリストが均一にシャッフルされていることです。これは、ダミーノードを追加(および削除)することで実現されます。

P.S.私の最初の直感は厳密にはほど遠いものでしたが、念のためにリストします。リストの要素に1からnまでの番号をランダムに割り当てると想像してください。そして今、これらの数値に関してマージソートを実行します。マージの任意のステップで、2つのリストのどちらが小さいかを決定する必要があります。ただし、一方が他方よりも大きくなる確率は正確に1/2である必要があるため、コインを投げることでこれをシミュレートできます。

P.P.S.ここにLaTeXを埋め込む方法はありますか?

26
foxcub

コード

アップシャッフルアプローチ

この(lua)バージョンは、foxcubの回答から改善され、ダミーノードの必要性がなくなりました。

この回答のコードを少し単純化するために、このバージョンでは、リストがそのサイズを知っていると想定しています。そうでない場合は、いつでもO(n)時間で見つけることができますが、さらに良い方法です。コードをいくつか簡単に調整して、事前に計算する必要がないようにすることができます(2つに1つに分割するなど)前半と後半の代わりに)。

function listUpShuffle (l)
    local lsz = #l
    if lsz <= 1 then return l end

    local lsz2 = math.floor(lsz/2)
    local l1, l2 = {}, {}
    for k = 1, lsz2     do l1[#l1+1] = l[k] end
    for k = lsz2+1, lsz do l2[#l2+1] = l[k] end

    l1 = listUpShuffle(l1)
    l2 = listUpShuffle(l2)

    local res = {}
    local i, j = 1, 1
    while i <= #l1 or j <= #l2 do
        local rem1, rem2 = #l1-i+1, #l2-j+1
        if math.random() < rem1/(rem1+rem2) then
            res[#res+1] = l1[i]
            i = i+1
        else
            res[#res+1] = l2[j]
            j = j+1
        end
    end
    return res
end

ダミーノードの使用を回避するには、各リストで選択する確率を変えることにより、2つの中間リストの長さが異なる可能性があるという事実を補正する必要があります。これは、(2つのリスト内の)ポップされたノードの総数に対する最初のリストからポップされたノードの比率に対して[0,1]の均一な乱数をテストすることによって行われます。

ダウンシャッフルアプローチ

再帰的に細分化しながらシャッフルすることもできます。これは、私の控えめなテストでは、わずかに(ただし一貫して)優れたパフォーマンスを示しました。命令が少ないために発生する可能性がありますが、一方で、luajitのキャッシュのウォームアップが原因で表示される可能性があるため、ユースケースのプロファイルを作成する必要があります。

function listDownShuffle (l)
    local lsz = #l
    if lsz <= 1 then return l end

    local lsz2 = math.floor(lsz/2)
    local l1, l2 = {}, {}
    for i = 1, lsz do
        local rem1, rem2 = lsz2-#l1, lsz-lsz2-#l2
        if math.random() < rem1/(rem1+rem2) then
            l1[#l1+1] = l[i]
        else
            l2[#l2+1] = l[i]
        end
    end

    l1 = listDownShuffle(l1)
    l2 = listDownShuffle(l2)

    local res = {}
    for i = 1, #l1 do res[#res+1] = l1[i] end
    for i = 1, #l2 do res[#res+1] = l2[i] end
    return res
end

テスト

完全なソースは my listShuffle.lua Gist にあります。

これには、実行時に、入力リストの各要素について、指定された回数の実行後に出力リストの各位置に表示される回数を表す行列を出力するコードが含まれています。かなり均一なマトリックスは、文字の分布の均一性、したがってシャッフルの均一性を示します。

これは、(2の累乗ではない)3要素リストを使用して1000000回の反復で実行された例です。

>> luajit listShuffle.lua 1000000 3
Up shuffle bias matrix:
333331 332782 333887
333377 333655 332968
333292 333563 333145
Down shuffle bias matrix:
333120 333521 333359
333435 333088 333477
333445 333391 333164
5
Aszarsha

Foxcubの答えは間違っていると思います。完全にシャッフルされたリストの有用な定義を紹介することを証明するために(配列やシーケンスなど、必要なものと呼んでください)。

定義:要素a1, a2 ... anとインデックス1, 2, 3..... nを含むリストLがあると仮定します。 Lをシャッフル操作(内部にアクセスできない)に公開すると、Lは、いくつかのk(k< n)要素のインデックスを知っている場合にのみ、完全にシャッフルされます。残りのn-k要素のインデックスを推測します。つまり、残りのn-k要素は、残りのn-kインデックスのいずれかで明らかになる可能性が等しくなります。

例:4つの要素リスト[a, b, c, d]があり、それをシャッフルした後、その最初の要素はa[a, .., .., ..])であり、要素b, c, dのいずれかが3番目のセルで発生する確率よりも大きいことがわかります。 1/3に等しい。


これで、アルゴリズムが定義を満たさない最小のリストには3つの要素があります。しかし、アルゴリズムはとにかくそれを4要素リストに変換するので、4要素リストの誤りを示すようにします。

入力を考えてみましょうL = [a, b, c, d]アルゴリズムの最初の実行に続いて、Lはl1 = [a, c]l2 = [b, d]に分割されます。これらの2つのサブリストをシャッフルした後(ただし、4要素の結果にマージする前)、4つの同じ確率の2要素リストを取得できます。

l1shuffled = [a , c]     l2shuffled = [b , d]
l1shuffled = [a , c]     l2shuffled = [d , b]
l1shuffled = [c , a]     l2shuffled = [b , d]
l1shuffled = [c , a]     l2shuffled = [d , b]


次に、2つの質問に答えてみてください。
1。最終結果にマージした後、aがリストの最初の要素になる確率はどれくらいですか。
簡単に言うと、上記の4つのペアのうち2つだけ(これも同じ確率で)がそのような結果をもたらすことがわかります(p1 = 1/2)。これらのペアのそれぞれについて、マージルーチン(p2 = 1/2)の最初の反転時にheadsを描画する必要があります。したがって、aの最初の要素としてLshuffledを持つ確率は、p = p1*p2 = 1/4であり、これは正しいです。


2。 aLshuffledの最初の位置にあることを知っていると、cを持つ確率はどのくらいですか(bまたはd一般性を失うことなく)Lshuffledの2番目の位置
これで、完全にシャッフルされたリストの上記の定義によれば、リストの残りの3つのセルに入れる数値が3つあるため、答えは1/3になります。
アルゴリズムがそれを保証するかどうか見てみましょう。
Lshuffledの最初の要素として1を選択すると、次のいずれかになります。
l1shuffled = [c] l2shuffled = [b, d]
または:
l1shuffled = [c] l2shuffled = [d, b]
どちらの場合も3を選択する確率は、headsp3 = 1/2)を反転する確率と同じです。したがって、知っている場合、3Lshuffledの2番目の要素として持つ確率です。 Lshuffledの最初の要素要素が1であるということは1/2に等しい。 1/2 != 1/3これはアルゴリズムの不正確さの証明を終了します。

興味深い部分は、アルゴリズムが完全なシャッフルに必要な(しかし十分ではない)条件を満たしていることです。

すべてのインデックスn<n)、すべての要素kに対してak要素のリストを指定:リストをシャッフルした後m回、akインデックスでkが発生した回数をカウントした場合、このカウントは確率でm/nになる傾向があり、mは無限大になる傾向があります。

4
GA1

実際にはそれよりもうまくいくことができます:最良のリストシャッフルアルゴリズムはO(n log n)timeとちょうどO(1)space。 (リストのポインター配列を作成することにより、O(n)時間およびO(n)スペースでシャッフルすることもできます。 Knuthを使用して所定の位置でシャッフルし、それに応じてリストを再スレッド化します。)

複雑さの証明

O(1)スペースでO(n log n)時間が最小である理由を確認するには、次の点に注意してください。

  • O(1)スペースを使用すると、任意のリスト要素の後続を更新するには、必然的にO(n)時間がかかります。
  • Wlog、1つの要素を更新するたびに、他のすべての要素も更新すると想定できます(必要に応じて変更しないでください)。これもO(n)時間しかかかりません。
  • O(1)スペースを使用すると、更新する要素の後継として選択できる要素は最大でO(1))になります(特定の要素)これらの要素は明らかにアルゴリズムに依存します)。
  • したがって、すべての要素を1回O(n)時間更新すると、最大でc ^ n個の異なるリスト順列が生成される可能性があります。
  • Nがあるので! = O(n ^ n)= O(c ^(n log n))可能なリスト順列、少なくともO(log n)の更新が必要です。

リンクリストデータ構造(Pythonのため)

import collections

class Cons(collections.Sequence):
    def __init__(self, head, tail=None):
        self.head = head
        self.tail = tail

    def __getitem__(self, index):
        current, n = self, index
        while n > 0:
            if isinstance(current, Cons):
                current, n = current.tail, n - 1
            else:
                raise ValueError("Out of bounds index [{0}]".format(index))
        return current

    def __len__(self):
        current, length = self, 0
        while isinstance(current, Cons):
            current, length = current.tail, length + 1
        return length

    def __repr__(self):
        current, rep = self, []
        while isinstance(current, Cons):
            rep.extend((str(current.head), "::"))
            current = current.tail
        rep.append(str(current))
        return "".join(rep)

マージスタイルのアルゴリズム

これがO(n log n)時間とO(1)反復マージソートに基づくスペースアルゴリズムです。基本的な考え方は単純です。左半分をシャッフルし、次に右半分をシャッフルしてからマージします。 2つのリストからランダムに選択します。注目に値する2つのこと:

  1. アルゴリズムを再帰的ではなく反復的にし、すべてのマージステップの最後に新しい最後の要素へのポインターを返すことにより、時間コストを維持しながら、必要なスペースをO(1)に削減します。最小限。
  2. すべての可能性がすべての入力サイズで等しくなる可能性があることを確認するために、マージ時にギルバート-シャノン-リードモデルのリフルシャッフルからの確率を使用します( http://en.wikipedia.org/wiki/Gilbert%E2を参照) %80%93Shannon%E2%80%93Reeds_model )。
import random

def riffle_lists(head, list1, len1, list2, len2):
    """Riffle shuffle two sublists in place. Returns the new last element."""
    for _ in range(len1 + len2):
        if random.random() < (len1 / (len1 + len2)):
            next, list1, len1 = list1, list1.tail, len1 - 1
        else:
            next, list2, len2 = list2, list2.tail, len2 - 1
        head.tail, head = next, next
    head.tail = list2
    return head

def shuffle_list(list):
    """Shuffle a list in place using an iterative merge-style algorithm."""
    dummy = Cons(None, list)
    i, n = 1, len(list)
    while (i < n):
        head, nleft = dummy, n
        while (nleft > i):
            head = riffle_lists(head, head[1], i, head[i + 1], min(i, nleft - i))
            nleft -= 2 * i
        i *= 2
    return dummy[1]

別のアルゴリズム

かなり均一でないシャッフルを生成するもう1つの興味深いO(n log n)アルゴリズムは、リストを3/2 log_2(n)回リフルシャッフルすることを含みます。 http://en.wikipedia.org/wiki/Gilbert%E2%80%93Shannon%E2%80%93Reeds_model で説明されているように、これは一定数の情報のみを残します。

2
Uri Zarfaty

考えられる解決策の1つは次のとおりです。

_#include <stdlib.h>

typedef struct node_s {
   struct node_s * next;
   int data;
} node_s, *node_p;

void shuffle_helper( node_p first, node_p last ) {
   static const int half = Rand_MAX / 2;
   while( (first != last) && (first->next != last) ) {
      node_p firsts[2] = {0, 0};
      node_p *lasts[2] = {0, 0};
      int counts[2] = {0, 0}, lesser;
      while( first != last ) {
         int choice = (Rand() <= half);
         node_p next = first->next;
         first->next = firsts[choice];
         if( !lasts[choice] ) lasts[choice] = &(first->next);
         ++counts[choice];
         first = next;
      }

      lesser = (counts[0] < counts[1]);

      if( !counts[lesser] ) {
         first = firsts[!lesser];
         *(lasts[!lesser]) = last;
         continue;
      }

      *(lasts[0]) = firsts[1];
      *(lasts[1]) = last;

      shuffle_helper( firsts[lesser], firsts[!lesser] );

      first = firsts[!lesser];
      last = *(lasts[!lesser]);
   }
}

void shuffle_list( node_p thelist ) { shuffle_helper( thelist, NULL ); }
_

これは基本的にクイックソートですが、ピボットがなく、ランダムに分割されています。

外側のwhileループは、再帰呼び出しを置き換えます。

内側のwhileループは、各要素を2つのサブリストのいずれかにランダムに移動します。

内側のwhileループの後、サブリストを相互に接続します。

次に、小さいサブリストで再帰し、大きいサブリストでループします。

小さいサブリストは最初のリストの半分を超えることはできないため、最悪の場合の再帰の深さは、要素数の2を底とする対数です。必要なメモリの量は、再帰の深さのO(1)倍です。

平均実行時間、およびRand()への呼び出しの数はO(N log N)です。

より正確なランタイム分析には、「ほぼ確実に」というフレーズを理解する必要があります。

1
BenGoldberg

比較なしのボトムアップマージソート。マージ中は比較を行わず、要素を交換するだけです。

0
Naveen