递归函数生成多维数组,基于数据库结果。

92
我想编写一个函数,该函数接受页面/类别数组(从扁平数据库结果中获取),并基于父ID生成嵌套的页面/类别项目数组。我希望以递归方式完成此操作,以便可以完成任何级别的嵌套。
例如:我正在一次查询中获取所有页面,并且这是数据库表的外观。
+-------+---------------+---------------------------+
|   id  |   parent_id   |           title           |
+-------+---------------+---------------------------+
|   1   |       0       |   Parent Page             |
|   2   |       1       |   Sub Page                |
|   3   |       2       |   Sub Sub Page            |
|   4   |       0       |   Another Parent Page     |
+-------+---------------+---------------------------+

这是我想要最终在视图文件中处理的数组:

Array
(
    [0] => Array
        (
            [id] => 1
            [parent_id] => 0
            [title] => Parent Page
            [children] => Array
                        (
                            [0] => Array
                                (
                                    [id] => 2
                                    [parent_id] => 1
                                    [title] => Sub Page
                                    [children] => Array
                                                (
                                                    [0] => Array
                                                        (
                                                            [id] => 3
                                                            [parent_id] => 1
                                                            [title] => Sub Sub Page
                                                        )
                                                )
                                )
                        )
        )
    [1] => Array
        (
            [id] => 4
            [parent_id] => 0
            [title] => Another Parent Page
        )
)

我已经查看并尝试了几乎每一个我遇到的解决方案(这里有很多),但是没有运气得到一个足够通用并适用于页面和类别的解决方案。

以下是我最接近的解决方案,但它不起作用,因为我将子元素分配给了第一级父元素。

function page_walk($array, $parent_id = FALSE)
{   
    $organized_pages = array();

    $children = array();

    foreach($array as $index => $page)
    {
        if ( $page['parent_id'] == 0) // No, just spit it out and you're done
        {
            $organized_pages[$index] = $page;
        }
        else // If it does, 
        {       
            $organized_pages[$parent_id]['children'][$page['id']] = $this->page_walk($page, $parent_id);
        }
    }

    return $organized_pages;
}

function page_list($array)
{       
    $fakepages = array();
    $fakepages[0] = array('id' => 1, 'parent_id' => 0, 'title' => 'Parent Page');
    $fakepages[1] = array('id' => 2, 'parent_id' => 1, 'title' => 'Sub Page');
    $fakepages[2] = array('id' => 3, 'parent_id' => 2, 'title' => 'Sub Sub Page');
    $fakepages[3] = array('id' => 4, 'parent_id' => 3, 'title' => 'Another Parent Page');

    $pages = $this->page_walk($fakepages, 0);

    print_r($pages);
}

1
你不能只使用一个包含所有父ID的数组和另一个页面数组吗? - djot
5个回答

260

一些非常简单、通用的树构建代码:

function buildTree(array $elements, $parentId = 0) {
    $branch = array();

    foreach ($elements as $element) {
        if ($element['parent_id'] == $parentId) {
            $children = buildTree($elements, $element['id']);
            if ($children) {
                $element['children'] = $children;
            }
            $branch[] = $element;
        }
    }

    return $branch;
}

$tree = buildTree($rows);
算法非常简单:
1. 获取所有元素的数组和当前父级的id(最初为0/无/ null /什么都没有)。 2. 循环遍历所有元素。 3. 如果一个元素的parent_id与您在步骤1中获得的当前父id匹配,则该元素是该父级的子级。将其放入当前子元素列表中(这里是$branch)。 4. 递归调用该函数,并使用您刚刚在步骤3中确定的元素的id,即找到该元素的所有子级,并将它们作为children元素添加。 5. 返回找到的子元素列表。
换句话说,函数的一次执行返回给定父id的子元素列表。通过使用buildTree($myArray, 1)调用它,它将返回具有parent id为1的元素列表。最初,使用父id为0调用此函数,因此返回没有父id的元素,这些元素是根节点。该函数通过递归调用自身来查找子级的子级。

