Java_Math和Random(10)

Math

Math 类定义在 java.lang 包下,其中实现了很多数学运算方法,都是静态方法直接 Math. 调用即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Test {
  public static void main(String[] args) {
    System.out.println(Math.PI); //3.141592653589793
    System.out.println(Math.abs(-10)); //10
    System.out.println(Math.ceil(9.1)); //10.0
    System.out.println(Math.floor(9.9)); //9.0
    System.out.println(Math.max(3, 5)); //5
    System.out.println(Math.min(3, 5)); //3
    System.out.println(Math.pow(2, 3)); //8.0
    //四舍五入返回int整数
    System.out.println(Math.round(12.3f)); //12
    System.out.println(Math.round(12.9d)); //13
    System.out.println(Math.sqrt(4)); //2.0
  }
}

Random

随机数可通过 System.currentTimeMillis 方法获取,实际上是获取当前时间毫秒数 long 值,获取 int 随机整数转型即可,例如获取 [0, 100) 之间的随机 int 整数

1
2
3
4
5
6
7
8
public class Test {
  public static void main(String[] args) {
    final long l = System.currentTimeMillis();
    final int i = (int) (l % 100);
    System.out.println(l); //1559923936440
    System.out.println(i); //40
  }
}

Math 类提供了 random 方法来获取随机数,返回 [0, 1) 之间的 double 值,同样要获取 int 随机整数转型即可,例如获取 [0, 100) 之间的随机 int 整数

1
2
3
4
5
6
7
8
public class Test {
  public static void main(String[] args) {
    final double d = Math.random();
    final int i = (int) (d * 100);
    System.out.println(d); //0.8837862773487299
    System.out.println(i); //88
  }
}

Math 类中的 random 方法其实依赖的是 java.util 包下的 Random 类,可以直接通过 Random 类来获取随机数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Test {
  public static void main(String[] args) {
    Random rd = new Random();
    //nextInt(n) 返回[0, n)的随机整数,只有nextInt方法可以传入参数限制范围
    System.out.println(rd.nextInt(100)); //19
    //返回随机long
    System.out.println(rd.nextLong()); //-7105566914869940040
    //返回[0, 1)的随机浮点数
    System.out.println(rd.nextDouble());  //0.21306810577924284
    System.out.println(rd.nextFloat());   //0.97695065
    //返回随机boolean
    System.out.println(rd.nextBoolean()); //true
    //返回随机字节数组
    byte[] bArr = new byte[3];
    rd.nextBytes(bArr);
    System.out.println(Arrays.toString(bArr)); //[14, -28, -23]
  }
}

Random 类是根据种子来生成随机数的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test {
  public static void main(String[] args) {
    //Random rd = new Random();   //空参构造默认使用纳秒值做种子
    Random rd = new Random(1000); //还提供了可接受long值的指定种子的构造方法
    //种子决定了生成的随机序列,种子相同则生成的序列就相同
    for (int i = 0; i < 3; i++) {
      System.out.print(rd.nextInt(100) + " ");
    }
    //87 35 76,每次运行都是这个3个数
  }
}

Random 类的源码分析,查看默认的空参构造方法是如何使用纳秒值做种子产生随机数的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public Random() {
  //按位与纳秒值异或
  this(seedUniquifier() ^ System.nanoTime());
}
private static long seedUniquifier() {
  // L'Ecuyer, "Tables of Linear Congruential Generators of
  // Different Sizes and Good Lattice Structure", 1999
  //CAS锁,防止在多线程环境下,两次调用返回两个相同的值,所以Random类是线程安全的
  for (;;) {
    long current = seedUniquifier.get();
    long next = current * 181783497276652981L;
    if (seedUniquifier.compareAndSet(current, next))
      return next;
  }
}
private static final AtomicLong seedUniquifier
  = new AtomicLong(8682522807148012L);

至于为什么选择那两个奇怪的数字,可以查看上面注释中提到的论文,在 stackoverflow 上也有讨论

