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

I am trying to select three unique random numbers between 1..10. I tried using this:
srand; my @array = ("1","2","3","4","5","6","7","8","9","10"); for (1..3) { push(@num, $array[rand(@array)]); } foreach my $num (@num){ print"$num\n"; }
but at times, this is giving me same two numbers out of the three (i.e. 1, 6, 6). How can I get three UNIQUE random numbers? Thanks.

Replies are listed 'Best First'.
Re: Select three random numbers between 1..10
by Paladin (Vicar) on Mar 16, 2004 at 21:08 UTC
    push @num, splice(@array, int rand @array, 1);
    Which will remove and return a random element from the array, there by, never picking the same number twice.
      Paladin,
      Wow. That certainly is much neater than my solution. I knew there were likely canned solutions, but it seemed like a cool problem to solve:
      #!/usr/bin/perl use strict; use warnings; print "$_\n" for Get_Unique_Random(10 , 3); sub Get_Unique_Random { my ($max , $total) = @_; # Requires error checking obviously $total ||= 3; my @return; while ( @return < $total ) { my @list = ( 1 .. $max ); for my $seen ( @return ) { @list = grep { $_ ne $seen } @list; } push @return , $list[ (int rand $#list) + 1 ]; } return wantarray ? @return : \@return; }
      Cheers - L~R
      Honestly,
      push @num, splice(@array, int rand @array, 1);
      This totally made my day - the efficiency. In an extension of the original requirement, I needed to generate long fixed length strings from random elements in an array.. this is so much faster than what I was using.. I've no absolute requirement for total uniqueness, but it looks like no letter is being repeated in multiple runs of:
      #!/usr/bin/perl -w use strict; my $counter = "0"; my $randstring; my @testarray = ( 'a' .. 'z', 'A' .. 'Z', '0' .. '9' ); do { $randstring .= splice(@testarray, int rand @testarray,1); ++$counter; } until $counter == 62; print "\n\$randstring: $randstring\n\n";
      -hsinclai
Re: Select three random numbers between 1..10
by halley (Prior) on Mar 16, 2004 at 21:11 UTC
    Instead of thinking 'picking unique numbers in a range', think 'dealing cards from a deck.'

    The List::Util module has a good (fast and suitably random) array shuffler, and then three simple pops or shifts will give you the first three choices.

    --
    [ e d @ h a l l e y . c c ]

Re: Select three random numbers between 1..10
by Aristotle (Chancellor) on Mar 16, 2004 at 21:15 UTC
    use List::Util qw(shuffle); my @random = ( shuffle 1 .. 10 )[ 0 .. 2 ];
    The shuffle function in List::Util implements a Fisher-Yates shuffle, which ensures that the probability for each of the 10 numbers to end up in any of the 10 slots is exactly equal. Then you just pull any three numbers out of the resulting list.

    Makeshifts last the longest.

