リンクリストを線形(n log n)時間と対数(log n)の余分なスペースでランダムにシャッフルする分割統治アルゴリズムを使用して、リンクリストをシャッフルしようとしています。
単純な値の配列で使用できるのと同様のクヌースシャッフルを実行できることは知っていますが、分割統治法でこれをどのように実行するかはわかりません。私が言いたいのは、私が実際に何を分割しているのかということです。リスト内の個々のノードに分割してから、ランダムな値を使用してリストをランダムに組み立て直しますか?
または、各ノードに乱数を与えてから、乱数に基づいてノードでマージソートを実行しますか?
次はどうですか?マージソートと同じ手順を実行します。マージするときは、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を埋め込む方法はありますか?
アップシャッフルアプローチ
この(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
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。 a
がLshuffled
の最初の位置にあることを知っていると、c
を持つ確率はどのくらいですか(b
またはd
一般性を失うことなく)Lshuffled
の2番目の位置
これで、完全にシャッフルされたリストの上記の定義によれば、リストの残りの3つのセルに入れる数値が3つあるため、答えは1/3
になります。
アルゴリズムがそれを保証するかどうか見てみましょう。Lshuffled
の最初の要素として1
を選択すると、次のいずれかになります。l1shuffled = [c] l2shuffled = [b, d]
または:l1shuffled = [c] l2shuffled = [d, b]
どちらの場合も3
を選択する確率は、heads
(p3 = 1/2
)を反転する確率と同じです。したがって、知っている場合、3
をLshuffled
の2番目の要素として持つ確率です。 Lshuffled
の最初の要素要素が1
であるということは1/2
に等しい。 1/2 != 1/3
これはアルゴリズムの不正確さの証明を終了します。
興味深い部分は、アルゴリズムが完全なシャッフルに必要な(しかし十分ではない)条件を満たしていることです。
すべてのインデックスn
(<n
)、すべての要素k
に対してak
要素のリストを指定:リストをシャッフルした後m
回、ak
インデックスでk
が発生した回数をカウントした場合、このカウントは確率でm/n
になる傾向があり、m
は無限大になる傾向があります。
実際にはそれよりもうまくいくことができます:最良のリストシャッフルアルゴリズムはO(n log n)timeとちょうどO(1)space。 (リストのポインター配列を作成することにより、O(n)時間およびO(n)スペースでシャッフルすることもできます。 Knuthを使用して所定の位置でシャッフルし、それに応じてリストを再スレッド化します。)
複雑さの証明
O(1)スペースでO(n 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つのこと:
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 で説明されているように、これは一定数の情報のみを残します。
考えられる解決策の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)です。
より正確なランタイム分析には、「ほぼ確実に」というフレーズを理解する必要があります。
比較なしのボトムアップマージソート。マージ中は比較を行わず、要素を交換するだけです。