所有暴露出来的 public 的 API 方法都调用了 next 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public int nextInt() {
  return next(32);
}
public long nextLong() {
  // it's okay that the bottom word remains signed.
  return ((long)(next(32)) << 32) + next(32);
}
public boolean nextBoolean() {
  return next(1) != 0;
}
//...

查看 next 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;
protected int next(int bits) {
  long oldseed, nextseed;
  AtomicLong seed = this.seed;
  do {
    oldseed = seed.get();
    //重点看下面这个公式,与&上mask表示取低48位
    //采用的是linear congruential formula线性同余方程
    nextseed = (oldseed * multiplier + addend) & mask;
  } while (!seed.compareAndSet(oldseed, nextseed));
  return (int)(nextseed >>> (48 - bits));
}

而指定种子的构造方法则只是简单地将传入的种子与 multiplier 进行异或,然后取低 48 位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public Random(long seed) {
  if (getClass() == Random.class)
    this.seed = new AtomicLong(initialScramble(seed));
  else {
    // subclass might have overriden setSeed
    this.seed = new AtomicLong();
    setSeed(seed);
  }
}
private static long initialScramble(long seed) {
  return (seed ^ multiplier) & mask;
}

随机数应用实例

随机密码

6 位数字密码

1
2
3
4
5
6
7
8
9
public class Test {
  public static void main(String[] args) {
    char[] chars = new char[6];
    Random rd = new Random();
    for (int i = 0; i < chars.length; i++)
      chars[i] = (char) ('0' + rd.nextInt(10));
    System.out.println(String.valueOf(chars));
  }
}

8 位,包含数字、大小写字母、特殊字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
  public static void main(String[] args) {
    char[] chars = new char[8];
    String s = "`~!@#$%^&*()-_=+[{]}|;:,<.>/?";
    Random rd = new Random();
    for (int i = 0; i < chars.length; i++)
      chars[i] = nextChar(rd, s);
    System.out.println(String.valueOf(chars));
  }
  
  private static char nextChar(Random rd, String s) {
    //先随机选择类型
    switch (rd.nextInt(4)) {
        //再返回具体的字符
      case 0: return (char) ('a' + rd.nextInt(26));
      case 1: return (char) ('A' + rd.nextInt(26));
      case 2: return (char) ('0' + rd.nextInt(10));
      default: return s.charAt(rd.nextInt(s.length()));
    }
  }
}

如果要求数字、大小写字母、特殊符号都至少要有一个,则可以采用下面的方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Test {
  public static void main(String[] args) {
    char[] chars = new char[8];
    String s = "`~!@#$%^&*()-_=+[{]}|;:,<.>/?";
    Random rd = new Random();
    //先随机生成四个不同类型的字符,放到随机的位置上
    chars[nextIndex(chars, rd)] = (char) ('a' + rd.nextInt(26));
    chars[nextIndex(chars, rd)] = (char) ('A' + rd.nextInt(26));
    chars[nextIndex(chars, rd)] = (char) ('0' + rd.nextInt(10));
    chars[nextIndex(chars, rd)] = s.charAt(rd.nextInt(s.length()));
    //再填充其他位置
    for (int i = 0; i < chars.length; i++)
      if (chars[i] == 0) chars[i] = nextChar(rd, s);
    System.out.println(String.valueOf(chars));
  }
  
  private static int nextIndex(char[] chars, Random rd) {
    int index = rd.nextInt(chars.length);
    //被占用了则重新生成随机索引
    while (chars[index] != 0)
      index = rd.nextInt(chars.length);
    return index;
  }
  
  private static char nextChar(Random rd, String s) {
		//复用上面的...
  }
}

数组重排序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
  public static void main(String[] args) {
    int[] iArr = new int[10];
    for (int i = 0; i < iArr.length; i++) iArr[i] = i;
    shuffle(iArr);
    System.out.println(Arrays.toString(iArr));
  }

  public static void shuffle(int[] arr) {
    Random rd = new Random();
    //从后往前,i逐渐减小,rd.nextInt(i)则表示从数组剩余的前面一段中随机挑选元素
    for (int i = arr.length; i > 1; i--)
      swap(arr, i-1, rd.nextInt(i));
  }

  private static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }
}

