等概率随机取数算法 - 洗牌算法 | 链表随机节点 | 随机数索引 - Java 实现

目录

1. 洗牌算法

洗牌算法 Java 实现

复杂度分析

概率分析

2. 链表随机节点

题解 Java 实现

复杂度分析

概率分析

3. 随机数索引

题解 Java 实现

复杂度分析

概率分析


1. 洗牌算法

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 个数

那么我们需要保证对数组打乱的过程,所有的元素在某个索引位置的概率都相等。

这种打乱过程就是等概率算法,典型的等概率算法就是洗牌算法

 

洗牌算法 Java 实现

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

 

2. 链表随机节点

该题目来自力扣 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();

 

题解 Java 实现

 
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 个节点的概率 * 不选中第 3 个节点的概率 * 不选中第 4 个节点的概率 * ... * 不选中第 n 个节点的概率

= 1/2 * 2/3 * 3/4 * ... * n-1/n = 1/n

......

选中第 个节点的概率

= 选中第 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,概率相等

 

3. 随机数索引

该题目来自力扣 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);

 

题解 Java 实现

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,概率相等

 

 

你可能感兴趣的:(数据结构与算法,LeetCode)