设计无限级分类

产品分类,多级的树状结构的论坛,邮件列表等许多地方我们都会遇到这样的问题:如何存储多级结构的数据?在PHP的应用中,提供后台数据存储的通常是关系 型数据库,它能够保存大量的数据,提供高效的数据检索和更新服务。然而关系型数据的基本形式是纵横交错的表,是一个平面的结构,如果要将多级树状结构存储 在关系型数据库里就需要进行合理的翻译工作。接下来我会将自己的所见所闻和一些实用的经验和大家探讨一下:
层级结构的数据保存在平面的数据库中基本上有两种常用设计方法:

  • 毗邻目录模式(adjacency list model)
  • 预排序遍历树算法(modified preorder tree traversal algorithm)

我不是计算机专业的,也没有学过什么数据结构的东西,所以这两个名字都是我自己按照字面的意思翻的,如果说错了还请多多指教。这两个东西听着好像很吓人,其实非常容易理解。

简单需求分析: 
1.实现无限级分类。 
2.实现无限级链接导航 
3.实现逐级分类下各条信息的查询,包括最多浏览量,最多评论量,最新信息。 
4.随意转移子分类到任何级别而不用修改分类下的信息表 
5.使用最少的参数得到所要的信息,URL参数最好只有一个,比如cID=1或者ID=1 
6.不管多少级,只有一个PHP文件实现类列表和各种方式的信息调用。 

表为两张,一张分类表,一张信息表。 
信息表如下: 

`ID` int(10) unsigned NOT NULL auto_increment, 
`cID` tinyint(3) unsigned NOT NULL default '0', 
`title` varchar(255) NOT NULL default 'No Title', 
`content` mediumtext NOT NULL, 

最简单的无限级分类数据表,只是设置一个parentID来判断父ID 
数据表如下: 

`cID` tinyint(3) unsigned NOT NULL auto_increment, 
`parentID` tinyint(3) unsigned NOT NULL default '0', 
`order` tinyint(3) NOT NULL default '0', 
`name` varchar(255) NOT NULL default '', 

这样可以根据cID = parentID来判断上一级内容,运用递归至最顶层。 
缺点是只能查询最小分类下的信息。这样就不能完成需求3、4点,而第二点也勉强符合 


第二种方法是设置parentID为varchar类型,将父类id都集中在这个字段里,用符号隔开,比如:1,3,6 
这样可以比较容易得到各上级分类的ID,而且在查询分类下的信息的时候,可以使用如:Select * From information Where cID Like "1,3%"。这样能比较好解决需求3。不过在添加分类和转移分类的时候操作将非常麻烦。 

我就说到这里,请大家讨论一下如何能够最简单的方法实现无限级分类——考虑性能,代码的简练性,前后台操作的容易性,扩展性!

 

2、预排序遍历树算法

