无限极分类 递归
建表:
涉及无限极分类的问题建表时,都会有一个专门的分类表,表中有一个pid字段默认为0,属于一级分类,pid字段关联当前分类表的id,用来标明当前分类所属的上级分类,例如建一个问题分类表的语句如下
CREATE TABLE `quescate` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '问题分类id',
`title` varchar(250) NOT NULL COMMENT '问题分类标题',
`pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '所属问题id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='问题分类表'
一、无限极分类的实现
接下来取出各级分类及分类下的子分类,自己做时用了最笨的办法:
(1)先查找一级分类(pid=0)
(2)通过上级分类查找的id作为pid,来查找下一级分类
(3)重复步骤(2)
代码实现如下
<?PHP
$cate = array();
// 一级分类
$cate_0 = $this->where('pid = 0')->select();
if (!empty($cate_0)){
// 二级分类
$ids_1 = array_map('reset', $cate_0); // 使用array_column()更简单,但是需要>=PHP5.5.0的版本
$map_1['pid'] = array('in', $ids_1);
$cate_1 = $this->where($map_1)->select();
if(!empty($cate_1)){
// 三级分类
$ids_2 = array_map('reset', $cate_1);
$map_2['pid'] = array('in', $ids_2);
$cate_2 = $this->where($map_2)->select();
}
$cate = array(0=>$cate_0, 1=>$cate_1, 2=>$cate_2);
}
unset($ids_1, $map_1, $ids_2, $map_2);
return $cate;
但是这样的办法,有几级分类就要查找几次,太过麻烦,也会造成代码冗余。根据上述步骤很轻易看出,这其实就是一个递归。于是在优化代码的过程中,可以通过递归实现无限极分类,通过查找资料还找到了引用,栈的实现方法,下面一一介绍,为了更直观,使用的数据如下
<?php
$res = array(
array('id' => 8, 'title' => '美国', 'pid' => 0),
array('id' => 9, 'title' => '纽约', 'pid' => 8),
array('id' => 10, 'title' => '韩国', 'pid' => 0),
array('id' => 11, 'title' => '日本', 'pid' => 0),
array('id' => 12, 'title' => '加拿大', 'pid' => 0),
array('id' => 13, 'title' => '中国', 'pid' => 0),
array('id' => 14, 'title' => '华盛顿', 'pid' => 8),
array('id' => 16, 'title' => '洛杉矶', 'pid' => 8),
array('id' => 17, 'title' => '北京', 'pid' => 13),
array('id' => 18, 'title' => '上海', 'pid' => 13),
array('id' => 19, 'title' => '广东', 'pid' => 13),
array('id' => 20, 'title' => '深圳', 'pid' => 13),
array('id' => 21, 'title' => '杭州', 'pid' => 13)
);
- 递归,实现无限极分类。递归应该是最容易想到的方法了。其实递归函数也是借助于栈的机制实现的,但是底层对于栈的处理对于程序员来说都是透明的,程序员只需要关心应用的实现逻辑。所以说使用递归处理上述问题理解起来比较容易,代码也比较简洁。在我看来,最容易理解的方法应该是使用栈的方式实现无限极分类了(可以参考以下的第三种方法)
主要参考博客:
php实现无限级分类查询(递归、非递归)中开头部分递归与栈的讲解
PHP实现无限极分类的两种方式,递归和引用中的递归方法
<?php
/**
* 递归实现无限极分类
* @param array $array 分类数据
* @param Int $pid 父ID
* @param Int $level 分类级别
* @return Array $list 分好类的数组 直接遍历即可 $level可以用来遍历缩进
*/
function getTree($array, $pid =0, $level = 0){
//声明静态数组,避免递归调用时,多次声明导致数组覆盖
static $list = [];
foreach ($array as $key => $value){
//第一次遍历,找到父节点为根节点的节点 也就是pid=0的节点
if ($value['pid'] == $pid){
//父节点为根节点的节点,级别为0,也就是第一级
$value['level'] = $level;
//把数组放到list中
$list[] = $value;
//把这个节点从数组中移除,减少后续递归消耗
unset($array[$key]);
//开始递归,查找父ID为该节点ID的节点,级别则为原级别+1
$this->getTree($array, $value['id'], $level+1);
}
}
return $list;
}
// 无限极分类的第一种方法:递归
$list = getTree($res, 0, 1);
/*
* 以下是根据自己需要对分好类的数据进行的处理,这里为了保持与上边需要的数据形式一致
*/
foreach ($list as $key=>$value){
if ($value['level'] == 1){
$cate[0][] = $value; // 一级分类
}elseif ($value['level'] == 2){
$cate[1][] = $value; // 二级分类
}
}
return $cate;
?>
- 引用,实现无限极分类。此方法非常巧用引用。如果不了解引用,对下面方法的理解是困难的,建议参考PHP手册引用一章
主要参考博客:PHP无限极分类实现中的方法一,里面的方法二只是判断将三目运算符改成了if判断,喜欢的可以看看
<?php
/**
* 将数据格式转换成树形结构数组
* @param array $items 要进行转换的数组
* @return array $items 转换完成的数组
*/
function arrayToTree(Array $items) {
foreach ($items as $key=>$item) {
$items[$item['pid']]['son'][$key] = &$items[$key];
}
return isset($items[0]['son']) ? $items[0]['son'] : array();
}
$cate[1] = array(); // 预定义二级分类为空
// 将数组索引改为id值
$res = array_reduce($res, create_function('$res, $val', '$res[$val["id"]] = $val; return $res;'));
$list = $this->arrayToTree($res);
/*
* 以下是根据自己需要对分好类的数据进行的处理,这里为了保持与上边需要的数据形式一致
*/
foreach ($list as $ke=>$val){
if (isset($val['son'])){
$cate[1] += $val['son']; // 二级菜单
unset($val['son']); // 从二级菜单中去除一级菜单
}
$cate[0][] = $val; // 一级菜单
}
return $cate;
?>
- 栈,实现无限极分类。栈的实现方式应该算是最容易理解的了,当然栈的核心:“先进后出”是必须要知道才能理解好下面的方法的。有关于栈,有兴趣的可以查一查《数据结构(C语言版)》中对于栈的讲解。
主要参考博客:php实现无限级分类查询(递归、非递归)中的“非递归,即使用栈机制实现无限级栏目的查询”
<?php
/**
* 非递归,即使用栈机制实现无限级栏目的查询
* 其实栈的核心机制也就四个字:先进后出。
* 在这对于栈的机制不多说,主要说一下如何借助栈实现无限级栏目查询:
* 1. 首先将顶级栏目压入栈中
* 2. 将栈顶元素出栈
* 3. 将出栈元素存入数组中,标记其深度(其深度就是在其父栏目的深度上面加1)
* 4. 以出栈的元素为父栏目,查找其子栏目
* 5. 将查找到的子栏目入栈,重复步骤2
* 6. 判断栈为空的话,流程结束;
* @param Array $array 要进行栈操作的数组
* @return Array $list 分好类的数组
*/
// 自定义入栈函数
function pushStack(&$stack, $channel, $dep)
{
$channel['dep'] = $dep;
array_push($stack, $channel);
}
// 自定义出栈函数
function popStack(&$stack)
{
return array_pop($stack);
}
function getStack($array)
{
$stack = array(); //定义一个空栈
$list = array(); //用来保存各个栏目之间的关系以及该栏目的深度
/*
* 首先将顶级栏目压入栈中
* */
foreach ($array as $key => $val) {
if ($val['pid'] == 0)
pushStack($stack, $val, 1);
}
/*
* 将栈中的元素出栈,查找其子栏目
* */
do {
$par = popStack($stack); //将栈顶元素出栈
/*
* 查找以此栏目为父级栏目的id,将这些栏目入栈
* */
foreach ($array as $key=>$value){
if ($value['pid'] == $par['id']){
pushStack($stack, $value, $par['dep']+1);
}
}
// 将出栈的栏目以及该栏目的深度保存到数组中
$list[] = $par;
} while (count($stack) > 0);
return $list;
}
//$res = $this->order('id desc')->select(); // 因为栈先进后出的特点,分类完后的数据会是原数据的倒序排序。这可以在处理数据前,将数据先倒序输出,这样得到的数据就是正序的。
$res = array_reverse($res);
$list = getStack($res);
foreach ($list as $key=>$value){
if ($value['dep'] == 1){
$cate[0][] = $value; // 一级分类
}elseif ($value['dep'] == 2){
$cate[1][] = $value; // 二级分类
}
}
return $cate;
?>
总结:通过以上三种方法可以发现:递归会是最容易想到也最常用到的一种方式,比较容易理解使用方便;引用使用非常巧妙而且代码简洁但是不太好理解;栈的实现最容易理解,但要理解并注意栈的特点:先进后出。
二、根据分类id倒着查找所有的所属父分类信息
本来想将这部分放在下一篇博客讲解的,但想趁热打铁,索性放在一篇博客里总结完。
项目中有时候需要根据分类的id倒回去查找所有所属的父级分类信息,比如面包屑,前端菜单栏里只允许展示有具体信息的分类时,我们就要涉及到这个查找。同样的这个查找也需要用到递归的实现。下面是两种递归的实现方法,但大同小异,基本原理都一样。
主要参考博文:php实现无限极分类中的“根据子类id查找出所有父级分类信息”
- 方法一:Yii2框架中的方法
<?php
/**
* Yii2框架中,根据子类id查找所有父级分类的方法
* @param array $arr 所有分类列表
* @param Int $id 父级分类id
* @return array $list 所有父级分类信息
*/
function get_parent_list($arr,$id){
static $list=array();
foreach($arr as $u){
if($u['id']== $id){//父级分类id等于所查找的id
$list[]=$u;
if($u['pid']>0){
get_parent_list($arr,$u['pid']);
}
}
}
return $list;
}
$cate = get_parent_list($res, $pid); // 此处$pid根据查到的具体分类信息中的pid传值查询所属上级分类
$cate = array_reverse($cate); // 此处根据自己需要做处理,我这里为了页面展示方便做了数组倒序处理
?>
- 方法二:递归查询分类信息,与方法一大同小异
<?php
function get_parents($id){
static $list = [];
$cateModel = new QuescateModel();
$cate = $cateModel->where('id = '.$id)->find(); // 此处就是查询分类表中一条语句
if($cate){
$list[] = $cate;
$id = $cate['pid'];
if($cate['pid'] > 0){
self::get_parents($id);
}
}
return $list;
}
$cate = get_parents($pid); // 根据需要查要查询分级分类的分类id
$cate = array_reverse($cate); // 此处我是根据自己前端展示分方便最数组做了倒序处理
?>
自关联
无限级自关联表的排序及下拉框展示
数据结构
部门表 sys_department
员工表 sys_staff
建表语句
功能需求
部门排序
树状图展示:部门+员工
下拉框1:部门
下拉框2:部门+员工
功能实现
部门排序
树状图
下拉框1:部门
下拉框2:部门+员工
数据结构
部门表 sys_department
通过id和parent_id无限级自关联,唯一根父级为0,同时有一个排序字段order_num,对于同一父级下的所有子部门,通过此字段排序,还有一个“level”字段,维护层级深度
员工表 sys_staff
通过department_id与部门表的id关联
建表语句
DROP TABLE IF EXISTS sys_department;
CREATE TABLE sys_department (
id int(11) NOT NULL AUTO_INCREMENT,
parent_id int(11) DEFAULT ‘0’ COMMENT ‘父级部门id’,
name_en varchar(200) DEFAULT ‘’ COMMENT ‘英文名’,
name_cn varchar(200) DEFAULT ‘’ COMMENT ‘中文名’,
level int(11) DEFAULT ‘0’,
order_num int(11) DEFAULT ‘99’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS sys_staff;
CREATE TABLE sys_staff (
id int(11) NOT NULL AUTO_INCREMENT,
department_id int(11) DEFAULT ‘0’ COMMENT ‘部门id’,
name_en varchar(200) DEFAULT ‘’ COMMENT ‘英文名’,
name_cn varchar(200) DEFAULT ‘’ COMMENT ‘中文名’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;
功能需求
部门排序
先展示第一个父级部门数据,之后紧跟其下的第一个子部门数据,如果子部门还有子部门继续往下展示,如果没有再展示第二个子部门……
树状图展示:部门+员工
本文使用zTree插件,zTree教程,在此仅讨论数据
下拉框1:部门
下拉框使用插件:select2,附教程
顺序同“部门排序”,有缩进层级效果,所有部门可选
下拉框2:部门+员工
顺序同“部门排序”,没有关联上部门的员工放最前面,所有部门不可选,所有员工可选
功能实现
部门排序
网上自关联的排序很多,但都是按id排的,稍微修改一下,使其按照排序字段order_num排序
先创建一个排序函数
delimiter ~
DROP FUNCTION getPriorityDept~
CREATE FUNCTION getPriorityDept(inID INT) RETURNS VARCHAR(255) DETERMINISTIC
begin
DECLARE gParentID INT DEFAULT 0;
DECLARE gPriority VARCHAR(255) DEFAULT '';
SELECT parent_id,LPAD(order_num,4, 0) INTO gParentID,gPriority FROM sys_department WHERE ID = inID;
WHILE gParentID > 0 DO /*0为根*/
SELECT parent_id,CONCAT(LPAD(order_num,4, 0), '.', gPriority) INTO gParentID,gPriority FROM sys_department WHERE ID = gParentID;
END WHILE;
RETURN gPriority;
end ~
delimiter ;
然后直接调用就行
SELECT * FROM sys_department ORDER BY getPriorityDept(id);
树状图
使用zTree的简单数据模式
simpleData: {
enable: true,
idKey: "id",
pIdKey: "pid",
rootPId: 0
},
数据的查询SQL如下
#员工表数据
(
SELECT a.id id,CONCAT('d-',b.id) pid,a.name_cn `name`
FROM sys_staff a
LEFT JOIN sys_department b ON a.department_id = b.id
) UNION ALL
#部门表数据
(
SELECT CONCAT('d-',id) id,CONCAT('d-',parent_id) pid,name_cn `name` FROM sys_department
)
下拉框1:部门
首先修改一下select2源码中的样式
在select2.js文件中查找“Results.prototype.option”方法,在最终返回前插入代码,依据数据中的“level”属性动态添加缩进样式
if(data.level){
var leftPx = 30 * parseInt(data.level);
option.style.paddingLeft = leftPx + "px";
}
然后直接调用上面的排序方法就可以了
SELECT * FROM sys_department ORDER BY getPriorityDept(id);
下拉框2:部门+员工
直接查询,将数据交给select2插件处理,部门在下拉框中作为组,不可选
<resultMap id="departmentStaff" type="map">
<id column="did" property="id"/>
<result column="name_cn" property="nameCn"/>
<collection property="children" javaType="java.util.ArrayList" ofType="map">
<result column="id" property="id"/>
<result column="text" property="text"/>
</collection>
</resultMap>
<select id="findDepartmentStaff" resultMap="departmentStaff">
(SELECT a.id did,a.name_cn name_cn,b.id,b.name_cn text FROM sys_department a
LEFT JOIN (SELECT * FROM sys_staff WHERE del_flag = 0 ) b ON a.id = b.department_id )
UNION ALL
(SELECT 0 did,'*无部门*' name_cn,a.id,a.name_cn text FROM sys_staff a LEFT JOIN sys_department b ON b.id = a.department_id
WHERE a.del_flag = 0 AND b.id IS NULL)
ORDER BY getPriorityDept(did)
</select>