无限极分类(adjacency list)的三种方式(迭代、递归、引用)
一般的分类树状结构有两种方式:
- 一种是adjacency list,也就是是id,parent id这中形式。
- 另一种是nested set,即左右值的形式。
左右值形式查询起来比较高效,无需递归等,推荐使用,但是没有pid形式简单直观,而且有些旧的数据库类似地区等结构设计一直是pid这种形式(貌似也有算法可以将两者转换,不做深入了解),所以。。。
下面所说的都为adjacency list的形式,数据表格式类似id,pid,name这种格式。
通常来说是将数据全部从数据库读取后,然后再组装数组来实现,当然也可以每次递归等都查询数据库,但是会造成数据库压力,且不容易封装成方法,不建议这样做。
目前来说常用的有三种方式,我们来实现select下拉菜单展示的样式:
1、首先是最常用最普通,同样也是效率最低的递归方法:就是不停的foreach循环递归。
function getTreeOptions3($list, $pid = 0)
{
$options = [];
foreach ($list as $key => $value) {
if ($value['id'] == $pid) {//查看是否为子元素,如果是则递归继续查询
$options[$value['id']] = $value['name'];
unset($list[$key]);//销毁已查询的,减轻下次递归时查询数量
$optionsTmp = $this->getTreeOptions3($list, $value['id']);//递归
if (!empty($optionsTmp)) {
//$options = array_merge($options, $optionsTmp);//销毁已查询的,减轻下次递归时查询数量
$options =$options+ $optionsTmp;//用array_merge会导致索引重排
}
}
}
return $options;
}
2、第二种是利用入栈、出栈的递归来计算,效率比上个好点,但是也挺慢的。流程是先反转数组,然后取出顶级数组入栈,开始while循环,先出栈一个查找其下有没有子节点,如果有子节点,则将此子节点也入栈,下次while循环就会查子节点的,依次类推:
function getTreeOptions2($list, $pid = 0)
{
$tree = [];
if (!empty($list)) {
//先将数组反转,因为后期出栈时会优先出最上面的
$list = array_reverse($list);
//先取出顶级的来压入数组$stack中,并将在$list中的删除掉
$stack = [];
foreach ($list as $key => $value) {
if ($value['pid'] == $pid) {
array_push($stack,$value);
unset($list[$key]);
}
}
while (count($stack)) {
//先从栈中取出第一项
$info = array_pop($stack);
//查询剩余的$list中pid与其id相等的,也就是查找其子节点
foreach ($list as $key => $child) {
if ($child[pid] == $info['id']) {
//如果有子节点则入栈,while循环中会继续查找子节点的下级
array_push($stack, $child);
unset($list[$key]);
}
}
//组装成下拉菜单格式
$tree[$info['id']] = $info['name'];
}
}
return $tree;
}
3、利用引用来处理,这个真的很巧妙,而且效率最高,可参照这里,如果想看再详细的解释,可以查看这个问答。
/**
* 先生成类似下面的形式的数据
* [
* 'id'=>1,
* 'pid'=>0,
* 'items'=>[
* 'id'=>2,
* 'pid'=>'1'
* 。。。
* ]
* ]
*/
function getTree($list, $pid = 0)
{
$tree = [];
if (!empty($list)) {
//先修改为以id为下标的列表
$newList = [];
foreach ($list as $k => $v) {
$newList[$v['id']] = $v;
}
//然后开始组装成特殊格式
foreach ($newList as $value) {
if ($pid == $value['pid']) {//先取出顶级
$tree[] = &$newList[$value['id']];
} elseif (isset($newList[$value['pid']])) {//再判定非顶级的pid是否存在,如果存在,则再pid所在的数组下面加入一个字段items,来将本身存进去
$newList[$value['pid']]['items'][] = &$newList[$value['id']];
}
}
}
return $tree;
}
然后再递归生成select下拉菜单所需要的,由于上方的特殊格式,导致递归起来非常快:
function formatTree($tree)
{
$options = [];
if (!empty($tree)) {
foreach ($tree as $key => $value) {
$options[$value['id']] = $value['name'];
if (isset($value['items'])) {//查询是否有子节点
$optionsTmp = $this->formatTree($value['items']);
if (!empty($optionsTmp)) {
//$options = array_merge($options, $optionsTmp);
$options =$options+ $optionsTmp;//用array_merge会导致索引重排
}
}
}
}
return $options;
}
以上三种,对于数据量小的来说,无所谓用哪种,但是对于数据量大的来说就非常明显了,用4000条地区数据测试结果效率对比:
- 第一种方法(递归)耗时:8.9441471099854左右
- 第二种方法(迭代)耗时:6.7250330448151左右
- 第三种方法(引用)耗时:0.028863906860352左右
我去,这差距,第三种方法真是逆天了。但是再次提醒,这只是一次性读取多的数据的时候,当数据量很小的时候,相差无几,不一定非要用最高效率的,还可以通过懒加载等其他方式来实现。
顺便封装个类,可以增加一些填充什么的。更多的细节可以查看下面的类:
/**
* parent_id类型树结构相关
* 没必要非要写成静态的方法,静态方法参数太多,所以用实例在构造函数中修改参数更合适
* 需要首先将所有数据取出,然后再用此方法重新规划数组,其它的边取边查询数据库的方法不推荐
* 经测试第一种方法要快很多,建议使用
* @author vishun <nadirvishun@gmail.com>
*/ class Tree
{
/**
* 图标
*/
public $icon = '└';
/**
* 填充
*/
public $blank = ' ';
/**
* 默认ID字段名称
*/
public $idName = 'id';
/**
* 默认PID字段名称
*/
public $pidName = 'pid';
/**
* 默认名称字段名称
*/
public $titleName = 'name';
/**
* 默认子元素字段名称
*/
public $childrenName = 'items'; /**
* 构造函数,可覆盖默认字段值
* @param array $config
*/
function __construct($config = [])
{
if (!empty($config)) {
foreach ($config as $name => $value) {
$this->$name = $value;
}
}
} /**
* 生成下拉菜单可用树列表的方法
* 经测试4000条地区数据耗时0.02左右,比另外两种方法快超级多
* 流程是先通过引用方法来生成一种特殊树结构,再通过递归来解析这种特殊的结构
* @param array $list
* @param int $pid
* @param int $level
* @return array
*/
public function getTreeOptions($list, $pid = 0, $level = 0)
{
//先生成特殊规格的树
$tree = $this->getTree($list, $pid);
//再组装成select需要的形式
return $this->formatTree($tree, $level);
} /**
* 通过递归来解析特殊的树结构来组装成下拉菜单所需要的样式
* @param array $tree 特殊规格的数组
* @param int $level
* @return array
*/
protected function formatTree($tree, $level = 0)
{
$options = [];
if (!empty($tree)) {
$blankStr = str_repeat($this->blank, $level) . $this->icon;
if ($level == 0) {//第一次无需有图标及空格
$blankStr = '';
}
foreach ($tree as $key => $value) {
$options[$value[$this->idName]] = $blankStr . $value[$this->titleName];
if (isset($value[$this->childrenName])) {//查询是否有子节点
$optionsTmp = $this->formatTree($value[$this->childrenName], $level + 1);
if (!empty($optionsTmp)) {
//$options = array_merge($options, $optionsTmp);//发现一个问题,这里直接用array_merge会导致key重排
$options = $options+$optionsTmp;
//$options = ArrayHelper::merge($options, $optionsTmp);//如果是用yii2带话可以用助手类,需要use其命名空间
}
}
}
}
return $options;
} /**
* 生成类似下种格式的树结构
* 利用了引用&来实现,参照:http://blog.csdn.net/gxdvip/article/details/24434801
* [
* 'id'=>1,
* 'pid'=>0,
* 'items'=>[
* 'id'=>2,
* 'pid'=>'1'
* 。。。
* ]
* ]
* @param array $list
* @param int $pid
* @return array
*/
protected function getTree($list, $pid = 0)
{
$tree = [];
if (!empty($list)) {
//先修改为以id为下标的列表
$newList = [];
foreach ($list as $k => $v) {
$newList[$v[$this->idName]] = $v;
}
//然后开始组装成特殊格式
foreach ($newList as $value) {
if ($pid == $value[$this->pidName]) {
$tree[] = &$newList[$value[$this->idName]];
} elseif (isset($newList[$value[$this->pidName]])) {
$newList[$value[$this->pidName]][$this->childrenName][] = &$newList[$value[$this->idName]];
}
}
}
return $tree;
} /**
* 第二种方法,利用出入栈迭代来实现
* 经测试4000条地区数据耗时6.5s左右,比较慢
* @param $list
* @param int $pid
* @param int $level
* @return array
*/
public function getTreeOptions2($list, $pid = 0, $level = 0)
{
$tree = [];
if (!empty($list)) { //先将数组反转,因为后期出栈时会有限出最上面的
$list = array_reverse($list);
//先取出顶级的来压入数组$stack中,并将在$list中的删除掉
$stack = [];
foreach ($list as $key => $value) {
if ($value[$this->pidName] == $pid) {
array_push($stack, ['data' => $value, 'level' => $level]);//将层级记录下来,方便填充空格
unset($list[$key]);
}
}
while (count($stack)) {
//先从栈中取出第一项
$info = array_pop($stack);
//查询剩余的$list中pid与其id相等的,也就是查找其子节点
foreach ($list as $key => $child) {
if ($child[$this->pidName] == $info['data'][$this->idName]) {
//如果有子节点则入栈,while循环中会继续查找子节点的下级
array_push($stack, ['data' => $child, 'level' => $info['level'] + 1]);
unset($list[$key]);
}
}
//组装成下拉菜单格式
$blankStr = str_repeat($this->blank, $info['level']) . $this->icon;
if ($info['level'] == 0) {//第一次无需有图标及空格
$blankStr = '';
}
$tree[$info['data'][$this->idName]] = $blankStr . $info['data'][$this->titleName];
}
}
return $tree;
} /**
* 第三种普通列表转为下拉菜单可用的树列表
* 经测试4000条地区数据耗时8.7s左右,最慢
* @param array $list 原数组
* @param int $pid 起始pid
* @param int $level 起始层级
* @return array
*/
public function getTreeOptions3($list, $pid = 0, $level = 0)
{
$options = [];
if (!empty($list)) {
$blankStr = str_repeat($this->blank, $level) . $this->icon;
if ($level == 0) {//第一次无需有图标及空格
$blankStr = '';
}
foreach ($list as $key => $value) {
if ($value[$this->pidName] == $pid) {
$options[$value[$this->idName]] = $blankStr . $value[$this->titleName];
unset($list[$key]);//销毁已查询的,减轻下次递归时查询数量
$optionsTmp = $this->getTreeOptions3($list, $value[$this->idName], $level + 1);//递归
if (!empty($optionsTmp)) {
//$options = array_merge($options, $optionsTmp);//发现一个问题,这里直接用array_merge会导致key重排
$options = $options+$optionsTmp;
//$options = ArrayHelper::merge($options, $optionsTmp);//如果是用yii2带话可以用助手类,需要use其命名空间
}
}
}
}
return $options;
}
}
以上记录下,如转载请标明来源地址
无限极分类(adjacency list)的三种方式(迭代、递归、引用)的更多相关文章
- php 无限极分类,根据父级 找子级
/** * @param $data 数据(二维数组格式) * @param $adminId 管理员ID * @param $pid 权限的上级ID * @param int $level 水平变量 ...
- PHP无限极分类的几种方法
导读:项目开发,经常栏目要做到无限极分类,几种方法PHP无限极分类的几种方法 复制代码 代码如下:namespace Util;class Category{static public functio ...
- PHP实现无限极分类的两种方式,递归和引用
面试的时候被问到无限极分类的设计和实现,比较常见的做法是在建表的时候,增加一个PID字段用来区别自己所属的分类 $array = array( array('id' => 1, 'pid' =& ...
- PHP实现无限极分类的两种方式
无限极分类说简单点就是一个类可以分成一个分子类,然后一个子类又可以分另一个子类这样无限分下去,就是好象windows可以新建一个文件夹,然后在这个文件夹里又可以建一个文件夹,PHP要实现无限极分类有两 ...
- sqlalchemy tree 树形分类 无限极分类的管理。预排序树,左右值树。sqlalchemy-mptt
简介: 无限极分类是一种比较常见的数据格式,生成组织结构,生成商品分类信息,权限管理当中的细节权限设置,都离不开无限极分类的管理. 常见的有链表式,即有一个Pid指向上级的ID,以此来设置结构.写的时 ...
- C#无限极分类树-创建-排序-读取 用Asp.Net Core+EF实现之方法二:加入缓存机制
在上一篇文章中我用递归方法实现了管理菜单,在上一节我也提到要考虑用缓存,也算是学习一下.Net Core的缓存机制. 关于.Net Core的缓存,官方有三种实现: 1.In Memory Cachi ...
- thinkphp5.0无限极分类及格式化输出
首先我们来看数据表 从上图中可以发现,中国下有贵州,北京两个子节点,而北京有天安门一个子节点,纽约的子节点是"纽约的子类". 从pid为0看出,中国和纽约是顶级节点. 因为贵州的p ...
- DotNet菜鸟入门之无限极分类(一)设计篇
写这个教程的原因,是因为,无限极分类,在许多项目中,都用得到.而对于新手来说,不是很好理解,同时,操作上也有一些误区或者不当之处.所以我就斗胆,抛砖引玉一下,已一个常见的后台左侧频道树为例子,讲解一下 ...
- 【laravel54】关于用户权限认证RBAC和无限极分类
1.权限认证方面: https://packagist.org/packages/spatie/laravel-permission 用户认证 HTTP本身是无状态,通常在系统交互的过程中,使用账号或 ...
- PHP 商城无限极分类
无限极分类,用的是递归,在外部调用fen()方法即可 index是刚开始寻找的顶级分类,suo是为了在前端展示的时候缩进 ,$suo=){ 一个数组用来返回的 $t=[]; 这是查询数据库的所有内容 ...
随机推荐
- 【Java】几道常见的秋招面试题
前言 只有光头才能变强 Redis目前还在看,今天来分享一下我在秋招看过(遇到)的一些面试题(相对比较常见的) 0.final关键字 简要说一下final关键字,final可以用来修饰什么? 这题我是 ...
- CSharpGL(46)用Billboard绘制头顶文字
CSharpGL(46)用Billboard绘制头顶文字 本文介绍CSharpGL用Billboard绘制头顶文字的方法.效果如下图所示. 下载 CSharpGL已在GitHub开源,欢迎对OpenG ...
- spring boot 文件上传大小限制
错误信息 : Spring Boot:The field file exceeds its maximum permitted size of 1048576 bytes. 解决方法一:在启动类添加如 ...
- js 异步转同步
在项目中有些逻辑或者请求依赖另一个异步请求,大家常用的方法是回调函数.现在有个高大上的解决方案:await async . async 是“异步”的简写,而 await 可以认为是 async wai ...
- SpaceSyntax【空间句法】之DepthMapX学习:第一篇 数据的输入 与 能做哪些分析
两部分,1需要喂什么东西给软件,2它能干什么(输出什么东西在下一篇讲) 博客园/B站/知乎/CSDN @秋意正寒 转载请在头部附上源地址 目录:https://www.cnblogs.com/onsu ...
- Android之友盟多渠道打包与数据统计
文章大纲 一.多渠道打包与数据统计介绍二.友盟实现多渠道打包实战三.友盟数据统计实战四.项目源码下载五.参考文章 一.多渠道打包与数据统计介绍 多渠道打包,相信很多同学都知道.在Android ...
- scp传输提示bash: scp: command not found
其中一端缺少scp相关的包源[oracle@rac1 dump_dir]$ scp /mnt/dump_dir/expdp_orders_2tabs2* 192.168.X.247:/home/ora ...
- 【译】MongoDb vs Mysql—以NodeJs为例
亲爱的读者,您可能想知道为什么要写关于MongoDb和MySql这篇文章.那是因为我与NodeJs开发人员讨论在应用程序中使用哪种数据存储作为主要的数据存储方式. 我看过很多评论都在争论这个问题. 有 ...
- 第八周LINUX学习笔记
vsftpd丶NFS丶SAMBA nfs基于rpcsamba基于cifs(smb) DRBD: ftp:File Transfer protocol 文件传输协议 两个连接: tcp:命 ...
- 周一01.2 计算机硬件&操作系统
一.计算机硬件组成 1)控制器:是计算机的指挥系统 2)运算器:数学运算&逻辑运算 3)存储器:存取数据 内存:相当于人的短期记忆(缺点:断电数据会丢失:优点:存取速度快) 外存:相当于一个本 ...