1
很高兴能帮到你。需要注意的是:这种方法有些低效,因为它总是将整个$elements数组传递下去。对于小型数组来说,这几乎没有影响,但对于大型数据集,您需要在传递之前从中删除已匹配的元素。不过这会变得有些混乱,所以我将其保持简单,以便您更容易理解。 :) - deceze
6
@deceze,我也想看到混乱的版本。提前感谢! - Jens Törnell
请问有人可以解释一下 buildTree() 函数中的第一个参数 'array' 是什么意思吗?它应该是你给出初始页面数组的变量吗,例如 '$tree = array'? 能否有人解释最后一行代码 '$tree = buildTree($rows)',因为 '$rows' 没有在任何地方定义过?最后,我正在苦恼于生成嵌套列表的 HTML 标记。 - Brownrice
1
@user array$elements类型提示,第一个参数只是 array $elements$rows 是类似于问题中的数据库结果的数组。 - deceze
1
@user 我已经添加了解释。忽略 $children = buildTree(...) 部分,这个函数应该很明显和简单。 - deceze
显示剩余7条评论

14

我知道这个问题很老了,但我遇到了一个非常类似的问题,只是数据量非常大。经过一些努力,我成功地使用引用在结果集的一次遍历中构建了树。这段代码不太美观,但它可以工作得相当快。它是非递归的,也就是说,在结果集上只有一次遍历,然后在最后进行一次array_filter

$dbh = new PDO(CONNECT_STRING, USERNAME, PASSWORD);
$dbs = $dbh->query("SELECT n_id, n_parent_id from test_table order by n_parent_id, n_id");
$elems = array();

while(($row = $dbs->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
    $row['children'] = array();
    $vn = "row" . $row['n_id'];
    ${$vn} = $row;
    if(!is_null($row['n_parent_id'])) {
        $vp = "parent" . $row['n_parent_id'];
        if(isset($data[$row['n_parent_id']])) {
            ${$vp} = $data[$row['n_parent_id']];
        }
        else {
            ${$vp} = array('n_id' => $row['n_parent_id'], 'n_parent_id' => null, 'children' => array());
            $data[$row['n_parent_id']] = &${$vp};
        }
        ${$vp}['children'][] = &${$vn};
        $data[$row['n_parent_id']] = ${$vp};
    }
    $data[$row['n_id']] = &${$vn};
}
$dbs->closeCursor();

$result = array_filter($data, function($elem) { return is_null($elem['n_parent_id']); });
print_r($result);

当应用于这份数据时:

mysql> select * from test_table;
+------+-------------+
| n_id | n_parent_id |
+------+-------------+
|    1 |        NULL |
|    2 |        NULL |
|    3 |           1 |
|    4 |           1 |
|    5 |           2 |
|    6 |           2 |
|    7 |           5 |
|    8 |           5 |
+------+-------------+

最后一个print_r生成了以下输出:

Array
(
    [1] => Array
        (
            [n_id] => 1
            [n_parent_id] => 
            [children] => Array
                (
                    [3] => Array
                        (
                            [n_id] => 3
                            [n_parent_id] => 1
                            [children] => Array
                                (
                                )

                        )

                    [4] => Array
                        (
                            [n_id] => 4
                            [n_parent_id] => 1
                            [children] => Array
                                (
                                )

                        )

                )

        )

    [2] => Array
        (
            [n_id] => 2
            [n_parent_id] => 
            [children] => Array
                (
                    [5] => Array
                        (
                            [n_id] => 5
                            [n_parent_id] => 2
                            [children] => Array
                                (
                                    [7] => Array
                                        (
                                            [n_id] => 7
                                            [n_parent_id] => 5
                                            [children] => Array
                                                (
                                                )

                                        )

                                    [8] => Array
                                        (
                                            [n_id] => 8
                                            [n_parent_id] => 5
                                            [children] => Array
                                                (
                                                )

                                        )

                                )

                        )

                    [6] => Array
                        (
                            [n_id] => 6
                            [n_parent_id] => 2
                            [children] => Array
                                (
                                )

                        )

                )

        )

)

正是我所需要的。


虽然这个解决方案很聪明,但是这段代码有 bug,在不同的情况下给我不同的结果。 - Mohammadhzp
@Mohammadhzp 我在生产环境中使用了这个解决方案已经一年了,没有遇到任何问题。如果你的数据不同,你会得到不同的结果 :) - Aleks G
@AleksG:在评论之前,我已经为您的答案点赞了。我必须在array_filter回调中添加isset($elem['children']),就像这样返回isset($elem['children']) && is_null($elem['n_parent_id']);以使其正常工作。 - Mohammadhzp
@Mohammadhzp,根据您的更改,如果顶级元素没有任何子元素,则代码将无法正常工作 - 它将被完全从数组中删除。 - Aleks G
谢谢,但我必须将$elems = array();更改为$data = array(); - michalzuber
显示剩余2条评论

