利用Python实现产生无重复随机数字
今天在和朋友聊天时朋友提出了上面这个问题:如何利用Python产生一串无重复的随机数字,注意所产生的数字必须是
无重复
且
随机
的。
我们知道产生随机数,Python有一个模块是random,只需要import random然后调用其方法就可以了,但是如何做到无重复呢?下面是我们总结的几种可行的方法,其中最后一种方法的做法十分巧妙,建议读者思考和理解。
实现
random的sample函数
在random模块中,直接调用sample函数即可解决问题,该方法的参数如下
random.sample(population, k)
如上,该方法用于从population序列中随机抽取k个独立的元素,让我们来实践一下:
In [1]: import random
In [2]: random.sample(range(20),5)
Out[2]: [18, 3, 13, 14, 7]
In [3]: random.sample(range(20),5)
Out[3]: [14, 19, 16, 8, 15]
In [4]: random.sample(range(20),5)
Out[4]: [1, 19, 12, 4, 17]
如上结果所示,即可实现产生无重复的随机数字。
set去重
一讲到无重复,我们很容易就想到了set,因为set里的元素是不重复的,set最经常的一个应用就是利用其来进行序列的去重,如下例子:
In [5]: test_list = [1,1,2,2,2,3,4,4,4,4,5]
In [6]: set(test_list)
Out[6]: {1, 2, 3, 4, 5}
In [7]: len(set(test_list))
Out[7]: 5
利用该性质,我们既可以实现列表去重,也可以很方便地统计列表中独立元素的个数。
所以利用set去产生无重复随机数字时,我们可以利用set保存所产生的随机数字,每次产生一个随机数字,检查其是否在set中,如果不在的话再加入set,如果在的话则重新产生随机数字,看如下代码:
In [8]: number = 5
In [9]: result_set = set()
In [10]: while number > 0:
...: pre_random_num = random.randint(0,19)
...: # 检查随机数是否在结果集中
...: if pre_random_num not in result_set:
...: result_set.add(pre_random_num)
...: number -= 1
...:
In [11]: result_set
Out[11]: {0, 7, 9, 12, 16}
如上,就可以得到我们想要的number个随机数。
但是此法有一个弊端就是:所需要的随机数个数逼近随机数的可选范围时,可想而知,每次我们产生的随机数重复的概率将大大增加,加大了时间开销,下面将介绍的方法可以比较完美地解决该问题。
利用列表抽取无重复随机数字
我们可以将要抽取的随机数范围先生成一个列表,再随机生成列表下标,pop出对应下标的元素加入结果集即可,具体实现如下:
In [12]: test_list = list(range(20))
In [13]: test_list
Out[13]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [14]: result_list = []
In [15]: number = 5
In [16]: list_len = len(test_list)-1
In [17]: while number > 0:
...: random_index = random.randint(0, list_len)
...: result_list.append(test_list[random_index])
...: test_list.pop(random_index)
...: number -= 1
...: list_len -= 1
...:
In [18]: result_list
Out[18]: [8, 3, 2, 13, 11]
In [19]: test_list
Out[19]: [0, 1, 4, 5, 6, 7, 9, 10, 12, 14, 15, 16, 17, 18, 19]
如上,我们首先根据随机数的产生范围生成一个列表test_list,然后随机生成test_list的下标,并pop出对应下标的元素加入到结果集result_list中。
此处需要注意的是:每次pop出一个元素,列表的长度会减 1,所以每次循环我们需要减少随即下标的产生范围。
那么有没有一个方法,不需要pop出列表元素以及每次都要将列表长度减去 1 呢?当然是有的。
我们知道,在上面的方法中,在随机数的产生范围表test_list中,如果一个元素被加入到结果集中,该元素便不可能再次出现,我们的法是将其pop出并将下次下标产生的范围减少 1,这提示我们可以将两者结合起来。
因为每次下标产生范围会减少 1,及当前下标以后的下标是无法取到的,那么我们就可以无需pop出列表元素,而将pop的元素与最后一个下标的元素交换即可,这样由于下一次下标产生范围会减少 1,所以被置换到后面的元素是取不到的,利用这个思想我们就可以完成
去重
但是我们还没有解决下标产生范围减 1的问题,其实我们可以换一个思路:顺序遍历,然后在当前元素及之后的元素中挑选随机数,将其与当前元素做置换即可,具体实现如下:
In [20]: import random
In [21]: total = 20
In [22]: random_list = list(range(total))
In [23]: random_list
Out[23]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [24]: result = []
In [25]: number = 5
In [26]: for i in range(number):
...: random_index = random.randint(i, total-1)
...: result.append(random_list[random_index])
...: # 交换元素到前面
...: random_list[random_index], random_list[i] = random_list[i], ran
...: dom_list[random_index]
...:
In [27]: result
Out[27]: [7, 19, 10, 4, 8]
如上,我们顺序遍历random_list,然后在当前元素及之后的元素中挑选随机数,将其与当前元素做置换,重复此过程,直到产生足够的随机数即可。
列表取用元素小tips
在产生了随机数之后(假设以列表形式保存),我们如何取用呢?
通常我们的取用方法是顺序遍历过去,对于所有以列表形式保存的数据的取用方法都是如此,可以用Python的 for in 进行遍历,也可以用下标遍历,如下:
In [28]: for meb in result:
...: print(meb)
...:
7
19
10
4
8
也可以用下标:
In [32]: index = 0
In [33]: result_len = len(result)
In [34]: while index < result_len:
...: print(result[index])
...: index += 1
...:
7
19
10
4
8
如上,如果我们用 for in 进行便利的话,其元素只能是顺序遍历而不能后退。
而如果我们用下标进行遍历,我们可以对下标 index 根据我们的需要进行控制,进而可以实现输出后面的元素。
而如果需要同时取得下标即下标对应的元素(上面所讲的下标遍历同时在输出下标也可以实现),并且不需要往后的话,我们还可以使用 enumerate 方法作用于列表返回其下标及对应的元素,如下:
In [35]: for index, element in enumerate(result):
...: print(f'下标:{index} 元素:{element}')
...:
下标:0 元素:7
下标:1 元素:19
下标:2 元素:10
下标:3 元素:4
下标:4 元素:8
那么对于元素的取用,我们有没有其他写法?我们首先联想到的就是:迭代器。
迭代器也是用来保存元素的,而且其对元素的遍历只能往前,不能后退。可以使用 next() 函数进行遍历,也可以用迭代器的 __ next __() 方法取得下一个元素。
所以我们可以将列表包装成 iter 对象,然后用 next 去调用其中的元素,具体如下:
In [35]: result
Out[35]: [7, 19, 10, 4, 8]
In [36]: result_iter = iter(result)
In [37]: next(result_iter)
Out[37]: 7
In [38]: next(result_iter)
Out[38]: 19
也可以用 __ next __() 方法取得下一个元素:
In [39]: result_iter.__next__()
Out[39]: 10
In [40]: result_iter.__next__()
Out[40]: 4
In [41]: result_iter.__next__()
Out[41]: 8
但应用此方法时我们需要特别注意的是:当迭代器取到最后一个元素后,即迭代器已无 next 元素时间,再次取用下一个元素会抛出 StopIteration 异常,如下:
In [42]: result_iter.__next__()
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-44-f61ceafc8386> in <module>
----> 1 result_iter.__next__()
StopIteration:
如上即是列表取用元素的常用方法,而最后一个将列表包装成迭代器看似没有多大用处,但是在某些应用场合会有出其不意的效果。
总结
以上就是本篇的全部内容,其实利用Python实现产生无重复随机数字的方法除了上面所讲,一定还有其他的方法,读者可以自己去思考。
在上面所讲的实现方法中,我们要重点去理解
利用列表抽取无重复随机数字
的方法,其将抽取过的元素与当前元素进行置换的思想是十分巧妙的。
这提示我们合理分配空间,以达到空间的最大利用率,从而节省内存。