问题
设计一个公平的洗牌算法。
什么叫公平?假如有 n 张牌,那么这 n 张牌共有 n!个排列方式(很简单的排列组合思想),所以洗牌中“公平”是指,系统能随机的输出这 n!个结果中的任意一个。暴力解法思想很简单,但时间复杂度很高O(n!),下面学习一下 Knuth Shuffle 洗牌算法,思想也很简单。
Knuth Shuffle 算法从高位到低位进行,依次从前面的元素中随机(用到了random模块)取出一个元素与当前元素互换。
我有一个疑问:为什么是互换?而不是每次直接从剩下的元素中随即取出一个放在高位。针对我的这个疑问,我的想法是互换不需要开辟新的空间,直接在原数组基础上进行操作,降低了空间复杂度。
实习面试的时候有问到类似的问题,但当时我还不懂什么是洗牌算法,所以按自己的想法回答了,现在想想当时的时间复杂度好像回答的不太对。。。(链接:【20190423】【笔经、面经集】2019年暑期实习找工作经历,分享给大家,也给自己长个记性~(持续更新))
(参考:神一样的随机算法)
(参考:洗牌算法——python实现)
(参考:python 多线程实现洗牌算法 (二))
(参考: 三种洗牌算法shuffle)
(参考:浅谈洗牌算法(面试题))
思路及解答
# Knuth Shuffle 洗牌算法
import random
nums = list(range(0, 11)) # 产生 [0, 10] 数组
def swap(a, b):
a, b = b, a
return a, b
for i in range(len(nums)-1, -1, -1): # 从后往前,因为[0, i]的随机数比[i, ...]的随机数更好获得
tmp_index = random.randint(0, i) % (i + 1) % 生成 [0,i] 之间的数,就对 (i+1) 取模
nums[i], nums[tmp_index] = swap(nums[i], nums[tmp_index])
print(nums)
知识点
1. swap 函数(参考: Python为什么不需要swap(a, b))
第一种形式:只是返回了颠倒了的输入值,并没有在数组内部交换两个数组元素。
但如果给这两个返回值名称,那么将改变数组元素。
第二种形式:直接用 Python 语句实现内部交换。
第三种形式:
下面的错误在于没有给函数返回值,因此没有修改 a, b 的值。
2. random 模块(参考:Python中的random模块用于生成随机数。)
(1) random.random():用于生成 [0, 1.0) 的随机浮点数。
(2) random.randint(a, b):用于生成 [a, b] 的整数。
(3) random.shuffle(List):用于将列表 List 的元素随机打乱输出。
3. O(n!) 时间复杂度
O(n!) 的时间复杂度比指数级的复杂度还高!
4. 【TypeError: 'range' object does not support item assignment”】
range() 函数返回的是 "range object",而不是想要的 list 值,因此要用 list(range(n))。
(参考:17个新手常见Python运行时错误)
5. 生成 [0, i] 之间的数,就对 (i+1) 取模。