洗牌算法及 random 中 shuffle 方法和 sample 方法浅析
对于算法书买了一本又一本却没一本读完超过 10%,Leetcode 刷题从来没坚持超过 3 天的我来说,算法能力真的是渣渣。但是,今天决定写一篇跟算法有关的文章。起因是读了吴师兄的文章《扫雷与算法:如何随机化的布雷(二)之洗牌算法》。因为扫雷这个游戏我是写过的,具体见:《Python:游戏:扫雷》。
游戏开始的时候需要随机布雷。扫雷的高级是 16 × 30 的网格,一共有 99 个雷。如果从 0 开始给所有网格做标记,那么布雷的问题就成了从 480 个数中随机选取 99 个数。
第一反应自然是记录已选项:
import random
mines = set()
for i in range(99):
j = random.randint(0, 480)
while j in mines:
j = random.randint(0, 480)
mines.add(j)
print(mines)
不过这算法看着似乎有点 low 啊。
其实从 480 个数中随机抽取 99 个数,那么只要将这 480 个数打乱,取前 99 个数就好了。这就引出了:高纳德置乱算法(洗牌算法)。
这个算法很牛逼却很好理解,通俗的解释就是:将最后一个数和前面任意 n-1 个数中的一个数进行交换,然后倒数第二个数和前面任意 n-2 个数中的一个数进行交换……以此类推。
这个原理很好理解,通俗得不能再通俗,稍微想一下就会明白,确实如此。
洗牌算法的 Python 实现如下:
import random
lst = list(range(10))
for i in reversed(range(len(lst))):
j = random.randint(0, i)
lst[i], lst[j] = lst[j], lst[i]
print(lst)
看了吴师兄的文章,我立马去翻了我的扫雷代码,我觉得,我一定是用的那个很 “low” 的算法。翻出代码一看,我用的是 Python 提供了随机取样算法:random.sample,感叹 python 的强大,这都有。然后我就想到了,随机打乱一个序列,random.shuffle 不就是干这事的吗?那么 random.shuffle 会是用的洗牌算法吗?
翻看 random.shuffle 的源码,发现正是洗牌算法。
def shuffle(self, x, random=None):
if random is None:
randbelow = self._randbelow
for i in reversed(range(1, len(x))):
j = randbelow(i + 1)
x[i], x[j] = x[j], x[i]
else:
_int = int
for i in reversed(range(1, len(x))):
j = _int(random() * (i + 1))
x[i], x[j] = x[j], x[i]
一切都是如此的自然而美好,然后我又去瞄了一眼 random.sample 的源码,然后就一头雾水了。我截了部分源码:
n = len(population)
result = [None] * k
setsize = 21 # size of a small set minus size of an empty list
if k > 5:
setsize += 4 ** _ceil(_log(k * 3, 4)) # table size for big sets
if n <= setsize:
# An n-length list is smaller than a k-length set
pool = list(population)
for i in range(k): # invariant: non-selected at [0,n-i)
j = randbelow(n-i)
result[i] = pool[j]
pool[j] = pool[n-i-1] # move non-selected item into vacancy
else:
selected = set()
selected_add = selected.add
for i in range(k):
j = randbelow(n)
while j in selected:
j = randbelow(n)
selected_add(j)
result[i] = population[j]
return result
setsize 变量虽然看得一头雾水,但是下面的 if 和 else 部分还是能看懂的。if 里是洗牌算法,而 else 里是那个却是我看着很 “low” 记录已选项算法。
这是怎么回事?为了弄明白其中的道理,我去搜了很多文章查看,最有价值的是下面这篇:https://blog.csdn.net/harry_128/article/details/81011739
随机取样有两种实现方式,一是随机抽取且不放回,就是洗牌算法;二是随机抽取且放回,就是我想到的记录已选项算法。random.sample 根据条件选择其中之一执行。那么就是说,洗牌算法和记录已选项算法之间是各有优劣的。这让我有点惊讶,不明摆着洗牌算法更优吗?
首先,这个抽样算法肯定不能改变原序列的顺序,而洗牌算法是会改变序列顺序的,所以只能使用序列的副本,代码中也是这么做的 pool = list(population) 创建副本,而记录已选项算法是不会改变原序列顺序的,所以无需创建副本。创建副本也需要消耗时间和空间,算法自然也是要把这考虑进去的。当需要取的样本数量 K 相较于样本总体数量 N 较小时,随机取到重复值的概率也就相对较小。
那 sample 是依据什么来判断应该用哪个算法的呢?源码中的判断基于 setsize 变量,其中还有一段让人看不懂的公式。其实这是在计算 set 所需的内存开销,算法的实现主要考虑的是额外使用的内存,如果 list 拷贝原序列内存占用少,那么用洗牌算法;如果 set 占用内存少,那么使用记录已选项算法。
What?居然是根据额外占用内存多少来判断?这有点太不可思议了。Why?
我们来看一下算法的时间复杂度。对于算法很渣渣的小伙伴(例如我)来说,计算算法的时间复杂度也是件挺困难的事,为了简单起见,我用一种简单的方式来说明。
先说洗牌算法,时间复杂度是 O(K),这个比较好理解。那么,对于记录已选项算法,时间复杂度是 O(NlogN)。这个别问我是怎么算出来的,我没算,抄的。有兴趣的小伙伴可以自行去计算一下。
我们来想一个简单的,对于记录已选项算法,如果每次选取的值恰好都没有重复,那么时间复杂度是多少呢?很显然是 O(K)。那么当 K 远小于 N 的时候,我们可以认为时间复杂度就是 O(K)。
而 sample 算法的思想就是,当 K 较 N 相对较小时,两种算法的时间复杂度都是 O(K),则选用占用内存较小的;当 K 较 N 相对较接近时,记录已选项算法的时间复杂度就会高于 O(K),这时就选用洗牌算法。
只得感叹算法真的博大精深。
洗牌算法及 random 中 shuffle 方法和 sample 方法浅析的更多相关文章
- Hibernate中evict方法和clear方法说明
Hibernate中evict方法和clear方法说明 先创建一个对象,然后调用session.save方法,然后调用evict方法把该对象清除出缓存,最后提交事务.结果报错: Exception i ...
- ThinkPHP 中M方法和D方法详解----转载
转载的地址,http://blog.163.com/litianyichuanqi@126/blog/static/115979441201223043452383/ 自己学到这里的时候,不能清除的分 ...
- ThinkPHP 中M方法和D方法的具体区别(转)
M方法和D方法的区别 ThinkPHP 中M方法和D方法都用于实例化一个模型类,M方法 用于高效实例化一个基础模型类,而 D方法 用于实例化一个用户定义模型类. 使用M方法 如果是如下情况,请考虑使用 ...
- 线程中sleep方法和wait方法有什么区别?(转)
本文转自https://www.cnblogs.com/linkstar/p/6043846.html 线程中sleep方法和wait方法有什么区别? 如果你没有接触过java的多线程,那么多对于 ...
- ThinkPHP 中M方法和D方法的具体区别
M方法和D方法的区别 ThinkPHP 中M方法和D方法都用于实例化一个模型类,M方法 用于高效实例化一个基础模型类,而 D方法 用于实例化一个用户定义模型类. 使用M方法 如果是如下情况,请考虑使用 ...
- jquery中prop()方法和attr()方法
接着上一篇笔记的疑惑,找了下prop()方法和attr()方法的区别. 原来query1.6中新加了一个方法prop(),一直没用过它,官方解释只有一句话:获取在匹配的元素集中的第一个元素的属性值. ...
- js进阶 12-13 jquery中one方法和trigger方法如何使用
js进阶 12-13 jquery中one方法和trigger方法如何使用 一.总结 一句话总结: 1.one()方法和on()方法的区别是什么? 除了one()只执行一次,其它和on()一模一样,包 ...
- java中equals方法和hashcode方法的区别和联系,以及为什么要重写这两个方法,不重写会怎样
一.在Object类中的定义为:public native int hashCode();是一个本地方法,返回的对象的地址值.但是,同样的思路,在String等封装类中对此方法进行了重写.方法调用得到 ...
- ExtJS中listener方法和handler方法的区别
listener方法和handler方法的区别在文档中的说明的太玄乎了,看不懂 listeners监听能够对一个click Event事件添加任意多个的事件响应处理函数 而handler处理只能够通过 ...
随机推荐
- 《大型网站系统与Java中间件》读书笔记(上)
前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 这本书买了一段时间了,之前在杭州没带过去,现在读完第 ...
- 画线缩放、瞳距缩放、Line延长到指定长度,内附效果,源码供应,解压就跑
前言 公司项目需要做个画线缩放,我司称之为瞳距缩放,简而言之就是:2张图,从第一张图画一条线,再从第二个图画一条线,第二条线以第一条为基准,延长到一致的长度,并同比缩放图片:文字太枯燥,请先实例图 例 ...
- tyvj 1387 迷你火车头
dp百题进度条[1/100] 一列火车有一个火车头拖着一长串的车厢,每个车厢有若干个乘客. 一旦火车头出了故障,所有的车厢就只能停在铁轨上了,因此铁路局给每列火车配备了三个迷你火车头,每个迷你火车头可 ...
- JS--- part6课程介绍 & part5复习
part6 课程介绍 scroll系列:-----重点,每个属性是什么意思 封装scroll系列的相关的属性,固定导航栏案例---事件浏览器的滚动条事件--能够写出来 封装动画函数---缓动动画--- ...
- Xposed的新打开方式--Xpatch工作流程分析
1. Xpatch概述 Xpatch是一款利用重打包的方式,使得被处理的Apk启动时自动加载Xposed模块,来实现应用内Hook的工具. 项目地址:https://github.com/WindyS ...
- 从无到有通过IDEA搭建SpringBoot项目
本人第一次写博客希望记录当下,努力成为IT界中的清流,写的不好多多包涵. SpringBoot是由Pivotal团队在2013年开始研发.2014年4月发布第一个版本的全新开源的轻量级框架.它基于Sp ...
- 破解另一家网站的反爬机制 & HMAC 算法
零.写在前面 本文涉及的反爬技术,仅供个人技术学习,禁止并做到: 干扰被访问网站的正常运行 抓取受到法律保护的特定类型的数据或信息 搜集到的数据禁止传播.交给第三方使用.或者牟利 如有可能,在爬到数据 ...
- weblogic启动服务器Authentication denied: Boot identity not valid
新分配的测试服务器,已经安装好了weblogic,使用命令nohub ./startWeblogic.sh启动weblogic没有问题 登录控制台,点击环境-服务器-新建,一步步完成后,点击部署.选择 ...
- 文件系统之parted 分区
parted分区命令 1.分区表区别 我们 Linux 系统中有两种常见的分区表 MBR 分区表(主引导记录分区表)和 GPT 分区表(GUID 分 区表) MBR 分区表:支持的最大分区是 2TB( ...
- Prism_Event Aggregator(4)
Event Aggregator Prism库提供了一种事件机制,可以在应用程序中松散耦合的组件之间进行通信.该机制基于事件聚合器服务,允许发布者和订阅者通过事件进行通信,但仍然没有彼此直接引用. 在 ...