我必须随机地置换长度为n的单链表的N个第一个元素。每个元素定义为:
typedef struct E_s
{
struct E_s *next;
}E_t;
我有一个根元素,我可以遍历整个大小为n的链表。什么是最有效的技术来随机置换N个第一个元素(从根开始)?
因此,给定a-> b-> c-> d-> e-> f-> ... x-> y-> z我需要制作smth。像f-> a-> e-> c-> b-> ... x-> y-> z
我的具体案例:
更新:我找到了this paper。它声明它提出了一个O(log n)堆栈空间和预期的O(n log n)时间的算法。
答案 0 :(得分:6)
我没试过,但你可以使用“随机merge-sort”。
更准确地说,您将merge
- 例程随机化。你没有系统地合并这两个子列表,但你是基于硬币投掷(即概率为0.5,你选择了第一个子列表的第一个元素,概率为0.5,你选择了右子列表的第一个元素)。 / p>
这应该在O(n log n)
中运行并使用O(1)
空格(如果正确实施)。
下面您将找到C中的示例实现,您可以根据自己的需要进行调整。请注意,此实现在两个位置使用随机化:splitList
和merge
。但是,您可以选择这两个地方中的一个。我不确定分布是否是随机的(我几乎可以肯定它不是),但是一些测试用例产生了不错的结果。
#include <stdio.h>
#include <stdlib.h>
#define N 40
typedef struct _node{
int value;
struct _node *next;
} node;
void splitList(node *x, node **leftList, node **rightList){
int lr=0; // left-right-list-indicator
*leftList = 0;
*rightList = 0;
while (x){
node *xx = x->next;
lr=rand()%2;
if (lr==0){
x->next = *leftList;
*leftList = x;
}
else {
x->next = *rightList;
*rightList = x;
}
x=xx;
lr=(lr+1)%2;
}
}
void merge(node *left, node *right, node **result){
*result = 0;
while (left || right){
if (!left){
node *xx = right;
while (right->next){
right = right->next;
}
right->next = *result;
*result = xx;
return;
}
if (!right){
node *xx = left;
while (left->next){
left = left->next;
}
left->next = *result;
*result = xx;
return;
}
if (rand()%2==0){
node *xx = right->next;
right->next = *result;
*result = right;
right = xx;
}
else {
node *xx = left->next;
left->next = *result;
*result = left;
left = xx;
}
}
}
void mergeRandomize(node **x){
if ((!*x) || !(*x)->next){
return;
}
node *left;
node *right;
splitList(*x, &left, &right);
mergeRandomize(&left);
mergeRandomize(&right);
merge(left, right, &*x);
}
int main(int argc, char *argv[]) {
srand(time(NULL));
printf("Original Linked List\n");
int i;
node *x = (node*)malloc(sizeof(node));;
node *root=x;
x->value=0;
for(i=1; i<N; ++i){
node *xx;
xx = (node*)malloc(sizeof(node));
xx->value=i;
xx->next=0;
x->next = xx;
x = xx;
}
x=root;
do {
printf ("%d, ", x->value);
x=x->next;
} while (x);
x = root;
node *left, *right;
mergeRandomize(&x);
if (!x){
printf ("Error.\n");
return -1;
}
printf ("\nNow randomized:\n");
do {
printf ("%d, ", x->value);
x=x->next;
} while (x);
printf ("\n");
return 0;
}
答案 1 :(得分:4)
转换为数组,使用Fisher-Yates shuffle,然后转换回列表。
答案 2 :(得分:4)
我不相信有任何有效的方法可以在没有中间数据结构的情况下随机混合单链表。我只是将前N个元素读入数组,执行Fisher-Yates shuffle,然后将前N个元素重构为单链表。
答案 3 :(得分:2)
首先,获取列表的长度和最后一个元素。你说在随机化之前你已经进行了遍历,那将是个好时机。
然后,通过将第一个元素链接到最后一个元素将其转换为循环列表。通过将大小除以4来获取列表中的四个指针并迭代它以进行第二次传递。 (这些指针也可以通过在前一次遍历中每四次迭代递增一次,两次和三次来从前一次传递中获得。)
对于随机化传递,再次遍历并以50%的概率交换指针0和2以及指针1和3。 (做两个交换操作或两者都做;只有一个交换将列表分成两部分。)
以下是一些示例代码。看起来它可能会更随机,但我想再多几次传球可以做到这一点。无论如何,分析算法比写它更困难:vP。抱歉没有缩进;我只是在浏览器中将它打入ideone。
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
struct list_node {
int v;
list_node *n;
list_node( int inv, list_node *inn )
: v( inv ), n( inn) {}
};
int main() {
srand( time(0) );
// initialize the list and 4 pointers at even intervals
list_node *n_first = new list_node( 0, 0 ), *n = n_first;
list_node *p[4];
p[0] = n_first;
for ( int i = 1; i < 20; ++ i ) {
n = new list_node( i, n );
if ( i % (20/4) == 0 ) p[ i / (20/4) ] = n;
}
// intervals must be coprime to list length!
p[2] = p[2]->n;
p[3] = p[3]->n;
// turn it into a circular list
n_first->n = n;
// swap the pointers around to reshape the circular list
// one swap cuts a circular list in two, or joins two circular lists
// so perform one cut and one join, effectively reordering elements.
for ( int i = 0; i < 20; ++ i ) {
list_node *p_old[4];
copy( p, p + 4, p_old );
p[0] = p[0]->n;
p[1] = p[1]->n;
p[2] = p[2]->n;
p[3] = p[3]->n;
if ( rand() % 2 ) {
swap( p_old[0]->n, p_old[2]->n );
swap( p_old[1]->n, p_old[3]->n );
}
}
// you might want to turn it back into a NULL-terminated list
// print results
for ( int i = 0; i < 20; ++ i ) {
cout << n->v << ", ";
n = n->n;
}
cout << '\n';
}
答案 4 :(得分:1)
对于N非常大的情况(因此它不适合您的记忆),您可以执行以下操作(某种Knuth的3.4.2P):
请注意这是O(N ^ 2),除非您可以在步骤3中确保随机访问。
如果N相对较小,那么N项适合内存,只需将它们加载到数组中并随机播放,就像@Mitch建议的那样。
答案 5 :(得分:1)
如果你同时知道N和n,我认为你可以做到这一点。它也是完全随机的。您只需遍历整个列表一次,并在每次添加节点时通过随机部分进行迭代。我认为是O(n + NlogN)或O(n + N ^ 2)。我不确定。它基于更新在给定先前节点发生的情况下为随机部分选择节点的条件概率。
我不知道C,但我可以给你伪代码。在这里,我将置换作为随机化的第一个元素。
integer size=0; //size of permutation
integer position=0 //number of nodes you've traversed so far
Node head=head of linked list //this holds the node at the head of your linked list.
Node current_node=head //Starting at head, you'll move this down the list to check each node, whether you put it in the list.
Node previous=head //stores the previous node for changing pointers. starts at head to avoid asking for the next field on a null node
While ((size not equal to N) or (current_node is not null)){ //iterating through the list until the permutation is full. We should never pass the end of list, but just in case, I include that condition)
pperm=(N-size)/(n-position) //probability that a selected node will be in the permutation.
if ([generate a random decimal between 0 and 1] < pperm) //this decides whether or not the current node will go in the permutation
if (j is not equal to 0){ //in case we are at start of list, there's no need to change the list
pfirst=1/(size+1) //probability that, if you select a node to be in the permutation, that it will be first. Since the permutation has
//zero elements at start, adding an element will make it the initial node of a permutation and percent chance=1.
integer place_in_permutation = round down([generate a random decimal between 0 and 1]/pfirst) //place in the permutation. note that the head =0.
previous.next=current_node.next
if(place_in_permutation==0){ //if placing current node first, must change the head
current_node.next=head //set the current Node to point to the previous head
head=current_node //set the variable head to point to the current node
}
else{
Node temp=head
for (counter starts at zero. counter is less than place_in_permutation-1. Each iteration, increment counter){
counter=counter.next
} //at this time, temp should point to the node right before the insertion spot
current_node.next=temp.next
temp.next=current_node
}
current_node=previous
}
size++ //since we add one to the permutation, increase the size of the permutation
}
j++;
previous=current_node
current_node=current_node.next
}
如果你坚持使用最近添加的节点,你可能会提高效率,以防你必须在其右边添加一个节点。
答案 6 :(得分:0)
类似于弗拉德的答案,这是一个小的改进(统计上):
算法中的指数是基于1的。
如果r!= N
4.1将列表遍历到项目r及其前身。
If lastR != -1
If r == lastR, your pointer for the of the r'th item predecessor is still there.
If r < lastR, traverse to it from the beginning of the list.
If r > lastR, traverse to it from the predecessor of the lastR'th item.
4.2将列表中的第r项删除为尾部的结果列表。
4.3 lastR = r
由于你没有随机访问权限,这将减少你在列表中需要的遍历时间(我假设减半,所以渐渐地,你将无法获得任何东西)。
答案 7 :(得分:0)
O(NlogN)易于实施的解决方案不需要额外的存储空间:
假设你想随机化L:
L已完成1或0个元素
创建两个空列表L1和L2
在L上循环,将其元素破坏性地移动到L1或L2,随机选择两者之间。
重复L1和L2的过程(递归!)
将L1和L2加入L3
返回L3
<强>更新强>
在步骤3,L应该被分成相等大小(+ -1)的列表L1和L2,以保证最佳的案例复杂度(N * log N)。这可以通过调整一个元素动态进入L1或L2的概率来完成:
p(insert element into L1) = (1/2 * len0(L) - len(L1)) / len(L)
,其中
len(M) is the current number of elements in list M
len0(L) is the number of elements there was in L at the beginning of step 3
答案 8 :(得分:0)
对于单个链接列表,有一个算法需要 O(sqrt(N))
空间和 O(N)
时间。
它不会在所有排列序列上产生均匀分布,但它可以提供不易区分的良好排列。基本思想类似于按行和列置换矩阵,如下所述。
让元素的大小为N
和m = floor(sqrt(N))
。假设“方阵”N = m*m
将使此方法更加清晰。
在第一遍中,您应该将每个m
元素分隔的元素指针存储为p_0, p_1, p_2, ..., p_m
。也就是说,p_0->next->...->next(m times) == p_1
应该是真的。
置换每一行
p_i->next
的数组p_(i+1)->next
到O(m)
之间的所有元素编入索引
置换每列。
A
以存储指针p_0, ..., p_m
。它用于遍历列A[0], A[1], ..., A[m-1]
m
编制索引
A[i] := A[i]->next
请注意,p_0
是指向第一个元素的元素,而p_m
指向最后一个元素。另外,如果N != m*m
,您可以对某些m+1
使用p_i
分隔。现在你得到一个“矩阵”,使p_i
指向每一行的开头。
空间复杂度:此算法需要O(m)
空间来存储行的开头。用于存储数组的O(m)
空间和用于在列置换期间存储额外指针的O(m)
空间。因此,时间复杂度为~O(3 * sqrt(N))。对于N = 1000000
,它大约有3000个条目, 12 kB内存。
时间复杂度:显然是O(N)
。它要么逐行或逐列地遍历“矩阵”
随机性:首先要注意的是,每个元素都可以通过行和列排列到达矩阵中的任何位置。元素可以转到链表中的任何位置非常重要。其次,尽管它不会生成所有排列序列,但它确实生成了部分排列序列。为了找到排列的数量,我们假设N=m*m
,每行排列都有m!
并且有m行,所以我们有(m!)^m
。如果列排列也包括在内,它恰好等于(m!)^(2*m)
,因此几乎不可能得到相同的序列。
强烈建议重复第二步和第三步至少一次以获得更随机的序列。因为它几乎可以抑制所有行和列的相关性到其原始位置。当您的列表不是“方形”时,这也很重要。取决于您的需要,您可能希望使用更多的重复。你使用的重复次数越多,它的排列就越多,随机性就越大。我记得有可能为N=9
生成均匀分布,我想有可能证明重复趋于无穷大,它与真正的均匀分布相同。
编辑:时间和空间的复杂性是紧密的,在任何情况下几乎都是一样的。我认为这种空间消耗可以满足您的需求。如果您有任何疑问,可以在小清单中试用,我认为您会发现它很有用。
答案 9 :(得分:0)
下面的列表随机发生器具有复杂度O(N * log N)和O(1)内存使用情况。
它是基于我在其他帖子上描述的递归算法修改为迭代而不是递归,以消除O(logN)内存使用。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef struct node {
struct node *next;
char *str;
} node;
unsigned int
next_power_of_two(unsigned int v) {
v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
return v + 1;
}
void
dump_list(node *l) {
printf("list:");
for (; l; l = l->next) printf(" %s", l->str);
printf("\n");
}
node *
array_to_list(unsigned int len, char *str[]) {
unsigned int i;
node *list;
node **last = &list;
for (i = 0; i < len; i++) {
node *n = malloc(sizeof(node));
n->str = str[i];
*last = n;
last = &n->next;
}
*last = NULL;
return list;
}
node **
reorder_list(node **last, unsigned int po2, unsigned int len) {
node *l = *last;
node **last_a = last;
node *b = NULL;
node **last_b = &b;
unsigned int len_a = 0;
unsigned int i;
for (i = len; i; i--) {
double pa = (1.0 + RAND_MAX) * (po2 - len_a) / i;
unsigned int r = rand();
if (r < pa) {
*last_a = l;
last_a = &l->next;
len_a++;
}
else {
*last_b = l;
last_b = &l->next;
}
l = l->next;
}
*last_b = l;
*last_a = b;
return last_b;
}
unsigned int
min(unsigned int a, unsigned int b) {
return (a > b ? b : a);
}
randomize_list(node **l, unsigned int len) {
unsigned int po2 = next_power_of_two(len);
for (; po2 > 1; po2 >>= 1) {
unsigned int j;
node **last = l;
for (j = 0; j < len; j += po2)
last = reorder_list(last, po2 >> 1, min(po2, len - j));
}
}
int
main(int len, char *str[]) {
if (len > 1) {
node *l;
len--; str++; /* skip program name */
l = array_to_list(len, str);
randomize_list(&l, len);
dump_list(l);
}
return 0;
}
/* try as: a.out list of words foo bar doz li 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
*/
请注意,此版本的算法完全缓存不友好,递归版本可能会表现得更好!
答案 10 :(得分:0)
如果同时满足以下两个条件:
然后,您可以选择一组足够大的特定排列,在编程时定义,编写代码以编写实现每个排列的代码,然后在运行时迭代它们。