休息一下
工作不是全部,钱是挣不完的,忙碌了一年,也该休息了。
今天开始休息了。祝大家新年快乐!
忙完今天的工作之后,开始继续昨晚未完的文章。昨天说了存储树状结构的第一种方式─领接表方式,现在我们来聊聊第二种方式─预排序遍历树方式(即通常所说的 MPTT,Modified Preorder Tree Traversal)。此算法是在第一种方式的基础之上,给每个节点增加一个左、右数字,用于标识节点的遍历顺序,如下图所示:

从根节点开始左边为 1,然后下一个节点的左边为 2,以此类推,到最低层节点之后,最低层节点的右边为其左边的数字加 1。顺着这些节点,我们可以很容易地遍历完整个树。根据上图,我们对数据表做一些改变,增加两个字段,lft 和 rgt 用于存储左右数字(由于 left 和 right 是 MySQL 的保留字,所以我们改用简写)。表中各行的内容也就变成了:

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

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;

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。
之所以写这两篇文,是因为经常在论坛上看到有人问这方面的问题。除了这两种算法之外,还有其它的,只不过这两种较为流行而已。
我们经常需要在关系型数据库中保存一些树状结构数据,比如分类、分组、权限控制 ACL、论坛帖子树状回复等等。常用的方法有两种,一种是领接表的方式,另一种是预排序遍历树方式。
本文以 MySQL 和 PHP 为例进行说明,假设树状结构如下:

领接表方式
领接表方式主要依赖于一个 parent 字段,用于指向上级节点,将相邻的上下级节点连接起来,从而将整个树的各个节点连接起来。这样,我们例子中的节点变成行记录如下:

id 为自动递增自动,parent_id 为上级节点的 id。一目了然,“PHP”是“Language”的子节点。
接下来我们要显示树,PHP 代码也可以很直观,显示树的子节点的代码如下:
<?php
/**
* @param $parent_id 父节点 id,0 则显示整个树结构。
* @param $level 当前节点所处的层级,用于缩进显示节点。
*/
function show_children($parent_id = 0, $level = 0)
{
// 获取父节点下的所有子节点
$result = mysql_query('SELECT id, name FROM tree WHERE parent_id='.intval($parent_id));
// 显示每个子节点
while ($row = mysql_fetch_array($result)) {
// 缩进显示
echo '<div style="margin-left:'.($level*12).'px">'.$row['name'].'</div>';
// 递归调用当前函数,显示再下一级的子节点
show_children($row['id'], $level+1);
}
}
?>
想要显示整个树结构,调用 show_children()。想要显示“Database”子树,则调用 show_children(2),因为“Database”的 id 是 2。
还有一个经常用到的功能是获取节点路径,即给出一个节点,返回从根节点到当前节点的路径。用函数实现如下:
<?php
/**
* @param $id 需要获取路径的当前节点的 id。
*/
function get_path($id)
{
// 获取当前节点的父节点 id 和当前节点名
$result = mysql_query('SELECT parent_id, name FROM tree WHERE id='.intval($id));
$row = mysql_fetch_array($result);
// 使用此数组保存路径
$path = array();
// 将当前节点名保存进路径数组中
$path[] = $row['name'];
// 如果父节点非 0,即非根节点,则进行递归调用获取父节点的路径
if ($row['parent_id']) {
// 递归调用,获取父节点的路径,并且合并到当前路径数组的其它元素前边
$path = array_merge(get_path($row['parent_id']), $path);
}
return $path;
}
?>
想要获取“MySQL 5.0”的路径,调用 get_path(4),4 即是这个节点的 id。
领接表方式的优点在于容易理解,代码也比较简单明了。缺点则是递归中的 SQL 查询会导致负载变大,特别是需要处理比较大型的树状结构的时候,查询语句会随着层级的增加而增加,WEB 应用的瓶颈基本都在数据库方面,所以这是一个比较致命的缺点,直接导致树结构的扩展困难重重。
(好久没写文章,太累,明天接着写第二种方式……)
周五的时候,士兵突击的主创人员做客了新浪聊天,我也趁着机会混进去和演员来了个面对面。在此,要隆重感谢easy同学的大力支持,敬礼!!!
士兵突击最早的时候是看电视上各个台都在放,我和nio还很不以为然,觉得肯定没什么意思,后来实在是各个台都在放,就跟着看了一两集,再后来就发展到每天晚上都在换台找看哪个还在放,抓住就看,再再后来实在不过瘾,去bt上下载了下来,嘿嘿,这下才圆满了!!,整部剧戏份最多的应该是许三多了,他把整个剧中的各个人物联系在了一起,老马,班长,561,连长,袁朗。。。。班长让他从一个农村的傻小子变成了一个兵,袁朗把一个兵变成了真正的兵王。新浪直播的那天,许三多没能到场,不过没关系,有我喜欢的班长,班副,袁朗,可惜缺了连长。这次还肩负着重要的任务是给嫂子送信,大家都很亲切,每个都很认真的听我唧唧歪歪,然后拿了信,对我说谢谢,纷纷表示回去之后一定会看。老段(袁朗的扮演者)竟然说:噢噢,我知道她,傻大兵的老婆!呵呵,看来是群里姐妹们前期的工作做的好啊!高峰(齐桓)和马帅(李梦)让我有点崩溃,二人有点茫然的听我叨叨,有可能是穿着军装,让我有点小紧张,嘿嘿。。。临走我还跟561交待一定要看嫂子的信啊,呵呵,班副是个很亲切和善的人。。。