NY Tennessean <> wrote in comp.lang.perl.misc:
> (Scott Stark) wrote in message
> news:<. com>...
> > I'm using rand() to generate a series of random numbers. Is there any
> > way to prevent the same number from being used more than once? All I
> > can think of is keeping a list of used numbers, and having some kind
> > of "if used, next" statement, which seems lame. (Theoretically that
> > could keep going forever, if it randomly kept selecting the same
> > numbers.) Any thoughts?
>
> You've got a couple of answers suggesting you use modules that allow
> you to "shuffle" an array, and I can't disagree: modules are usually
> the way to go as they have been used and tested more than any home
> grown solution.
>
> But still I wanted to share with you the home-grown code I've rolled
> since my original suggestion. Beware, its a little clunky. It could
> stand some perfecting but "works" for me, at least for the dozen or so
> times I tested it:
Yes, it does what it should, and yes, it *is* a little clunky.
> sub uniq_random_int_in ($$$) {
^^^^^
Don't give a function a prototype unless there is a distinct advantage.
Normally, perl subs are not prototyped.
> #based on random_int_in in perlfaq4, data manipulation
> my($min, $max, $qty) = @_;
> # Assumes that the three arguments are integers themselves!
> # Assumes $min < $max
> # Assumes $qty <= $max - $min + 1
> my @set, @uniq;
This doesn't declare @uniq. "my (@set, @uniq)" is what you need. You
didn't run this under strict, did you?
> for (my $i=0; $i <= $max - $min; $i++) {
> $set[$i] = $i;
> }
That could be more simply written
@set = 0 .. $max - $min;
But what you really want in @set is the set of possible results, so
you can make that
@set = $min .. $max;
> for (my $i=0; $i < $qty; $i++) {
> my $rand = int rand($max - $min - $i + 1);
The number "$max - $min - $i + 1" is exactly the number of elements
left in @set in the $i-th step. You start with $max - $min + 1 elements
in the 0-th step, and you lose one element each time $i increases. So
my $rand = int rand @set;
does the same thing. The selection process is now even clearer:
Select a random number between 0 and the number of elements left
(exclusive). Pick that element.
> $uniq[$i] = $set[$rand] + $min;
When @set contains the numbers $min .. $max directly, you don't need
the addition. $set[$rand] is the wanted random number.
> @set = (@set[0..$rand-1], @set[$rand+1..scalar(@set)-1]);
^^^^^^
"@set" is already in scalar context, no need for "scalar".
But Perl's splice() function is what you really want here:
splice( @set, $rand, 1);
This returns the value(s) that are spliced out, so you can find
the random index and move the indicated element from @set to @uniq
in one step:
push @uniqe, splice( @set, rand @set, 1);
> print $uniq[$i], "\n"; #display only, can comment out
> }
> return @uniq;
> }
>
> @uniq_rands = uniq_random_int_in(11,20,5); # for testing
Putting it all together, this is how I would write it:
sub uniq_random_int_in {
my($min, $max, $qty) = @_;
my @set = ( $min .. $max);
map splice( @set, rand @set, 1), 1 .. $qty;
}
Anno