写在前面
这个算法也是想写了很久了,这几天又恰好碰到几道需要整理的题目都属于这一类,就一并写了,这个算法的名字听起来陌生,但事实上是经常会问到的面试算法之一,这个算法的一个经典问题就是扑克牌洗牌问题,类似的问法比如以k/n的概率从一堆数中采样k个数,之类的描述。
采样算法在wiki上有较为完整的介绍,这里先给出链接 池采样。
我们完整分析采样算法的运行过程。
问题描述
这里借用维基百科上关于此问题的描述,原文请点击上述链接:
现在要从序列S中随机抽取k个样本,序列S的大小为n,其中n的大小未知。(n也有可能远大于内存能够存储的数量)
乍一看问题似乎是不可解的,因为n的个数未知,也就无法保证以k/n的概率完成随机采样,但下面的数学分析证明问题本身是可解的。
算法描述
我们先说明问题解的思路,再进行分析。
题解可以简单描述为:
1. 从序列中取前k个样本放入采样池。
2. 对第k+1个数,以k/k+1的概率选择并对池中的数随机替换为该数。
3. 对第k+i个数,以k/k+i的概率选择并对池中的数随机替换为该数。
4. 重复上述过程直到k+i到达n
所以本算法的终止条件一定是到达n个数才满足,因为这个时候才满足k/n的题目要求。
关于概率关系的实现:
我们先考虑k=1的最简单情况,这时候采样池中只包含一个数,我们要保证后续的概率1/k+i,只需要rand()得到的随机数%(1+i )= 某个固定的值即可(这个值必须<1+i),通常我们将这个值设置为0,当然它可以是满足上述条件的任何数,以此为逻辑,要满足k/k+i的概率,我们只需要rand()得到的数在0-k之间即可,即 rand()%k+i
leetcode 382.Linked List Random Node
Given a singly linked list, return a random node’s value from the linked list. Each node must have the same probability of being chosen.
Follow up:
What if the linked list is extremely large and its length is unknown to you? Could you solve this efficiently without using extra space?
Example:
// Init a singly linked list [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() should return either 1, 2, or 3 randomly. Each element should have equal probability of returning.
solution.getRandom();
解题思路
很明显的采样算法,我们不需要去管题目中别的描述,采样算法往里套就行了,首先池的大小为1,那么就首先把第一个数放进去,后面的数以rand()%(i)==0的方式去替换池中的数,到达n后的返回值即为所得。
代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* headNode;
/** @param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node. */
Solution(ListNode* head) {
headNode = head;
}
/** Returns a random node's value. */
int getRandom() {
int ret = headNode->val;
auto next = headNode->next;
for(int i = 2;next!=nullptr;i++) {
if(rand()%i==0){
ret = next->val;
}
next = next->next;
}
return ret;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(head);
* int param_1 = obj.getRandom();
*/
leetcode 398. Random Pick Index
iven an array of integers with possible duplicates, randomly output the index of a given target number. You can assume that the given target number must exist in the array.
Note:
The array size can be very large. Solution that uses too much extra space will not pass the judge.
Example:
int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);
// pick(3) should return either index 2, 3, or 4 randomly. Each index should have equal probability of returning.
solution.pick(3);
// pick(1) should return 0. Since in the array only nums[0] is equal to 1.
solution.pick(1);
解题思路
也是一道很显然的池采样问题,只不过这次的要求是对重复数字的随机采样,对于非重复数字,只要求返回他们的下标,对于重复数字,则返回他们的随机采样位置。这其实就是一个简单的变种,我们认为对重复数字的采样也相当于一个完全由该数字组成的数组的采样,满足rand()%i == 0就进行替换。只不过这里的i要替换成该数的个数,具体实现如下:
class Solution {
public:
//还是随机采样问题
vector<int> iNums;
Solution(vector<int> nums) {
iNums = nums;
}
int pick(int target) {
int count = 0;
int result = -1;
for(int i = 0;i<iNums.size();++i) {
if(iNums[i] == target) {
if(rand()%(++count) == 0)
result = i;
}
}
return result;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution obj = new Solution(nums);
* int param_1 = obj.pick(target);
*/
对于单个数字,上述代码也是满足要求的,因为这里的++count在只存在一个数字时为1,也就是说rand的值永远为0。
这题的思路还是很巧妙的,利用了count和rand的关系,值得仔细思考。
洗牌问题
洗牌问题的思路跟上述问题很类似,只不过这里要求的不是替换池中内容,而是保留,我们的做法也类似,首先完全随机一个数,将其与最后一个数做交换,剩下的53个数再完全随机,与倒数第二个数交换。这种方法模拟了随机采样的过程,最后的概率都是1/54,数组的交换很简单,这里不再给出代码。