•Re: Select three random numbers between 1..10
by merlyn (Sage) on Mar 16, 2004 at 21:49 UTC
    What ya need is a modified fisher-yates shuffle, shuffling only the part you're looking at:
    my @population = (1..10); # destructive source my $n = 3; # desired selection size my @selection; # result appears here while (@selection < $n) { my $swapper = int rand @population; push @selection, $population[$swapper]; if ($swapper < $#population) { $population[$swapper] = $population[-1]; } pop @population; }

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

      Urgle. While you pick a fast algorithm, your implementation is not efficient. There's no need to build @selection element by element, while popping off one element of of @population at the time. Just run $n iterations of Fisher-Yates loop, and return the last $n elements of the array. Furthermore,
      if ($swapper < $#population) { $population[$swapper] = $population[-1]; }
      is less efficient than
      $population[$swapper] = $population[-1];
      Sure, the if statement prevents a needless assignment, but the expected number of needless assignments is
      Σi=0$n 1/(@population - i)
      
      which, while unbounded, grows very very slowly if the array to sample grows. OTOH, the number of compares done is linear with the size of the sample taken.

      Abigail

        Oddly enough, Abi, I was hoping you'd point out the error of my ways. You have a brilliance that I can only aspire towards. I'm just a street-trained programmer of average class.

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

      Out of curiosity, why did you choose to build a new list instead of transposing items within the existing one:

      my @sample = (1..10); my $n = 3; for my $l (0..$n-1) { my $r = rand (@sample); @sample[ $l, $r ] = @sample[ $r, $l ]; } print join (", ", @sample[0..$n-1]), "\n";

      Transposition is safe against multiple hits to the same index, since each swap just shuffles the list a bit more. That eliminates the need to cull values that have already been seen, as you do with the pop at the tail of your loop.

      Even the degenerate case, where $l equals $r, leaves the list no less shuffled than it was before. And since every non-degenerate swap exchanges two values in the list, even the value left in place by a degenerate swap has a good chance of being moved by another, later swap.

Re: Select three random numbers between 1..10
by iburrell (Chaplain) on Mar 16, 2004 at 21:13 UTC
    You have the write code to select a single random element from the array. To get unique random numbers, you need to remove that element from the array and then keep selecting from the shorter set. splice lets you do this in one operation.
    my @num; my @array = 1 .. 10; for (1..3) { push @num, splice(@array, rand(@array), 1); }
      You have the write code to select a single random element from the array. To get unique random numbers, you need to remove that element from the array and then keep selecting from the shorter set.
      Luckely, you don't have to. While it hardly matters for an array of 10 elements, the principle doesn't scale. splice is an expensive operation, whose expected run time is Θ (N) for a single delete. If you have to select k elements from an array of size N, you have an expected running time of Θ (k * N). OTOH, if you perform a partial Fisher-Yates shuffle (you only need to get k elements), your running time reduces to O (k) (after setting up the array).

      Abigail

        I didn't think about the efficiency of splice. I originally had it coded to shuffle after selecting the element. In other words, select the random element, swap the last element with the selected one, and shorten the array. Technically, the array does not need to be shortened.
Re: Select three random numbers between 1..10 (efficiency)
by tye (Sage) on Mar 16, 2004 at 21:54 UTC

    First, just don't call srand, it isn't useful here. At least you didn't pass an argument to it. :)

    I find it unfortunate that List::Util's shuffle() doesn't support stopping the shuffle process after the first N new items have been selected. This makes using it for this situation somewhat inefficient. But to really make it inefficient, you should pick from a really huge list. But, having a really huge list means this method becomes inefficient in memory as well as in wasting a lot of time doing shuffling that doesn't matter.

    So here is a solution that scales very well as far as memory and CPU use is concerned. It isn't as flexible (it only lets you select from a range of integers) and it probably isn't even faster for such small cases as originally requested, but it was an interesting challenge. Maybe I should add it to Algorithm::Loops.

    It uses the same algorithm as the Fisher-Yates shuffle, but using a sparse array.

    use strict; use warnings; # My solution: sub pickInts { my( $count, $min, $max )= @_; my( @pick, %pick ); while( 0 < $count-- ) { my $i= int( $min + rand($max-$min+1) ); for( $pick{$i} ) { $i= $_ if defined; $_= $min++; } push @pick, $i; } return wantarray ? @pick : \@pick; } # For comparison: #use List::Util qw( shuffle ); # I don't have this module, so I stole its code: sub shuffle (@) { my @a=\(@_); my $n; my $i=@_; map { $n = rand($i--); (${$a[$n]}, $a[$n] = $a[$i])[0]; } @_; } # Sample uses: # Pick 3 integers from the range 1..10: my @list= pickInts( 3, 1, 10 ); print "@list\n"; print join " ", (shuffle(1..10))[0..2], $/; $|= 1; # Pick 10 integers from the range 100000..999999: @list= pickInts( 10, 100000, 999999 ); print "@list\n"; print join " ", (shuffle(100000..999999))[0..9], $/;

    which produces the output like:

    > perl pick.pl 2 1 9 4 10 9 676232 765332 337745 257079 672444 306794 410807 382463 952100 532838 ^C >

    Sorry, I didn't have time to wait for the full output. (:

    - tye        

    (Trivial updates applied)
Re: Select three random numbers between 1..10
by hawtin (Prior) on Mar 17, 2004 at 02:31 UTC

    I was just looking at the code here and it struck me that there must be a simpler way than having to mess with lists.

    Suppose I have to pick just one random number from the range 1..10. I can do it by using the following code:

    sub pick_one { my($from,$to) = @_; for(my $i=$from;$i<=$to;$i++) { return $i if(rand(1) <= 1/($to-$i+1)); } }

    (Notice that when $i==$to the test must always succeed so we don't have to worry about a final return)

    If I have to pick $n more numbers and I have ($to-$from+1) numbers left then the chance that the next number will be one of those picked is ($n/($to-$from+1)), so the final form of the subroutine is:

    sub pick_n { my($n,$from,$to) = @_; my(@picked); for(my $i=$from;$i<=$to;$i++) { if(rand(1) <= ($n/($to-$i+1))) { push(@picked,$i); $n--; } } return @picked; }

    Without having to shuffle lists or even use them. Notice the time is always the proportional to the range and the results are always unique. Just to prove that the results are fair:

    my(@counts); for(my $i=0;$i<100000;$i++) { foreach my $c (pick_n(3,1,10)) { $counts[$c]++; } } for(my $c=0;$c<=$#counts;$c++) { print "Count[$c] = $counts[$c]\n"; }

    =>

    Count[0] = Count[1] = 29974 Count[2] = 30044 Count[3] = 29860 Count[4] = 29949 Count[5] = 30167 Count[6] = 30253 Count[7] = 29808 Count[8] = 30092 Count[9] = 29830 Count[10] = 30028

    Update: This algorithm will work well if there are lots of numbers to be selected from a shorter list. If selecting fewer numbers from longer lists then the simple hash approach that most other suggestions are based on will be more efficient.

      Very nice.

      Take another look at my solution (which also doesn't deal with a list). For picking relatively few items from large ranges, my solution should be much faster than yours. But I can certainly see situations where I'd prefer yours over mine (for example, if I wanted the items returned in sorted order).

      I plan to make iterator versions of both.

      - tye        

Re: Select three random numbers between 1..10
by kvale (Monsignor) on Mar 16, 2004 at 21:10 UTC
    my @num = map { int( 1 + rand 10 ) } (1..3);
    Update: Missed the unique criterion! Try this:
    my %num; while(keys %num < 3) { my $x = int( 1 + rand 10 ); $num{$x}++ unless exists $num{$x}; } my @num = keys %num;

    -Mark

Re: Select three random numbers between 1..10
by matija (Priest) on Mar 16, 2004 at 21:17 UTC
    If you're filtering, you could get into a worst-case loop (particularly if you were picking nine out of ten numbers, or 99 out of a hundred).

    A better method is something like this:

    @arr=1..10; for (1..3) { $i=int(rand(scalar @arr)); push(@num,splice(@arr,$i,1)); } foreach my $num (@num){ print"$num\n"; }
    At the cost of storing all the numbers you are likely to generate, this code guarantees that it will finish in time proportional to n (instead of time growing at the same rate as n/m where n is the number of numbers chosen, and m is the size of the set).
Re: Select three random numbers between 1..10
by etcshadow (Priest) on Mar 16, 2004 at 21:22 UTC
    [me@host]$ perl -le 'print join(",", map { splice (@{$x ||= [1..10]},i +nt rand @$x,1) } (1..3))' 5,1,8 [me@host]$
    Fore!
    ------------ :Wq Not an editor command: Wq
Re: Select three random numbers between 1..10
by delirium (Chaplain) on Mar 16, 2004 at 21:17 UTC
    perl -le 'while(@list<3){$n=int(10*rand)+1;push @list,$n unless grep{$n==$_}@list}print for @list;'

    Update: Even better:

    perl -le '@list=1..10;for(1..3){push @list,shift @list for 1..rand 10; + print pop @list}
      perl -le '@n=(1..10);while(@l<3){push@l, splice@n,int rand@n,1}print for@l'


      -Waswas
        or... perl -le '@l=sort{rand$_<=>rand$_}(1..10);print for@l[0..2]]'


        -Waswas
Re: Select three random numbers between 1..10
by graff (Chancellor) on Mar 17, 2004 at 02:33 UTC
    My gosh! All those replies with different approaches, and nobody (so far) seems to have tried using a hash <update> kvale's was the only one to use a hash -- it was so short, I missed it, and wrote it again myself </update>:
    #!/usr/bin/perl use strict; my %chosen; # you don't need a 10-element array to draw from while ( keys %chosen < 3 ) { my $pick = int( rand(10)) + 1; $chosen{$pick} = undef; } print join " ", keys %chosen, "\n"; # (you could "sort {$a<=>$b) keys %chosen", if that matters)
    This sort of problem does not merit a lot of optimization, but I think the extra iterations that might be needed on occasion to avoid repeat picks would, on balance, take less time than various amounts of array splicing, shuffling, etc...
Re: Select three random numbers between 1..10
by Perl_User (Acolyte) on Mar 16, 2004 at 21:57 UTC
    Thank you all for your suggestions..I really appreciate it.
Re: Select three random numbers between 1..10
by periapt (Hermit) on Mar 17, 2004 at 16:07 UTC
    In my experience, the circumstances for random number selection are relevent to the process. Many selection algorithms are costly in terms of time or computing power or are unnecessarily complex for the task at hand. Selecting 10 random numbers may not require the same complexity as selecting 10,000. Here are two possibilities I've used before.
    #!/usr/perl use warnings; use strict; my @tmpary = (); my @num = (); my $ulmt = 10; my $nrneeded = 3; srand; # if the order of selection of random number is not important my $idx = 0; my $cntr = 0; # this does waste the 1st element but simplifies indexing # and does cost the memory but for small lists this isn't # significant push @tmpary, undef for (0..$ulmt); while($cntr < $nrneeded){ $idx = int(rand($ulmt-1)+1); # map range into (1..ulmt) unless($tmpary[$idx]){ $cntr++; $tmpary[$idx] = 1; } } $tmpary[$_] && push @num, $_ for (0..$ulmt); print "$_, " foreach (@num); print "\n"; # if order of selection of random numbers is important my %idx = (); my $val = 0; @num = (); while(scalar keys %idx < $nrneeded){ $val = int(rand($ulmt-1)+1); # map range to (1..ulmt) $idx{$val} ||= 1 && push @num, $val; } print "$_, " foreach (@num); print "\n"; exit;
Re: Select three random numbers between 1..10
by artist (Parson) on Mar 16, 2004 at 21:07 UTC
    Filter using the Hash.