时间:2021-07-01 10:21:17 帮助过:50人阅读
无论你想建立自己的论坛,在你的网站上从一个邮件列表发布消息,或编写自己的cms:你将会遇到一种情况:将分层数据(层级结构)存储在数据库中。除非你使用一个类似xml的数据库,通用的关系型数据库是很难做到这一点。关系型数据库中的表不分层;他们只是一个简单
无论你想建立自己的论坛,在你的网站上从一个邮件列表发布消息,或编写自己的cms:你将会遇到一种情况:将分层数据(层级结构)存储在数据库中。除非你使用一个类似xml的数据库,通用的关系型数据库是很难做到这一点。关系型数据库中的表不分层;他们只是一个简单列表。你必须找到一个方法来把层次结构数据转换为一个简单文件数据。
存储树是一种常见的问题,可以有多个解决方案来实现。有两种主要方法:邻接表模型和修改后的树前序遍历的算法。
在本文中,我们将探讨这两种方法来存储层级结构数据。我将使用从一个虚构的在线食品商店虚拟的这棵树为例。这个食品商店通过类别、颜色以及种类来来组织它的食品。
这颗树如下:
这篇文章包含一些代码示例展示了如何保存和检索数据。原文是使用的PHP,在这我将使用java来演示它的代码展示。
我们最先尝试或者最优雅的方式我们称为“邻接表模式”或者我们称它为“递归方法”。
这是一种优雅的方法,因为你只需要一个简单的函数就可以遍历树。在我们的食品商店的例子中,邻接表的表可以这样表示:
如您所见,在邻接表的方法,你只保存每个节点的父节点。我们可以从表上清楚的看到,“Pear”是“Green”的一个孩子,同时“Green”又是'Fruit'的孩子。
根节点“Food”没有父节点。为简单起见,我使用了“标题”值来确定每个节点。当然,在一个真正的数据库,可以使用id来标示。
既然我们已经在数据库中插入我们的树,是时候写一个显示功能。这个函数将不得不从根节点(没有父节点)开始,应该显示所有该节点的孩子。对于这些孩子节点来说,这个函数应该检索和显示这些孩子节点的所有子节点。然后在显示这些孩子节点的子节点,递归的进行。
public void DisplayTree(int parentId,int level){ String sql = "select NodeId,NodeName,ParentId from Tree where parentid="+ parentId; conn = sqlManager.GetConn(); try { statement = conn.createStatement(); ResultSet rs = statement.executeQuery(sql); while(rs.next()){ for(int i = 0;i < level*2;i++){ System.out.print("-"); } System.out.println(rs.getString("NodeName")); DisplayTree(rs.getInt("NodeId"),level+1); } } catch (SQLException e) { e.printStackTrace(); } }注:sqlManager类是数据库操作类。
数据库存储的数据:
打印整个树DisplayTree(0,0)
结果:
如果你只是想看到一个子树,你可以告诉函数这个开始节点的Id即可。
例如,显示“Fruit”子树,运行DisplayTree(2,0);
有时候我们需要知道某个节点所在的路径。举例来说,“Cherry”所在的路径为Food > Fruit > Red > Cherry。在这里,我们可以从Cherry开始查起,然后递归查询查询节点前的节点,直到某节点的父节点ID为0。
//获取某个节点所在路径 public ArrayListGetPath(int nodeId){ String sql = "select ParentId,NodeName from Tree where NodeId = "+nodeId; ArrayList path = new ArrayList (); try { conn = sqlManager.GetConn(); statement = conn.createStatement(); ResultSet rs = statement.executeQuery(sql); rs.next(); int parentId = rs.getInt("ParentId"); String nodeName = rs.getString("NodeName"); if(parentId != 0){ path.addAll(GetPath(parentId)); } path.add(nodeName); } catch (SQLException e) { e.printStackTrace(); } return path; }
ArrayListid= 9 对应为Banana 路径为:Food->Fruit->Yellow->Bananapath = main.GetPath(9); int index = 0; for (String node : path) { if(index != 0){ System.out.print("->"); } System.out.print(node); index++; }
第二个原因是在你可能使用的编程语言这个方法不是那么快,。对于一门程序语言来说,除了Lisp这种,大多数不是为了递归而设计。当一个节点深度为4时,它得同时生成4个函数实例,它们都需要花费时间、占用一定的内存空间。所以,邻接表模型效率的低下可想而知。
那么就让我们来看另外一种存储树形结构的方法。如之前所讲,我们希望能够减少查询的数量,最好是只做到查询一次数据库。
现在我们把树“横”着放。如下图所示,我们首先从根节点(“Food”)开始,先在它左侧标记“1”,然后我们到“Fruit”,左侧标记“2”,接着按照前序遍历的顺序遍历完树,依次在每个节点的左右侧标记数字。最后一个数字写在“Food”节点右边的。在这张图片里,你可以看到用数字标记的整个树和几个箭头指示编号顺序。
我们叫这些数字为Left和Right(例如“Food”的Left值是1,Right值是18)。正如你所看到的,这些数字表示每个节点之间的关系。
比如,“Red”节点左边的数为3、右边的数为6,它是Food(1-18)的后代。同样的,我们可以注意到,左数大于2、右数小于11的节点都是“Fruit”的子孙。现在,所有的节点将以左数-右数的方式存储,这种通过遍历一个树、然后给每一个节点标注左数、右数的方式称为修改过的前序遍历算法。
在我们继续之前,让我们来看看在我们的表的这些值:
注意,“Left”和“Right”在SQL中有特殊的意义。因此,我们必须使用“lft”和“rgt”标识列。还要注意,我们并不真的需要“parent”列了。我们现在有lft和rgt值存储树结构。
SELECT * FROM FoodTree WHERE Lft BETWEEN 2 AND 11;注:FoodTree是存储数据的表
返回:
整个树只需要一次查询。
显示这棵树就像我们做递归函数,我们将不得不ORDER BY子句添加到查询语句中。如果你从你的表添加和删除行,你的表可能不会以正确的顺序存储。我们应该按左值进行排序。
SELECT * FROM FoodTree WHERE Lft BETWEEN 2 AND 11 ORDER BY Lft ASC;
现在唯一的问题是缩进问题。
显示树结构,孩子应该缩进略高于他们的父母。
正如我们面对树的问题常常会想到的方案——栈。这里,我们可以维护一个只保存右数的栈。我们知道该节点所有的孩子的Rgt值小于父的Rgt值,所以通过比较当前节点的Rgt值和堆栈中最后一个节点的Rgt值。当当前节点的Rgt值大于栈顶元素的值(说明栈顶元素的子树都以遍历完毕),这个时候弹出栈顶值。再循环检查栈顶值,直到栈顶值小于当前查询节点的Rgt值。这个时候只要检查栈中元素,有多少个元素说明当前查询节点有多少个祖先节点(设为n)。只需要打印n个空格即可。代码如下: