左右值无限值分类算法

一、引言

品分类,多级的树状结构的论坛,邮件列表等许多地方我们都会遇到这样的问题:如何存储多级结构的数据?在PHP的应用中,提供后台数据存储的通常是关系型
数据库,它能够保存大量的数据,提供高效的数据检索和更新服务。然而关系型数据的基本形式是纵横交错的表,是一个平面的结构,如果要将多级树状结构存储在
关系型数据库里就需要进行合理的翻译工作。接下来我会将自己的所见所闻和一些实用的经验和大家探讨一下:
层级结构的数据保存在平面的数据库中基本上有两种常用设计方法:

* 毗邻目录模式(adjacency list model)
    * 预排序遍历树算法(modified preorder tree traversal algorithm)

我不是计算机专业的,也没有学过什么数据结构的东西,所以这两个名字都是我自己按照字面的意思翻的,如果说错了还请多多指教。这两个东西听着好像很吓人,其实非常容易理解。

二、模型
这里我用一个简单食品目录作为我们的示例数据。
我们的数据结构是这样的,以下是代码:

  1. Food
  2. |
  3. |---Fruit
  4. |    |
  5. |    |---Red
  6. |    |    |
  7. |    |    |--Cherry
  8. |    |
  9. |    +---Yellow
  10. |          |
  11. |          +--Banana
  12. |
  13. +---Meat
  14. |--Beef
  15. +--Pork

复制代码

为了照顾那些英文一塌糊涂的PHP爱好者

  1. Food  : 食物
  2. Fruit : 水果
  3. Red   : 红色
  4. Cherry: 樱桃
  5. Yellow: 黄色
  6. Banana: 香蕉
  7. Meat  : 肉类
  8. Beef  : 牛肉
  9. Pork  : 猪肉

复制代码

三、实现

1、毗邻目录模式(adjacency list model)

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

例子中的数据可以转化成如下的表:
以下是代码:

  1. +-----------------------+
  2. |   parent |    name    |
  3. +-----------------------+
  4. |          |    Food    |
  5. |   Food   |   Fruit    |
  6. |   Fruit  |    Green   |
  7. |   Green  |    Pear    |
  8. |   Fruit  |    Red     |
  9. |   Red    |    Cherry  |
  10. |   Fruit  |    Yellow  |
  11. |   Yellow |    Banana  |
  12. |   Food   |    Meat    |
  13. |   Meat   |    Beef    |
  14. |   Meat   |    Pork    |
  15. +-----------------------+

复制代码


们看到 Pear 是Green的一个子节点,Green是Fruit的一个子节点。而根节点‘Food‘没有父节点。
为了简单地描述这个问题,这个例子中只用了name来表示一个记录。
在实际的

数据库中,你需要用数字的id来标示每个节点,数据库的表结构大概应该像这样:id, parent_id, name,
descrīption。
有了这样的表我们就可以通过数据库保存整个多级树状结构了。

显示多级树,如果我们需要显示这样的一个多级结构需要一个递归函数。
以下是代码:

  1. <?php
  2. // $parent is the parent of the children we want to see
  3. // $level is increased when we go deeper into the tree,
  4. //        used to display a nice indented tree
  5. function display_children($parent, $level) {
  6. // 获得一个 父节点 $parent 的所有子节点
  7. $result = mysql_query("
  8. SELECT name
  9. FROM tree
  10. WHERE parent = ‘" . $parent . "‘
  11. ;"
  12. );
  13. // 显示每个子节点
  14. while ($row = mysql_fetch_array($result)) {
  15. // 缩进显示节点名称
  16. echo str_repeat(‘  ‘, $level) . $row[‘name‘] . "\n";
  17. //再次调用这个函数显示子节点的子节点
  18. display_children($row[‘name‘], $level+1);
  19. }
  20. }
  21. ?>

复制代码

对整个结构的根节点(Food)使用这个函数就可以打印出整个多级树结构,由于Food是根节点它的父节点是空的,所以这样调用: display_children(‘‘,0)。将显示整个树的内容:

  1. Food
  2. Fruit
  3. Red
  4. Cherry
  5. Yellow
  6. Banana
  7. Meat
  8. Beef
  9. Pork

