views:

276

answers:

5

rand(1,N) but excluding array(a,b,c,..),

is there already a built-in function that I don't know or do I have to implement it myself(how?) ?

UPDATE

The qualified solution should have gold performance whether the size of the excluded array is big or not.

+6  A: 

I don't think there's such a function built-in ; you'll probably have to code it yourself.

To code this, you have two solutions :

  • Use a loop, to call rand() or mt_rand() until it returns a correct value
    • which means calling rand() several times, in the worst case
    • but this should work OK if N is big, and you don't have many forbidden values.
  • Build an array that contains only legal values
    • And use array_rand to pick one value from it
    • which will work fine if N is small
Pascal MARTIN
Both of your solutions will have extremely bad performance under certain situations.
Gtker
@Runner: These are both straight forward and simple algorithms to solve the problem. There aren't too many (probably None) other ways to approach the problem: You either eliminate the excluded values if you encounter them (alg 1 above) or you select from the allowed values (alg 2 above). Even a clever implementation will just be a variation on a theme given above. The method that you're looking for, `Magic()`, hasn't been written in PHP yet.
KevenK
What is "gold performance" and what is "big" for you ?
Arkh
+2  A: 

No built-in function, but you could do this:

function randWithout($from, $to, array $exceptions) {
    sort($exceptions); // lets us use break; in the foreach reliably
    $number = rand($from, $to - count($exceptions)); // or mt_rand()
    foreach ($exceptions as $exception) {
        if ($number >= $exception) {
            $number++; // make up for the gap
        } else /*if ($number < $exception)*/ {
            break;
        }
    }
    return $number;
}

That's off the top of my head, so it could use polishing - but at least you can't end up in an infinite-loop scenario, even hypothetically.

Note: The function breaks if $exceptions exhausts your range - e.g. calling randWithout(1, 2, array(1,2)) or randWithout(1, 2, array(0,1,2,3)) will not yield anything sensible (obviously), but in that case, the returned number will be outside the $from-$to range, so it's easy to catch.

If $exceptions is guaranteed to be sorted already, sort($exceptions); can be removed.

Eye-candy: Somewhat minimalistic visualisation of the algorithm.

pinkgothic
I don't see the logic that will certainly exclude `$exceptions` in your code.
Gtker
`if ($number >= $exception)`. Essentially what the code is doing is reducing the range of numbers to the amount you actually need (thus requiring only one run through the function), then skipping over the gaps as per what `$exceptions` define.
pinkgothic
Example: `randWithout(1, 10, array(5, 8))`. `$number` is `rand(1, 10-2)` == `rand(1, 8)`, so, say, in this example, that we get `$number == 7`. Then the foreach runs through the ordered array and checks `$number >= $exception`, which is `7 >= 5` (yes, so `$number == 8`), then `8 >= 8` (yes, so `$number == 9`), and spits out `9` at you.
pinkgothic
Another example: `randWithout(1, 10, array(5, 8))` again, with `rand` giving us `$number == 2`. The foreach begins to run through the array, but notices `$number < 5`, breaks out of the loop and returns `2`. [BTW, I edited my post to fix the `else`, the extra if-condition was extraneous, it was always going to be true after the `if`.]
pinkgothic
Got around to testing this, working like a charm (though if $exceptions contains all possible values, or exceeds them, it does break). But: Am *so* keeping this! It could probably use a check if the `count()` returns a number larger than the supplied range for hardening, just as a heads-up.
pinkgothic
More 'entirety of range contained within $exceptions' gotchas: `randWithout(1, 2, array(1, 2, 3, 4))` gives `0` or `5`. `randWithout(1, 2, array(1, 2, 3, 4, 5))` gives `-1`, `0` or `6`. You get the gist. They're corner cases and unlikely, perhaps, but good to know about. You'll know your `$exceptions` array was bogus if the value the function returned is outside the range `$from`-`$to`.
pinkgothic
But it's not universally distributed.
Gtker
Err? ...I'm not sure what you're trying to say. The numbers it will return all have the same possibility of being returned. How well they are distributed depends entirely on `rand()` (or `mt_rand()` if you go with that). If you trust that function of your choice to return well-distributed values, then this will, too. Of course it has gaps, but only the gaps you want. So... iunno, you've confused me. What *are* you criticising?
pinkgothic
+1  A: 

