不可枚举组合如何不重复的随机抽取若干次
趁着程序跑的时间,来总结一下,不可枚举组合如何不重复的随机抽取若干次的实现。
排列和组合真是一个神奇的东西,一切都要从
说起
比如:我们在进行计算的时候,一个有38个特征,另一个有19个特征,我们想计算所有的匹配组合,那么就是C38|19,这个数是多大呢?别小瞧他,300亿!!!而我的需求还是要将特征旋转一次,也就是还要乘19,那就是六千亿,显然这是计算机无法做到的。
那解决的办法就是先通过comb计算,组合的结果大概有多大,然后根据给定的阈值判断,超过的话就要进行随机抽取组合,而不是枚举所有组合。
那么怎么才能在不枚举出所有组合结果的条件下,随机的从中抽取组合呢?
- 最简单的就是我每抽取一次进行重复性判断,这样做的弊端是当抽取的次数很多后,重复性判断会非常慢,这里要讲一下时间复杂度和空间复杂度了(本人转行小菜鸡)
if comb(l_max, l_min) > threshold:
matrix = random_choice(l_max, l_min, threshold)
def random_choice(l_max, l_min, choice_times):
l_end=[]
i=0
while i <= choice_times:
l = random.sample(range(l_max), l_min)
if l in l_end:
continue
l_end.append(l)
i+=1
return l_end
复杂度
时间复杂度
- 时间频度
一个算法中的语句执行次数称为语句频度或时间频度。记为T(n) - 时间复杂度
若有某个辅助函数f(n),存在一个正常数c使得fn*c>=T(n)恒成立。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
其实,个人理解就是找到算法最底层的循环的执行次数,说白了就是有几层循环,如果有三层循环那么时间复杂度就是O(n^3),有的时候时间复杂度是有条件限制的
eg1:T(n)=n^2+3n+4 与 T(n)=4n^2+2n+1 它们的频度不同,但时间复杂度相同,都为O(n^2)
常数阶O(1),对数阶O(log2n)(以2为底n的对数,下同),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n^2), 立方阶O(n^3),…, k次方阶O(n^k), 指数阶O(2^n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低
eg2:在数值A[0…n-1]中查找给定值K的算法大致如下:
(1)i=n-1;
(2)while(i>=0&&(A[i]!=k))
(3) i–;
(4)return i;
此算法中的语句(3)的频度不仅与问题规模n有关,还与输入实例中A的各元素取值及K的取值有关:
①若A中没有与K相等的元素,则语句(3)的频度f(n)=n;
②若A的最后一个元素等于K,则语句(3)的频度f(n)是常数0。
eg3:
void aFunc(int n) {
for (int i = 2; i < n; i++) {
i *= 2;
printf("%i\n", i);
}
}
假设循环次数为 t,则循环条件满足 2^t < n
可以得出,执行次数t = log(2)(n),即 T(n) = log(2)(n),可见时间复杂度为 O(log(2)(n)),即 O(log n)。
eg4:
long aFunc(int n) {
if (n <= 1) {
return 1;
} else {
return aFunc(n - 1) + aFunc(n - 2);
}
}
显然运行次数,T(0) = T(1) = 1,同时 T(n) = T(n - 1) + T(n - 2) + 1,这里的 1 是其中的加法算一次执行。
显然 T(n) = T(n - 1) + T(n - 2) 是一个斐波那契数列,通过归纳证明法可以证明,当 n >= 1 时 T(n) < (5/3)^n,同时当 n > 4 时 T(n) >= (3/2)^n。
所以该方法的时间复杂度可以表示为 O((5/3)^n),简化后为 O(2^n)。
eg5:
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
O(n) = nlog(n)
可以看出算法所需的时间并不完全由时间复杂度决定
可以看出,算法应该尽量采用nlog(n)或者n^k(k越小越好)的复杂度进行计算,而不要采用指数
常用算法的时间复杂度
此处附上python常用内置结构的时间复杂度
空间复杂度
与时间复杂度类似,空间复杂度是指算法在计算机内执行时所需存储空间的度量。记作:S(n)=O(f(n))
算法执行期间所需要的存储空间包括3个部分:
1.算法程序所占的空间
存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。
2.输入的初始数据所占的存储空间
算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地"进行的,是节省存储的算法
3.算法执行过程中所需要的额外空间
有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如将在第九章介绍的快速排序和归并排序算法就属于这种情况。
如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
eg1:
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
以上说完了时间复杂度与空间复杂度,可以看得出结论我的算法时间复杂度while语句为O(n),但是我还有一个if判断语句,而if判断语句的时间复杂度是按照其中 时间复杂度最大的路径 计算的。也就是说,当组合数很接近threshold时,找出在l_end中没重复过的最多的耗时相当于把所有的组合列举一遍,这是很不划算的。
有没有更好的方法呢?
if comb(l_max, l_min) > threshold:
matrix = random_choice(l_max, l_min, threshold)
def random_choice(l_max, l_min, choice_times):
l_end=[]
i=0
while i <= choice_times:
l = random.sample(range(l_max), l_min)
if l in l_end:
continue
l_end.append(l)
i+=1
return l_end
测试了这个的算法,以C38|19为例,迭代次数在10000次以内的速度非常快,当到达100000的时候,电脑就比较卡了。
我不知道random.sample的时间复杂度是多少?他是怎么随机取得不重复的元素呢,我推测是把每个range(l_max)中的元素产生一个hash值,然后开始抽取,如果抽取到重复的hash值则再次抽取,这样的话时间复杂度为O(k^2),抽取为O(k),判重为O(k)
以下参考了概率算法 – 从集合中选取N个不重复的元素博客,但代码都是自己code
- 方法1.借鉴上面的hash算法,随机抽取是不能避免的了,那么我们可以为每组抽取出来的组合赋hash值,然后不必再每个元素的对比是否重复,只需要对比hash值就可以了,说干就干
def random_choice(l_max, l_min, choice_times):
l_end=[]
h_end=[]
i=0
while i <= choice_times:
l = random.sample(range(l_max), l_min)
h=hash(l)
h_end.append(h)
if h in h_end:
continue
l_end.append(l)
i+=1
return l_end
TypeError: unhashable type: 'list'
啪啪打脸,list根本不实用hash索引,因为其内的匀速是可以重复的,python中
- 不可哈希:字符串str、元组tuple、对象集objects
- 可哈希:字典dict,列表list,集合set
那稍微改一下就好了,因为随机抽取的元素并没有重复,所以只需要把list转化为tuple就好了
def random_choice(l_max, l_min, choice_times):
l_end=[]
h_end=[]
i=0
while i <= choice_times:
l = random.sample(range(l_max), l_min)
t=tuple(l)
h=hash(t)
if h in h_end:
continue
h_end.append(h)
l_end.append(l)
i+=1
return l_end
在对应超过一万次的选取时确实有明显的速度提升,这对我目前是很有用的,可以通过提升迭代次数来提升我的组合上限阈值,这样得到的数据会更多。
- 方法2第二种是第一种的改进。先在1到n-k+1中随机选一个,然后把范围扩大到n-k+2,再随机选一个,如果随机选中的是之前已经选过的,那么就选择第n-k+2个元素。
比如:从1-8中选4个数字。
第一步:从1到5中随机选一个,比如选了2.
第二步:从1到6中随机选一个。如果选出的是i=1,3,4,5,6,那么就选i即可。如果选出的是2,这个时候就重复了,此时的处理方式是选择i=6。
第三步:从1到7中随机选一个数,如果跟之前的重复,那么就选那种7.
第四步:从1到8中随机选一个数,如个之前的重复,就选8。
空间复杂度:O(1)
时间复杂度:O(k^2)
这种方法确保每次都能产生随机数。避免了第一种方法的问题。
方法1和方法2都可以用hash来解决判重时的消耗。
此时方法2的复杂度为:
空间:O(n)
时间:O(k)
从时间复杂度上2没有1低,所以这里不尝试了
- 方法3经典的交换法。产生第一个随机元数,将其交换到数组的第一个位置a[0]。这样,只要在a[1]和a[n-1]中随机选泽,即可以确保不重复了。所以,再在a[1]到a[n-1]中产生一个随机数,交换到a[1]位置
空间:O(n)
时间:O(k)
import random
l=[1,2,3,4,5,4,19,57,79,10,67,15,30,34,78,23,47,54,67,7,23,58]
times=10
i=0
while i<=times:
n = l.index(random.choice(l[i:]))
l[i], l[n] = l[n], l[i]
i+=1
print(len(l[:times]))
print(l[:times])
>>>10
[57, 4, 79, 47, 34, 54, 58, 78, 23, 5]
结果倒是不错的,不重复的抽取实现了。但是有一个问题,如果原列表或集合中有重复元素呢?这个方法肯定还会重复的!那就还得用哈希,这里就不赘述了
- 方法4因为是从n个中产生k个,所以每个元素选中的概率是k/n。
所以,对于a[0],以k/n的概率选择他。如果选中了,那么对剩下的数组来说,就是在n-1个元素中选k-1个元素的问题;如果没选中,那么
对剩下的数组来说,就是在n-1个元素中选k个元素的问题。迭代即可。
时间复杂度:O(n)
空间复杂度:O(1)
这个算法不一定要遍历到最后才结束,如果已经选够了n个数,就可以结束了。
(5)先选中前k个元素,然后开始遍历剩下的元素,如第k+1个元素来的时候,以k/(1+k)的概率去替换之前k个元素中的一个。至于替换哪一个,就是均匀的选了。
对于新来的这个元素,被选中的概率是:k/(k+1) (替换)
对于之前的元素,被选中的概率是:(1-k/(k+1))+k/(k+1) * (k-1)/k = 1/(k+1) + (k-1)/(k+1)= k/(+1) (不替换+替换但没被选中替换出去)
第k+2个元素来的时候,以k/(k+2)的概率去替换之前k个元素中的一个。
时间复杂度:O(n)
空间复杂度:O(1)
这种方法还适合在总数量未知的情况下使用。
O(n)的算法未必比O(k^2)的算法好,注意n和k的区别
后两种方法的代码欢迎大神补充,这里提供一个小思路
import random
def random_pick(some_list, probabilities):
x = random.uniform(0,1)
print(x)
cumulative_probability = 0.0
for item, item_probability in zip(some_list, probabilities):
cumulative_probability += item_probability
if x < cumulative_probability:
break
return item
some_list = [1,2,3,4]
probabilities = [0.2,0.1,0.6,0.1]
print(random_pick(some_list,probabilities))