PHP实现无限级分类的2种方法——父值与左右值2
来源:互联网 发布:游族网络 大皇帝 编辑:程序博客网 时间:2024/05/02 02:19
产品分类,多级的树状结构的论坛,邮件列表等许多地方我们都会遇到这样的问题:如何存储多级结构的数据?在PHP的应用中,提供后台数据存储的通常是关系型数据库,它能够保存大量的数据,提供高效的数据检索和更新服务。然而关系型数据的基本形式是纵横交错的表,是一个平面的结构,如果要将多级树状结构存储在关系型数据库里就需要进行合理的翻译工作。
层级结构的数据保存在平面的数据库中基本上有两种常用设计方法:
- 毗邻目录模式(adjacency list model)【这种方法说白了就是子类,依赖父类,父类依赖爷爷类,爷爷类可以有多个儿子类,跟父类平级的类。一层一层的】
- 预排序遍历树算法(modified preorder tree traversal algorithm)【mysql官方推荐的左右算法】
这里我用一个简单食品目录作为我们的示例数据。
我们的数据结构是这样的
- Food : 食物
- Fruit : 水果
- Red : 红色
- Cherry: 樱桃
- Yellow: 黄色
- Banana: 香蕉
- Meat : 肉类
- Beef : 牛肉
- Pork : 猪肉
1、毗邻目录模式(adjacency list model)
这种模式我们经常用到,很多的教程和书中也介绍过。我们通过给每个节点增加一个属性 parent 来表示这个节点的父节点从而将整个树状结构通过平面的表描述出来。根据这个原则,例子中的数据可以转化成如下的表:
以下是代码:
- +-----------------------+
- | parent | name |
- +-----------------------+
- | Food | Fruit |
- | Fruit | Green |
- | Green | Pear |
- | Fruit | Red |
- | Red | Cherry |
- | Fruit | Yellow |
- | Yellow | Banana |
- | Food | Meat |
- | Meat | Beef |
- | Meat | Pork |
- +-----------------------+
显示多级树,如果我们需要显示这样的一个多级结构需要一个递归函数。
以下是代码:
< ?php
// $parent is the parent of the children we want to see, $level is increased when we go deeper into the tree,used to display a nice indented tree
function display_children($parent, $level) {
// 获得一个 父节点 $parent 的所有子节点
$result = mysql_query("
SELECT name
FROM tree
WHERE parent = '" . $parent . "'
;");
// 显示每个子节点
while ($row = mysql_fetch_array($result)) {
// 缩进显示节点名称
echo str_repeat(' ', $level) . $row['name'] . "\n";
//再次调用这个函数显示子节点的子节点
display_children($row['name'], $level+1);
}
}
?>
对整个结构的根节点(Food)使用这个函数就可以打印出整个多级树结构,由于Food是根节点它的父节点是空的,所以这样调用: display_children('',0)。将显示整个树的内容:
- Food
- Fruit
- Red
- Cherry
- Yellow
- Banana
- Meat
- Beef
- Pork
几乎使用同样的方法我们可以知道从根节点到任意节点的路径。比如 Cherry 的路径是 "Food >; Fruit >; Red"。 为了得到这样的一个路径我们需要从最深的一级"Cherry"开始, 查询得到它的父节点"Red"把它添加到路径中, 然后我们再查询Red的父节点并把它也添加到路径中,以此类推直到最高层的"Food",以下是代码:
<? php
// $node 是那个最深的节点
function get_path($node) {
// 查询这个节点的父节点
$result = mysql_query("
SELECT parent
FROM tree
WHERE name = '" . $node ."'
;");
$row = mysql_fetch_array($result);
// 用一个数组保存路径
$path = array();
// 如果不是根节点则继续向上查询,(根节点没有父节点)
if ($row['parent'] != '') {
// the last part of the path to $node, is the name of the parent of $node
$path[] = $row['parent'];
// we should add the path to the parent of this node to the path
$path = array_merge(get_path($row['parent']), $path);
}
// return the path
return $path;
}
?>
如果对"Cherry"使用这个函数:print_r(get_path('Cherry')),就会得到这样的一个数组了:
- Array (
- [0] => Food
- [1] => Fruit
- [2] => Red
- )
这种方法很简单,容易理解,好上手。但是也有一些缺点。主要是因为运行速度很慢,由于得到每个节点都需要进行数据库查询,数据量大的时候要进行很多查询才能完成一个树。另外由于要进行递归运算,递归的每一级都需要占用一些内存所以在空间利用上效率也比较低。
2、预排序遍历树算法
现在让我们看一看另外一种不使用递归计算,更加快速的方法,这就是预排序遍历树算法(modified preorder tree traversal algorithm)
这种方法大家可能接触的比较少,初次使用也不像上面的方法容易理解,但是由于这种方法不使用递归查询算法,有更高的查询效率。
我们首先将多级数据按照下面的方式画在纸上,在根节点Food的左侧写上 1 然后沿着这个树继续向下 在 Fruit 的左侧写上 2 然后继续前进,沿着整个树的边缘给每一个节点都标上左侧和右侧的数字。最后一个数字是标在Food 右侧的 18。 在下面的这张图中你可以看到整个标好了数字的多级结构。(没有看懂?用你的手指指着数字从1数到18就明白怎么回事了。还不明白,再数一遍,注意移动你的手指)。
这些数字标明了各个节点之间的关系,"Red"的号是3和6,它是 "Food" 1-18 的子孙节点。 同样,我们可以看到 所有左值大于2和右值小于11的节点 都是"Fruit" 2-11 的子孙节点
- 1 Food 18
- |
- +------------------------------+
- | |
- 2 Fruit 11 12 Meat 17
- | |
- +-------------+ +--------------+
- | | | |
- 3 Red 6 7 Yellow 10 13 Beef 14 15 Pork 16
- | |
- 4 Cherry 5 8 Banana 9
以下是代码:
- +----------+------------+-----+-----+
- | parent | name | lft | rgt |
- +----------+------------+-----+-----+
- | | Food | 1 | 18 |
- | Food | Fruit | 2 | 11 |
- | Fruit | Red | 3 | 6 |
- | Red | Cherry | 4 | 5 |
- | Fruit | Yellow | 7 | 10 |
- | Yellow | Banana | 8 | 9 |
- | Food | Meat | 12 | 17 |
- | Meat | Beef | 13 | 14 |
- | Meat | Pork | 15 | 16 |
- +----------+------------+-----+-----+
注意:由于"left"和"right"在 SQL中有特殊的意义,所以我们需要用"lft"和"rgt"来表示左右字段。 另外这种结构中不再需要"parent"字段来表示树状结构。也就是 说下面这样的表结构就足够了。
以下是代码:
- +------------+-----+-----+
- | name | lft | rgt |
- +------------+-----+-----+
- | Food | 1 | 18 |
- | Fruit | 2 | 11 |
- | Red | 3 | 6 |
- | Cherry | 4 | 5 |
- | Yellow | 7 | 10 |
- | Banana | 8 | 9 |
- | Meat | 12 | 17 |
- | Beef | 13 | 14 |
- | Pork | 15 | 16 |
- +------------+-----+-----+
- SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;
- +------------+-----+-----+
- | name | lft | rgt |
- +------------+-----+-----+
- | Fruit | 2 | 11 |
- | Red | 3 | 6 |
- | Cherry | 4 | 5 |
- | Yellow | 7 | 10 |
- | Banana | 8 | 9 |
- +------------+-----+-----+
- SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;
以下是代码:
<? php
function display_tree($root) {
// 得到根节点的左右值
$result = mysql_query("
SELECT lft, rgt
FROM tree
WHERE name = '" . $root . "'
;");
$row = mysql_fetch_array($result);
// 准备一个空的右值堆栈
$right = array();
// 获得根基点的所有子孙节点
$result = mysql_query("
SELECT name, lft, rgt
FROM tree
WHERE lft BETWEEN '" . $row['lft'] . "' AND '" . $row['rgt'] ."'
ORDER BY lft ASC
;");
// 显示每一行
while ($row = mysql_fetch_array($result)) {
// only check stack if there is one
if (count($right) > 0) {
// 检查我们是否应该将节点移出堆栈
while ($right[count($right) - 1] < $row['rgt']) {
array_pop($right);
}
}
// 缩进显示节点的名称
echo str_repeat(' ',count($right)) . $row['name'] . "\n";
// 将这个节点加入到堆栈中
$right[] = $row['rgt'];
}
}
?>
如果你运行一下以上的函数就会得到和递归函数一样的结果。只是我们的这个新的函数可能会更快一些,因为只有2次数据库查询。
要获知一个节点的路径就更简单了,如果我们想知道Cherry 的路径就利用它的左右值4和5来做一个查询。
- SELECT name FROM tree WHERE lft < 4 AND rgt >; 5 ORDER BY lft ASC;
- +------------+
- | name |
- +------------+
- | Food |
- | Fruit |
- | Red |
- +------------+
- descendants = (right – left - 1) / 2
这的确是个很好的办法,但是有什么办法能够帮我们建立这样有左右值的数据表呢?这里再介绍一个函数给大家,这个函数可以将name和parent结构的表自动转换成带有左右值的数据表。
以下是代码:
< ?phpfunction rebuild_tree($parent, $left) {
// the right value of this node is the left value + 1
$right = $left+1;
// get all children of this node
$result = mysql_query("
SELECT name
FROM tree
WHERE parent = '" . $parent . "'
;");
while ($row = mysql_fetch_array($result)) {
// recursive execution of this function for each child of this node
// $right is the current right value, which is incremented by the rebuild_tree function
$right = rebuild_tree($row['name'], $right);
}
// we've got the left value, and now that we've processed the children of this node we also know the right value
mysql_query("
UPDATE tree
SET
lft = '" . $left . "',
rgt= '" . $right . "'
WHERE name = '" . $parent . "'
;");
// return the right value of this node + 1
return $right + 1;
}
?>
当然这个函数是一个递归函数,我们需要从根节点开始运行这个函数来重建一个带有左右值的树
rebuild_tree('Food',1);[/php]
这个函数看上去有些复杂,但是它的作用和手工对表进行编号一样,就是将立体多层结构的转换成一个带有左右值的数据表。
那么对于这样的结构我们该如何增加,更新和删除一个节点呢?
增加一个节点一般有两种方法:
第一种,保留原有的name 和parent结构,用老方法向数据中添加数据,每增加一条数据以后使用rebuild_tree函数对整个结构重新进行一次编号。
第二种,效率更高的办法是改变所有位于新节点右侧的数值。举例来说:我们想增加一种新的水果"Strawberry"(草莓)它将成为"Red"节点的最后一个子节点。首先我们需要为它腾出一些空间。"Red"的右值应当从6改成8,"Yellow 7-10 "的左右值则应当改成 9-12。 依次类推我们可以得知,如果要给新的值腾出空间需要给所有左右值大于5的节点 (5 是"Red"最后一个子节点的右值) 加上2。 所以我们这样进行数据库操作:
- UPDATE tree SET rgt = rgt + 2 WHERE rgt > 5;
- UPDATE tree SET lft = lft + 2 WHERE lft > 5;
- INSERT INTO tree SET lft=6, rgt=7, name='Strawberry';
四、关于预排序遍历算法
某个节点到底有多少子孙节点?
子孙总数 =(父节点的右值 -父节点的左值-1)/2
以节点“食品”举例,其子孙总数=(11-2-1)/ 2 = 4
如何判断某一节点下有没有子节点?
当该节点左值-1等于其右值时,其下没有子节点。
检索某一父节点的所有子节点?
假定我们要对节点“食品”及其子孙节点进行先序遍历的列表,只需使用如下一条sql语句:
SELECT * FROM `tree` WHERE `lft` BETWEEN 2 AND 11 ORDER BY `lft` ASC
检索之后如何列表?
当左值+1==右值时,该节点没有子节点,则下一节点不为其子节点
若下一节点的左值==上一节点右值+1,则2个节点是同级关系
若下一节点的左值==上一节点的左值+1时,则第2个节点应是第一个节点的子节点
若下一节点的左值-上一节点的右值>1时,则下一节点比上一节点高(下一节点的左值-上一节点的右值)级
在某一父节点下添加一个子节点?
1. 要求该子节点为该父节点下排序第一的节点,则$left_node =父节点left_node+1, $right_node = $left_node+1;
2. 要求该节点位于父节点下一个子节点A后面,则$left_node = 节点A的right_node+1, $right_node = $left_node+1;
3. 要求该节点是位于父节点下排序最后一位的节点,则$left_node =父节点right_node, $right_node = $left_node+1;
Sql:
UPDATE `tree` SET `right_node`=`right_node`+2 WHERE `right_node`>=$left_node
UPDATE `tree` SET `left_node`=`left_node`+2 WHERE `left_node`>=$left_node
INSERT INTO `tree` (`name` , `left_node` , `right_node`) VALUES
(`名字` , $left_node , $right_node)
移动节点,包括其子节点至节点A下?
设该节点左值$left_node ,右值$right_node
其子节点的数目为$count = ($right_node - $left_node -1 )/2 ,节点A左值为$A_left_node ,
UPDATE `tree` SET `right_node`=`right_node`-$right_node-$left_node-1 WHERE `right_node`$amp;>right_node AND `right_node`$amp;UPDATE `tree` SET `left_node`=`left_node`-$right_node-$left_node-1 WHERE `left_node`$amp;>right_node AND `left_node`<=$A_left_node
UPDATE `tree` SET `left_node`=`left_node`+$A_left_node-$right_node , `right_node`=`right_node`+$A_left_node-$right_node WHERE `left_node`>=$left_node
AND `right_node`<=$right_node
删除所有子节点?
DELETE FROM `tree` WHERE `left_node`>父节点的左值 AND `right_node`>父节点的右值
五、关于左右值无限级分类
什么是左右值无限级分类:
左右值无限级分类,也称为预排序树无限级分类,是一种有序的树状结构,位于这些树状结构中的每一个节点都有一个“左值”和“右值”,其规则是:每一个后代节点的左值总是大于父类,右值总是小于父级,右值总是小于左值。处于这些结构中的每一个节点,都可以轻易的算出其祖先或后代节点。因此,可以用它来实现无限分类。
左右值无限分类的优缺点:
优点:
通过一条SQL就可以获取所有的祖先或后代,这在复杂的分类中非常必要
通过简单的四则运算就可以得到后代的数量
缺点
分类操作麻烦
无法简单的获取子代
1. 测试数据准备
- CREATE TABLE `nested_category` (
- `category_id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
- `name` varchar(18) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
- `lft` int(4) NOT NULL,
- `rgt` int(4) NOT NULL,
- KEY `category_id` (`category_id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
- INSERT INTO `nested_category` VALUES
- (1,'商品',1,26),
- (2,'化妆品',2,7),
- (3,'食品',8,9),
- (4,'酒',10,15),
- (5,'服装',16,17),
- (6,'家电',18,23),
- (7,'鞋帽',24,25),
- (8,'面霜',3,4),
- (9,'面膜',5,6),
- (10,'白酒',11,12),
- (11,'红酒',13,14),
- (12,'冰箱',19,20),
- (13,'空调',21,22);
- 数据查看
- mysql> select * from nested_category;
- +-------------+-----------+-----+-----+
- | category_id | name | lft | rgt |
- +-------------+-----------+-----+-----+
- | 1 | 商品 | 1 | 26 |
- | 2 | 化妆品 | 2 | 7 |
- | 3 | 食品 | 8 | 9 |
- | 4 | 酒 | 10 | 15 |
- | 5 | 服装 | 16 | 17 |
- | 6 | 家电 | 18 | 23 |
- | 7 | 鞋帽 | 24 | 25 |
- | 8 | 面霜 | 3 | 4 |
- | 9 | 面膜 | 5 | 6 |
- | 10 | 白酒 | 11 | 12 |
- | 11 | 红酒 | 13 | 14 |
- | 12 | 冰箱 | 19 | 20 |
- | 13 | 空调 | 21 | 22 |
- +-------------+-----------+-----+-----+
分析一下两种情况:
插入最顶级节点:它的左右值与该树中最大的右值有关:左值=最大右值+1,右值=最大右值+2,你可以自己模拟一下;
插入子节点:它的左右值与它的父级有关:左值=父级的右值,右值=当前的左值+1,这时要更新的数据有:父级的右值,所有左值大于父级左级,右值大于低级右值的节点左右值都应该+2;
3. 获取所有的后代节点
从图中可以看出找出某个节点的所有子节点,lft 大于左值 rgt 小于右值
- select * from nested_category where lft > 18 and rgt < 23;
- +-------------+--------+-----+-----+
- | category_id | name | lft | rgt |
- +-------------+--------+-----+-----+
- | 12 | 冰箱 | 19 | 20 |
- | 13 | 空调 | 21 | 22 |
- +-------------+--------+-----+-----+
- 2 rows in set (0.00 sec)
有人认为使用 count 配合上面的语句就可以算出,这当然是可以的,但有更快的方式:每有子类节点中每个节点占用两个值,而这些值都是不一样且连续的,那些就可以计算出子代的数 量=(右值-左值-1)/2。减少1的原因是排除该节点,你可以想像一个,一个单节点,左值是1,右值是2,没有子类节点,而这时它的右值-左值=1.
5. 检索单一路径
方法:通过上述结果可以发子类的lft,rgt 都可以父类的lft,rgt之前
- select
- parent.name,
- parent.category_id,
- parent.lft,
- parent.rgt
- from
- nested_category as node, nested_category as parent
- where
- node.lft between parent.lft and parent.rgt and node.name = '空调'
- order by parent.lft;
- +--------+-------------+-----+-----+
- | name | category_id | lft | rgt |
- +--------+-------------+-----+-----+
- | 商品 | 1 | 1 | 26 |
- | 家电 | 6 | 18 | 23 |
- | 空调 | 13 | 21 | 22 |
- +--------+-------------+-----+-----+
- 3 rows in set (0.00 sec)
检索出所有的叶子节点,使用嵌套集合模型的方法比邻接表模型的LEFT JOIN方法简单多了。如果你仔细得看了nested_category表,你可能已经注意到叶子节点的左右值是连续的。要检索出叶子节点,我们只要查找满足rgt=lft+1的节点:
- select
- *
- from
- nested_category
- where rgt = lft + 1;
- +-------------+--------+-----+-----+
- | category_id | name | lft | rgt |
- +-------------+--------+-----+-----+
- | 3 | 食品 | 8 | 9 |
- | 5 | 服装 | 16 | 17 |
- | 7 | 鞋帽 | 24 | 25 |
- | 8 | 面霜 | 3 | 4 |
- | 9 | 面膜 | 5 | 6 |
- | 10 | 白酒 | 11 | 12 |
- | 11 | 红酒 | 13 | 14 |
- | 12 | 冰箱 | 19 | 20 |
- | 13 | 空调 | 21 | 22 |
- +-------------+--------+-----+-----+
- 9 rows in set (0.01 sec)
- select
- node.name as name, (count(parent.name) - 1) as deep
- from
- nested_category as node,
- nested_category as parent
- where node.lft between parent.lft and parent.rgt
- group by node.name
- order by node.lft
- +-----------+------+
- | name | deep |
- +-----------+------+
- | 商品 | 0 |
- | 化妆品 | 1 |
- | 面霜 | 2 |
- | 面膜 | 2 |
- | 食品 | 1 |
- | 酒 | 1 |
- | 白酒 | 2 |
- | 红酒 | 2 |
- | 服装 | 1 |
- | 家电 | 1 |
- | 冰箱 | 2 |
- | 空调 | 2 |
- | 鞋帽 | 1 |
- +-----------+------+
- 13 rows in set (0.03 sec)
可以想象一下,你在零售网站上呈现电子产品的分类。当用户点击分类后,你将要呈现该分类下的产品,同时也需列出该分类下的直接子分类,而不是该分类下的全部分类。为此,我们只呈现该节点及其直接子节点,不再呈现更深层次的节点
如上述获取深度的例子,可以根椐深度来小于等于1获得直接子节点
- select * from (
- select
- node.name as name,
- (count(parent.name) - 1) as deep
- from
- nested_category as node,
- nested_category as parent
- where node.lft between parent.lft and parent.rgt
- group by node.name
- order by node.lft
- ) as a where a.deep <= 1;
- +-----------+------+
- | name | deep |
- +-----------+------+
- | 商品 | 0 |
- | 化妆品 | 1 |
- | 食品 | 1 |
- | 酒 | 1 |
- | 服装 | 1 |
- | 家电 | 1 |
- | 鞋帽 | 1 |
- +-----------+------+
- 7 rows in set (0.00 sec)
删除分类是基础操作,删除分类的处理过程跟节点在分层中所处的位置是有关,在删除时需要考虑两种情况,1 删除单个的叶子分类 2 删除子节点 相对而言删除单个的叶子分类比较简单,
就好比新增的逆过程,我们删除节点的同时该节点右边所有的左右值和该父节点的右值都会减去该节点的宽度值
- lock table nested_category write;
- select @myleft := lft, @myright := rgt, @mywidth := rgt-lft+1 from nested_category where name = '家电';
- delete from nested_category where lft between @myleft and @myright;
- update nested_category set lft = lft - @mywidth where rgt > @myright
- update nested_category set rgt = rgt - @mywidth where lft > @myright
- unlock tables;
- PHP实现无限级分类的2种方法——父值与左右值2
- PHP实现无限级分类的2种方法——父值与左右值1
- PHP无限分类——左右值实现
- PHP无限分类-左右值实现
- PHP + MySQL 实现无限分类的2种方法
- 左右值无限分类实现算法[转]
- 左右值无限分类实现算法
- 左右值无限分类实现算法
- 左右值无限分类实现算法[转]
- 左右值无限分类实现算法
- 无限分类左右值实现算法
- 基于thinkphp的左右值无限分类
- 基于左右值的无限分类算法
- 左右值无限级分类算法
- 采用左右值无限级分类
- 无限级分类,左右值算法
- 【Mysql左右值】左右值法实现Mysql无限级分类-代码例子
- PHP无限级分类简单实现方法
- Secure Delivery Center常见用例(一)
- 冒泡算法
- Spring applicationContext.xml文件解析
- exec函数族用法
- uva 11462 - Age Sort
- PHP实现无限级分类的2种方法——父值与左右值2
- 遍历Map集合
- 安装jdk配置环境变量
- iOS 收起键盘
- 五个习惯——让你的工作更高效
- 颜色基础知识——CIE 1931色度坐标
- solr ------ 名词解释
- Oracle运维操作集锦[持续更新]
- iOS面试必备看看总有好处