The simplest way...

<?php

function rand_except($min, $max, $excepting = array()) {

    $num = mt_rand($min, $max);

    return in_array($num, $excepting) ? rand_except($min, $max, $excepting) : $num;
}
?>
sasa
Just so you know, this is the "Use a loop, to call rand() or mt_rand() until it returns a correct value" that Pascal MARTIN suggested, replacing 'loop' with 'recursion'.
pinkgothic
+1  A: 

What you need to do is calculate an array of skipped locations so you can pick a random position in a continuous array of length M = N - #of exceptions and easily map it back to the original array with holes. This will require time and space equal to the skipped array. I don't know php from a hole in the ground so forgive the textual semi-psudo code example.

  1. Make a new array Offset[] the same length as the Exceptions array.
  2. in Offset[i] store the first index in the imagined non-holey array that would have skipped i elements in the original array.
  3. Now to pick a random element. Select a random number, r, in 0..M the number of remaining elements.
  4. Find i such that Offset[i] <= r < Offest[i+i] this is easy with a binary search
  5. Return r + i

Now, that is just a sketch you will need to deal with the ends of the arrays and if things are indexed form 0 or 1 and all that jazz. If you are clever you can actually compute the Offset array on the fly from the original, it is a bit less clear that way though.

Ukko
I started this, then got coffee, then finished--never a good idea. I see pinkgothic has a basically the same solution as I described but she is calculating the offsets. I'll leave this up incase it makes the process clear for someone.
Ukko
It's not universally distributed,is it?
Gtker
+1 for another single-pass solution (the other stuff depresses me), except I'm out of votes for today.
pinkgothic
@Runner: Please define what you mean with 'universally distributed'.
pinkgothic
If you are asking if it is uniformly distributed, it is assuming your random number generator is. You are doing the selection on the gapless array and then mapping it back to the array with gaps. This mapping is one-to-one on the remaining elements so uniformity is preserved.
Ukko
+1  A: 

Depending on exactly what you need, and why, this approach might be an interesting alternative.

$numbers = array_diff(range(1, N), array(a, b, c));
// Either (not a real answer, but could be useful, depending on your circumstances)
shuffle($numbers); // $numbers is now a randomly-sorted array containing all the numbers that interest you
// Or:
$x = $numbers[array_rand($numbers)]; // $x is now a random number selected from the set of numbers you're interested in

So, if you don't need to generate the set of potential numbers each time, but are generating the set once and then picking a bunch of random number from the same set, this could be a good way to go.

Matt Gibson
+1 for using in-built functions (but I'm out of votes for today, need to come back later) and getting the job done in one pass. Both `shuffle()` and `array_rand()` combined might be overkill (or might not be enough, depends on the context), but it definitely gets the job done!
pinkgothic
The performance will be terrible when `N` is huge.@pinkgothic ,seems I need some time to digest your solution:)
Gtker
@pinkgothic :) Thanks! Yup, I'm not suggesting doing both shuffle() and array_rand(), just one or the other, depending on the reason the numbers are needed, and what type of results might work for the problem.@Runner Yeah, it won't be brilliant for a huge N :) But I figured it might be a viable answer if you only want to generate the number set once, and then generate random answers multiple times. And depending on $huge :)The answer is a bit of a balancing act depending on the exact question, when it comes to performance...
Matt Gibson
For small `N` I prefer your solution to mine, because yours is far more readable. :) Never underestimate readable code!
pinkgothic