复制代码

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


乎使用同样的方法我们可以知道从根节点到任意节点的路径。比如 Cherry 的路径是 "Food >; Fruit >; Red"。
为了得到这样的一个路径我们需要从最深的一级"Cherry"开始,

查询得到它的父节点"Red"把它添加到路径中,然后我们再查询Red的父节点并把它也添加到路径中,以此类推直到最高层的"Food",以下是代码:

  1. <?php
  2. // $node 是那个最深的节点
  3. function get_path($node) {
  4. // 查询这个节点的父节点
  5. $result = mysql_query("
  6. SELECT parent
  7. FROM tree
  8. WHERE name = ‘" . $node ."‘
  9. ;"
  10. );
  11. $row = mysql_fetch_array($result);
  12. // 用一个数组保存路径
  13. $path = array();
  14. // 如果不是根节点则继续向上查询
  15. // (根节点没有父节点)
  16. if ($row[‘parent‘] != ‘‘) {
  17. // the last part of the path to $node, is the name
  18. // of the parent of $node
  19. $path[] = $row[‘parent‘];
  20. // we should add the path to the parent of this node
  21. // to the path
  22. $path = array_merge(get_path($row[‘parent‘]), $path);
  23. }
  24. // return the path
  25. return $path;
  26. }
  27. ?>;

复制代码

如果对"Cherry"使用这个函数:print_r(get_path(‘Cherry‘)),就会得到这样的一个数组了:

  1. Array (
  2. [0] => Food
  3. [1] => Fruit
  4. [2] => Red
  5. )

复制代码

接下来如何把它打印成你希望的格式,就是你的事情了。

缺点:
这种方法很简单,容易理解,好上手。但是也有一些缺点。主要是因为运行速度很慢,由于得到每个节点都需要进行数据库查询,数据量大的时候要进行很多查询才能完成一个树。

另外由于要进行递归运算,递归的每一级都需要占用一些内存所以在空间利用上效率也比较低。

2、预排序遍历树算法

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


们首先将多级数据按照下面的方式画在纸上,在根节点Food的左侧写上 1 然后沿着这个树继续向下 在 Fruit 的左侧写上 2
然后继续前进,沿着整个树的边缘给每一个节点都标上

左侧和右侧的数字。最后一个数字是标在Food 右侧的
18。在下面的这张图中你可以看到整个标好了数字的多级结构。(没有看懂?用你的手指指着数字从1数到18就明白怎么回事

了。还不明白,再数一遍,注意移
动你的手指)。
这些数字标明了各个节点之间的关系,"Red"的号是3和6,它是 "Food" 1-18 的子孙节点。 同样,我们可以看到 所有左值大于2和右值小于11的节点 都是"Fruit" 2-11 的子孙节

以下是代码:

  1. 1 Food 18
  2. |
  3. +------------------------------+
  4. |                              |
  5. 2 Fruit 11                     12 Meat 17
  6. |                              |
  7. +-------------+                 +------------+
  8. |             |                 |            |
  9. 3 Red 6      7 Yellow 10       13 Beef 14   15 Pork 16
  10. |             |
  11. 4 Cherry 5    8 Banana 9

复制代码

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

以下是代码:

  1. +----------+------------+-----+-----+
  2. |  parent  |    name    | lft | rgt |
  3. +----------+------------+-----+-----+
  4. |          |    Food    | 1   | 18  |
  5. |   Food   |   Fruit    | 2   | 11  |
  6. |   Fruit  |    Red     | 3   |  6  |
  7. |   Red    |    Cherry  | 4   |  5  |
  8. |   Fruit  |    Yellow  | 7   | 10  |
  9. |   Yellow |    Banana  | 8   |  9  |
  10. |   Food   |    Meat    | 12  | 17  |
  11. |   Meat   |    Beef    | 13  | 14  |
  12. |   Meat   |    Pork    | 15  | 16  |
  13. +----------+------------+-----+-----+

复制代码

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

结构就足够了。

