在PHP应用中提供后台数据存储通常是关系型数据库它能够保存大量数据提供高效数据检索和更新服务然而关
系型数据基本形式是纵横交错表是个平面结构如果要将多级树状结构存储在关系型数据库里就需要进行合理翻
译工作接下来我会将自己所见所闻和些实用经验和大家探讨下
层级结构数据保存在平面数据库中基本上有两种常用设计思路方法:
毗邻目录模式(adjacencylistmodel)
预排序遍历树算法(modiedpreordertreetraversalalgorithm)
我不是计算机专业也没有学过什么数据结构东西所以这两个名字都是我自己按照字面意思翻如果说错了还请多
多指教
这两个东西听着好像很吓人其实非常容易理解这里我用个简单食品目录作为我们举例数据我们数据结构是这样:
Food
|
|---Fruit
||
||---Red
|||
|||--Cherry
||
||---Yellow
||
||--Banana
|
|---Meat
|
|--Beef
|
|--Pork
为了照顾那些英文塌糊涂PHP爱好者
Food:食物
Fruit:水果
Red:红色
Cherry:樱桃
Yellow:黄色
Banana:香蕉
Meat:肉类
Beef:牛肉
Pork:猪肉
毗邻目录模式(adjacencylistmodel)
这种模式我们经常用到很多教程和书中也介绍过我们通过给每个节点增加个属性parent来表示这个节点父节点
从而将整个树状结构通过平面表描述出来根据这个原则例子中数据可以转化成如下表:
+-----------------------+
|parent|name|
+-----------------------+
||Food|
|Food|Fruit|
|Fruit|Green|
|Green|Pear|
|Fruit|Red|
|Red|Cherry|
|Fruit|Yellow|
|Yellow|Banana|
|Food|Meat|
|Meat|Beef|
|Meat|Pork|
+-----------------------+
我们看到Pear是Green个子节点Green是Fruit个子节点而根节点'Food'没有父节点为了简单地描述这个问题
这个例子中只用了name来表示个记录在实际数据库中你需要用数字id来标示每个节点数据库表结构大概应该像
这样:id,parent_id,name,descrīption有了这样表我们就可以通过数据库保存整个多级树状结构了
显示多级树
如果我们需要显示这样个多级结构需要个递归
($result))
{
//缩进显示节点名称
echostr_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/"
($result);
//用个保存路径
$path=.gif' />;
//如果不是根节点则继续向上查询
//(根节点没有父节点)
($row['parent']!='')
{
//thelastpartofthepathto$node,isthename
//oftheparentof$node
$path=$row['parent'];
//weshouldaddthepathtotheparentofthisnode
//tothepath
$path=.gif' />_merge(get_path($row['parent']),$path);
}
//thepath
$path;
}
>
如果对/"Cherry/"使用这个:pr_r(get_path('Cherry'))就会得到这样个了:
Array
(
[0]=>Food
[1]=>Fruit
[2]=>Red
)
接下来如何把它打印成你希望格式就是你事情了
缺点:这种思路方法很简单容易理解好上手但是也有些缺点主要是运行速度很慢由于得到每个节点都需要进行数
据库查询数据量大时候要进行很多查询才能完成个树另外由于要进行递归运算递归每级都需要占用些内存所以
在空间利用上效率也比较低 [Page]
现在让我们看看另外种不使用递归计算更加快速思路方法这就是预排序遍历树算法
(modiedpreordertreetraversalalgorithm)这种思路方法大家可能接触比较少初次使用也不像上面思路方法容
易理解但是由于这种思路方法不使用递归查询算法有更高查询效率
我们首先将多级数据按照下面方式画在纸上在根节点Food左侧写上1然后沿着这个树继续向下在Fruit左侧写上
2然后继续前进沿着整个树边缘给每个节点都标上左侧和右侧数字最后个数字是标在Food右侧18在下面这张图
中你可以看到整个标好了数字多级结构(没有看懂 用你手指指着数字从1数到18就明白如何回事了还不明白再
数遍注意要移动你手指)
这些数字标明了各个节点的间关系/"Red/"号是3和6它是/"Food/"1-18子孙节点同样我们可以看到所有左值大
于2和右值小于11节点都是/"Fruit/"2-11子孙节点
1Food18
|
+---------------------------------------+
||
2Fruit1112Meat17
||
+---------------------------------------------+
||||
3Red67Yellow1013Beef1415Pork16
||
4Cherry58Banana9
这样整个树状结构可以通过左右值来存储到数据库中继续的前我们看看下面整理过数据表
+-----------------------+-----+-----+
|parent|name|lft|rgt|
+-----------------------+-----+-----+
||Food|1|18|
|Food|Fruit|2|11|
|Fruit|Red|3|6|
|Red|Cherry|4|5|
|Fruit|Yellow|7|10|
|Yellow|Banana|8|9|
|Food|Meat|12|17|
|Meat|Beef|13|14|
|Meat|Pork|15|16|
+-----------------------+-----+-----+
注意:由于/"left/"和/"right/"在SQL中有特殊意义所以我们需要用/"lft/"和/"rgt/"来表示左右字段另外这种结构
中不再需要/"parent/"字段来表示树状结构也就是说下面这样表结构就足够了
+------------+-----+-----+
|name|lft|rgt|
+------------+-----+-----+
|Food|1|18|
|Fruit|2|11| [Page]
|Red|3|6|
|Cherry|4|5|
|Yellow|7|10|
|Banana|8|9|
|Meat|12|17|
|Beef|13|14|
|Pork|15|16|
+------------+-----+-----+
好了我们现在可以从数据库中获取数据了例如我们需要得到/"Fruit/"项下所有所有节点就可以这样写查询语句
:SELECT*FROMtreeWHERElftBETWEEN2AND11;这个查询得到了以下结果
+------------+-----+-----+
|name|lft|rgt|
+------------+-----+-----+
|Fruit|2|11|
|Red|3|6|
|Cherry|4|5|
|Yellow|7|10|
|Banana|8|9|
+------------+-----+-----+
看到了吧只要个查询就可以得到所有这些节点为了能够像上面递归那样显示整个树状结构我们还需要对这样查
询进行排序用节点左值进行排序:
SELECT*FROMtreeWHERElftBETWEEN2AND11ORDERBYlftASC;
剩下问题如何显示层级缩进了
($result);
//准备个空右值堆栈
$right=.gif' />;
//获得根基点所有子孙节点
$result=mysql_query('SELECTname,lft,rgtFROMtree'.
'WHERElftBETWEEN'.$row['lft'].'AND'.
$row['rgt'].'ORDERBYlftASC;');
//显示每行
while($row=mysql_fetch_.gif' />($result))
{
//onlycheckstackthereisone
(count($right)>0)
{
//检查我们是否应该将节点移出堆栈
while($right[count($right)-1]_pop($right);
}
}
//缩进显示节点名称
echostr_repeat('',count($right)).$row['name']./"n/";
//将这个节点加入到堆栈中
$right=$row['rgt'];
}
}
> [Page]
如果你运行下以上就会得到和递归样结果只是我们这个新可能会更快些只有2次数据库查询要获知个节点路径就
更简单了如果我们想知道Cherry路径就利用它左右值4和5来做个查询
SELECTnameFROMtreeWHERElft5ORDERBYlftASC;
这样就会得到以下结果:
+------------+
|name|
+------------+
|Food|
|Fruit|
|Red|
+------------+
那么某个节点到底有多少子孙节点呢 很简单子孙总数=(右值-左值-1)/2descendants=(right–left-1)/2不相信
自己算算啦用这个简单公式我们可以很快算出/"Fruit2-11/"节点有4个子孙节点而/"Banana8-9/"节点没有子
孙节点也就是说它不是个父节点了
很神奇吧 虽然我已经多次用过这个思路方法但是每次这样做时候还是感到很神奇
这确是个很好办法但是有什么办法能够帮我们建立这样有左右值数据表呢 这里再介绍个给大家这个可以将
name和parent结构表自动转换成带有左右值数据表
($result)){
//recursiveexecutionofthisfunctionforeach
//childofthisnode
//$rightisthecurrentrightvalue,whichis
//incrementedbytherebuild_treefunction
$right=rebuild_tree($row['name'],$right);
}
//we'vegottheleftvalue,andnowthatwe'veprocessed
//thechildrenofthisnodewealsoknowtherightvalue
mysql_query('UPDATEtreeSETlft='.$left.',rgt='.
$right.'WHEREname=/"'.$parent.'/";');
//therightvalueofthisnode+1
$right+1;
}
>
当然这个是个递归我们需要从根节点开始运行这个来重建个带有左右值树 [Page]
rebuild_tree('Food',1);
这个看上去有些复杂但是它作用和手工对表进行编号样就是将立体多层结构转换成个带有左右值数据表
那么对于这样结构我们该如何增加更新和删除个节点呢 增加个节点般有两种思路方法:
保留原有name和parent结构用老思路方法向数据中添加数据每增加条数据以后使用rebuild_tree对整个结构重
新进行次编号
效率更高办法是改变所有位于新节点右侧数值举例来说:我们想增加种新水果/"Strawberry/"(草莓)它将成为
/"Red/"节点最后个子节点首先我们需要为它腾出些空间/"Red/"右值应当从6改成8/"Yellow7-10/"左右值则应
当改成9-12依次类推我们可以得知如果要给新值腾出空间需要给所有左右值大于5节点(5是/"Red/"最后个子节
点右值)加上2所以我们这样进行数据库操作:
UPDATEtreeSETrgt=rgt+2WHERErgt>5;
UPDATEtreeSETlft=lft+2WHERElft>5;
这样就为新插入值腾出了空间现在可以在腾出空间里建立个新数据节点了它左右值分别是6和7
INSERTINTOtreeSETlft=6,rgt=7,name='Strawberry';
再做次查询看看吧!如何样 很快吧
好了现在你可以用两种区别思路方法设计你多级数据库结构了采用何种方式完全取决于你个人判断但是对于层
次多数量大结构我更喜欢第 2种思路方法如果查询量较小但是需要频繁添加和更新数据则第种思路方法更为简
便
另外如果数据库支持话你还可以将rebuild_tree和腾出空间操作写成数据库端触发器在插入和更新时候自动执
行这样可以得到更好运行效率而且你添加新节点SQL语句会变得更加简单
类递归法
Postedby访客on2004,May31-9:18am.
我用类递归法写了段跟文章中递归不完全样
正准备移植到xoops中:
http://dev.xoops.org/modules/xfmod/project/ ulink
已经出现内存溢出现象
不过准备继续采用递归法只是需要继续改进
希望有机会跟各位讨论cms
replytothiscomment
还是两种思路方法的比较
Postedby访客on2004,March17-8:30pm.
仔细研究了下这篇文章觉得受益非浅但后来又想了想觉得有下问题(为了好记忆毗邻目录模式我称为递归思
路方法预排序遍历树算法我称为预排序树思路方法):
1,两种思路方法比较大区别是递归是在查询时候要用到堆栈进行递归预排序树则是在更新节点时要进行半数
(指所插入节点后半部分)节点更新虽然您也说了如果节点多了更新又频繁预排序树效率会降低采用递归会好些而
如果节点层次较多话首先递归会导致堆栈溢出再者递归本身效率就不高加上每层递归都要操作数据库总体效果
也不会理想我目前做法是次性把数据全取出来然后对进行递归操作会好些;再进步改进话可以为每行记录增加
个ROOT根节点(目前是只记录相邻父节点)这样在查找分支树时效率就会比较高了更新树时候也是十分便捷应该
是种比较好方式 [Page]
2,改进递归方式文章中在计算预排序树节点左右值时候其实也用到了种遍历方式通过替代堆栈手工实现压栈和
弹出;这种思路方法如果引用到递归算法中在进行递归时候也用替代堆栈话也可以提高递归效率
3,并发如果考虑到并发情况尤其是更新树时候预排序树大面积更新节点信息思路方法需要额外注意采用加锁和
事务机制保证数据致性
4,多根节点或者多父节点情况在这种情况下显然就不是个标准 2叉树或者多叉树了预排序树算法需要进行比较
大改进才能适应而递归思路方法则应用自如所以在这种情况下递归适应性较强这是当然了递归思路方法就是链
表种形式树,图都可以用链表来表达当然适应力强了
5,直观如果不用操作直接观察数据库中存储数据话显然递归方式下存储数据比较直观而预排序树数据很难直接
阅读(针对层次关系来说)这在数据交换中是不是会有影响呢
总体来说我个人比较喜欢用递归思路方法但直担心递归对效率影响所幸还没有接触过规模较大分类层次递
归用替代堆栈会是种比较好改进思路方法而预排序树不失为种解决简单树高效思路方法用习惯了也应该是非常
出色尤其是它从叶子节点到根节点反向查找非常方便
Fwolf
www.fwolf.com
replytothiscomment
非常高兴看到你回复
Postedbyshukeon2004,March18-5:47am.
非常高兴你这么认真读完这篇文章这篇文章其实是原来发表在sitepo.com上我把它翻译了下希望给希望初学入
门朋友介绍些思路方法抛砖引玉你思路方法也很好有机会我会试下(如果你有兴趣话何不就上面例子把你思路方
法和具体实现代码也写成教程发出来吧这样大家就用更加实际例子来模仿了)如果你对数据库中保存多级结构有
兴趣研究话这里还有两个连接也很不错可以作为参考:
介绍了常见4中思路方法
次查询排序脚本我想你脚本肯定比这个强
另外我看到你也用drupal它还有个高级功能叫分布式用户验证系统只要在任何个drupal站点注册以后就可以登
录访问其它drupal站点了挺有意思
祝好!
replytothiscomment
用循环来建树已经实现了
Postedby访客on2004,March25-10:10pm.
你上次提供资料我已经都看过了不过老实说第篇文章里没有太多新东西或许是我没看太明白吧第 2个居然是
PHP3写结构没有细看用到太多交叉
正好我在个系统中用户角色要用到分级按照思路就把遍历写了下来没有时间整理先放到这里你看看吧数据库用
是ADODB是直接从系统中摘出来希望能够描述得清楚主要是利用了PHP强大操作用循环来进行递归注释里是种
相近思路方法只是处理结果时机区别而已
mIsDispListIndex=true;
//echo('增加新角色
');_fcksavedurl=/"/" action=&part=role/">增加新角色 ');/"
//
//$this->mListTitle='用户角色列表';
//$this->SetDataOption('list');
//
//$this->SetQueryTable(.gif' />($this->mTableUserRole));
//
////查询顺序
//$this->SetQueryOrder('asc',$this->mTableUserRole,'sequence');
//
//$this->Query('list');
//parent::DispList;
////另外种显示方式用作为堆栈A:压栈时存role压完就删除source
//$this->CheckProperty('mrDb');
//$this->CheckProperty('mrSql');
//$this->mrSql->Select('role,title,parent');
//$this->mrSql->From($this->mTableUserRole);
//$this->mrSql->Orderby('parent,sequence');
//$this->mRs=$this->mrDb->Execute($this->mrSql->Sql);
//(0mRs))
//{
//$source=&$this->mRs->GetArray;//数字索引
//$stack=.gif' />('');//堆栈
//$stacki=.gif' />(-1);//和堆栈对应记录堆栈中数据在树中层次
//$target=.gif' />;
//while(0_sht($stack);
//$lev=.gif' />_sht($stacki);
//(!empty($item))
//{
////在这里把加工过数据放到target
//.gif' />_push($target,str_repeat('',$lev).$item);
////$s1=str_repeat('',$lev).$item;
//}
//$del=.gif' />;//要从$source中删除节点
//$ar=.gif' />;//需要添加到堆栈中节点
//foreach($sourceas$key=>$val)
//{
////寻找匹配子节点
//(empty($item))
//{
//$find=empty($source[$key]['parent']); [Page]
//}
//
//{
//$find=($item$source[$key]['parent']);
//}
//($find)
//{
//.gif' />_unsht($ar,$source[$key]['role']);
//$del=$key;
//}
//}
//foreach($aras$val)
//{
//.gif' />_unsht($stack,$val);
//.gif' />_unsht($stacki,$lev+1);
//}
//foreach($delas$val)
//{
//un($source[$val]);
//}
//echo(implode(',',$stack).''.implode(',',$stacki).''.implode(',',$target).'');
//}
//debug_.gif' />;
//}
//
//{
//echo('没有检索到数据');
//}
//另外种显示方式用作为堆栈B:压栈时存索引出栈并使用完后再删除source
$this->CheckProperty('mrDb');
$this->CheckProperty('mrSql');
$this->mrSql->Select('role,title,parent');
$this->mrSql->From($this->mTableUserRole);
$this->mrSql->Orderby('parent,sequence');
$this->mRs=$this->mrDb->Execute($this->mrSql->Sql);
(!empty($this->mRs)&&!$this->mRs->EOF)
{
$source=&$this->mRs->GetArray;//数字索引
$stack=.gif' />(-1);//堆栈
$stacki=.gif' />(-1);//和堆栈对应记录堆栈中数据在树中层次
$target=.gif' />;
while(0_sht($stack);
$lev=.gif' />_sht($stacki);
(-1!=$item)
{
//在这里把加工过数据放到target
$s1=str_repeat('',$lev).''.$source[$item]['title'].'';
$s2='编辑
删除'; [Page]
.gif' />_push($target,.gif' />($s1,$s2));
}
$del=.gif' />;//要从$source中删除节点
$ar=.gif' />;//需要添加到堆栈中节点
foreach($sourceas$key=>$val)
{
//寻找匹配子节点
(-1$item)
{
$find=empty($source[$key]['parent']);
}
{
$find=($source[$item]['role']$source[$key]['parent']);
}
($find)
{
.gif' />_unsht($ar,$key);
}
}
foreach($aras$val)
{
.gif' />_unsht($stack,$val);
.gif' />_unsht($stacki,$lev+1);
}
//从source中删除
un($source[$item]);
//echo(implode(',',$stack).''.implode(',',$stacki).''.implode(',',$target).'');
}
//输出
echo('增加新角色 ');
.gif' />_unsht($target,.gif' />('角色','操作'));
$this->CheckProperty('mrLt');
$this->mrLt->SetData($target);
$this->mrLt->mListTitle='用户角色列表';
$this->mrLt->mIsDispIndex=false;
$this->mrLt->Disp;
}
{
echo('没有检索到数据');
}
}//endoffunctionDispList
>
php树形结构:php树形结构的算法
最新推荐文章于 2025-12-05 12:53:34 发布
本文探讨了在关系型数据库中存储树状结构数据的两种常见方法:毗邻目录模式和预排序遍历树算法,并对比了它们的特点及适用场景。
2980

被折叠的 条评论
为什么被折叠?



