洗牌算法思路_随机洗牌算法

洗牌算法思路_随机洗牌算法1.背景    笔试时,遇到一个算法题:差不多是在n个不同的数中随机取出不重复的m个数。洗牌算法是将原来的数组进行打散,使原数组的某个数在打散后的数组中的每个位置上等概率的出现,刚好可以解决该问题。2.洗牌算法    由抽牌、换牌和插牌衍生出三种洗牌算法,其中抽牌和换牌分别对应Fisher-YatesShuffle和Knuth-DurstenfeldShhuffle算法。 …

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

1. 背景

       笔试时,遇到一个算法题:差不多是 在n个不同的数中随机取出不重复的m个数。洗牌算法是将原来的数组进行打散,使原数组的某个数在打散后的数组中的每个位置上等概率的出现,刚好可以解决该问题。

2. 洗牌算法

       由抽牌、换牌和插牌衍生出三种洗牌算法,其中抽牌和换牌分别对应Fisher-Yates Shuffle和Knuth-Durstenfeld Shhuffle算法。

   2.1 Fisher-Yates Shuffle算法

            

       最早提出这个洗牌方法的是 Ronald A. Fisher 和 Frank Yates,即 Fisher–Yates Shuffle,其基本思想就是从原始数组中随机取一个之前没取过的数字到新的数组中,具体如下:

        1. 初始化原始数组和新数组,原始数组长度为n(已知);

        2. 从还没处理的数组(假如还剩k个)中,随机产生一个[0, k)之间的数字p(假设数组从0开始);

        3. 从剩下的k个数中把第p个数取出;

        4. 重复步骤2和3直到数字全部取完;

        5. 从步骤3取出的数字序列便是一个打乱了的数列。

        下面证明其随机性,即每个元素被放置在新数组中的第i个位置是1/n(假设数组大小是n)。

        证明:一个元素m被放入第i个位置的概率P = 前i-1个位置选择元素时没有选中m的概率 * 第i个位置选中m的概率,即

                                                               洗牌算法思路_随机洗牌算法   

#define N 10
#define M 5
void Fisher_Yates_Shuffle(vector<int>& arr,vector<int>& res)
{
     srand((unsigned)time(NULL));
     int k;
     for (int i=0;i<M;++i)
     {
     	k=rand()%arr.size();
     	res.push_back(arr[k]);
     	arr.erase(arr.begin()+k);
     }
}

       
时间复杂度为O(n*n),空间复杂度为O(n).

    2.2 Knuth-Durstenfeld Shuffle  

           
Knuth 和 Durstenfeld  在Fisher 等人的基础上对算法进行了改进,在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的基本思想和 Fisher 类似,每次从未处理的数据中随机取出一个数字,然后把该数字放在数组的尾部,即数组尾部存放的是已经处理过的数字。
               算法步骤为:
                 1. 建立一个数组大小为 n 的数组 arr,分别存放 1 到 n 的数值;
                 2. 生成一个从 0 到 n – 1 的随机数 x;
                 3. 输出 arr 下标为 x 的数值,即为第一个随机数;
                 4. 将 arr 的尾元素和下标为 x 的元素互换;
                 5. 同2,生成一个从 0 到 n – 2 的随机数 x;
                 6. 输出 arr 下标为 x 的数值,为第二个随机数;
                 7. 将 arr 的倒数第二个元素和下标为 x 的元素互换;
                    ……
                 如上,直到输出 m 个数为止
该算法是经典洗牌算法。它的proof如下:

对于arr[i],洗牌后在第n-1个位置的概率是1/n(第一次交换的随机数为i)

在n-2个位置概率是[(n-1)/n] * [1/(n-1)] = 1/n,(第一次交换的随机数不为i,第二次为arr[i]所在的位置(注意,若i=n-1,第一交换arr[n-1]会被换到一个随机的位置))

在第n-k个位置的概率是[(n-1)/n] * [(n-2)/(n-1)] *…* [(n-k+1)/(n-k+2)] *[1/(n-k+1)] = 1/n

