网站,一定离不开多级分类:小到导航栏的两三级分类,大到其它货物的无限分类等。PHP无限分类有多种算法,本文将介绍的是PHP左右值无限分类,也称为预排序树无限分类 ...

什么是左右值无限级分类

左右值无限级分类,也称为预排序树无限级分类,是一种有序的树状结构,位于这些树状结构中的每一个节点都有一个“左值”和“右值”,其规则是:每一个后代节点的左值总是大于父类,右值总是小于父级,右值总是小于左值。处于这些结构中的每一个节点,都可以轻易的算出其祖先或后代节点。因此,可以用它来实现无限分类。

左右值无限分类的优缺点

优点:

  • 通过一条SQL就可以获取所有的祖先或后代,这在复杂的分类中非常必要
  • 通过简单的四则运算就可以得到后代的数量

缺点

  • 分类操作麻烦
  • 无法简单的获取子代(请看请“子代”和"后代")

PHP无限分类研究对象

本文用于演示的对象:狼魂博客[http://pjiaxu.com]的导航栏,其结构如下图:

1.[1]舞文弄墨[8] [9]洞若观火[12] [13]WEB开发[20]
2.------------------------------------------------------
3.| | |
4.| | |
5.| [10]用户体验[11] |
6.| |
7.-------------------------------------------- ----------------------------------
8.[2]似水流年[3] [4]纸上谈兵[5] [6]隔岸观火[7] [14]PHP[15] [16]JS[17] [18]HTML[19]

其中的[数字]就是每一级的“左右值”,这里的左右值是早已经存在的,我们就以此为例,逐步研究这种算法。

创建PHP左右值无限分类的SQL数据

01.createtable `navs`(
02.`id`int primary key auto_increment,
03.`name`varchar(32) notnull,
04.`lft`int not null,
05.`rght`int not null
06.);
07.insertinto `navs`(`name`,`lft`,`rght`)
08.values
09.('舞文弄墨',1,8),
10.('似水流年',2,3),
11.('纸上谈兵',4,5),
12.('隔岸观火',6,7),
13.('洞若观火',9,12),
14.('用户体验',10,11),
15.('WEB开发',13,20),
16.('PHP',14,15),
17.('JS',16,17),
18.('HTML',18,19);

上图,我为了方便,将SQL有意识的缩进了一下,这应该可以一目了然这种结构了吧。

获取所有的后代节点

了解了PHP预排序村的规则后,我们就可以很清晰的知道如何获取某个节点的所有子代——左值比它大,右值比它小。如,我们要获取“舞文弄墨”的所有子后代,可以使用以下的SQL语句:

1.select* from `navs` where `lft`>1 and `rght`<8
得到如下的结果:


计算所有子类的数量

有人认为使用 count 配合上面的语句就可以算出,这当然是可以的,但有更快的方式:每有子类节点中每个节点占用两个值,而这些值都是不一样且连续的,那些就可以计算出子代的数量=(右值-左值-1)/2。减少1的原因是排除该节点,你可以想像一个,一个单节点,左值是1,右值是2,没有子类节点,而这时它的右值-左值=1.

插入分类算法

还是先以图为例,比如,现在狼魂的博客[http://pjiaxu.com]发行了一个pblog博客系统,那么它想要在顶级导航上加上pblog这个导航菜单,需要怎么做(这是加顶级菜单的例子)?然而,它又想在Javascript中加入个jQuery分类,又该怎么做?做这些操作有两个重难点:

  1. 为插入分类分配一个左右值
  2. 更新其它节点的左右值

结果的示例图如下:

01.[1]舞文弄墨[8] [9]洞若观火[12] [13]WEB开发[22] [23]pblog[24]
02.-----------------------------------------------------------------------
03.| | |
04.| | |
05.| [10]用户体验[11] |
06.| |
07.-------------------------------------------- ----------------------------------
08.[2]似水流年[3] [4]纸上谈兵[5] [6]隔岸观火[7] [14]PHP[15] [16]JS[19] [20]HTML[21]
09.|
10.[17]jquery[18]
我们分析一下这两种情况:
  • 插入最顶级节点:它的左右值与该树中最大的右值有关:左值=最大右值+1,右值=最大右值+2,你可以自己模拟一下;
  • 插入子节点:它的左右值与它的父级有关:左值=父级的右值,右值=父级的左值+1,这时要更新的数据有:父级的右值,所有左值大于父级左级,右值大于低级右值的节点左右值都应该+2;你可以模拟在jquery下再加一个节点看是不是如此。

有了这些分析,就不难用PHP来实现了,下面,就使用PHP写一个插入算法;

PHP插入节点代码

01.define('DB_HOST','localhost');
02.define('DB_USER','root');
03.define('DB_PWD','`12345');
04.define('DB_NAME','test');
05.classEndlessByLR
06.{
07.functioninsert($name,$parentid)
08.{
09.$db= new mysqli(DB_HOST,DB_USER,DB_PWD,DB_NAME);
10.if($parentid== 0)
11.{
12.//增加最高级,即与WEB开发同级,详见[http://pjiaxu.com/]导航中的Pblog
13.$sql= 'select max(`rght`) as `maxid` from `navs`;';
14.$result= $db->query($sql);
15.//这里基本不用做出错判断,一般都有数据的
16.$row= $result->fetch_array(MYSQLI_ASSOC);
17.$lft= $row['maxid']+1;
18.$rght= $lft+1;
19.}
20.else
21.{
22.$sql= "select * from `navs` where `id`={$parentid};";
23.$result= $db->query($sql);
24.//这里应做个出错判断
25.$row= $result->fetch_array(MYSQLI_ASSOC);
26.$rght= $row['rght'];
27.$sql= "update `navs` set `rght`=`rght`+2 where `rght`>={$rght};";
28.$db->query($sql);
29.$sql= "update `navs` set `lft`=`lft`+2 where `lft`>{$rght};";
30.$db->query($sql);
31.$lft= $row['rght'];
32.$rght= $lft+1;
33.}
34.$sql= "insert into `navs`(`name`,`lft`,`rght`)values('{$name}',{$lft},{$rght});";
35.return$db->query($sql);
36.}
37.}
注意,使用这段PHP函数需要以上SQL的支持;现在,我们就使用这个函数插入刚才所说的两个节点,看看结果是否一致?
1.$eblr= new EndlessByLR();
2.$eblr->insert('pblog',0);//0是顶级, 9是JS,在插入之前,你应该是知道的!
3.$eblr->insert('jquery',9);
产生了以下的结果:


你可以花点时间验证一下是不是跟演示图一样。

网上搜索引擎有一个相似的文章占了前几页,在更新又值时是使用 `rght`>{$rght} 而不是 `rght`>={$rght};就我今天目测了N久的得出结果:这是不科学的!

解决了插入的问题,实际上就解决了很多实际应用了。现在,我们需要研究另一种操作:移动节点——这个是要了亲命的操作!

移动节点演示图

我们还是来演示推算一下再到代码中去吧。突然间,狼魂博客[http://pjiaxu.com/map.html]觉得pblog大多的内容都是PHP,那何不把它移动到PHP分类下呢?

01.[1]舞文弄墨[8] [9]洞若观火[12] [13]WEB开发[22](24) [25]other[26](假设存在)
02.----------------------------------------------------------------------
03.| | |
04.| | |
05.| [10]用户体验[11] |
06.| |
07.-------------------------------------------- --------------------------------------
08.[2]似水流年[3] [4]纸上谈兵[5] [6]隔岸观火[7] [14]PHP[15](17) (18)[16]JS[19](21) (22)[20]HTML[21](23)
09.| |
10.(15)[23]pblog[24](16) (19)[17]jquery[18](20)
方括号[]中的数字是现在的左右值,小括号()中的数字是期望值,也就是移动后应该得到的左右值。最终将是以下的样子:
01.[1]舞文弄墨[8] [9]洞若观火[12] [13]WEB开发[24]
02.----------------------------------------------------------------------
03.| | |
04.| | |
05.| [10]用户体验[11] |
06.| |
07.-------------------------------------------- ---------------------------------------
08.[2]似水流年[3] [4]纸上谈兵[5] [6]隔岸观火[7] [14]PHP[17] [18]JS[21] [22]HTML[23]
09.| |
10.[15]pblog[16] [19]jquery[20]

我们继续分析——移动共可以四个方向移动:

  • pblog移动到php之下成为它的子类
  • pblog移动到php之上成为它的父类
  • pblog移动到php左右成为它的兄弟

我们以子类为例子演示推算一下,假设(x1,y1)、(x2,y2)是php和pblog的左右值,那么,我们能过分析得到以下的结论:

  • 左值在min(y1,y2)最小右值和max(x1,x2)最大左值之间都需要加2
  • 右值在min(x1,x2)最小左值和max(y1,y2)最大右值之间都需要加2
  • 移动节点的左值=父类节点的右值,右值=父类节点右值+1

通过以上的分析,就可以写出移动节点的PHP代码。

PHP移动分类函数

01.<?php
02.classEndlessByLR{
03.//......
04.//......
05.//接上面的代码
06.functionmove($formid,$toid, $derect)
07.{
08.if($formid== $toid)
09.{
10.returntrue;
11.}
12.$sql= "select * from `navs` where `id`=$formid
13.union
14.select * from `navs` where `id`=$toid";
15.$db= new mysqli(DB_HOST,DB_USER,DB_PWD,DB_NAME);
16.$result= $db->query($sql);
17.$rows= $result->fetch_all(MYSQLI_ASSOC);//php版本要大于5.3才可以用
18.if($rows[0]['lft'] >$rows[1]['lft'])
19.{
20.$maxlft= $rows[0]['lft'];
21.$minlft= $rows[1]['lft'];
22.}
23.else
24.{
25.$maxlft= $rows[1]['lft'];
26.$minlft= $rows[0]['lft'];
27.}
28.if($rows[0]['rght'] >$rows[1]['rght'])
29.{
30.$minrght= $rows[1]['rght'];
31.$maxrght= $rows[0]['rght'];
32.}
33.else
34.{
35.$minrght= $rows[0]['rght'];
36.$maxrght= $rows[1]['rght'];
37.}
38.
39.switch($derect)
40.{
41.case'child':
42.$sql= "update `navs` set `lft`=`lft`+2 where `lft` between $minrght and $maxlft";
43.$db->query($sql);
44.//echo $sql;
45.$sql= "update `navs` set `rght`=`rght`+2 where `rght` between $minlft and $maxrght";
46.$db->query($sql);
47.//echo $sql;
48.$lft= $minlft+1;
49.$rght= $lft+1;
50.$sql= "update `navs` set `lft`=$lft,`rght`=$rght where id=$formid limit 1;";
51.//echo $sql;
52.$db->query($sql);
53.break;
54.case'parent':
55.break;
56.case'before':
57.break;
58.case'after':
59.break;
60.}
61.}
这个函数就将我们刚才的分析以代码的方式实现,现在我们就将pblog移到PHP子类中去:
1.$eblr->move(11,8,'child');
得到的结果如下图:


如要移动看其它位置,看各位模仿移动子类自行分析。

获取所有的父级

这个为什么要放最后,那是——只有在最后,jquery才有直属两个祖先——js和web开发,其实看一下就知道了,获取一个分类的直属祖先(类似于我们平时看到网站的"你所在的位置"一样获取一条直达首页的路径)。规则就是:左值小于该分类,右值大于该分类的节点;如,获取jquery的路径就是:

1.select* from `navs` where `lft`<17 and `rght`>28