存储树状结构(下)─预排序遍历树方式

预排序遍历树方式(即通常所说的 MPTT,Modified Preorder Tree Traversal)。此算法是在第一种方式的基础之上,给每个节点增加一个左、右数字,用于标识节点的遍历顺序,如下图所示:
树状结构图 
从根节点开始左边为 1,然后下一个节点的左边为 2,以此类推,到最低层节点之后,最低层节点的右边为其左边的数字加 1。顺着这些节点,我们可以很容易地遍历完整个树。根据上图,我们对数据表做一些改变,增加两个字段,lft 和 rgt 用于存储左右数字(由于 left 和 right 是 MySQL 的保留字,所以我们改用简写)。表中各行的内容也就变成了:
mptt

接下来看看显示树/子树是多么简单,只需要一条SQL 语句即可,比如显示“Database”子树,则需要获取到“Database”的左右数字,左为 2,右为 11,那么 SQL 语句是:
SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

subtree
SQL 语句是简单了,但我们所希望的缩进显示却是个问题。什么时候应该显示缩进?缩进多少单位?解决这个问题,需要使用堆栈,即后进先出(LIFO),每到一个节点,将其右边的数字压入堆栈中。我们知道,所有节点右边的值都比其父节点右边的值小,那么将当前节点右边的值和堆栈最上边的右边值进行比较,如果当前节点比堆栈最上边的值小,表示当前堆栈里边剩下的都是父节点了,这时可以显示缩进,堆栈的元素数量即是缩进深度。PHP 代码实现如下:
<?php
/**
 * @param $root_id 需要显示的树/子树根节点 id。
 */
function show_tree($root_id 1
{
    
// 获取当前根节点的左右数值
    
$result mysql_query('SELECT lft, rgt FROM tree WHERE id='.intval($root_id));
    
$row mysql_fetch_array($result);
    
// 堆栈,存储节点右边的值,用于显示缩进
    
$stack = array();
    
// 获取 $root_id 节点的所有子孙节点
    
$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)) {
        if (
count($stack)>0) { //仅当堆栈非空的时候检测
            // 如果当前节点右边的值比堆栈最上边的值大,则移除堆栈最上边的值,因为这个值对应的节点不是当前节点的父节点
            
while ($row['rgt'] > $stack[count($stack)-1]) {
                
array_pop($stack);
            } 
//while 循环结束之后,堆栈里边只剩下当前节点的父节点了
        
}
        
// 现在可以显示缩进了
        
echo '<div style="margin-left:'.(count($stack)*12).'px">'.$row['name'].'</div>';
        
// 将当前的节点压入堆栈里边,为循环后边的节点缩进显示做好准备
        
array_push($stack$row['rgt']);
    }
}
?>

获取整个树调用 show_tree(),获取“Database”子树调用show_tree(2)。在这个函数中,我们总算不需要用到递归了,呵呵。

接下来是显示从根节点到某节点的路径,这比起领接表方式来说也简单了很多,只需要一句 SQL 就行,不用递归. 比如获取“ORACLE”这个节点的路径,其左右值分别是 7 和 10,则 SQL 语句为:
SELECT name FROM tree WHERE lft <= 7 AND rgt >= 10 ORDER BY lft ASC;

mptt-path

PHP 函数实现如下:
<?php
/**
 * @param $node_id 需要获取路径的节点 id
 */
function get_path2($node_id) {
    
// 获取当前节点的左右值
    
$result mysql_query('SELECT lft, rgt FROM tree WHERE id='.intval($node_id));
    
$row mysql_fetch_array($result);
    
// 获取路径中的所有节点
    
$result mysql_query('SELECT name FROM tree WHERE lft <= '.$row['lft'].' AND rgt >= '.$row['rgt'].' ORDER BY lft ASC');
    
$path = array();
    while (
$row mysql_fetch_array($result)) {
        
$path[] = $row['name'];
    }
    return 
$path;
}
?>

显示树和路径都没问题了,现在需要了解一下如何插入一个节点。插入新节点之前,首先要给这个节点腾出空位来,假设我们现在要在“ORACLE 9i”这个节点右边增加一个“ORACLE 10”,则腾位的 SQL 语句如下(“ORACLE 9i”的右边值为 9):

UPDATE tree SET rgt=rgt+2 WHERE rgt>9;
UPDATE tree SET lft=lft+2 WHERE lft>9;

位置空出来了,开始插入新节点吧:

INSERT INTO tree SET lft=10, rgt=11, name='ORACLE 10';

调用 show_tree() 看看结果对不对  具体的 PHP 实现代码这里就不写了。

现在总结一下预排序遍历树方式的优缺点。缺点是算法比较抽象,不容易理解,增加节点的时候虽然只用了几条 SQL 语句,但可能会需要更新很多记录,从而造成阻塞。优点是树的构造,路径获取方面性能都比领接表方式好很多。也就是说,这个算法牺牲了一些写的性能来换取读的性能,在 WEB 应用中,读数据库的比例远大于写数据库的比例,所以预排序遍历树方式比领接表方式更加受欢迎,更加实用,很多应用中都能看到 MPTT 的影子,通常所用的表里都有字段 lft 和 rgt。

之所以写这两篇文,是因为经常在论坛上看到有人问这方面的问题。除了这两种算法之外,还有其它的,只不过这两种较为流行而已。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值