以下是代码:

  1. +------------+-----+-----+
  2. |    name    | lft | rgt |
  3. +------------+-----+-----+
  4. |    Food    | 1   | 18  |
  5. |    Fruit   | 2   | 11  |
  6. |    Red     | 3   |  6  |
  7. |    Cherry  | 4   |  5  |
  8. |    Yellow  | 7   | 10  |
  9. |    Banana  | 8   |  9  |
  10. |    Meat    | 12  | 17  |
  11. |    Beef    | 13  | 14  |
  12. |    Pork    | 15  | 16  |
  13. +------------+-----+-----+

复制代码

好了我们现在可以从数据库中获取数据了,例如我们需要得到"Fruit"项下的所有所有节点就可以这样写查询语句:

  1. SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

复制代码

这个查询得到了以下的结果。

以下是代码:

  1. +------------+-----+-----+
  2. |    name    | lft | rgt |
  3. +------------+-----+-----+
  4. |    Fruit   | 2   | 11  |
  5. |    Red     | 3   |  6  |
  6. |    Cherry  | 4   |  5  |
  7. |    Yellow  | 7   | 10  |
  8. |    Banana  | 8   |  9  |
  9. +------------+-----+-----+

复制代码

看到了吧,只要一个查询就可以得到所有这些节点。为了能够像上面的递归函数那样显示整个树状结构,我们还需要对这样的查询进行排序。用节点的左值进行排序:

  1. SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;

复制代码

剩下的问题如何显示层级的缩进了。
以下是代码:

  1. <?php
  2. function display_tree($root) {
  3. // 得到根节点的左右值
  4. $result = mysql_query("
  5. SELECT lft, rgt
  6. FROM tree
  7. WHERE name = ‘" . $root . "‘
  8. ;"
  9. );
  10. $row = mysql_fetch_array($result);
  11. // 准备一个空的右值堆栈
  12. $right = array();
  13. // 获得根基点的所有子孙节点
  14. $result = mysql_query("
  15. SELECT name, lft, rgt
  16. FROM tree
  17. WHERE lft BETWEEN ‘" . $row[‘lft‘] . "‘ AND ‘" . $row[‘rgt‘] ."‘
  18. ORDER BY lft ASC
  19. ;"
  20. );
  21. // 显示每一行
  22. while ($row = mysql_fetch_array($result)) {
  23. // only check stack if there is one
  24. if (count($right) > 0) {
  25. // 检查我们是否应该将节点移出堆栈
  26. while ($right[count($right) - 1] < $row[‘rgt‘]) {
  27. array_pop($right);
  28. }
  29. }
  30. // 缩进显示节点的名称
  31. echo str_repeat(‘  ‘,count($right)) . $row[‘name‘] . "\n";
  32. // 将这个节点加入到堆栈中
  33. $right[] = $row[‘rgt‘];
  34. }
  35. }
  36. ?>

复制代码

如果你运行一下以上的函数就会得到和递归函数一样的结果。只是我们的这个新的函数可能会更快一些,因为只有2次数据库查询。
要获知一个节点的路径就更简单了,如果我们想知道Cherry 的路径就利用它的左右值4和5来做一个查询。

  1. SELECT name FROM tree WHERE lft < 4 AND rgt >; 5 ORDER BY lft ASC;

复制代码

这样就会得到以下的结果:

以下是代码:

  1. +------------+
  2. |    name    |
  3. +------------+
  4. |    Food    |
  5. |    Fruit   |
  6. |    Red     |
  7. +------------+

复制代码

那么某个节点到底有多少子孙节点呢?很简单,子孙总数=(右值-左值-1)/2

  1. descendants = (right – left - 1) / 2

复制代码

不相信?自己算一算啦。
用这个简单的公式,我们可以很快的算出"Fruit 2-11"节点有4个子孙节点,而"Banana 8-9"节点没有子孙节点,也就是说它不是一个父节点了。
很神奇吧?虽然我已经多次用过这个方法,但是每次这样做的时候还是感到很神奇。
这的确是个很好的办法,但是有什么办法能够帮我们建立这样有左右值的数据表呢?这里再介绍一个函数给大家,这个函数可以将name和parent结构的表自动转换成带有左右值的

