如何将数组拆分为所有可能的组合

3
我该如何循环遍历一个数组,将其分成两个数组,并为每种可能的组合运行一个函数?顺序无关紧要。
// Original array
$array = array('a','b','c','d','e');

// Result 1
array('a');
array('b','c','d','e');

// Result 2
array('a', 'b');
array('c','d','e');

// Result 3
array('a', 'c');
array('b','d','e');

And so on...


那很简单,你只想要输出还是数组? - ArtisticPhoenix
@ArtisticPhoenix 请看一下我对你的回答的评论。 - Esser
你目前尝试了什么? - Pedro Lobito
1
你认为 [a,c],[b,d,e][c,a],[e,b,d] 是相同的吗? - Nick
你认为 [a,b],[c,d,e][c,d,e],[a,b] 是相同的吗? - Ultimater
显示剩余2条评论
2个回答

2
这是我能做到的最好翻译:

这是我能做到的最好翻译:

class Combos
{

    /**
     * getPossible then getDivide
     * 
     * @param array $input
     * @return array
     */
    public function getPossibleAndDivided( array $input )
    {
        return $this->getMultiShiftAndDivided( $this->getPossible( $input ) );
    }

    /**
     * return all possible combinations of input
     * 
     * @param array $input
     * @return array
     */
    public function getPossible( array $inputs )
    {
        $result = [];

        if ( count( $inputs ) <= 1 ) {
            $result = $inputs;
        } else {
            $result = array();
            foreach($inputs as $input){

                //make it an array
                $first = [ $input ];  

                //get all inputs not in first
                $remaining = array_diff( $inputs, $first );

                //send the remaining stuff but to ourself
                $combos = $this->getPossible( $remaining );//recursive

                //iterate over the above results (from ourself)
                foreach( $combos as $combo ) {
                    $last = $combo;
                    //convert to array if it's not
                    if( !is_array( $last ) ) $last = [ $last ];
                    //merge them
                    $result[] = array_merge( $first, $last );
                }
            }
        }
        return $result;
    }

    /**
     * shift and divide a multi level array
     *
     * @param array $array
     * @return array
     */
    public function getMultiShiftAndDivided( array $mArray )
    {
        $divided = [];
        foreach ( $mArray as $array ) {
            $divided = array_merge($divided, $this->getShiftAndDivided( $array ));
        }
        return $divided;
    }

    /**
     * shift and divide a single level array
     * 
     * @param array $array
     * @return array
     */
    public function getShiftAndDivided( array $array )
    {        
        $divided = [];
        $array1 = [];
        while( count( $array ) ){
            $array1[] = array_shift( $array );
            $divided[] = [ $array, $array1 ];
        }

        return $divided;
    }
}

如何工作

总体来说,我不想深入讨论所有的细节。这基本上是一个2步骤的过程,或者至少更容易以这种方式找到答案。我将其构建为一个类,以保持一切整洁。它还允许更好的单元测试和可重用性。

这需要2个操作,或者至少对我来说分成2个操作比较容易。它们在此方法中合并。

public function getPossibleAndDivided( array $input )
{
   return $this->getMultiShiftAndDivided( $this->getPossible( $input ) );
}

我将其制作为一个类的主要原因是为了保持所有内容的整洁性和封装性。我会把这称为一个包装函数。

步骤一

