目录
1. 洗牌算法
洗牌算法 Java 实现
复杂度分析
概率分析
2. 链表随机节点
题解 Java 实现
复杂度分析
概率分析
3. 随机数索引
题解 Java 实现
复杂度分析
概率分析
1.1 如何从一个大小为 M 的数组中,随机选出 1 个数
最容易想到的方式是:利用 Random 随机一个 [0 - (M-1)] 范围的值,然后取数组中对应索引中的值。
1.2 如何从一个大小为 M 的数组中,随机选出 N 个不重复的数
循环 -> (利用 Random 方法随机一个 [0 - (M-1)] 范围的值,然后取数组中找对应索引中的值)
这种情况下,随机出的数是会重复的,我们可以记录选出的数,如果随机出的数已选出,则继续选,直到选出 N 个不重复的数。
对于上面这种方法,在 N 与 M 越接近、随机出来的数越多,数字重复的概率也就越高,时间复杂度也会越高。
针对上述 N 与 M 接近的时候处理方式,我们可以考虑将数组中的元素顺序打乱,然后选取索引值为 0-(N-1) 的 N 个数
那么我们需要保证对数组打乱的过程,所有的元素在某个索引位置的概率都相等。
这种打乱过程就是等概率算法,典型的等概率算法就是洗牌算法
public static void main(String[] args) {
int[] cards = new int[54];
for (int i = 0; i < cards.length; i++) {
cards[i] = i;
}
System.out.println("洗牌前 = " + Arrays.toString(cards));
Shuffle shuffle = new Shuffle();
shuffle.shuffle(cards);
System.out.println("洗牌后 = " + Arrays.toString(cards));
}
/**
* 洗牌算法
*/
public void shuffle(int[] cards) {
Random random = new Random();
for (int i = cards.length - 1; i >= 0; i--) {
swap(cards, i, random.nextInt(i + 1));
}
}
public void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
时间复杂度:我们只循环了一遍数组,时间复杂度为 O(n)
空间复杂度:使用常数级空间,空间复杂度为 O(1)
上述代码中,我们使用 1-54 个数字来模拟扑克牌中的 54 张牌
对于任意一个索引 k (0<=k<=53),我们来计算它在数组打乱后所处每个位置的概率
打乱后 k 索引的值在索引 53 位置的概率
即第一次随机生成的数为 k,k 与 53 交换,概率为 1/54
打乱后 k 索引的值在索引 52 位置的概率
首先 k 不能在索引 53 的位置,第二次随机生成的数为 k ,k 与 52 交换,概率为 53/54 * 1/53 = 1/54
......
打乱后 k 索引的值在索引 x 位置的概率
首先 k 不能在索引 53 的位置,不能再索引 52 的位置....... k 在索引 x 的位置
概率为 = 53/54 * 52/53 * 51/52 * ... * (x+1)/(x+2) * 1/(x+1) = 1/54
洗牌算法保证了数组中任意一个值在数组打乱后,出现在任何位置的概率都是 1/n
该题目来自力扣 382 题:链表随机节点 (https://leetcode-cn.com/problems/linked-list-random-node/)
给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。保证每个节点被选的概率一样。
进阶:
如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现?
示例:
// 初始化一个单链表 [1,2,3].
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution(head);
// getRandom()方法应随机返回1,2,3中的一个,保证每个元素被返回的概率相等。
solution.getRandom();
public Solution(ListNode head) {
this.head = head;
}
public int getRandom() {
int val = 0;
// 已遍历节点数
int count=0;
// 当前遍历的节点
ListNode node = head;
Random random = new Random();
while(node!=null){
// 节点不为空,遍历节点数 +1
count++;
// 在 [0,count) 范围内随机数,当随机数为 0 时,将返回结果设置为当前值
// 每次取到 0 的概率为 1/count
if(random.nextInt(count)==0){
val = node.val;
}
node = node.next;
}
return val;
}
时间复杂度:我们只遍历了一遍单链表,时间复杂度为 O(n)
空间复杂度:使用常数级空间,空间复杂度为 O(1)
针对包含 n 个节点的单链表
选中第 1 个节点的概率
= 选中第 1 个节点的概率 * 不选中第 2 个节点的概率 * 不选中第 3 个节点的概率 * ... * 不选中第 n 个节点的概率
= 1 * 1/2 * 2/3 * ... * n-1/n = 1/n
选中第 2 个节点的概率
= 选中第 2 个节点的概率 * 不选中第 3 个节点的概率 * 不选中第 4 个节点的概率 * ... * 不选中第 n 个节点的概率
= 1/2 * 2/3 * 3/4 * ... * n-1/n = 1/n
......
选中第 k 个节点的概率
= 选中第 k 个节点的概率 * 不选中第 k+1 个节点的概率 * 不选中第 k+2 个节点的概率 * ... * 不选中第 n 个节点的概率
= 1/k * k/k+1 * k+1/k+2 * ... * n-1/n = 1/n
选中第 n 个节点的概率
= 1/n
对于包含 n 个节点的单链表,每个节点被选中的概率都为 1/n,概率相等
该题目来自力扣 398 题:随机数索引 (https://leetcode-cn.com/problems/random-pick-index/)
给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:
数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。
示例:
int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);
// pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。
solution.pick(3);
// pick(1) 应该返回 0。因为只有nums[0]等于1。
solution.pick(1);
public int pick(int target) {
Random random = new Random();
// 需要返回的索引
int index = 0;
// 符合值为 target 的所有索引的个数
int n = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
// 索引对应的值 == target,则符合条件的索引个数 +1
n++;
// 对符合的索引个数求余数,余数==0 表示选择该索引,余数!=0 表示不选择该索引
if (random.nextInt() % n == 0) {
index = i;
}
}
}
return index;
}
时间复杂度:我们只对数组进行了一次循环,时间复杂度为 O(n)
空间复杂度:使用常数级空间,空间复杂度为 O(1)
如题目中的示例:solution.pick(3);
3 所在的索引为 2,3,4 ,符合条件的索引个数为 n = 3;
返回索引为 2 的概率:
返回索引 2 的概率 = 没选中索引 3 的概率 * 没选中索引 4 的概率 = 1/2 * 2/3
选择索引 2 的概率为 1/3
返回索引为 3 的概率:
不需要考虑索引 3 之前的索引是否选中,只需要用选中索引 3 的概率 * 索引 3 之后的索引选不中的概率 即可
返回索引 3 的概率 = 选中索引 3 的概率 * 没选中索引 4 的概率 = 1/2 * 2/3
选择索引 3 的概率为 1/3
返回索引为 4 的概率:
最后一个索引,无论前面选择哪个索引都没有影响,只需要计算最后一个索引被选中的概率即可
返回索引 4 的概率 = 选中索引 4 的概率 = 1/3
选择索引 4 的概率为 1/3
针对多个符合条件的 n 个索引,每个索引被选中的概率都为 1/n,概率相等