洗牌算法及 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处理只能够通过 ...
随机推荐
- UWP 利用Windows.UI.Composition实现简单的放大🔍效果
看一下效果先 我这里实现了鼠标进入和退出的效果,当然也可以添加其他的事件,比如获得焦点和失去焦点的. 先随便写一个xaml布局,一个Grid,里面一张图片. <Grid x:Name=" ...
- python多线程编程—同步原语入门(锁Lock、信号量(Bounded)Semaphore)
摘录python核心编程 一般的,多线程代码中,总有一些特定的函数或者代码块不希望(或不应该)被多个线程同时执行(比如两个线程运行的顺序发生变化,就可能造成代码的执行轨迹或者行为不相同,或者产生不一致 ...
- Oracle 创建用户,赋予指定表名/视图只读权限
步骤指南 创建用户 格式:; 语法:create user 用户名 identified by 密码; 注:密码不行的话,前后加(单引号):' create user TEST identified ...
- 分布式事物 - 基于RPC调用 - TCC模式
前提 前端业务(主服务)可以以同步或异步调用TCC框架,或者TCC框架本身就是同步异步兼备的. 假定TCC框架拥有断电后的自动恢复能力.同时,在下游业务出现无限失败的情况下,也会进行无限的重试,以达到 ...
- CSS 解决z-index上层元素遮挡下层元素点击事件问题
解决z-index上层元素遮挡下层元素点击事件问题 by:授客 QQ:1033553122 开发环境 Win 10 element-ui "2.8.2" Vue 2.9.6 需求 ...
- Hack the 21LTR: Scene 1 VM (Boot to Root)
靶机下载链接: https://www.vulnhub.com/entry/21ltr-scene-1,3/ 主机扫描: ╰─ nmap -p1-65535 -sV -sC -A 192.168.2 ...
- 群晖NAS再再折腾
问题 最近电信把我的公网地址收回去了,之前做好的网络端口映射失效了,在公司已经不能愉快地访问家里的网络.原先网络结构示意图如下: (直接访问方案网络结构图) 只需要对电信光猫(也是个路由器)和家用 ...
- Dubbo学习系列之十四(Seata分布式事务方案AT模式)
一直说写有关最新技术的文章,但前面似乎都有点偏了,只能说算主流技术,今天这个主题,我觉得应该名副其实.分布式微服务的深水区并不是单个微服务的设计,而是服务间的数据一致性问题!解决了这个问题,才算是把分 ...
- python3 print() 函数带颜色输出 示例
1.1 实现过程: 终端的字符颜色是用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无关. 转义序列是以ESC开头,即用\033来完成(ESC的ASCII码用十进制表示是27,用八进制表示就 ...
- jieba的使用
1. 分词 分词是自然语言处理中最基础的一个步骤.而jieba分词是中文分词的一个比较好的工具.下面看看可以怎么用jieba进行分词. import jieba # 全模式 seg_list1 = j ...