$Combos->getPossible(array $input)

  1. 如果只剩下一个项目,则返回$inputs
    • 这就是它可以拥有的所有组合(毕竟只有一个元素)。
  2. 否则,使用foreach遍历$inputs中的$input
    1. $input作为名为$first的数组进行包装
      • 这是来自$inputs的单个元素
    2. 以非破坏性方式获取$inputs中的剩余元素作为$remaining
      • 使用array_diff,我们需要将两个元素作为数组(见上方)
    3. 递归调用$this->getPossible($remaining)(见#1),并将其返回为$combos
    4. $combo为循环变量遍历$combos
      1. 如果$combo不是数组,则将其分配给$last并转换为数组
        • 有时combo是具有多个元素的数组
        • 有时是单个元素。 这取决于递归调用
      2. 我们将$first$last的合并添加到结果集中
        • 我们需要将两者作为数组,以便我们可以合并而不会引起嵌套
  3. 结束 返回$result中的任何内容。

这基本上旋转了数组的所有组合,并将它们作为类似以下数组的形式返回:

Array
(
    [0] => Array
        (
            [0] => a
            [1] => b
            [2] => c
            [3] => d
            [4] => e
        )

    [1] => Array
        (
            [0] => a
            [1] => b
            [2] => c
            [3] => e
            [4] => d
        )

    [2] => Array
        (
            [0] => a
            [1] => b
            [2] => d
            [3] => c
            [4] => e
        )
 ...
    [117] => Array
        (
            [0] => e
            [1] => d
            [2] => b
            [3] => c
            [4] => a
        )

    [118] => Array
        (
            [0] => e
            [1] => d
            [2] => c
            [3] => a
            [4] => b
        )

    [119] => Array
        (
            [0] => e
            [1] => d
            [2] => c
            [3] => b
            [4] => a
        )

)

是的,它返回119个结果,但我不会全部包含。

第二步

不要忘记上面的输出是一个多维数组(这很重要)。

$Combos->getMultiShiftAndDivided(array $mArray)

这个方法旨在与多维数组一起使用(因此它的名称)。我们从“步骤1”中获得这个。它基本上是$Combos->getShiftAndDivided($array)的一个封装。

  1. 针对$mArray进行迭代,作为$array
  2. 它调用$this->getShiftAndDivided($array),并与$divided合并。
    • 没有必要存储结果,所以我没有浪费一个变量。

输出示例:

$input = array(array('a','b','c','d','e'));
print_r($combos->getMultiShiftAndDivided($input));

Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => b
                    [1] => c
                    [2] => d
                    [3] => e
                )

            [1] => Array
                (
                    [0] => a
                )

        )
    ....
    [4] => Array
        (
            [0] => Array
                (
                )

            [1] => Array
                (
                    [0] => a
                    [1] => b
                    [2] => c
                    [3] => d
                    [4] => e
                )

        )

)

$Combos->getShiftAndDivided(array $array)

这个方法旨在用于单层数组。

  1. 只要 $array 的计数大于0,就会循环,使用 while 循环
  2. $array1$array 中添加第一个元素,并将该元素从 $array 中删除(破坏性地)
  3. 我们在结果 $divided 中存储 $array$array1
    • 这记录了它们当前的“状态”
  4. $array 中没有更多项时,我们返回结果

输出示例:

$input = array('a','b','c','d','e');
print_r($combos->getShiftAndDivided($input));

Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => b
                    [1] => c
                    [2] => d
                    [3] => e
                )

            [1] => Array
                (
                    [0] => a
                )

        )
....
[4] => Array
    (
        [0] => Array
            (
            )

        [1] => Array
            (
                [0] => a
                [1] => b
                [2] => c
                [3] => d
                [4] => e
            )

    )

)

基本上,这将单个数组的元素移动到两个结果数组中,并记录每次移动的状态。我将其分成了两个函数,以便更容易测试和重复使用。
另一个问题是很难检查多维数组。我知道如何做,但我觉得不太好看,有更好的方法。我这么说是因为在所谓的getMultiShiftAndDivided中可以使用一级数组,但它不会给你预期的结果。可能会出现这样的错误:
//I say probably, but I actually test it ... lol
Warning:  array_shift() expects parameter 1 to be array

这可能会让人感到困惑,有人可能认为代码有问题。因此,在第二个方法调用时设置类型为getShiftAndDivided(array $array)。当包装方法尝试用字符串调用它时,它会崩溃,但是更好的方式是:

Catchable fatal error:  Argument 1 passed to Combos::getShiftAndDivided() must be of the type array, string given

希望我的表述让您明白,这是在这种情况下我总是尝试做的事情。长远来看,这会使生活更加方便。两个函数都以相同的格式返回数据,这很方便(不客气)。

概述

因此,它的作用是找到我们输入的所有组合,然后将每个组合分解成移位和分裂的数组。因此,可以推断出我们将所有可能的组合划分为两个数组。因为这几乎就是我所说的。

现在我并不100%确定它是否如此操作,如果您愿意,可以检查一下,它最终会返回约599个元素。祝你好运,我建议只测试$combos->getPossible($input)的结果。如果它有应有的所有组合,那么就具备了必要的功能。我不确定它是否会返回重复项,我认为没有被指定。但我没有真正检查过。

您可以像这样调用主方法:

$input = array('a','b','c','d','e');  
print_r((new Combos)->getPossibleAndDivided($input));

点击测试!

附言:我把breaks拼写成brakes,但我可以编写这样的代码,想想看...


点赞这个详细的回答。它返回那么多是因为它关心排序顺序。我不关心排序顺序。所以现在我只需要过滤掉重复项。 - Esser

2
这是我的看法:
<?php
$ar = ['a','b','c','d','e'];