(第一个随机数不要为i,第二次不为arr[i]所在的位置(随着交换有可能会变)……第n-k次为arr[i]所在的位置).

void Knuth_Durstenfeld_Shuffle(vector<int>&arr)
{
	for (int i=arr.size()-1;i>=0;--i)
	{
		srand((unsigned)time(NULL));
		swap(arr[rand()%(i+1)],arr[i]);
	}
} 

    时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n.

原始数组被修改了,这是一个原地打乱顺序的算法,算法时间复杂度也从Fisher算法的 O(n2)提升到了O(n)。由于是从后往前扫描,无法处理不知道长度或动态增长的数组。

    2.3 Inside-Out Algorithm

       Knuth-Durstenfeld Shuffle 是一个内部打乱的算法,算法完成后原始数据被直接打乱,尽管这个方法可以节省空间,但在有些应用中可能需要保留原始数据,所以需要另外开辟一个数组来存储生成的新序列。

        Inside-Out Algorithm 算法的基本思思是从前向后扫描数据,把位置i的数据随机插入到前i个(包括第i个)位置中(假设为k),这个操作是在新数组中进行,然后把原始数据中位置k的数字替换新数组位置i的数字。其实效果相当于新数组中位置k和位置i的数字进行交互。

       如果知道arr的lengh的话,可以改为for循环,由于是从前往后遍历,所以可以应对arr[]数目未知的情况,或者arr[]是一个动态增加的情况
证明如下:
原数组的第 i 个元素(随机到的数)在新数组的前 i 个位置的概率都是:(1/i) * [i/(i+1)] * [(i+1)/(i+2)] *…* [(n-1)/n] = 1/n,(即第i次刚好随机放到了该位置,在后面的n-i 次选择中该数字不被选中)。
原数组的第 i 个元素(随机到的数)在新数组的 i+1 (包括i + 1)以后的位置(假设是第k个位置)的概率是:(1/k) * [k/(k+1)] * [(k+1)/(k+2)] *…* [(n-1)/n] = 1/n(即第k次刚好随机放到了该位置,在后面的n-k次选择中该数字不被选中)。
         

void Inside_Out_Shuffle(const vector<int>&arr,vector<int>& res)
{
	res.assign(arr.size(),0);
	copy(arr.begin(),arr.end(),res.begin());
	int k;
	for (int i=0;i<arr.size();++i)
	{
		srand((unsigned)time(NULL));
		k=rand()%(i+1);
		res[i]=res[k];
		res[k]=arr[i];
	}
} 

    时间复杂度为O(n),空间复杂度为O(n). 

     2.4 蓄水池抽样

             从N个元素中随机等概率取出k个元素,N长度未知。它能够在o(n)时间内对n个数据进行等概率随机抽取。
如果数据集合的量特别大或者还在增长(相当于未知数据集合总量),该算法依然可以等概率抽样.
            伪代码:

               

Init : a reservoir with the size: k  
        for    i= k+1 to N  
            M=random(1, i);  
            if( M < k)  
                 SWAP the Mth value and ith value  
       end for 

上述伪代码的意思是:先选中第1到k个元素,作为被选中的元素。然后依次对第k+1至第N个元素做如下操作:
每个元素都有k/x的概率被选中,然后等概率的(1/k)替换掉被选中的元素。其中x是元素的序号。

proof:

每次都是以 k/i 的概率来选择 
例: k=1000的话, 从1001开始作选择,1001被选中的概率是1000/1001,1002被选中的概率是1000/1002,与我们直觉是相符的。 
接下来证明: 
假设当前是i+1, 按照我们的规定,i+1这个元素被选中的概率是k/i+1,也即第 i+1 这个元素在蓄水池中出现的概率是k/i+1 
此时考虑前i个元素,如果前i个元素出现在蓄水池中的概率都是k/i+1的话,说明我们的算法是没有问题的。 

