Write a truly inclusive random method for javascript

前端 未结 9 1479
没有蜡笔的小新
没有蜡笔的小新 2020-11-30 11:25

Javascript\'s MATH object has a random method that returns from the set [0,1) 0 inclusive, 1 exclusive. Is there a way to return a truly random method that includes 1.

相关标签:
9条回答
  • 2020-11-30 11:32

    This will return [0,1] inclusive:

    if(MATH.random() == 0)
        return 1;
    else
        return MATH.random();
    

    Explanation: If the first call to random() returns 0, return 1. Otherwise, call random again, which will be [0,1). Therefore, it will return [0,1] all inclusive.

    0 讨论(0)
  • 2020-11-30 11:39

    Addendum:

    Taking a look at the java.util.Random source code included with the distribution of Oracle JDK 7 ("Copyright (c) 1995, 2010, Oracle and/or its affiliates. All rights reserved. ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms") shows this simple code:

    class Random {
    
        public float nextFloat() {
            return next(24) / ((float)(1 << 24));
        }
    
        protected int next(int bits) {
            long oldseed, nextseed;
            AtomicLong seed = this.seed;
            do {
                oldseed = seed.get();
                nextseed = (oldseed * multiplier + addend) & mask;
            } while (!seed.compareAndSet(oldseed, nextseed));
            return (int)(nextseed >>> (48 - bits));
        }
    }
    

    Thus, for nextFloat():

    1. Take a "random integer value" between 0 and 2^24-1 (or rather, a random 24-bit bitpattern interpreted as an integer value),
    2. Convert it to float (in Java, "float" is mandated to be an IEEE 724 32-bit floating point, which can represent up to 2^24 with no loss of precision, and this will thus be a value between 0 and 1.6777215E7)
    3. Then divide it by the float representation of 2^24, again just representable with no loss of precision as 1.6777216E7. 2^24+1 = 16777217 would drop down to 1.6777216E7 when forced to be float. In the code, this should really be a constant. Hey Sun, cycles don't grow on trees!!
    4. Division results in a float in [0.0 .. 0.99999994] (the correct division result would be around 0.999999940395355224609375), with, I think, all the possible IEEE 724 floating point values in between 'equally possible'.

    See also IEEE floating point and Floating-Point Arithmetic on the JVM.

    The Javadoc comments for `next() is:

    /**
     * Generates the next pseudorandom number. Subclasses should
     * override this, as this is used by all other methods.
     *
     * <p>The general contract of {@code next} is that it returns an
     * {@code int} value and if the argument {@code bits} is between
     * {@code 1} and {@code 32} (inclusive), then that many low-order
     * bits of the returned value will be (approximately) independently
     * chosen bit values, each of which is (approximately) equally
     * likely to be {@code 0} or {@code 1}. The method {@code next} is
     * implemented by class {@code Random} by atomically updating the seed to
     *  <pre>{@code (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)}</pre>
     * and returning
     *  <pre>{@code (int)(seed >>> (48 - bits))}.</pre>
     *
     * This is a linear congruential pseudorandom number generator, as
     * defined by D. H. Lehmer and described by Donald E. Knuth in
     * <i>The Art of Computer Programming,</i> Volume 3:
     * <i>Seminumerical Algorithms</i>, section 3.2.1.
     *
     * @param  bits random bits
     * @return the next pseudorandom value from this random number
     *         generator's sequence
     * @since  1.1
     */
    

    The Javadoc comments for nextFloat() is:

    /**
     * Returns the next pseudorandom, uniformly distributed {@code float}
     * value between {@code 0.0} and {@code 1.0} from this random
     * number generator's sequence.
     *
     * <p>The general contract of {@code nextFloat} is that one
     * {@code float} value, chosen (approximately) uniformly from the
     * range {@code 0.0f} (inclusive) to {@code 1.0f} (exclusive), is
     * pseudorandomly generated and returned. All 2<font
     * size="-1"><sup>24</sup></font> possible {@code float} values
     * of the form <i>m&nbsp;x&nbsp</i>2<font
     * size="-1"><sup>-24</sup></font>, where <i>m</i> is a positive
     * integer less than 2<font size="-1"><sup>24</sup> </font>, are
     * produced with (approximately) equal probability.
     *
     * <p>The method {@code nextFloat} is implemented by class {@code Random}
     * as if by:
     *  <pre> {@code
     * public float nextFloat() {
     *   return next(24) / ((float)(1 << 24));
     * }}</pre>
     *
     * <p>The hedge "approximately" is used in the foregoing description only
     * because the next method is only approximately an unbiased source of
     * independently chosen bits. If it were a perfect source of randomly
     * chosen bits, then the algorithm shown would choose {@code float}
     * values from the stated range with perfect uniformity.<p>
     * [In early versions of Java, the result was incorrectly calculated as:
     *  <pre> {@code
     *   return next(30) / ((float)(1 << 30));}</pre>
     * This might seem to be equivalent, if not better, but in fact it
     * introduced a slight nonuniformity because of the bias in the rounding
     * of floating-point numbers: it was slightly more likely that the
     * low-order bit of the significand would be 0 than that it would be 1.]
     *
     * @return the next pseudorandom, uniformly distributed {@code float}
     *         value between {@code 0.0} and {@code 1.0} from this
     *         random number generator's sequence
     */
    
    0 讨论(0)
  • 2020-11-30 11:44

    The Math.random function returns a random number between 0 and 1, where 0 is inclusive and 1 is exclusive. This means that the only way to properly distribute the random numbers as integers in an interval is to use an exclusive upper limit.

    To specify an inclusive upper limit, you just add one to it to make it exclusive in the calculation. This will distribute the random numbers correctly between 7 and 12, inclusive:

    var min = 7;
    var max = 12;
    var rnd = min + Math.floor(Math.random() * (max - min + 1));
    
    0 讨论(0)
  • 2020-11-30 11:44

    Since this question has been asked again, and I didn't read this approach here I'll add another answer.

    IMO the best you can do, without too much hassle would be:

    exclusive:

    //simply ignore the 0
    for(var rand=0; !rand;rand = Math.random());
    
    //or simpler:
    var rand = Math.random() || Math.random();
    //because the probability for two consecutive `0` is pretty much non existant.
    

    this doesn't even introduce an error, since we just excluded the possibility of returning 0, every other value between 0 and 1 has the same probability

    inclusive:

    var rand = Math.random() * 2;
    if(rand > 1) rand-=1;
    //so the 0 shares it's probability with the 1.
    

    just to be clear about how tiny the "error" is:

    • the probability for a 0 or a 1 is
      1 / Math.pow(2, 54) or about 5.55e-17
    • the probability for any other value between 0 and 1 is
      1 / Math.pow(2, 53) or about 11.1e-17

    and the whole random-function would be:

    function random(min, max, inclusive){
        var r = Math.random();
        if(inclusive)
            r = r>0.5? 2*r-1: 2*r;
        else 
            while(!r) r = Math.random();
    
        return r * (max - min) + min;
    }
    

    Edit: I'm not sure wether I make a mistake, but shouldn't the probability be fixed on the inclusive approach, if I add another bit to the zeroes and ones, and therefore duplicate their probability:

    var rand = Math.random() * 4;
    rand = (rand % 1) || (rand & 1);
    
    0 讨论(0)
  • 2020-11-30 11:45

    To put it bluntly, what you're trying to do doesn't make sense.

    Remember that under a continuous probability distribution, the probability of getting a specific value is infinitesimal, so mathematically speaking you wi'll never see the exact value of 1.

    Of course, in the world of computers, the distribution of an RNG isn't truly continuous, so it's "possible" that you'll encounter a specific value (as silly as that sounds), but the probability will be so vanishingly small that in practice you will never observe it.

    To make the point a bit more clearly: if you did manage to write such a function in terms of double-precision floats, the probability of getting exactly 1 would be approximately 2-64. If you called the function 1 million times per second, you would have to wait around 600,000 years before you got a 1.

    0 讨论(0)
  • 2020-11-30 11:52

    From what I can see from the JavaScript console in Chrome, Math.random() generates a number from 0 up to 0.9999999999999999. Taking this into account, you can get what you want by adding a modifier. For example, here's a function that will give you quasi-random float between 0 and 1, with 1 being inclusive:

    function randFloat() {
      // Assume random() returns 0 up to 0.9999999999999999
      return Math.random()*(1+2.5e-16);
    }
    

    You can try this in the console by enter 0.9999999999999999*(1+2.5e-16) -- it will return exactly 1. You can take this further and return a float between 0 and 1024 (inclusive) with this function:

    function randFloat(nMax) {
      // Assume random() returns 0 up to 0.9999999999999999
      // nMax should be a float in the range 1-1024
      var nMod;
      if (nMax<4) nMod = 2.5e-16;
      else if (nMax<16) nMod = 1e-15;
      else if (nMax<32) nMod = 3.5e-15;
      else if (nMax<128) nMod = 1e-14;
      else if (nMax<512) nMod = 3.5e-14;
      else if (nMax<1024) nMod = 1e-13;
      return Math.random()*(nMax+nMod);
    }
    

    There's probably a more efficient algorithm to be had somewhere.

    0 讨论(0)
提交回复
热议问题