【问题标题】:Adaptive parent relative to children's attributes in a recursive array递归数组中相对于子属性的自适应父
【发布时间】:2017-12-18 22:41:24
【问题描述】:

我正在尝试创建一个简单的树结构,其中每个任务都有一定的完成百分比,并且其父级必须继承其直接子级的平均完成率,如下图所示。 (0 是完成的百分比,例如 subtask2 可能是 100%,subtask2 可能是 0%,这将使 task1 完成 50%,因此 stackoverflow 将有 25%,假设 task2 为 0)

我遇到的问题是,显然,我需要从最深的孩子开始,但我似乎无法弄清楚如何实现从叶子到根的这种反向遍历。

我尝试过使用普通递归和双循环,两者都只实现一级计算(在图片示例中 task1 被计算,但 stackoverflow 将保持为 0)。

注意:只有叶子实际上可以有完成百分比,因为不是叶子的所有其他元素都从其子元素继承百分比。 (多么自相矛盾)

如果你们中的任何人对如何实现这样的算法有任何想法,无论是概念上的还是实际的代码,我都非常感谢任何输入。

下面是这个数组的结构(只保留了相关信息):

[0] => Array
(
    [title] => stackoverflow
    [completion] => 0
    [children] => Array
        (
            [0] => Array
                (
                    [title] => task2
                    [completion] => 0
                )

            [1] => Array 
                (
                    [title] => task1
                    [completion] => 0
                    [children] => Array 
                        (
                            [0] => Array
                                (
                                    [title] => subtask2
                                    [completion] => 100
                                )

                            [1] => Array
                                (
                                    [title] => subtask1
                                    [completion] => 0
                                )
                        )
                )
        )
)

我似乎遇到了与此线程中的问题类似的问题:Percentages and trees 但是,我需要我的任务有实际百分比,而不仅仅是完成/未完成。所有的数学都是完全线性的,这意味着父母的百分比=(所有孩子百分比的加法)/(孩子的数量)

还有 var_export:

array (
      0 => 
      array (
            'uuid' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
            'title' => 'stackoverflow',
            'completed' => '0',
            'parent' => NULL,
            'children' => 
                array (
                  0 => 
                  array (
                    'uuid' => '72ce49a6-76e5-495e-a3f8-0f13d955a3b5',
                    'title' => 'task2',
                    'completed' => '0',
                    'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
                  ),
              1 => 
              array (
                    'uuid' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
                    'title' => 'task1',
                    'completed' => '0',
                    'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
                    'children' => 
                        array (
                          0 => 
                          array (
                            'uuid' => 'ac5e9d37-8f14-4169-bcf2-e7b333c5faea',
                            'title' => 'subtask2',
                            'completed' => '0',
                            'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
                          ),
                      1 => 
                      array (
                        'uuid' => 'f74b801f-c9f1-40df-b491-b0a274ffd301',
                        'title' => 'subtask1',
                        'completed' => '0',
                        'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
                      ),
                    ),
                  ),
                ),
              ),
)

【问题讨论】:

  • 你的输入数组是什么样的?
  • 刚刚添加 :)
  • 请使用var_export 作为您的数组,而不是var_dump,以便我们复制/粘贴它。
  • 也添加了 var_export,尽管我在格式化方面遇到了一些问题

标签: php arrays recursion tree


【解决方案1】:

这是一个递归函数,它通过引用传递父级,直到它找到一个叶子并更新向后工作的总数。

function completionTree(&$elem, &$parent=NULL) {
    // Handle arrays that are used only as a container... if we have children but no uuid, simply descend.
    if (is_array($elem) && !isset($elem['uuid'])) {
        foreach($elem AS &$child) {
            completionTree($child, $elem);
        }
    }

    // This array has children. Iterate recursively for each child.
    if (!empty($elem['children'])) {
        foreach ($elem['children'] AS &$child) {
            completionTree($child, $elem);
        }
    }

    // After recursion to handle children, pass completion percentages up to parent object
    // If this is the top level, nothing needs to be done (but suppress that error)
    if (@$parent['completed'] !== NULL) {
        // Completion must be multiplied by the fraction of children it represents so we always add up to 100. Since values are coming in as strings, cast as float to be safe.
        $parent['completed'] = floatval($parent['completed']) + (floatval($elem['completed']) * (1/count($parent['children'])));
    }
}