数据表。
以下是代码:

  1. <?php
  2. function rebuild_tree($parent, $left) {
  3. // the right value of this node is the left value + 1
  4. $right = $left+1;
  5. // get all children of this node
  6. $result = mysql_query("
  7. SELECT name
  8. FROM tree
  9. WHERE parent = ‘" . $parent . "‘
  10. ;"
  11. );
  12. while ($row = mysql_fetch_array($result)) {
  13. // recursive execution of this function for each
  14. // child of this node
  15. // $right is the current right value, which is
  16. // incremented by the rebuild_tree function
  17. $right = rebuild_tree($row[‘name‘], $right);
  18. }
  19. // we‘ve got the left value, and now that we‘ve processed
  20. // the children of this node we also know the right value
  21. mysql_query("
  22. UPDATE tree
  23. SET
  24. lft = ‘" . $left . "‘,
  25. rgt= ‘" . $right . "‘
  26. WHERE name = ‘" . $parent . "‘
  27. ;"
  28. );
  29. // return the right value of this node + 1
  30. return $right + 1;
  31. }
  32. ?>

复制代码

当然这个函数是一个递归函数,我们需要从根节点开始运行这个函数来重建一个带有左右值的树

  1. rebuild_tree(‘Food‘,1);

复制代码

这个函数看上去有些复杂,但是它的作用和手工对表进行编号一样,就是将立体多层结构的转换成一个带有左右值的数据表。

那么对于这样的结构我们该如何增加,更新和删除一个节点呢?
增加一个节点一般有两种方法:
第一种,保留原有的name 和parent结构,用老方法向数据中添加数据,每增加一条数据以后使用rebuild_tree函数对整个结构重新进行一次编号。

二种,效率更高的办法是改变所有位于新节点右侧的数值。举例来说:我们想增加一种新的水果"Strawberry"(草莓)它将成为"Red"节点的最后
一个子节点。首先我们需要为

它腾出一些空间。"Red"的右值应当从6改成8,"Yellow 7-10 "的左右值则应当改成
9-12。依次类推我们可以得知,如果要给新的值腾出空间需要给所有左右值大于5的节点

(5 是"Red"最后一个子节点的右值)
加上2。所以我们这样进行数据库操作:

  1. UPDATE tree SET rgt = rgt + 2 WHERE rgt > 5;
  2. UPDATE tree SET lft = lft + 2 WHERE lft > 5;

复制代码

这样就为新插入的值腾出了空间,现在可以在腾出的空间里建立一个新的数据节点了, 它的左右值分别是6和7

  1. INSERT INTO tree SET lft=6, rgt=7, name=‘Strawberry‘;

复制代码

再做一次查询看看吧!怎么样?很快吧。

四、结语
好了,现在你可以用两种不同的方法设计你的多级数据库结构了,采用何种方式完全取决于你个人的判断,但是对于层次多数量大的结构我更喜欢第二种方法。如果查询量较小但是

需要频繁添加和更新的数据,则第一种方法更为简便。
另外,如果数据库支持的话 你还可以将rebuild_tree()和 腾出空间的操作写成数据库端的触发器函数, 在插入和更新的时候自动执行, 这样可以得到更好的运行效率, 而且你添加

新节点的SQL语句会变得更加简单。

时间: 2024-10-20 08:46:38

左右值无限值分类算法的相关文章

php基于左右值排序的无限分类算法

