8

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.

Gtker
  • 2,237
  • 9
  • 29
  • 37
  • Is your 'excluded array' likely to be sorted? If so you can remove the `asort()` call in my function, which should speed things up considerably. – pinkgothic Apr 23 '10 at 13:26
  • Er, `sort()`. Sorry, been staring at this for too long. >.> (Off to grab some fresh air!) – pinkgothic Apr 23 '10 at 13:35
  • Re: "seems I need some time to digest your solution", have a visualisation of what my algorithm does: http://pandora.pinkgothic.com/randWithout.png (except please imagine the rand(0,8) reads rand(1,8), I'm jumping through hoops here to get images uploaded, I was in a rush. XD) – pinkgothic Apr 23 '10 at 14:44
  • http://pandora.pinkgothic.com/randWithoutFixed.png - there. It was bugging me. – pinkgothic Apr 23 '10 at 14:51
  • Glee. Sometimes a picture says more than a thousand words? :) Glad I could help! – pinkgothic Apr 23 '10 at 15:01
  • @pinkgothic,thanks a lot!BTW,what graphical editing tool did you use?It's very handy! – Gtker Apr 23 '10 at 15:30
  • Just GIMP ( http://www.gimp.org/ ). There are better tools to draw filled boxes, mind you! :) It just happened to be the one I had to use (pre-installed work computer and all). – pinkgothic Apr 23 '10 at 16:09

7 Answers7

17

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
  • 6,081
  • 3
  • 47
  • 72
  • I don't see the logic that will certainly exclude `$exceptions` in your code. – Gtker Apr 23 '10 at 12:38
  • `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 Apr 23 '10 at 12:46
  • 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 Apr 23 '10 at 12:49
  • 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 Apr 23 '10 at 12:52
  • 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 Apr 23 '10 at 13:08
  • 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 Apr 23 '10 at 13:19
  • 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 Apr 23 '10 at 14:06
9

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
  • 395,085
  • 80
  • 655
  • 663
  • 1
    Both of your solutions will have extremely bad performance under certain situations. – Gtker Apr 23 '10 at 12:36
  • 3
    @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 Apr 23 '10 at 12:51
6

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
  • 37,886
  • 9
  • 99
  • 128
  • +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 Apr 23 '10 at 14:14
  • The performance will be terrible when `N` is huge.@pinkgothic ,seems I need some time to digest your solution:) – Gtker Apr 23 '10 at 14:26
  • @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 Apr 23 '10 at 15:15
  • 1
    For small `N` I prefer your solution to mine, because yours is far more readable. :) Never underestimate readable code! – pinkgothic Apr 23 '10 at 15:19
3

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
  • 2,443
  • 5
  • 23
  • 35
  • 1
    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 Apr 23 '10 at 13:12
1

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
  • 2,236
  • 2
  • 21
  • 33
  • 1
    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 Apr 23 '10 at 13:51
  • +1 for another single-pass solution (the other stuff depresses me), except I'm out of votes for today. – pinkgothic Apr 23 '10 at 14:11
  • @Runner: Please define what you mean with 'universally distributed'. – pinkgothic Apr 23 '10 at 14:21
  • 1
    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 Apr 23 '10 at 15:24
0

Maybe its too late for answer, but I found this piece of code somewhere in my mind when trying to get random data from Database based on random ID excluding some number.

$excludedData = array(); // This is your excluded number
$maxVal = $this->db->count_all_results("game_pertanyaan"); // Get the maximum number based on my database

$randomNum = rand(1, $maxVal); // Make first initiation, I think you can put this directly in the while > in_array paramater, seems working as well, it's up to you
while (in_array($randomNum, $excludedData)) {
  $randomNum = rand(1, $maxVal);
}

$randomNum; //Your random number excluding some number you choose
  • Thanks for contributing! :) Though you should know this is basically a variant of Pascal MARTIN's and sasa's answers. Hypothetically (but vanishingly unlikely), those solutions can end up in an infiniteloop, or in the more benign case take a long time if `$excludedData` is large, which is the sort of thing Gtker seems to want to avoid. – pinkgothic Mar 22 '15 at 00:37
0

This is the fastest & best performance way to do it :

$all =  range($Min,$Max);
$diff = array_diff($all,$Exclude);
shuffle($diff );
$data = array_slice($diff,0,$quantity);
shamaseen
  • 2,193
  • 2
  • 21
  • 35