// Data set defined statically for demonstration purposes
$tree = array(array (
            'uuid' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
            'title' => 'stackoverflow',
            'completed' => '0',
            'parent' => NULL,
            'children' => 
                array (
                  0 => 
                  array (
                    'uuid' => '72ce49a6-76e5-495e-a3f8-0f13d955a3b5',
                    'title' => 'task2',
                    'completed' => '0',
                    'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
                  ),
              1 => 
              array (
                    'uuid' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
                    'title' => 'task1',
                    'completed' => '0',
                    'parent' => '157ed2b2-0d0c-4f0c-b1d2-7126255f4023',
                    'children' => 
                        array (
                          0 => 
                          array (
                            'uuid' => 'ac5e9d37-8f14-4169-bcf2-e7b333c5faea',
                            'title' => 'subtask2',
                            'completed' => '0',
                            'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
                          ),
                      1 => 
                      array (
                        'uuid' => 'f74b801f-c9f1-40df-b491-b0a274ffd301',
                        'title' => 'subtask1',
                        'completed' => '100',
                        'parent' => '4975d08d-55f0-4cd8-9de5-2d056111ec2d',
                      ),
                    ),
                  ),
                ),
              ),
);

// Launch recursive calculations
completionTree($tree);

// Dump resulting tree
var_dump($tree);

【讨论】:

  • 我唯一需要改变的是公式,因为只有叶子实际上可以有值,所以: $parent['completed'] = floatval($parent['completed']) + (floatval ($elem['completed'])) * (1/count($parent['children']));.但这正是我想要的,谢谢伙计,我实际上遇到的主要问题是通过引用传递,因为它从未在我的代码中出于某种原因更新原始元素。
【解决方案2】:

虽然已经回答了这个问题,但我想留下一个看起来有点更直观的解决方案(恕我直言)。与其传递父级,不如先处理子级:

/**
 * @param array $nodes
 *
 * @return array
 */
function calcCompletion(array $nodes): array {
    // for each node in nodes
    return array_map(function (array $node): array {
        // if it has children
        if (array_key_exists('children', $node) && is_array($node['children'])) {
            // handle the children first
            $node['children'] = calcCompletion($node['children']);

            // update this node by *averaging* the children values
            $node['completed'] = array_reduce($node['children'], function (float $acc, array $node): float {
                return $acc + floatval($node['completed']);
            }, 0.0) / count($node['children']);
        }

        return $node;
    }, $nodes);
}

【讨论】:

    【解决方案3】:

    嗯,这可能有点开销,但您可以使用RecursiveArrayIterator。首先,你必须扩展它来处理你的树结构:

    class MyRecursiveTreeIterator extends RecursiveArrayIterator
    {
        public function hasChildren()
        {
            return isset($this->current()['children']) 
                && is_array($this->current()['children']);    
        }
    
        public function getChildren()
        {
            return new static($this->current()['children']);
        }
    }
    

    然后使用RecursiveIteratorIterator,您可以创建一个迭代器,它将从叶子开始处理您的树:

    $iterator = new RecursiveIteratorIterator(
        new MyRecursiveTreeIterator($tasks),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    

    然后有了这个,你就可以添加你的计算逻辑了:

    $results = [];
    $temp = [];
    $depth = null;
    
    foreach ($iterator as $node) {
        if ($iterator->getDepth() === 0) {
            // If there were no children use 'completion'
            // else use children average
            if (
                is_null($depth)
                || !isset($temp[$depth])
                || !count($temp[$depth])
            ) {
                $percentage = $node['completed']; 
            } else {
                $percentage = array_sum($temp[$depth]) / count($temp[$depth]);
            }
    
            $results[$node['title']] = $percentage;
            continue;
        }
    
        // Set empty array for current tree depth if needed.
        if (!isset($temp[$iterator->getDepth()])) {
            $temp[$iterator->getDepth()] = [];
        }
    
        // If we went up a tree, collect the average of children
        // else push 'completion' for children of current depth.
        if ($iterator->getDepth() < $depth) {
            $percentage = array_sum($temp[$depth]) / count($temp[$depth]);
            $temp[$depth] = [];
            $temp[$iterator->getDepth()][] = $percentage;
        } else {
            $temp[$iterator->getDepth()][] = $node['completed'];
        }
    
        $depth = $iterator->getDepth();
    }
    

    这里是a demo

    【讨论】:

    • 请重写您的方法以使用您注释掉的 $tasks 数组——这是 OP 的实际输入。
    • @mickmackusa,据我所知,它们具有相同的结构。
    • 我没有调查原因,但是当我换入完整/真实数组时,它不起作用。 sandbox.onlinephpfunctions.com/code/…如果您不想更改答案,请不要。我不是你的老板,我很确定你也不喜欢我。
    • @mickmackusa,它在结果中显示 0,因为问题的第二个数组中的所有 completed 字段都有 0 值。
    • @mickmackusa,正如 OP 所说“下面是这个数组的结构(只保留相关信息)”,这就是为什么我在演示中留下了两个版本的数组。
    猜你喜欢
    • 2019-12-12
    • 2020-10-09
    • 1970-01-01
    • 1970-01-01
    • 2011-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多