function permuteThrough($ar, $callback, $allowMirroredResults = true, $mode = 'entry', $desiredLeftCount = null, $left = [], $right = [])
{
    switch($mode)
    {
        case 'entry':
            // Logic:
            // Determine how many elements we're gonna put into left array
            $len = $allowMirroredResults ? count($ar) : floor(count($ar)/2);

            for($i=0; $i <= $len; $i++)
            {
                    call_user_func(__FUNCTION__, $ar, $callback, $allowMirroredResults, 'permute',$i);
            }
            break;
        case 'permute':
            // We have nothing left to sort, let's tell our callback
            if( count($ar) === 0 )
            {
                $callback($left,$right);
                break;
            }

            if( count($left) < $desiredLeftCount )
            {
                // Note: PHP assigns arrays as clones (unlike objects)
                $ar1 = $ar;
                $left1 = $left;
                $left1[] = current(array_splice($ar1,0,1));
                call_user_func(__FUNCTION__, $ar1, $callback, $allowMirroredResults, 'permute', $desiredLeftCount, $left1, $right);
            }

            // This check is needed so we don't generate permutations which don't fulfill the desired left count
            $originalLength = count($ar) + count($left)+count($right);
            if( count($right) < $originalLength - $desiredLeftCount )
            {
                $ar2 = $ar;
                $right1 = $right;
                $right1[] = current(array_splice($ar2,0,1));
                call_user_func(__FUNCTION__, $ar2, $callback, $allowMirroredResults, 'permute', $desiredLeftCount, $left, $right1);
            }
            break;
    }
}

function printArrays($a,$b)
{
    echo '['.implode(',',$a).'],['.implode(',',$b)."]\n";
}

permuteThrough($ar, 'printArrays', true); // allows mirrored results
/*
[],[a,b,c,d,e]
[a],[b,c,d,e]
[b],[a,c,d,e]
[c],[a,b,d,e]
[d],[a,b,c,e]
[e],[a,b,c,d]
[a,b],[c,d,e]
[a,c],[b,d,e]
[a,d],[b,c,e]
[a,e],[b,c,d]
[b,c],[a,d,e]
[b,d],[a,c,e]
[b,e],[a,c,d]
[c,d],[a,b,e]
[c,e],[a,b,d]
[d,e],[a,b,c]
[a,b,c],[d,e]
[a,b,d],[c,e]
[a,b,e],[c,d]
[a,c,d],[b,e]
[a,c,e],[b,d]
[a,d,e],[b,c]
[b,c,d],[a,e]
[b,c,e],[a,d]
[b,d,e],[a,c]
[c,d,e],[a,b]
[a,b,c,d],[e]
[a,b,c,e],[d]
[a,b,d,e],[c]
[a,c,d,e],[b]
[b,c,d,e],[a]
[a,b,c,d,e],[]

*/
echo "==============\n"; // output separator
permuteThrough($ar, 'printArrays', false); // doesn't allow mirrored results
/*
[],[a,b,c,d,e]
[a],[b,c,d,e]
[b],[a,c,d,e]
[c],[a,b,d,e]
[d],[a,b,c,e]
[e],[a,b,c,d]
[a,b],[c,d,e]
[a,c],[b,d,e]
[a,d],[b,c,e]
[a,e],[b,c,d]
[b,c],[a,d,e]
[b,d],[a,c,e]
[b,e],[a,c,d]
[c,d],[a,b,e]
[c,e],[a,b,d]
[d,e],[a,b,c]
*/

我的permuteThrough函数接受三个参数。数组,回调函数和一个可选的布尔值,指示是否允许镜像结果。逻辑非常简单:首先决定要放入左侧数组中的元素数量。然后像这样递归调用函数:
我们的工作数组剩下要排序的元素。将一个元素移出并放入左侧数组中。结果被发送到另一层递归。
将一个元素移出并放入右侧数组中。结果被发送到另一层递归。
如果没有元素可以移出,则使用左侧和右侧数组调用回调函数。
最后,请确保我们不会超过由开始时的for循环确定的所需左侧数组元素大小,并确保右侧大小不会变得太大,以使所需的左侧大小无法满足。通常,这需要两个单独的函数。一个用于决定多少元素应该放入左侧数组中。另一个用于递归。但是,我将另一个参数投入递归函数中,以消除对单独函数的需求,因此可以通过同一递归函数处理所有内容。

这就是我在寻找的答案。感谢 @Ultimater。对于排除镜像选项,加上一个赞。 - Esser

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接