I would just count the numbers.
This means each item of the base key plus the same for it's children.
In opposite to recursion, I decided to use a stack for this.
To have a value per level, the counter needs a level context as well, which is added to the stack next to the node:
$count = array();
$stack[] = array(-1, $arr);
while($item = array_shift($stack)) {
list($level, $node) = $item;
$count[$level] = isset($count[$level]) ? $count[$level]+1 : 1;
if (!isset($node['_children']))
continue;
foreach($node['_children'] as $item)
$stack[] = array($level+1, $item);
}
var_dump($count);
This does output:
array(4) {
[-1]=> int(1)
[0] => int(2)
[1] => int(1)
[2] => int(1)
}
where -1
is the root node only containing children. So you might want to remove it after processing:
array_shift($count);
Edit: Storing the nodes
The following code adds a collector of nodes per level as well, called $nodes
. It's a bit redundant as per count($nodes[$level])
is $count[$level]
, but it's just to show how to collect nodes as well:
$count = array();
$nodes = array(); // nodes per level go here.
$stack[] = array(-1, $arr);
while($item = array_shift($stack)) {
list($level, $node) = $item;
$count[$level] = isset($count[$level]) ? $count[$level]+1 : 1;
$nodes[$level][] = $node; // set here!
if (!isset($node['_children']))
continue;
foreach($node['_children'] as $item)
$stack[] = array($level+1, $item);
}
var_dump($count, $nodes);
Discussion
Using a stack prevents to make recursion function calls and reduces complexity. The idea behind it is fairly simple:
- The stack is initialized with the first node (which only contains children) - the tree's root node.
- An entry on the stack contains the node to be counted and it's level
- Each element on the stack is treated the same:
- An item gets removed from the beginning (or end) of the stack.
- The counter for it's level is increased by one.
- If it has children, each child-node is added to the stack with it's level.
- The stack will be processed until all elements are consumed.
This ensures that every node will be counted with a minimal overhead. The strategy of consuming the stack can be fully controlled. Either FIFO (First In, First Out), LIFO (Last In, First Out) - which is easy to implement - or even a weighted strategy (first process child-nodes that have no/the most children to consume them fast) or even random stack consumption to distribute the constraints the data-structure implies accros the whole stack.
Comparing Stack to Recursion Function Stack
Comparing using an own stack to the PHP's function stack first of all shows that both ways have things in common. Both make use of a stack.
But the main difference is, that an own stack prevents using the function calls. Calling a function, especially recursively has multiple implications that can be avoided.
Another key difference is, that an own stack always allows to interrogate with it while there is no control over the PHP language's own stack.
Next to that, an own stack can be processed sequentially, while the function stack is a recursive construct that maps the processing of the data 1:1 into the program flow.
While recursion will hide away the recursive nature of the data (tree) for the code within the function (which can simplify processing), there is the need to offer the state that gets hidden away by function calls to be introduced as function parameters.
Managing the data to solve the problem therefore is not less but equal or more complex with recursion.
So next to the overhead of the function calls, one can already smell an overhead for data.
It's worth to see this in the light of the PHP language itself.
To make recursion work, PHP needs to add additional variable tables onto the function stack. These tables need to be managed before and after each function call.
The recursion also needs you to pass variables by references. This is an additional overhead, as those values need to be set/processed before and after the function ends by PHP.
Such a reference is needed in the recursion to share the data structure for the counter. This overhead in a recursive implementation could be reduced by implementing the recursion into a class of it's own in PHP, which has not been suggested so far and is probably worth to consider.
Like the counter array in a user stack implementation, such a class could share the counter between all function calls, providing a hash accessible to the class's member function(s) w/o making use of variable references. Further on, complexity in solving the problem can be distributed over different functions with in the class or even injected as a dependency. PHP offers a default recursive iterator interface already.
Another place to reduce one could make use of is the global variable table ($GLOBALS
) but with the downside of making the code less re-useable which signals a design flaw. Even if design is not an issue, that so called superglobals array needs to be managed between function calls as as well. That results in a quite-like behavior to passing by reference for which it was considered to prevent it. Therefore it is not a preferable solution.
Recursive Implementation by Class
This is a recursive implementation based on the idea raised in the discussion above to get more grip on it:
/**
* class implementation of a recursive child counter
*
* Usage:
*
* Object use:
*
* $counter = new LevelChildCounter($arr, '_children');
* $count = $counter->countPerLevel();
*
* Functional use:
*
* $count = LevelChildCounter::count($arr, '_children');
*/
class LevelChildCounter {
private $tree;
private $childKey;
private $counter;
public function __construct(array $tree, $childKey) {
$this->tree = $tree;
$this->childKey = $childKey;
}
/**
* functional interface of countPerLevel
*/
public static function count(array $tree, $childKey) {
$counter = new self($tree, $childKey);
return $counter->countPerLevel();
}
private function countUp($level) {
isset($this->counter[$level])
? $this->counter[$level]++
: $this->counter[$level] = 1;
}
private function countNode($node, $level) {
// count node
$this->countUp($level);
// children to handle?
if (!isset($node[$this->childKey]))
return;
// recursively count child nodes
foreach($node[$this->childKey] as $childNode)
$this->countNode($childNode, $level+1)
;
}
/**
* Count all nodes per level
*
* @return array
*/
public function countPerLevel() {
$this->counter = array();
$this->countNode($this->tree, -1);
return $this->counter;
}
}
$count = LevelChildCounter::count($arr, '_children');
array_shift($count);
var_dump($count);
And it's output:
array(3) {
[0]=> int(2)
[1]=> int(1)
[2]=> int(1)
}