树状分类结构,数据库构建(预排序历遍算法)

来自:   http://my.oschina.net/XYleung/blog/99604


树状结构的数据保存在数据库中的常用方法有一下两种: 
1、邻接表(adjacency list model)  
2、预排序遍历树算法(modified preorder tree traversal algorithm)  


用一下的例子讨论这两种方法的差异: 

现有一棵树如下:


邻接表模式:

这种模式我们经常用到,很多的教程和书中也介绍过。我们通过给每个节点增加一个属性 parent 来表示这个节点的父节点从而将整个树状结构通过平面的表描述出来。根据这个原则,例子中的数据可以转化成如下的表: 
 

我们看到 Pear 是Green的一个子节点,Green是Fruit的一个子节点。而根节点'Food'没有父节点。 为了简单地描述这个问题, 这个例子中只用了name来表示一个记录。 在实际的数据库中,你需要用数字的id来标示每个节点,数据库的表结构大概应该像这样:id, parent_id, name, description。  
以下是代码: 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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 

如果你只想显示整个结构中的一部分,比如说水果部分,就可以这样调用:display_children('Fruit',0); 

几乎使用同样的方法我们可以知道从根节点到任意节点的路径。比如 Cherry 的路径是 "Food >; Fruit >; Red"。 为了得到这样的一个路径我们需要从最深的一级"Cherry"开始, 查询得到它的父节点"Red"把它添加到路径中, 然后我们再查询Red的父节点并把它也添加到路径中,以此类推直到最高层的"Food" 
以下是代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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  
)  
接下来如何把它打印成你希望的格式,就是你的事情了。  
缺点:   
这种方法很简单,容易理解,好上手。但是也有一些缺点。主要是因为运行速度很慢,由于得到每个节点都需要进行数据库查询,数据量大的时候要进行很多查询才能完成一个树。另外由于要进行递归运算,递归的每一级都需要占用一些内存所以在空间利用上效率也比较低。 


预排序遍历树算法

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

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

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

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

看到了吧,只要一个查询就可以得到所有这些节点。为了能够像上面的递归函数那样显示整个树状结构,我们还需要对这样的查询进行排序。用节点的左值进行排序: 
SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;

那么某个节点到底有多少子孙节点呢?很简单,子孙总数=(右值-左值-1)/2 
descendants = (right – left - 1) / 2 ,如果不是很清楚这个公式,那就去翻下书,我们在上数据结构写的很清楚!

添加同一层次的节点的方法如下:

?
1
2
3
4
5
6
7
8
9
10
LOCK TABLE nested_category WRITE;
 
SELECT @myRight := rgt FROM nested_category WHERE name = 'Cherry' ;
 
UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft + 2 WHERE lft > @myRight;
 
INSERT INTO nested_category( name , lft, rgt) VALUES ( 'Strawberry' , @myRight + 1, @myRight + 2);
 
UNLOCK TABLES;
添加树的子节点的方法如下: 
?
1
2
3
4
5
6
7
8
9
10
LOCK TABLE nested_category WRITE;
 
SELECT @myLeft := lft FROM nested_category WHERE name = 'Beef' ;
 
UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myLeft;
UPDATE nested_category SET lft = lft + 2 WHERE lft > @myLeft;
 
INSERT INTO nested_category( name , lft, rgt) VALUES ( 'charqui' , @myLeft + 1, @myLeft + 2);
 
UNLOCK TABLES;
每次插入节点之后都可以用以下SQL进行查看验证: 
?
1
2
3
4
5
6
SELECT CONCAT( REPEAT( ' ' , ( COUNT (parent. name ) - 1) ), node. name ) AS name
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;
删除节点的方法,稍微有点麻烦是有个中间变量,如下: 
?
1
2
3
4
5
6
7
8
9
10
11
LOCK TABLE nested_category WRITE;
 
SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category WHERE name = 'Cherry' ;
 
DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;
 
UPDATE nested_category SET rgt = rgt - @myWidth WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft - @myWidth WHERE lft > @myRight;
 
UNLOCK TABLES;
这种方式就是有点难的理解,但是适合数据量很大规模使用,查看所有的结构只需要两条SQL语句就可以了,在添加节点和删除节点的时候略显麻烦,不过相对于效率来说还是值得的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Taihom原创,因为是原创才要分的请支持一下, 该存储过程可以在版本>=MSSQL2000下使用 但在MSSQL2000下,MPTT_NODEAction的resetnode操作不能使用,但不影响整个分类的主体应用 感谢ben一同测试。如果你支持原创,请保留存储过程中对作者Taihom的文字注释和描述。 ---------------------------------------- MPTT分类算法的添加,修改,删除其实很容易,但是这个算法排序就不是这么容易了。 我这里已经把分类的移动和排序都重新处理了,实现了MPTT分类排序和移动 为了保证分类左右节点的连续性,这个存储过程有检测节点连续性和完整性的处理。 理论上不会因为在添加、修改、删除、移动或者排序的操作中出现节点不正确的情况。 另外,这个分类也同时兼容传统的递归。表中的PID就是上一级的父节点。 完成和发布时间:17:23 2009/5/7 ---------------------------------------- 参考文档: http://dev.mysql.com/tech-resources/articles/hierarchical-data.html ---------------------------------------- 存储过程说明: MPTT_NODEAction @Act, @ID, @Name ---------------------------------------- 这个存储过程主要处理分类的添加删除修改和恢复节点等操作 ------------------参数说明-------------- @Act add:@ID=在哪个节点下添加,@Name=添加的名称 mod:@ID=修改哪个ID,@Name=修改的名称 del:@ID=删除哪个ID remove:@ID=从数据库删除已经被删除的ID resetnode:初始化所有节点,把所有节点初始化成根节点下的子节点 restore:恢复节点:@ID,把删除的节点恢复到哪个节点下,@Name(节点ID):恢复哪个节点 ---------------------------------------- MPTT_NODEMove @ID1 int,--从哪里移动 @ID2 int,--移动到哪里? @Dir varchar(2)='>>' --移动方式 ---------------------------------------- 这个存储过程主要负责节点的移动、排序 ------------------参数说明-------------- @Dir='<' 把节点@ID1移动到@ID2的前面 @Dir='>' 把节点@ID1移动到@ID2的后面 @Dir='>>' 把节点@ID1加入到@ID2,并且作为节点@ID2的最后一个子节点 ---------------------------------------- MPTT_NODEGet @Act nvarchar(10), @ID INT=0 ---------------------------------------- 这个存储过程主要负责节点的筛选和选择 ------------------参数说明-------------- @Act='chklink' 用来检测节点的排序连接是否断开,0表示正常,没有断开,不等于0表示排序有错 -------------- @Act='subnode' 用来获取节点@ID的子节点 -------------- @Act='fullpath' 用来获取节点@ID的全路径 -------------- @Act='delnode' 用来获取被删除的节点列表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值