PHP无限分类[左右值]算法 <?php /** * 基于左右值排序的无限分类算法 * 数据库结果为 CREATE TABLE om_catagory ( CatagoryID int(10) unsigned NOT NULL auto_increment, Name varchar(50) default '', Lft int(10) unsigned NOT NULL default '0', Rgt int(10) unsigned NOT NULL default '0', PRIM

数据库结果为 基于左右值排序的无限分类算法

<?php /**     * 基于左右值排序的无限分类算法     * 数据库结果为 CREATE TABLE om_catagory (      CatagoryID int(10) unsigned NOT NULL auto_increment,     Name varchar(50) default '',      Lft int(10) unsigned NOT NULL default '0',      Rgt int(10) unsigned NOT NULL defau

分类算法总结

      目前看到的比较全面的分类算法,总结的还不错.       主要分类方法介绍解决分类问题的方法很多[40-42] ,单一的分类方法主要包括:决策树.贝叶斯.人工神经网络.K-近邻.支持向量机和基于关联规则的分类等:另外还有用于组合单一分类方法的集成学习算法,如Bagging和Boosting等.      (1)决策树       决策树是用于分类和预测的主要技术之一,决策树学习是以实例为基础的归纳学习算法,它着眼于从一组无次序.无规则的实例中推理出以决策树表示的分类规则.构造决策树的

基于朴素贝叶斯分类器的文本分类算法

源代码下载:NaviveBayesClassify.rar Preface 文本的分类和聚类是一个比较有意思的话题,我以前也写过一篇blog<基于K-Means的文本聚类算法>,加上最近读了几本数据挖掘和机器学习的书籍,因此很想写点东西来记录下学习的所得. 在本文的上半部分<基于朴素贝叶斯分类器的文本分类算法(上)>一文中简单介绍了贝叶斯学习的基本理论,这一篇将展示如何将该理论运用到中文文本分类中来,具体的文本分类原理就不再介绍了,在上半部分有,也可以参见代码的注释. 文本特征向量

聚类算法和分类算法总结

原文:http://blog.chinaunix.net/uid-10289334-id-3758310.html 聚类算法的种类: 基于划分聚类算法(partition clustering) k-means: 是一种典型的划分聚类算法,它用一个聚类的中心来代表一个簇,即在迭代过程中选择的聚点不一定是聚类中的一个点,该算法只能处理数值型数据 k-modes: K-Means算法的扩展,采用简单匹配方法来度量分类型数据的相似度 k-prototypes: 结合了K-Means和K-Modes两种

二分类算法评估指标

我们都知道机器学习要建模,但是对于模型性能的好坏我们并不知道是怎样的,很可能这个模型就是一个差的模型,对测试集不能很好的预测.那么如何知道这个模型是好是坏呢?必须有个评判的标准,需要用某个指标来衡量,这就是性能度量的意义.有了一个指标,就可以对比不同模型了,从而知道哪个模型更好,或者通过这个指标来调参优化选用的模型. 对于分类.回归.聚类等,分别有各自的评判标准.本篇主要介绍二分类算法(多分类可以扩展转化成二分类)的相关指标.评估一个二分类的分类器的性能指标有:准确率.查准率.查全率.F1值.A

KNN分类算法实现手写数字识别

需求: 利用一个手写数字"先验数据"集,使用knn算法来实现对手写数字的自动识别: 先验数据(训练数据)集: ?数据维度比较大,样本数比较多. ? 数据集包括数字0-9的手写体. ?每个数字大约有200个样本. ?每个样本保持在一个txt文件中. ?手写体图像本身的大小是32x32的二值图,转换到txt文件保存后,内容也是32x32个数字,0或者1,如下: 数据集压缩包解压后有两个目录:(将这两个目录文件夹拷贝的项目路径下E:/KNNCase/digits/) ?目录trainingD

数据挖掘中分类算法小结

数据挖掘中分类算法小结 数据仓库,数据库或者其它信息库中隐藏着许多可以为商业.科研等活动的决策提供所需要的知识.分类与预测是两种数据分析形式,它们可以用来抽取能够描述重要数据集合或预测未来数据趋势的模型.分类方法(Classification)用于预测数据对象的离散类别(Categorical Label);预测方法(Prediction )用于预测数据对象的连续取值. 分类技术在很多领域都有应用,例如可以通过客户分类构造一个分类模型来对银行贷款进行风险评估;当前的市场营销中很重要的一个特点是强

各种分类算法比较

1决策树(Decision Trees)的优缺点 决策树的优点: 一.           决策树易于理解和解释.人们在通过解释后都有能力去理解决策树所表达的意义. 二.           对于决策树,数据的准备往往是简单或者是不必要的.其他的技术往往要求先把数据一般化,比如去掉多余的或者空白的属性. 三.           能够同时处理数据型和常规型属性.其他的技术往往要求数据属性的单一. 四.           决策树是一个白盒模型.如果给定一个观察的模型,那么根据所产生的决策树很容易