随机置换单链表的N个第一个元素

时间:2010-12-12 12:31:08

标签: c++ c algorithm math permutation

我必须随机地置换长度为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

我的具体案例:

  • n-N相对于n 约为20%
  • 我的RAM资源有限,最好的算法应该到位
  • 我必须在循环中进行多次迭代,因此速度很重要
  • 理想的随机性(均匀分布)不是必需的,如果它“几乎”随机就可以了
  • 在进行排列之前,我已经遍历了N个元素(满足其他需求),所以也许我可以将它用于排列

更新:我找到了this paper。它声明它提出了一个O(log n)堆栈空间和预期的O(n log n)时间的算法。

11 个答案:

答案 0 :(得分:6)

我没试过,但你可以使用“随机merge-sort”。

更准确地说,您将merge - 例程随机化。你没有系统地合并这两个子列表,但你是基于硬币投掷(即概率为0.5,你选择了第一个子列表的第一个元素,概率为0.5,你选择了右子列表的第一个元素)。 / p>

这应该在O(n log n)中运行并使用O(1)空格(如果正确实施)。

下面您将找到C中的示例实现,您可以根据自己的需要进行调整。请注意,此实现在两个位置使用随机化:splitListmerge。但是,您可以选择这两个地方中的一个。我不确定分布是否是随机的(我几乎可以肯定它不是),但是一些测试用例产生了不错的结果。

#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。

http://ideone.com/9I7mx

#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):

  1. j = N
  2. k = 1和j之间的随机
  3. 遍历输入列表,找到第k项并输出;从序列中删除所述项目(或以某种方式标记它以便在下次遍历时不会考虑它)
  4. 减少j并返回2,除非j == 0
  5. 输出列表的其余部分
  6. 请注意这是O(N ^ 2),除非您可以在步骤3中确保随机访问。

    如果N相对较小,那么N项适合内存,只需将它们加载到数组中并随机播放,就像@Mitch建议的那样。

答案 5 :(得分:1)

如果你同时知道N和n,我认为你可以做到这一点。它也是完全随机的。您只需遍历整个列表一次,并在每次添加节点时通过随机部分进行迭代。我认为是O(n + NlogN)或O(n + N ^ 2)。我不确定。它基于更新在给定先前节点发生的情况下为随机部分选择节点的条件概率。

  1. 确定在给定先前节点发生的情况下为随机部分选择某个节点的概率(p =(N-size)/(n-position),其中size是先前选择的节点数,并且position是number先前考虑过的节点)
  2. 如果未为随机部件选择节点,请转到步骤4.如果为随机部件选择了节点,则根据当前的大小随机选择随机部件中的位置(place =(0到1之间的随机)* size ,size也是先前节点的数量。)
  3. 将节点放在需要的位置,更新指针。增加大小。更改为查看之前指向您正在查看和移动的节点的节点。
  4. 增加位置,查看下一个节点。
  5. 我不知道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的。

  1. 初始化lastR = -1
  2. 如果N <= 1,则转到步骤6。
  3. 在1和N之间随机化数字r。
  4. 如果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

  5. 将N减少1并转到步骤2.
  6. 将结果列表的尾部链接到剩余输入列表的头部。您现在拥有原始列表,其中前N个项目处于排列状态。
  7. 由于你没有随机访问权限,这将减少你在列表中需要的遍历时间(我假设减半,所以渐渐地,你将无法获得任何东西)。

答案 7 :(得分:0)

O(NlogN)易于实施的解决方案不需要额外的存储空间:

假设你想随机化L:

  1. L已完成1或0个元素

  2. 创建两个空列表L1和L2

  3. 在L上循环,将其元素破坏性地移动到L1或L2,随机选择两者之间。

  4. 重复L1和L2的过程(递归!)

  5. 将L1和L2加入L3

  6. 返回L3

  7. <强>更新

    在步骤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) 时间。

它不会在所有排列序列上产生均匀分布,但它可以提供不易区分的良好排列。基本思想类似于按行和列置换矩阵,如下所述。

算法

让元素的大小为Nm = floor(sqrt(N))。假设“方阵”N = m*m将使此方法更加清晰。

  1. 在第一遍中,您应该将每个m元素分隔的元素指针存储为p_0, p_1, p_2, ..., p_m。也就是说,p_0->next->...->next(m times) == p_1应该是真的。

  2. 置换每一行

    • 对于i = 0到m,请执行:
    • 使用大小为p_i->next的数组
    • 将链接列表中p_(i+1)->nextO(m)之间的所有元素编入索引
    • 使用标准方法
    • 对此数组进行随机播放
    • 使用此混洗数组重新链接元素
  3. 置换每列。

    • 初始化数组A以存储指针p_0, ..., p_m。它用于遍历列
    • 对于i = 0到m
    • 使用大小为A[0], A[1], ..., A[m-1]
    • 的数组为链接列表中的所有元素m编制索引
    • 随机播放此阵列
    • 使用此混洗数组重新链接元素
    • 将指针前进到下一列A[i] := A[i]->next
  4. 请注意,p_0是指向第一个元素的元素,而p_m指向最后一个元素。另外,如果N != m*m,您可以对某些m+1使用p_i分隔。现在你得到一个“矩阵”,使p_i指向每一行的开头。

    分析和随机性

    1. 空间复杂度:此算法需要O(m)空间来存储行的开头。用于存储数组的O(m)空间和用于在列置换期间存储额外指针的O(m)空间。因此,时间复杂度为~O(3 * sqrt(N))。对于N = 1000000,它大约有3000个条目, 12 kB内存

    2. 时间复杂度:显然是O(N)。它要么逐行或逐列地遍历“矩阵”

    3. 随机性:首先要注意的是,每个元素都可以通过行和列排列到达矩阵中的任何位置。元素可以转到链表中的任何位置非常重要。其次,尽管它不会生成所有排列序列,但它确实生成了部分排列序列。为了找到排列的数量,我们假设N=m*m,每行排列都有m!并且有m行,所以我们有(m!)^m。如果列排列也包括在内,它恰好等于(m!)^(2*m),因此几乎不可能得到相同的序列。

    4. 强烈建议重复第二步和第三步至少一次以获得更随机的序列。因为它几乎可以抑制所有行和列的相关性到其原始位置。当您的列表不是“方形”时,这也很重要。取决于您的需要,您可能希望使用更多的重复。你使用的重复次数越多,它的排列就越多,随机性就越大。我记得有可能为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)

如果同时满足以下两个条件:

  • 你有足够的程序存储器(许多嵌入式硬件直接从闪存执行);
  • 你的解决方案并不会让你的“随机性”经常重复,

然后,您可以选择一组足够大的特定排列,在编程时定义,编写代码以编写实现每个排列的代码,然后在运行时迭代它们。