我正在尝试使用分而治之算法对链表进行混洗,该算法随后在线性(n log n)时间和对数(log n)额外空间中随机混洗链接列表。
我知道我可以做一个类似于可以在一个简单的值数组中使用的Knuth shuffle,但是我不知道如何用分而治之的方法做到这一点。我的意思是,我实际上分裂了什么?我只是划分到列表中的每个单独节点,然后使用一些随机值将列表随机组合在一起吗?
或者我是否给每个节点一个随机数,然后根据随机数在节点上进行合并?
答案 0 :(得分:25)
以下怎么样?执行与合并排序相同的过程。合并时,不是从排序顺序的两个列表中选择一个元素(逐个),而是翻转硬币。根据硬币翻转的结果选择是从第一个列表还是从第二个列表中选择一个元素。
<强>算法。强>
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
空间的关键点是将列表拆分为两个不需要任何额外空间。我们需要的唯一额外空间是在递归期间保持堆栈上的log n元素。
虚拟节点的要点是认识到插入和移除虚设元素使得元素的分布保持均匀。
<强>分析。强>
为什么分布均匀?在最终合并之后,在位置P_i(n)
中结束的任何给定数字的概率i
如下。它是:
i
位置,该列表赢得了第一次i
次投币,其概率为1/2^i
; i-1
- st位置,该列表赢了硬币投掷i-1
次包括最后一个并丢失一次,这个概率是(i-1) choose 1
次1/2^i
; i-2
- nd位置,该列表赢了投币i-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^{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
是偶数,并且两个列表均匀地进行了混洗。这是通过添加(然后删除)虚拟节点来实现的。
P.S。我原来的直觉远没有严格,但我列举以防万一。想象一下,我们将1和n之间的数字随机分配给列表的元素。现在我们对这些数字进行合并排序。在合并的任何给定步骤中,它需要确定两个列表中的哪个头部较小。但是一个大于另一个的概率应该恰好是1/2,所以我们可以通过掷硬币来模拟这个。
P.P.S。有没有办法在这里嵌入LaTeX?
答案 1 :(得分:3)
Up shuffle方法
这个(lua)版本从foxcub的答案中得到改进,以消除虚拟节点的需要。
为了略微简化本答案中的代码,此版本假设您的列表知道其大小。如果他们不这样做,你总能在O(n)
时间找到它,但更好的是:在代码中可以进行一些简单的调整,而不需要事先计算它(比如将一个人细分为两个而不是第一和第二半)。
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
为了避免使用虚拟节点,您必须通过改变在每个列表中选择的概率来弥补两个中间列表可以具有不同长度的事实。这是通过测试一个[0,1]均匀随机数来完成的,该随机数是从第一个列表中弹出的节点与弹出的节点总数(在两个列表中)的比率。
Down shuffle方法
你可以在递归细分时进行随机播放,这在我的简单测试中表现出稍微(但始终如一)的更好表现。它可能来自较少的指令,或者另一方面它可能由于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
答案 2 :(得分:3)
我会说,foxcub的答案是错误的。为了证明我将为一个完美的混乱列表引入一个有用的定义(称之为数组或序列或任何你想要的)。
定义:假设我们有一个包含元素L
和索引a1, a2 ... an
的列表1, 2, 3..... n
。如果我们将L
公开给一个shuffle操作(我们没有访问权限的内部组件)L
被完全洗牌,当且仅当知道某些k(k< n
)元素的索引时,我们才能不推断剩余n-k
元素的索引。这是剩余的n-k
元素同样可能在任何剩余的n-k
索引中显示。
示例:如果我们有一个四元素列表[a, b, c, d]
并且在洗牌之后,我们知道它的第一个元素是a
([a, .., .., ..]
),而不是任何元素的概率{ {1}}发生,比方说,第三个单元格等于b, c, d
。
现在,算法不符合定义的最小列表有三个元素。但算法无论如何都将它转换为4元素列表,因此我们将尝试显示4元素列表的不正确性。
考虑输入1/3
在首次运行算法后,L将分为L = [a, b, c, d]
和l1 = [a, c]
。在对这两个子列表进行混洗之后(但在合并到四元素结果之前),我们可以得到四个同样可能的2元素列表:
l2 = [b, d]
现在尝试回答两个问题。
的 1。合并到最终结果l1shuffled = [a , c] l2shuffled = [b , d]
l1shuffled = [a , c] l2shuffled = [d , b]
l1shuffled = [c , a] l2shuffled = [b , d]
l1shuffled = [c , a] l2shuffled = [d , b]
之后的概率是该列表的第一个元素。
很简单,我们可以看到上面四对中只有两对(同样可能)可以给出这样的结果(a
)。对于这些对中的每一对,必须在合并例程(p1 = 1/2
)中首次翻转时绘制heads
。因此,将p2 = 1/2
作为a
的第一个元素的概率为Lshuffled
,这是正确的。
的 2。知道p = p1*p2 = 1/4
位于a
的第一个位置,Lshuffled
的概率是多少(我们也可以选择c
或b
而不会丢失一般性)在d
的第二个位置
现在,根据完全混洗列表的上述定义,答案应该是Lshuffled
,因为列表中的三个剩余单元格中有三个数字
让我们看看算法是否可以保证。
选择1/3
作为1
的第一个元素后,我们现在要么:
Lshuffled
或:
l1shuffled = [c] l2shuffled = [b, d]
在两种情况下选择l1shuffled = [c] l2shuffled = [d, b]
的概率等于翻转3
(heads
)的概率,因此将p3 = 1/2
作为第二个元素的可能性知道3
的第一个元素元素Lshuffled
等于Lshuffled
时1
的{{1}}。 1/2
结束了算法错误的证明。
有趣的是,该算法满足了完美洗牌的必要条件(但不充分),即:
为每个元素1/2 != 1/3
提供n
元素的列表k
,每个元素<n
:在对列表进行混洗后{{1} }次,如果我们计算了ak
索引上发生m
次的时间,则此计数将概率为ak
,k
趋于无穷大。
答案 3 :(得分:2)
您实际上可以做得更好:最佳列表混洗算法是 O(n log n)时间,只有 O(1)空间。 (您还可以通过为列表构造指针数组,在 O(n)时间和 O(n)空间中进行随机播放,使用Knuth将其重新排列并重新线程化相应的清单。)
复杂性证明
要了解为什么O(n log n)时间对于O(1)空间来说是最小的,请注意:
链接列表数据结构(因为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)空间算法。基本思路很简单:将左半部分和右半部分混合,然后通过从两个列表中随机选择来合并它们。值得注意的两件事:
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]
另一种算法
另一个有趣的O(n log n)算法产生不太均匀的混洗,只需简单地将列表改组为3/2 log_2(n)次。如http://en.wikipedia.org/wiki/Gilbert%E2%80%93Shannon%E2%80%93Reeds_model中所述,这只留下了恒定数量的信息。
答案 4 :(得分:1)
自上而下合并排序而不进行比较。虽然合并不进行任何比较,但只需交换元素。
答案 5 :(得分: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
循环将每个元素随机移动到两个子列表中的一个。
在内部while
循环之后,我们将子列表彼此连接。
然后,我们递归到较小的子列表,然后循环放大。
由于较小的子列表永远不会超过初始列表大小的一半,因此最坏情况下的递归深度是两个元素数量的对数基数。所需的内存量是递归深度的O(1)倍。
平均运行时间和rand()
的调用次数为O(N log N)。
更精确的运行时分析需要理解短语&#34;几乎可以肯定。&#34;
答案 6 :(得分:0)
您可以遍历列表,在每个节点上随机生成0或1。
如果为1,请删除该节点并将其放置为列表的第一个节点。 如果它是0,则什么也不做。
将其循环播放,直到到达列表末尾为止。