Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

unsorted list

by thekestrel (Friar)
on Apr 24, 2005 at 06:16 UTC ( [id://450883]=perlquestion: print w/replies, xml ) Need Help??

thekestrel has asked for the wisdom of the Perl Monks concerning the following question:

Hi Monks,

I'm running a number of operations on a set of data, but I want the list of data to be in a random order each time i process it.

$stuff = [ 'cat', 'dog', 'pig', 'cow' ];

and running through the list would give random combinations... i.e.
cat dog pig cow cow cat pig dog dog cow cat pig ... ...
What I want to ask advice for is what is the fastest/most efficient way to do this? Is there some neat equivalent of sort that equates to unsort. I was thinking of having either a hash or array and picking a value from the list then removing that selection and repeating until the list was empty each iteration, but that seems a little unwheeldly. I don't want any of the values repeated from the list i.e. dog dog cat pig, so just doing a rand on the size of my list is not what I'm after to select the elements.

Regards Paul.

Replies are listed 'Best First'.
Re: unsorted list
by sgifford (Prior) on Apr 24, 2005 at 07:18 UTC

    The technique you're looking for is called a shuffle. It can be done very quickly (linear time IIRC) using standard algorithms. You can try Algorithm::Numerical::Shuffle or List::Util's shuffle function.

    If your data is very large, consider sorting a list of numbers corresponding to array indices, then using those numbers as array subscripts. It will avoid making a copy of your data.

Re: unsorted list
by eibwen (Friar) on Apr 24, 2005 at 07:29 UTC
    There's always the Fisher-Yates shuffle in perlfaq4 or the pleac version:
    sub fisher_yates_shuffle { my $array = shift; my $i; for ($i = @$array; --$i; ) { my $j = int rand ($i+1); next if $i == $j; @$array[$i,$j] = @$array[$j,$i]; } } fisher_yates_shuffle( \@array );
      Thanks eibwen,
      This is the method that I ended up going with. Nice and clean and didn't require another module. Its working a treat =).

      Regards Paul.
Re: unsorted list
by rg0now (Chaplain) on Apr 24, 2005 at 10:23 UTC
      rg0now,
      Thanks, its usually the first port of call... the keyword though is 'shuffle', I kept thinking 'randomly sorted list', I didn't realize shuffle was the standard term for this =P

      Regards Paul
Re: unsorted list
by dbwiz (Curate) on Apr 24, 2005 at 07:04 UTC

    You can use rand within the sort code.

    @stuff = qw[ cat dog pig cow ]; print "$_\n" for sort { rand() <=> rand() } @stuff

    Update. I have the (theoretical) feeling that this trick may cause an infinite loop when used with a large array, but a few tests I did returned nothing suspicious.

    HTH

      In older Perl versions, inconsistent results from the sort block could produce core dumps or duplicated values. I don't believe modern Perl ever does that, but it's still not a way to get a fair sort.

      -- Randal L. Schwartz, Perl hacker
      Be sure to read my standard disclaimer if this is a reply.

Re: unsorted list
by holli (Abbot) on Apr 24, 2005 at 06:26 UTC
    I was thinking of having either a hash or array and picking a value from the list then removing that selection and repeating until the list was empty each iteration
    I think that is the exactly the way to go. To add some syntactic sugar (and to not repeat the logic for every loop) you could use a tied array class.


    holli, /regexed monk/
Re: unsorted list
by TedPride (Priest) on Apr 24, 2005 at 07:39 UTC
    EDIT: My bad. I mixed up @$array[$i,$j] with @$array[$i..$j] and misread your code. Yes, your code is functionally pretty much the same as mine, and yes, I did code a Fischer (sp?)-Yates shuffle.

    I'd -- myself, but it doesn't let me :)
    -------------------------------

    The problem with that shuffle is it requires moving chunks of the array, quite inefficient with large arrays. A better linear shuffle is as follows:

    use strict; use warnings; my ($n, $t); my @arr = (0,1,2,3,4,5,6,7,8,9); for (0..($#arr-1)) { $n = int (rand() * ($#arr - $_ + 1)) + $_; $t = $arr[$n]; $arr[$n] = $arr[$_]; $arr[$_] = $t; }
    Which is equivalent to:
    For all items except last Pick random item between current item and last Swap that item with current item
    I have an expanded version (not included here) which tests this with a large number of iterations and then counts how many of each number ends up in each slot, for purposes of analyzing the randomness of the sort. The results were within a percent or two of perfectly random.
      As far as I can tell, your code is a "recoding" of the Fisher-Yates shuffle:
      sub fisher_yates_shuffle { my $array = shift; my $i; for ($i = @$array; --$i; ) { my $j = int rand ($i+1); next if $i == $j; @$array[$i,$j] = @$array[$j,$i]; } }

      And permuting your version:

      for (0..($#arr-1)) { $n = int (rand() * ($#arr - $_ + 1)) + $_; $t = $arr[$n]; $arr[$n] = $arr[$_]; $arr[$_] = $t; }

      for (0..($#arr-1)) { $n = int (rand() * ($#arr - $_ + 1)) + $_;
      $t = $arr[$n]; $arr[$n] = $arr[$_]; $arr[$_] = $t;
      }

      for (0..($#arr-1)) { $n = int (rand() * ($#arr - $_ + 1)) + $_;
      @$arr[$_,$n] = @$arr[$n,$_];
      }

      for (0..($#arr-1)) { $n = int (rand() * ($#arr - $_ + 1)) + $_;
      # next if $_ == $n;
      @$arr[$_,$n] = @$arr[$n,$_]; }

      The remaining two lines are functionally equivalent:

      for (0..($#arr-1)) { # Your Version $n = int (rand() * ($#arr - $_ + 1)) + $_; for ($i = @$array; --$i; ) { # Fisher-Yates my $j = int rand ($i+1);

      Frankly, I prefer your formulation (with the addition of the next if line) as it doesn't use an incomplete for loop, but the codes are equivalent.

      UPDATE: For the benefit of the astute who realized that the remaining two lines weren't entirely equivalent:

      for (0..($#arr-1)) { $n = int (
      rand() * ($#arr - $_ + 1)
      ) + $_; # next if $_ == $n; @$arr[$_,$n] = @$arr[$n,$_]; }

      for (0..($#arr-1)) { $n = int (
      rand($#arr - $_ + 1)
      ) + $_; # next if $_ == $n; @$arr[$_,$n] = @$arr[$n,$_]; }

      for (0..($#arr-1)) { $n = int (rand($#arr - $_ + 1))
      + $_
      ; # next if $_ == $n; @$arr[$_,$n] = @$arr[$n,$_]; }

      Fisher-Yates traverses down the array, whereas this version traverses up the array (due to the appended + $_). Remove the addition and the codes are equivalent; leave it in and the codes are functionally equivalent.

Re: unsorted list
by cog (Parson) on Apr 24, 2005 at 12:48 UTC
    While everybody seems to be pointing to shuffling the list, how about randomizing the next element you're getting?

    Something in the lines of (untested):

    $stuff = [ 'cat', 'dog', 'pig', 'cow' ]; my %stuff map { $_ => 1 } @$stuff; my @elements = keys $stuff; for ($elements[int rand scalar @elements]) { $stuff{$_} = 0; @elements = grep $_, keys %stuff; # your code here, $_ being either a cat, a dog, etc. } for (keys %stuff) { $stuff{$_} = 1 }

    Do notice: I just got up! :-) If it doesn't work, that why :-) And I'm pretty sure there are better ways of doing it :-)

Re: unsorted list
by kwaping (Priest) on Apr 25, 2005 at 21:59 UTC
    This may not be the fanciest solution, but it seems to work for me:
    my $number_of_elements = 5; # arbitrary, can be anything my @order = (); my @base = (0 .. $number_of_elements - 1); srand; push @order, splice(@base, rand @base, 1) while (@base);

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://450883]
Approved by crashtest
Front-paged by rnahi
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others taking refuge in the Monastery: (5)
As of 2024-04-18 00:04 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found