洗牌算法及 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处理只能够通过 ...
随机推荐
- Vue学习笔记:提升开发效率和体验的常用工具
Vetur 用途: 语法高亮 标签补全,模板生成 Lint检查 格式化 vs code环境配置文件 文件-->首选项-->搜索veture(找不到需要自行安装)-->在setting ...
- Scrum Meeting - 第七周【Alpha阶段】
每日任务内容: 本次会议为第七次Scrum Meeting会议 本次会议项目经理召开时间为20:00,在北区男生宿舍楼召开,召开时长约10分钟,探讨了本周选课网站编写的后续工作. 小组成员 本周任务 ...
- harbor部署之SSL
harbor部署之SSL 1 签名证书与自签名证书 签名证书:由权威颁发机构颁发给服务器或者个人用于证明自己身份的东西. 自签名证书:由服务器自己颁发给自己,用于证明自己身份的东西,非权威颁发机构发布 ...
- 使用SC命令操作(安装、开启、配置、关闭、删除)Windows下的服务
目录 一.直接使用cmd命令行操作windows服务 二.使用bat批处理-操作windows服务 一.直接使用cmd命令行操作windows服务 1.安装服务 sc create 服务名 binPa ...
- go笔记--rpc和grpc使用
目录 go笔记--rpc和grpc使用 rpc server.go client.go (sync) client.go (async) grpc protoc server.go client.go ...
- Python语法规则
Python基本语法 Python的语法相对比C,C++,Java更加简洁,比较符合人的正常思维.本篇介绍Python的基本语法,通过本篇文章你可以学到以下内容. 掌握Python的基本语法 识别Py ...
- 零基础想学习C语言,没资源、没人带、不知道从何开始?
初学编程的小伙伴经常会遇到的问题,1.没资源 2.没人带 3.不知道从何开始 ? 小编也是从新手期过来的,所以很能理解萌新的难处,现在整理一些以前自己学习买来的一些资料送给大家,希望对广大初学小伙伴有 ...
- C#基础之多线程与异步
1.基本概念 多线程与异步是两个不同概念,之所以把这两个放在一起学习,是因为这两者虽然有区别,但也有一定联系. 多线程是一个技术概念,相对于单线程而言,多线程是多个单线程同时处理逻辑.例如,假如说一个 ...
- diango入门(持续更新中)
学习注意点:理顺项目逻辑,记住重点,项目做好重点注释保留好,以后做项目了能知道这样可以实现,忘了回来查 下载 命令行 pip install django==1.11.26 -i https://py ...
- C++ std::array 基本用法
#include <iostream> #include <string> #include <array> using namespace std; // htt ...