无限级分类之引用算法详解

什么是无限级分类?

无限极分类简单点说就是一个类可以分成多个子类,其子类又可以分成另外多个子类,一直这样无限分下去,就好象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:
第四步结果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:
第四步结果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实现无限极分类》

猜你喜欢

转载自blog.csdn.net/weixin_38125045/article/details/106858673