Java代码  收藏代码
  1. --  
  2. -- 表的结构 `category`  
  3. --  
  4.   
  5. CREATE TABLE IF NOT EXISTS `category` (  
  6.   `id` int(11) NOT NULL AUTO_INCREMENT,  
  7.   `type` int(11) NOT NULL COMMENT '1为文章类型2为产品类型3为下载类型',  
  8.   `title` varchar(50) NOT NULL,  
  9.   `lft` int(11) NOT NULL,  
  10.   `rgt` int(11) NOT NULL,  
  11.   `lorder` int(11) NOT NULL COMMENT '排序',  
  12.   `create_time` int(11) NOT NULL,  
  13.   PRIMARY KEY (`id`)  
  14. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=10 ;  
  15.   
  16. --  
  17. -- 导出表中的数据 `category`  
  18. --  
  19.   
  20. INSERT INTO `category` (`id`, `type`, `title`, `lft`, `rgt`, `lorder`, `create_time`) VALUES  
  21. (1, 1, '顶级栏目', 1, 18, 1, 1261964806),  
  22. (2, 1, '公司简介', 14, 17, 50, 1264586212),  
  23. (3, 1, '新闻', 12, 13, 50, 1264586226),  
  24. (4, 2, '公司产品', 10, 11, 50, 1264586249),  
  25. (5, 1, '荣誉资质', 8, 9, 50, 1264586270),  
  26. (6, 3, '资料下载', 6, 7, 50, 1264586295),  
  27. (7, 1, '人才招聘', 4, 5, 50, 1264586314),  
  28. (8, 1, '留言板', 2, 3, 50, 1264586884),  
  29. (9, 1, '总裁', 15, 16, 50, 1267771951);  

现在让我们看一看另外一种不使用递归计算,更加快速的方法,这就是预排序遍历树算法(modified preorder tree traversal algorithm)
这种方法大家可能接触的比较少,初次使用也不像上面的方法容易理解,但是由于这种方法不使用递归查询算法,有更高的查询效率。

我们首先将多级数据按照下面的方式画在纸上,在根节点Food的左侧写上 1 然后沿着这个树继续向下 在 Fruit 的左侧写上 2 然后继续前进,沿着整个树的边缘给每一个节点都标上左侧和右侧的数字。最后一个数字是标在Food 右侧的 18。 在下面的这张图中你可以看到整个标好了数字的多级结构。(没有看懂?用你的手指指着数字从1数到18就明白怎么回事了。还不明白,再数一遍,注意移动你的 手指)。
这些数字标明了各个节点之间的关系,"Red"的号是3和6,它是 "Food" 1-18 的子孙节点。 同样,我们可以看到 所有左值大于2和右值小于11的节点 都是"Fruit" 2-11 的子孙节点

Java代码  收藏代码
  1.                          1 Food 18  
  2.                              |  
  3.             +------------------------------+  
  4.             |                              |  
  5.         2 Fruit 11                     12 Meat 17  
  6.             |                              |  
  7.     +-------------+                 +------------+  
  8.     |             |                 |            |  
  9.  3 Red 6      7 Yellow 10       13 Beef 14   15 Pork 16  
  10.     |             |  
  11. 4 Cherry 5    8 Banana 9  

 这样整个树状结构可以通过左右值来存储到数据库中。继续之前,我们看一看下面整理过的数据表。

Java代码  收藏代码
  1. +----------+------------+-----+-----+  
  2. |  parent  |    name    | lft | rgt |  
  3. +----------+------------+-----+-----+  
  4. |          |    Food    | 1   | 18  |  
  5. |   Food   |   Fruit    | 2   | 11  |  
  6. |   Fruit  |    Red     | 3   |  6  |  
  7. |   Red    |    Cherry  | 4   |  5  |  
  8. |   Fruit  |    Yellow  | 7   | 10  |  
  9. |   Yellow |    Banana  | 8   |  9  |  
  10. |   Food   |    Meat    | 12  | 17  |  
  11. |   Meat   |    Beef    | 13  | 14  |  
  12. |   Meat   |    Pork    | 15  | 16  |  
  13. +----------+------------+-----+-----+  

注意:由于"left"和"right"在 SQL中有特殊的意义 ,所以我们需要用"lft"和"rgt"来表示左右字段。 另外这种结构中不再需要"parent"字段来表示树状结构。也就是 说下面这样的表结构就足够了。

 

好了我们现在可以从数据库中获取数据了,例如我们需要得到"Fruit"项下的所有所有节点就可以这样写查询语句:

Java代码  收藏代码
  1. SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;  

这个查询得到了以下的结果。

Java代码  收藏代码
  1. +------------+-----+-----+  
  2. |    name    | lft | rgt |  
  3. +------------+-----+-----+  
  4. |    Fruit   | 2   | 11  |  
  5. |    Red     | 3   |  6  |  
  6. |    Cherry  | 4   |  5  |  
  7. |    Yellow  | 7   | 10  |  
  8. |    Banana  | 8   |  9  |  
  9. +------------+-----+-----+  

 要获知一个节点的路径就更简单了,如果我们想知道Cherry 的路径就利用它的左右值4和5来做一个查询。

Java代码  收藏代码
  1. SELECT name FROM tree WHERE lft < 4 AND rgt >; 5 ORDER BY lft ASC;  

 那么某个节点到底有多少子孙节点呢?很简单,子孙总数=(右值-左值-1)/2

用这个简单的公式,我们可以很快的算出"Fruit 2-11"节点有4个子孙节点,而"Banana 8-9"节点没有子孙节点,也就是说它不是一个父节点了。

 

那么对于这样的结构我们该如何增加,更新和删除一个节点呢?
增加一个节点一般有两种方法:
第一种,保留原有的name 和parent结构,用老方法向数据中添加数据,每增加一条数据以后使用rebuild_tree函数对整个结构重新进行一次编号。
第二种,效率更高的办法是改变所有位于新节点右侧的数值。举例来说:我们想增加一种新的水果"Strawberry"(草莓)它将成为"Red"节点的最 后一个子节点。首先我们需要为它腾出一些空间。"Red"的右值应当从6改成8,"Yellow 7-10 "的左右值则应当改成 9-12。 依次类推我们可以得知,如果要给新的值腾出空间需要给所有左右值大于5的节点 (5 是"Red"最后一个子节点的右值) 加上2。 所以我们这样进行数据库操作:

Java代码  收藏代码
  1. UPDATE tree SET rgt = rgt + 2 WHERE rgt > 5;  
  2. UPDATE tree SET lft = lft + 2 WHERE lft > 5;  

 这样就为新插入的值腾出了空间,现在可以在腾出的空间里建立一个新的数据节点了, 它的左右值分别是6和7

Java代码  收藏代码
  1. INSERT INTO tree SET lft=6, rgt=7, name='Strawberry';  

 数据库结构:
采用左右值编码的保存该树的数据记录如下(设表名为tree):
c_id | name | left_node | right_node
1     |商品    |      1       |   18
2     | 食品  |       2      |  11
3     | 肉类  |     3         |  6
4     | 猪肉    |    4        |    5
5     | 菜类  |     7        |   10
6     | 白菜  |    8         |    9
7     | 电器  |    2         |    17
8     | 电视  |    13       |    14
9     | 电棒  |    15       |     16

第一次看见上面的数据记录,相信大部分人都不清楚左值(left_node)和右值(right_node)是根据什么规则计算出来的,而且,这种表设计似乎没有保存父节点的信息。下面把左右值和树结合起来,请看:
          1商品18
     +---------------------------------------+

        2食品11                                12电器17

+-----------------+               +---------------------+
3肉类6           7菜类10       13电视14              15电棒16
4猪肉5            8白菜9
请用手指指着上图中的数字,从1数到18,学习过数据结构的朋友肯定会发现什么吧?对,你手指移动的顺序就是对这棵树的进行先序遍历的顺序。接下来,让我讲述一下如何利用节点的左右值,得到该节点的父节点,子孙节点数量,及自己在树中的层数。

采用左右值编码的设计方案,在进行类别树的遍历时,由于只需进行2次查询,消除了递归,再加上查询条件都为数字比较,效率极高,类别树的记录条目越多,执行效率越高。

应用
某个节点到底有多少子孙节点?
子孙总数=(父节点的右值 - 父节点的左值-1)/2
以节点“食品”举例,其子孙总数=(11-2-1)/ 2 = 4

如何判断某一节点下有没有子节点?
  当 该节点左值-1 等于 其右值 时,其下没有子节点。

检索某一父节点的所有子节点?
假定我们要对节点“食品”及其子孙节点进行先序遍历的列表,只需使用如下一条sql语句:
SELECT * FROM `tree` WHERE `left_node` BETWEEN 2 AND 11 ORDER BY `left_node` ASC

如何取得父类?
SELECT * FROM `tree` WHERE `left_node`<$left_node AND `right_node`>$right_node

检索之后如何列表?

当左值+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`>$right_node AND `right_node`<$A_left_node
UPDATE `tree` SET `left_node`=`left_node`-$right_node-$left_node-1 WHERE `left_node`>$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`>父节点的右值

删除一个节点及其子节点?
在上例中的<号>号后面各加一个=号

posted @ 2018-01-19 13:58  追忆丶年华  阅读(438)  评论(0编辑  收藏  举报