本文已参与「新人创作礼」活动,一起开启掘金创作之路。
什么是无限级分类?
无限极分类简单点说就是一个类可以分成多个子类,其子类又可以分成另外多个子类,一直这样无限分下去,就好象windows可以新建一个文件夹,然后在这个文件夹里又可以建一些文件夹,在文件夹底下还可以建一些文件夹……
无限级分类的实现算法常见的有两种:递归算法和引用算法。
本文着重讲解引用算法,因为递归算法是函数调用自身 ,而函数调用是有时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址以及临时变量,而往栈中压入数据和弹出数据都需要时间。
引用算法只使用一层foreach既已完成无限级分类,函数调用深度完全不是递归算法所能比拟的。此外,递归算法的实现较为简单,并没有太多的难以理解,故略过不讲。
第一步:最简单的引用例子
首先,给大家看一个最简单的引用例子。每种语言都有引用的存在,这里就不做过多解释,如果有兴趣研究PHP具体的内存空间分配的话,可以阅读这篇文章(各个版本的php内核处理存在差异,仅建议学习)
实验代码:
<?php
$a = 1;
$b = $a;
$a = 2;
echo 'b:' . $b . PHP_EOL;
echo '--------------------' . PHP_EOL;
$a = 1;
$b = &$a; //$a使用的内存空间也给$b使用
$a = 2; //$a值被更改,$b指向$a,输出的$b值也随之更改
echo 'b:' . $b . PHP_EOL;
输出结果:
第二步:数组的初步引用
引用同样对数组的元素起作用,利用这一特性,实现了数组不关先后的动态更新。我们接着往下看
实验代码:
<?php
//拿一个易于理解的数据数组来演示下
function init_array()
{
$a = ['id' => 1, 'pid' => 0, 'name' => '安徽省'];
$b = ['id' => 2, 'pid' => 0, 'name' => '浙江省'];
$c = ['id' => 3, 'pid' => 1, 'name' => '合肥市'];
$d = ['id' => 4, 'pid' => 3, 'name' => '长丰县'];
$e = ['id' => 5, 'pid' => 1, 'name' => '安庆市'];
return [$a, $b, $c, $d, $e];
}
echo '没有引用:' . PHP_EOL;
[$a, $b, $c, $d, $e] = init_array();
$a['son'][] = $c;
$c['son'][] = $d; //更改了$c的值,但是没有引用关系,并不会改变上面$a的数据
print_r($a);
echo '---------------------' . PHP_EOL;
echo '有引用:' . PHP_EOL;
[$a, $b, $c, $d, $e] = init_array();
$a['son'][] = &$c;
$c['son'][] = $d; //更改了$c数组所指向的内存空间内容,而$a指向的是跟$c同一块内存空间,也就获取$c更改后的值,进而动态改变上面$a的数据
print_r($a);
输出结果:
第三步:人工逻辑处理结果
现在,我们终于可以正式开始我们的树形菜单数组生成之旅了。不过为了方便理解,这里还是一个小例子,采用最简单的人工逻辑处理,对数据一条条进行处理,务必让大伙理解充分(懂了的可以尽情跳过,下面还是在水)。
实验代码:
<?php
//拿上面那个易于理解的数据数组再来演示下
function init_array()
{
$a = ['id' => 1, 'pid' => 0, 'name' => '安徽省'];
$b = ['id' => 2, 'pid' => 0, 'name' => '浙江省'];
$c = ['id' => 3, 'pid' => 1, 'name' => '合肥市'];
$d = ['id' => 4, 'pid' => 3, 'name' => '长丰县'];
$e = ['id' => 5, 'pid' => 1, 'name' => '安庆市'];
return [$a, $b, $c, $d, $e];
}
[$a, $b, $c, $d, $e] = init_array();
//数组中,pid父节点=0的$a,$b属于根节点,这里使用$tree,将$a和$b存放在其中
$tree[] =& $a; //引用,因为后续$a的更改需要动态更新tree
$tree[] =& $b;
//$c的pid是1,也就是$a['id'],建立$a的son有$c
$a['son'][] =& $c; //同样的,使用引用,$c后续操作也需要动态更新
//$d的pid是3,也就是$c['id'],建立$c的son有$c
$c['son'][] =& $d; //后续变量都可能会再被更新值,剩下的都是用引用,不再注释
//$e的pid是1,也就是$a['id'],建立$a的son有$e
$a['son'][] =& $e;
//输出结果,看看这是不是你想要的树
print_r($tree);
输出结果:
第四步:无限级分类代码雏形完成
当然,第三步的操作是基于数据不会变动的情况,而且人工操作,数据一多或者一变动,麻烦的可不是零星半点。对于这种动态数据来说,能使用foreach来处理当然最为简便。
假设你从数据库获取到这样一个菜单数据(顺序被打乱啦)
$items = [
['id' => 4, 'pid' => 3, 'name' => '长丰县'], //原$d
['id' => 1, 'pid' => 0, 'name' => '安徽省'], //原$a
['id' => 3, 'pid' => 1, 'name' => '合肥市'], //原$c
['id' => 2, 'pid' => 0, 'name' => '浙江省'], //原$b
['id' => 5, 'pid' => 1, 'name' => '安庆市'], //原$e
];
根据第三步中,以下代码随着数据变动,可能有n行
$a['son'][] = &$c;
$c['son'][] = &$d;
$a['son'][] = &$e;
我们需要使用foreach,改造成下面的语句,用一条通用的语句来进行处理
foreach ($items as $key => $item) {
$xxx['son'][] = &$item;
}
即第二步的 $a['son'][] = &$c; 等价于 本步骤的$xxx['son'][] = $item;
那么$xxx(父节点变量)怎么来呢?我们能依靠的$key和$item都没办法直接帮我们确定$xxx怎么来的
改变一下思路:如果是这样子的数据数组呢:使用每行数据里面的id来作为数组的键名(这里使用array_column($items,null,'id')就能得到所要的数组)
$items = [
'4' => ['id' => 4, 'pid' => 3, 'name' => '长丰县'],
'1' => ['id' => 1, 'pid' => 0, 'name' => '安徽省'],
'3' => ['id' => 3, 'pid' => 1, 'name' => '合肥市'],
'2' => ['id' => 2, 'pid' => 0, 'name' => '浙江省'],
'5' => ['id' => 5, 'pid' => 1, 'name' => '安庆市'],
];
现在就好办了,$item['pid']就是节点$item对应的父节点id,按照上面的数组,我们可以知道$items[父节点id]就是我们所要得到的父节点变量$xxx,也就是$xxx等价于$items[$item['pid']]
改造完成,如下:
实验代码1:
<?php
$items = [
['id' => 4, 'pid' => 3, 'name' => '长丰县'], //原$d
['id' => 1, 'pid' => 0, 'name' => '安徽省'], //原$a
['id' => 3, 'pid' => 1, 'name' => '合肥市'], //原$c
['id' => 2, 'pid' => 0, 'name' => '浙江省'], //原$b
['id' => 5, 'pid' => 1, 'name' => '安庆市'], //原$e
];
$items = array_column($items, null, 'id');
foreach ($items as $item) {
if (isset($items[$item['pid']])) {
//上面所述,改成通用的
$items[$item['pid']]['son'][] = &$item;
} else {
//没有父节点,代表是根节点,直接添加在树中
$tree[] = &$item;
}
}
//输出看看结果吧
print_r($tree);
实验结果1:
这结果咋不对劲了呢?
原来,在foreach里面,$item是根据$items当前循环出的元素,所复制出的一个临时变量,没有指向$items元素里面的内存空间,不能够达到我们想要的动态改变数组$items的目的。
解决方法很简单,把foreach中的$item给成引用就可以了。
实验代码2:
<?php
$items = [
['id' => 4, 'pid' => 3, 'name' => '长丰县'], //原$d
['id' => 1, 'pid' => 0, 'name' => '安徽省'], //原$a
['id' => 3, 'pid' => 1, 'name' => '合肥市'], //原$c
['id' => 2, 'pid' => 0, 'name' => '浙江省'], //原$b
['id' => 5, 'pid' => 1, 'name' => '安庆市'], //原$e
];
$items = array_column($items, null, 'id');
foreach ($items as &$item) {
if (isset($items[$item['pid']])) {
//上面所述,改成通用的
$items[$item['pid']]['son'][] = &$item;
} else {
//没有父节点,代表是根节点,直接添加在树中
$tree[] = &$item;
}
}
//这个结果没问题啦
print_r($tree);
实验结果2:
至此,无限极分类菜单的主要实现流程已经完成了。不过,为了方便调用,我们再封装一下它。
第五步:封装完成的无限级分类代码
到这里已经懒得再写注释了,大伙将就着看吧。
封装成过程函数
<?php
$items = [
['id' => 1, 'pid' => 0, 'name' => '安徽省'],
['id' => 2, 'pid' => 0, 'name' => '浙江省'],
['id' => 3, 'pid' => 1, 'name' => '合肥市'],
['id' => 4, 'pid' => 3, 'name' => '长丰县'],
['id' => 5, 'pid' => 1, 'name' => '安庆市'],
];
function generateTree(array $items, string $pk = 'id', string $pid = 'pid', string $son = 'son'): array
{
$tree = [];
$items = array_column($items, null, $pk);
foreach ($items as &$item) {
if (isset($items[$item['pid']])) {
$items[$item[$pid]][$son][] = &$item;
} else {
$tree[] = &$item;
}
}
return $tree;
}
//调用方法,获取结果
print_r(generateTree($items));
封装成类库
<?php
//封装成类
class TreeBuilder
{
public $items = [];
public $pk = 'id';
public $pid = 'pid';
public $son = 'son';
public $checkItemsFlag = true;
public function setItems(array $items)
{
$this->items = $items;
return $this;
}
public function setPk(string $pk): self
{
$this->pk = $pk;
return $this;
}
public function setPid(string $pid): self
{
$this->pid = $pid;
return $this;
}
public function setSon(string $son): self
{
$this->son = $son;
return $this;
}
public function setCheckItemsFlag(bool $checkItemsFlag): self
{
$this->checkItemsFlag = $checkItemsFlag;
return $this;
}
public function build(): array
{
//是否验证数组数据
if ($this->checkItemsFlag) {
$this->checkItems();
}
return (new Tree($this))->generateTree();
}
private function checkItems()
{
try {
if (empty($this->items)) {
throw new Exception('数据不可为空');
}
array_walk($this->items, array($this, 'checkItemsKey'));
} catch (Exception $e) {
exit($e->getMessage());
}
}
private function checkItemsKey($item, $key)
{
if (!array_key_exists($this->pk, $item)) {
throw new Exception('key为' . $key . '的数据中不存在' . $this->pk);
}
if (!array_key_exists($this->pid, $item)) {
throw new Exception('key为' . $key . '的数据中不存在' . $this->pid);
}
}
}
class Tree
{
public $items;
public $pk;
public $pid;
public $son;
function __construct(TreeBuilder $treeBuilder)
{
$this->items = $treeBuilder->items;
$this->pk = $treeBuilder->pk;
$this->pid = $treeBuilder->pid;
$this->son = $treeBuilder->son;
}
function generateTree(): array
{
$tree = [];
$items = array_column($this->items, null, $this->pk);
foreach ($items as &$item) {
if (isset($items[$item['pid']])) {
$items[$item[$this->pid]][$this->son][] = &$item;
} else {
$tree[] = &$item;
}
}
return $tree;
}
}
//调用类库方法-----------------------------------------------------
$items = [
['id' => 1, 'pid' => 0, 'name' => '安徽省'],
['id' => 2, 'pid' => 0, 'name' => '浙江省'],
['id' => 3, 'pid' => 1, 'name' => '合肥市'],
['id' => 4, 'pid' => 3, 'name' => '长丰县'],
['id' => 5, 'pid' => 1, 'name' => '安庆市'],
];
$treeBuilder = new TreeBuilder();
$tree = $treeBuilder->setItems($items)
->setPk('id')
->setPid('pid')
->setSon('child')
->setCheckItemsFlag(true)
->build();
print_r($tree);
如要了解更多实现方法,可参考文章:《php实现无限极分类》