算法中经常需要做笛卡尔积。笛卡尔积结果的数据量会随着参与运算的list的数量成类似于指数的变化。
比如,100个只有两个元素的list做笛卡尔积,其结果的数据量会有多大呢?
答案是2的100次方个元素,这个数字非常大。想想那个用麦粒放棋盘的著名寓言吧。
因此,在实际应用笛卡尔积时要特别小心,否则它有可能是灾难性的。
算法的实现通常用python来实现。在Python中计算笛卡尔积的语法很简单。
以下是一个示例代码段:
import itertools
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
list3 = ['X', 'Y', 'Z']
products = list(itertools.product(list1, list2, list3))
for i in products:
do some thing
上面的代码中,products 的值如下:
[(1, 'a', 'X'), (1, 'a', 'Y'), (1, 'a', 'Z'),
(1, 'b', 'X'), (1, 'b', 'Y'), (1, 'b', 'Z'),
(1, 'c', 'X'), (1, 'c', 'Y'), (1, 'c', 'Z'),
(2, 'a', 'X'), (2, 'a', 'Y'), (2, 'a', 'Z'),
(2, 'b', 'X'), (2, 'b', 'Y'), (2, 'b', 'Z'),
(2, 'c', 'X'), (2, 'c', 'Y'), (2, 'c', 'Z'),
(3, 'a', 'X'), (3, 'a', 'Y'), (3, 'a', 'Z'),
(3, 'b', 'X'), (3, 'b', 'Y'), (3, 'b', 'Z'),
(3, 'c', 'X'), (3, 'c', 'Y'), (3, 'c', 'Z')]
可以看出来,即使就是这样简简单单的3个list进行笛卡尔积,得到的结果已经是一个挺长的结果了。
所以在写程序的时候,下面三点很重要。可能只需做一点改变,就会使程序运行的快上几,几十倍。
第一点是,在笛卡尔积之前,就对原始数据进行筛选,不要让一些一定不需要的数据参与运算。这将大大减少后续的运算量,也会减少内存的减少。
第二点是,在对笛卡尔积进行运算得到结果以后,要找到合适的逻辑,避免遍历结果中的每一个元素,否则你的程序可能会跑上几个月也并不夸张。
第三点,是要着重介绍的。
上面的示例程序中,在进行笛卡尔积的同时,将数据转化成了list数据,赋值给了products。
这在很多情况下是一个好的编程习惯,很多算法工程师也是这样应用的,这会使程序看起来更加简洁。
但是,这里可能会使程序遇到大麻烦!
这是因为如果参与运算的入参数据量比较大(其实看起来也并不是很大,就像上面说的),你得到的list将可能是一个天文数字,即使是服务器几百个G的内存,也将瞬间被占满,程序将卡死在这里。
其实解决这个问题很简单。
itertools.product
是一个生成器表达式。它将给定的可迭代对象的笛卡尔积作为元组逐个生成,而不是一次性生成并存储在内存中。因此在运行itertools.product(list1, list2, list3)时并没有把最终结果生成出来存入内存。单独运行下面这行代码其实很快,并且内存占用几乎可以忽略。
itertools.product(list1, list2, list3)
但是前面加了个list的强转,就像示例程序那样,就相当于要求这个生成器表达式将所有结果运算出来,存入到列表中,放入计算机内存。所以这样的做法很危险。
list(itertools.product(list1, list2, list3))
现实情况是,当面对如此巨大的数据量时,我们的算法往往并不是真的需要所有这些数据来进行接下来的运算,或者作为最终结果输出。接下来往往面对着数据的进一步处理,可能是一个循环,因此在处理时,通过对这个生成器表达式进行处理就可以了。这样,在每次用到笛卡尔积结果中的一个元素时,才会运算得到这个结果,内存的问题得到解决。但应该注意到,这也将是一个超级大的计算量。所以这种情况下,应该找到合适的逻辑,能够尽早的跳出循环。
同样的道理,在其他语言中进行笛卡尔积时,也要注意,在数据量大的时候,不能一下子将结果全部生成出来存到内存中。