3
public function testTree(){
    $array = [
        ['id'=>7,'parent_id'=>3],
        ['id'=>1,'parent_id'=>0],
        ['id'=>2,'parent_id'=>0],
        ['id'=>3,'parent_id'=>1],
        ['id'=>4,'parent_id'=>1],
        ['id'=>5,'parent_id'=>2],
        ['id'=>6,'parent_id'=>1],
        ['id'=>8,'parent_id'=>4],
        ['id'=>9,'parent_id'=>4],
        ['id'=>10,'parent_id'=>0]
    ];
    $res = $this->buildTree($array);
    print_r($res);
}

public function buildTree($array,$id_key = 'id',$parent_key = 'parent_id'){
    $res = [];
    foreach($array as $y){
        $array_with_id[$y[$id_key]] = $y;
    }
    foreach($array_with_id as $key => $element){
        if($element[$parent_key]){
            $array_with_id[$element[$parent_key]]['childrens'][$key] = &$array_with_id[$key];
        }else{
            $res[$element[$id_key]] = &$array_with_id[$key];
        }
    }
    return $res;
}

提供太多递归操作,我认为这是最好的方法。


1
从其他答案中汲取灵感,我提出了自己的版本,用于对关联数组的数组进行递归分组(到任意深度),使用自定义函数列表来在每个级别上获取分组键

这是原始更复杂的变体的简化版本(具有更多的参数来调整旋钮)。请注意,它采用一个简单的迭代函数groupByFn作为子程序,用于执行单个级别上的分组。

/**
 * - Groups a (non-associative) array items recursively, essentially converting it into a nested
 *   tree or JSON like structure. Inspiration taken from: https://dev59.com/IGoy5IYBdhLWcg3wZdGr#8587437
 * OR
 * - Converts an (non-associative) array of items into a multi-dimensional array by using series
 *   of callables $key_retrievers and recursion
 *
 * - This function is an extension to above 'groupByFn', which also groups array but only till 1 (depth) level
 *   (whereas this one does it till any number of depth levels by using recursion)
 * - Check unit-tests to understand further
 * @param array $data Array[mixed] (non-associative) array of items that has to be grouped / converted to
 *                    multi-dimensional array
 * @param array $key_retrievers Array[Callable[[mixed], int|string]]
 *                    - A list of functions applied to item one-by-one, to determine which
 *                    (key) bucket an item goes into at different levels
 *                    OR
 *                    - A list of callables each of which takes an item or input array as input and returns an int
 *                    or string which is to be used as a (grouping) key for generating multi-dimensional array.
 * @return array A nested assoc-array / multi-dimensional array generated by 'grouping' items of
 *               input $data array at different levels by application of $key_retrievers on them (one-by-one)
 */
public static function groupByFnRecursive(
    array $data,
    array $key_retrievers
): array {
    // in following expression we are checking for array-length = 0 (and not nullability)
    // why empty is better than count($arr) == 0 https://dev59.com/d3E95IYBdhLWcg3wp_qn#2216159
    if (empty($data)) {
        // edge-case: if the input $data array is empty, return it unmodified (no need to check for other args)
        return $data;

        // in following expression we are checking for array-length = 0 (and not nullability)
        // why empty is better than count($arr) == 0 https://dev59.com/d3E95IYBdhLWcg3wp_qn#2216159
    } elseif (empty($key_retrievers)) {
        // base-case of recursion: when all 'grouping' / 'nesting' into multi-dimensional array has been done,
        return $data;
    } else {
        // group the array by 1st key_retriever
        $grouped_data = self::groupByFn($data, $key_retrievers[0]);
        // remove 1st key_retriever from list
        array_shift($key_retrievers);

        // and then recurse into further levels
        // note that here we are able to use array_map (and need not use array_walk) because array_map can preserve
        // keys as told here:
        // https://www.php.net/manual/en/function.array-map.php#refsect1-function.array-map-returnvalues
        return array_map(
            static function (array $item) use ($key_retrievers): array {
                return self::groupByFnRecursive($item, $key_retrievers);
            },
            $grouped_data
        );
    }
}

请查看要素,其中包括更多的数组实用函数集合以及单元测试


-1

可以使用PHP将MySQL结果转换为数组,然后使用它。

$categoryArr = Array();
while($categoryRow = mysql_fetch_array($category_query_result)){
    $categoryArr[] = array('parentid'=>$categoryRow['parent_id'],
            'id'=>$categoryRow['id']);
   }

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