带权重的随机选择

例如抽奖 1 元、5 元、10 元的权重分别为 7、2、1,对应的累计概率位 70%、90%、100%,随机选择则可以通过 nextDouble 方法生成一个 0 到 1的随机数,然后查看落在哪个区间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class Test {
  public static void main(String[] args) {
    ArrayList<Pair<String>> pairList = new ArrayList<>();
    pairList.add(new Pair<>("1元", 7));
    pairList.add(new Pair<>("5元", 2));
    pairList.add(new Pair<>("10元", 1));
    WeightRandom<String> rd = new WeightRandom<>(pairList);
    for (int i = 0; i < 100; i++)
      System.out.print(rd.nextItem() + " ");
  }
}

class Pair<E> {
  E item;
  double weight;
  public Pair(E item, double weight) {
    this.item = item;
    this.weight = weight;
  }
  public E getItem() {
    return item;
  }
  public double getWeight() {
    return weight;
  }
}

class WeightRandom<E> {
  private ArrayList<Pair<E>> pairList;
  private double[] cumulativeProbabilities;
  private Random rd;

  public WeightRandom(ArrayList<Pair<E>> pairList) {
    this.pairList = pairList;
    this.rd = new Random();
    prepare();
  }

  private void prepare() {
    int weights = 0;
    for (Pair<E> pair : pairList)
      weights += pair.getWeight();
    cumulativeProbabilities = new double[pairList.size()];
    double sum = 0;
    //计算累积概率0.7, 0.9, 1.0
    for (int i = 0; i < pairList.size(); i++) {
      sum += pairList.get(i).getWeight();
      cumulativeProbabilities[i] = sum / weights;
    }
  }

  public E nextItem() {
    double randomD = rd.nextDouble();
    //cumulativeProbabilities中的元素是从小到大排序的,所以可以使用二分查找
    int index = Arrays.binarySearch(cumulativeProbabilities, randomD);
    //生成的随机数大概率是不会正好等于累积概率中的元素的
    //如果没有找到关键字,会返回值为负的插入点值
    //插入点值就是第一个比关键字大的元素在数组中的位置索引,并且这里的索引是从1开始的
    if (index < 0) {
      //例如生成的randomD为0.89,index为-2,所以要取负再-1
      index = -index - 1;
    }
    return pairList.get(index).getItem();
  }
}

抢红包

有剩余的红包金额和可抢人数,分配时,如果可抢人数等于 1则直接返回红包金额,如果大于 1,则先计算平均值,并设置随机数生成器的最大值为平均值的 2 倍,然后生成一个随机数,这个随机数就是一个人抢的红包金额,单位为分,所以如果这个随机数小于 1 则返回 1,因为最小只有一分钱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Test {
  public static void main(String[] args) {
    RandomRedEnvelope rre = new RandomRedEnvelope(300, 3);
    for (int i = 0; i < 4; i++)
      System.out.print(rre.nextMoney() + " ");
  }
}

class RandomRedEnvelope {
  private int balance;   //单位为分
  private int remainNum;
  private Random rd;

  public RandomRedEnvelope(int totalMoney, int totalNum) {
    balance = totalMoney;
    remainNum = totalNum;
    rd = new Random();
  }

  public int nextMoney() {
    if (remainNum == 0)
      throw new IllegalStateException("抢光了");
    else if (remainNum == 1) {
      remainNum--;
      return balance;
    } else {
      //每个红包的最大值为平均值的2倍,由于int会截断小数,所以比理论值略小
      int max = balance / remainNum * 2;
      int money = (rd.nextInt(max));
      money = Math.max(1, money);
      balance -= money;
      remainNum --;
      return money;
    }
  }
}

以上内容是玉山整理的笔记,如有错误还请指出