获取集合的所有子集
前言
今天看到一个有意思的问题,要获取一个集合的所有子集,使用PHP语言表示的话,就是要找到一个数组的所有子集数组,如[1,2,3]
,结果应该是
[1]
[2]
[3]
[1,2]
[1,3]
[2,3]
[1,2,3]
PHP代码
function powerSet($in,$minLength = 1) {
$count = count($in);
$members = pow(2,$count);
$return = [];
for ($i = 1; $i < $members; $i++) {
$b = sprintf("%0".$count."b",$i);
$out = [];
for ($j = 0; $j < $count; $j++) {
if ($b{$j} == '1') $out[] = $in[$j];
}
if (count($out) >= $minLength) {
$return[] = $out;
}
}
return $return;
}
上面代码的巧妙之处在于通过二进制的方式来解决问题。拿[1,2,3]
来举例,一共有3个数字,构造子集就是从数组中拿出若干数字组成数组。3个数字可以看成二进制的3位,一共有如下情况
001 --- 1
010 --- 2
011 --- 3
100 --- 4
101 --- 5
110 --- 6
111 --- 7
可以看出我们只要把二进制表示方式中,找出所有「1」所在位置在数组中对应的数字找出来就是所有子集了。比如[1,2,3]
对应关系如下
001 --- [3]
010 --- [2]
011 --- [2,3]
100 --- [1]
101 --- [1,3]
110 --- [1,2]
111 --- [1,2,3]
这样就找到了所有子集了。sprintf("%0".$count."b",$i);
就是把1 - \(2^n\)用二进制方式表示出来,$b{$j} == '1'
就是找出1所在的数组索引下标。
代码优化
上面的代码有可以优化的地方,如下
function powerSetUpgrade($in,$minLength = 1) {
$count = count($in);
$members = pow(2,$count);
$return = [];
for ($i = 1; $i < $members; $i++) {
$out = [];
for ($j = 0; $j < $count; $j++) {
if ($i>>$j&1) $out[] = $in[$j];
}
if (count($out) >= $minLength) {
$return[] = $out;
}
}
return $return;
}
与上面代码的区别有2点
- 去掉了
$b = sprintf("%0".$count."b",$i);
- 把取1的判断条件由
$b{$j} == '1'
变成$i>>$j&1
通过位运算符>>
和&
来找出「1」所在位置下标,这种方法比上面的性能要好,但是不是很好理解。还拿[1,2,3]
举例,对应关系如下
001 --- [1]
010 --- [2]
011 --- [1,2]
100 --- [3]
101 --- [1,3]
110 --- [2,3]
111 --- [1,2,3]
这种方式从对应的关系来看不是很直观,和正常的顺序是相反的,比如上面的方法001
找到的是第3个数字「3」,很直观,但是通过位运算符001
找到的却是第1个数字「1」,不过最终结果是一样的。