对这个问题可以用归纳法来证明:k < i <=N 
1.当i=k+1的时候,蓄水池的容量为k,第k+1个元素被选择的概率明显为k/(k+1), 此时前k个元素出现在蓄水池的概率为 k/(k+1), 很明显结论成立。 
2.假设当 j=i 的时候结论成立,此时以 k/i 的概率来选择第i个元素,前i-1个元素出现在蓄水池的概率都为k/i。 
证明当j=i+1的情况: 
即需要证明当以 k/i+1 的概率来选择第i+1个元素的时候,此时任一前i个元素出现在蓄水池的概率都为k/(i+1). 
前i个元素出现在蓄水池的概率有2部分组成, ①在第i+1次选择前得出现在蓄水池中,②得保证第i+1次选择的时候不被替换掉 
①.由2知道在第i+1次选择前,任一前i个元素出现在蓄水池的概率都为k/i 
②.考虑被替换的概率: 
首先要被替换得第 i+1 个元素被选中(不然不用替换了)概率为 k/i+1,其次是因为随机替换的池子中k个元素中任意一个,所以不幸被替换的概率是 1/k,故 
前i个元素(池中元素)中任一被替换的概率 = k/(i+1) * 1/k = 1/i+1 
则(池中元素中)没有被替换的概率为: 1 - 1/(i+1) = i/i+1 
综合① ②,通过乘法规则 
得到前i个元素出现在蓄水池的概率为 k/i * i/(i+1) = k/i+1 
故证明成立

如果m被选中,则随机替换水库中的一个对象。最终每个对象被选中的概率均为k/n,证明如下:

   证明:第m个对象被选中的概率=选择m的概率*(其后元素不被选择的概率+其后元素被选择的概率*不替换第m个对象的概率),即

洗牌算法思路_随机洗牌算法

void Reservoir_Sampling(vector<int>& arr)
{
	int k;
	for (int i=M;i<arr.size();++i)
	{
		srand((unsigned)time(NULL));
		k=rand()%(i+1);
		if (k<M)
			swap(arr[k],arr[i]); 
	}
}

因此,蓄水池抽样因为不需知道n的长度,可用于机器学习的数据集的划分,等概率随机抽样分为测试集和训练集。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/190881.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • python使用教程_新手python入门教程

    python使用教程_新手python入门教程作者:Vamei出处:http://www.cnblogs.com/vamei欢迎转载,也请保留这段声明。谢谢!怎么能快速地掌握Python?这是和朋友闲聊时谈起的问题。Python包含的内容

  • 全排列 leetcode_8的全排列

    全排列 leetcode_8的全排列给定一个没有重复数字的序列,返回其所有可能的全排列。示例:输入:[1,2,3]输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]classSolution:defpermute(self,nums):res=[]defbackt…

  • jquery输入数字随机抽奖特效

    简介:jQuery自定义数值抽奖活动代码是一款点击开始按钮计算机会产生玩家输入范围内的随机数,点击停止按钮,将显示数字最终结果的效果。效果展示 http://hovertree.com/te

    2021年12月26日
  • dirsearch安装和使用[通俗易懂]

    dirsearch安装和使用[通俗易懂]目录dirsearch介绍下载及安装如何使用简单用法递归扫描线程前缀/后缀黑名单筛选器原始请求Wordlist格式排除扩展扫描子目录代理报告其他命令小贴士选项选项强制性字典设置一般设置请求设置连接设置配置dirsearch介绍dirsearch是一个基于python3的命令行工具,常用于暴力扫描页面结构,包括网页中的目录和文件。相比其他扫描工具disearch的特点是:支持HTTP……

  • Android 如何有效修改包名

    Android 如何有效修改包名

  • ac测评题库_awing

    ac测评题库_awing杭州人称那些傻乎乎粘嗒嗒的人为 62(音:laoer)。杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。不吉利的数字为所有含有 4 或 62 的号码。例如:62315,73418,88914 都属于不吉利号码。但是,61152 虽然含有 6 和 2,但不是 连号,所以不属于不吉利数字之列。你的任务是,对于每次给出的一个牌照号区间 [n,m],推断出交管局今后又要实际上给多少辆新的士车上牌

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号