打印

[asp] [ASP]无限级分类的简单算法实现及代码重点讲解。

一、前言
       很多情况下二级分类已经不能满足需要了,而网上可用的多级分类的例子实在是不好找,故有此文。
http://bbs.blueidea.com/viewthread.php?tid=1182243
大家可以先看这个,它介绍了一种超级好的算法,反正我是看不大懂呀。

二、我们要解决的问题:
1、 分类算法常常表现为树的表示和遍历问题。那么,请问:如果用数据库中的一个Table来表达树型分类,应该有几个字段?
2、 如何快速地从这个Table恢复出一棵树;
3、 如何判断某个分类是否是另一个分类的子类;
4、 如何查找某个分类的所有产品;
5、 如何生成分类所在的路径。
6、 如何新增分类;

三、递归实现的优点与缺点
       该怎么实现多级分类呢?
       估计首先想到的都是递归,实现简单,在指定节点(就是分类,下同)下添加、修改、删除节点都不是问题,
而且节点移动实现起来也不是很难,只是要注意移动目的父节点不能是当前节点的父节节点(等于没移动),也不能是当前节点的子节点(类似于window文件夹,一个文件夹是不能移动到自己的字文件夹里的)。
       但是最愁人的是搜索指定节点下的东西,怎么办?也就是上面的问题3。记住,这是要包括所有子节点的,难道还去递归吗?

四、介绍下我的简单算法(是我所用的,不是我发明的)
       以常见的商品系统为例。
       4.1 表结构
              [1]分类表,T_Sort,表结构如图一所示。其中sortPath保存的是节点路径,这是个重点。
              [2]商品表,T_Product,表结构如图二所示。
[center]图一


图二

[/center]
       4.2 算法简要说明
              [1]parentID保存的自然是节点的父节点,如果一个节点的parentID=0时,认为它是一级分类。
              [2]一个节点的sortPath为它的父节点的sortPath+自己的sortID+","。如sortID=32的节点的父节点是节点21,节点21的sortPath是"0,21,",那么节点32的sortPath就是"0,21,32,"。有点绕,看图三清楚啦。可能你想不通为啥最后要多个逗号啊,后面你就明白啦。所有节点的sortPath的左边两位都是"0,",因为它们都在根节点下。一个节点的sortPath一定包含在它的子节点的sortPath中。
[center]
图三
[/center]
       4.3 代码重点讲解。
              这里以我们要实现的功能为例讲解。
              [1]添加节点
                     <1>选择父节点,可以是根节点,或是下级所有节点(最好列出一个树型菜单让用户选择,别愁,可以实现),其实就是选择parentID。
                     <2>如果parentID=0,那么上级sortPath="0,",如果parentID<>0,那么到表T_Sort根据parentID取得上级sortPath。
                     <3>给T_Sort新增记录,sortPath=上级sortPath +新记录的sortID +","。
                     <4>范例代码见图4、图5。其中noRecord,closeRs(),showMsg(),closeConn()都是我定义的Function或Sub,它们的功能都是顾名思义的,我就不说了。注意一下,如果你用MS SQL,代码略有不同。我也很奇怪MS SQL时,addNew后,这个新的自动编号可以输出,但是和字符一连接就没有了。各位如果知道为什么,还请相告。
[center]
图四


图五
[/center]
              [2]修改节点
                     节点的属性只有一个名字而已,直接update就可以了,就不说了。
              [3]删除节点
                     <1>选择节点
                     <2>如果parentID=0,报错,根节点不能删除。
                     <3>删除该节点及所有子节点。你可能想是不是很麻烦啊,哈哈,其实我只用了一个SQL语句就搞定啦。
复制内容到剪贴板
代码:
(Access)sql="delete from T_Sort where Instr(sortPath,',"&parentID&",')>0"
复制内容到剪贴板
代码:
(MS SQL)sql="delete from T_Sort where CHARINDEX(',"&parentID&",',sortPath)>0"
本算法的精华就在这里啦,仔细想想吧,sortPath最后那个逗号的作用也在这里啦。
                     <4>删除上述所有节点下的商品。同上,表名不同而已。
复制内容到剪贴板
代码:
(Access)sql="delete from T_Product where Instr(sortPath,',"&parentID&",')>0"
复制内容到剪贴板
代码:
(MS SQL)sql="delete from T_Product where CHARINDEX(',"&parentID&",',sortPath)>0"
<5>范例代码见图6。MS SQL的代码就不贴了。
[center]
图六
[/center]
              [4]移动节点
                     难点哦,睁大眼睛仔细看。
                     <1>选择要移动的节点parentID,选择目的节点toParentID(也就是把当前节点放到谁的下面)。
                     <2>如果parentID=0报错,根节点不能移动。
                     <3>如果toParentID=parentID,这是要把自己放到自己下面,报错。
                     <4>根据parentID,取得它的sortPath,我们叫它fromPath。
                     <5>如果toParentID=0,那么toPath="0,",如果toParentID<>0,取得它的sortPath,叫它toPath。
                     <6>如果toParentID等于要移动节点的父节点,不需要移动,报错。判断方法是看toPath & parentID &","是否等于fromPath。
                     <7>如果toParentID是要移动节点的子节点,不能移动,报错。判断方法是看Instr(toPath,fromPath)是否大于0。
                     <8>组合要移动节点的新sortPath,也就是newPath=toPath & parentID &","。
                     <9>更新要移动节点及其所有子节点的sortPath()。如"0,2,3,5,"移动到"0,1,"下,那么新的sortPath就是"0,1,5,"了(想想,对吧)。而"0,2,3,5,"的所有子节点的左半部分都是"0,2,3,5,",那么只要把"0,2,3,5,"替换成"0,1,5,"就行了。
复制内容到剪贴板
代码:
(Access)sql="update T_Sort set sortPath='"&newPath&"'+Mid(sortPath,Len('"&fromPath&"')+1) where Instr(sortPath,'"&fromPath&"')>0"
复制内容到剪贴板
代码:
(MS SQL)sql="update T_Sort set sortPath=replace(sortPath,'"&fromPath&"','"&newPath&"') where CHARINDEX[('"&fromPath&"',sortPath)>0"
因为Access好像没有内置repalce函数,所以麻烦了一些。
                     <10>更新要移动节点的parentID。直接update就行啦。
                     <11>商品是跟随分类走的,所以商品的parentID不用更新,只要更新它的sortPath就行了。语句同<9>,只是表名换成T_Product。
                     <12>范例代码见图7。MS SQL的代码只有上面2个SQL语句不同,不贴了。
[center]
图七
[/center]
              [5]前台分类浏览商品
                     前台一般都不会把所有类别一下子都列出来,都是分级浏览的,一层一层的看。我们要做的只是浏览一个分类时,把它的下级分类列出来。
                     <1>取直接子类别,很简单啦。
复制内容到剪贴板
代码:
sql="select sortID,sortName from T_Sort where parentID="&sortID
就行啦。
                     <2>一般我们都会显示一个当前位置,就是分类所在的路径,怎么办呢?难道去递归查询吗?当然不,我这里用了一个小技巧。
                     浏览某一个分类的时候,我们会有一个sortID,可以根据它从T_Sort取得sortPath....不说了,大家看示范代码吧,不懂问我。
                     范例代码见图8。
[center]
图八
[/center]
                     显示的时候用
复制内容到剪贴板
代码:
<%for i=1 to UBound(myArray)
    response.write "-&gt; <a href='product.asp?sortID="&myArray(i)&"'>"&getValueByID(myArray(i),nameArray)&"</a>"
next%>
就OK了。getValueByID是我写的一个Function,见后。
                     <3>显示该类别及其所有子类别下的所有商品。
复制内容到剪贴板
代码:
sql="select * from T_Product where Instr(sortPath,',"&sortID&",')>0"
如果只显示该类别下的商品,那么就用parentID判断就行了。
              [6]前台检索商品
                     如果你的检索表单不包含商品类别,那么没有什么特殊的。如果有商品类别的话,也很简单,SQL的where条件里加一个
复制内容到剪贴板
代码:
"and Instr(sortPath,',"&sortID&",')>0"
就行了。
              [7]后台商品添加、修改
                     添加是要选择所在的分类,这样就可以得到sortID,并能取得它的sortPath,保存到商品记录就行了。
                     修改类似。
              [8]后台商品删除
                     和分类不相关,直接根据productID删除就可。
五、附加信息
       目前还没有拆分出来的代码给大家(太麻烦),主要的东西都在上面啦。
       师傅领进门,修行在个人啦。
复制内容到剪贴板
代码:
'-----根据ID取得name的Sub-----------------
Function getValueByID(sortID,inArray)
    dim i
    if NOT IsArray(inArray) then
        getValueByID=""
        Exit Function
    end if
    for i=0 to UBound(inArray,2)
        if Cstr(sortID)=Cstr(inArray(0,i)) then
            getValueByID=inArray(1,i) '返回name
            Exit Function
        end if
    next
    getValueByID=""
End Function
本帖最近评分记录
最好能把分类树生成数组,调用时就不用那麻烦,又去写一次,并且可以任意改变输出的形状

TOP

实践证明这类和"无限级"有关的任务用XSLT来做是最高效的
山有木兮木有枝,心悦君兮君不知.

TOP

哎,终于有人顶了,不枉我写一大堆字。
to Hubro:要一个全树的时候很少,也就是选择分类的时候用到。(那个我已经用一个动态取数据的树形菜单解决了,也就是没有场合用到一个全树的啦。)

to 龟龟:同意!也用过,写节点倒是不难,自动编号我也实现了(其实就是在xml里保存一个最大数,+1就是),配合xslt很简单就出来了。但关联查询是个麻烦事呀。呱呱。

TOP

请问什么是关联查询? 为什么要自动编号?
听起来你好像偿试了用xml存数据而不是sql?

我喜欢挑战XSLT难题, 有什么它不能做的说来听听
山有木兮木有枝,心悦君兮君不知.

TOP

呵呵,俺知道xslt非常非常非常……强大,俺只知道皮毛而已,

我是用xml文件直接保存目录结构的,然后每个节点的内容在数据库里,所以得有个ID来关联呀。所以我就在xml文件里保存一个maxNumber,添加节点的时候就读取它+1,然后按照这个ID保存它的内容。

现在如果数据库保存目录结构的话,问题还是生成xml的问题,还是要递归呀。

有简单的方法吗?直接用xslt配合数据库生成多极菜单。

TOP

你不是用parentID在描述记录的关系么. 我们把这些记录看成节点, 那么无非只有两种组合:
1.包含子节点的节点 和 不包含子节点的节点
2.没有父节点的节点 和 有父节点的节点
随便哪一种, 都只须写两个template就能完成所有节点的匹配了.
山有木兮木有枝,心悦君兮君不知.

TOP

嗯嗯,假装懂啦,哈哈。
那用template匹配后是输出成xml形式,还是直接就出Html啊?

那读取数据库的时候怎么读啊?具体怎么由记录集到被xslt转换?疑惑中…………

还忘小龟解惑呀…………刮呱。

TOP

山有木兮木有枝,心悦君兮君不知.

TOP

太经典了,太厉害啦。以前我只见过把树型的xml格式化的xslt,真没见过这样就格式化的,

小龟好厉害!收藏!

TOP

谢谢!收藏了!

TOP

谢谢!
收载

TOP

把SORTPATH换成定长的二进制形式得是不是会更好些...?
桃花运啊桃花劫...

TOP

真他妈的经典,能想到这个算法的人真神!!!

真他妈的经典,能想到这个算法的人真神!!!

真的是经典的教程啊!

TOP

经典!

真是太好了,太帮了,都想一个人偷偷的学,哈哈

TOP

靠。
这是什么?这是什么??
告诉我这是什么!
太经典了。
这种方法我喜欢
楼主我爱你。

TOP

厉害,厉害。不过楼主想这个问题已经很久了。因为之前的你的一篇帖子就在讨论这个问题。这个帖子距离那个是快两年的时间呀。

TOP

楼主请教,用你这个设计的数据库要怎么读出树型菜单的。我想了半天了,不得要领。在线等待。

TOP

二年前的帖子了,都事过境迁了。楼主还看得到我的问题么?

TOP

树型菜单一般都是客户端用JS来生成的,你要做的就是按JS的数据格式要求生成就行了。

TOP

从前年看到今年.
受益了.
http://www.shangbanla.net/baibao
春哥,曾哥,谷哥,有你,有谷哥

TOP

<ul id="containerul">
  <li> 动网新闻系统
    <ul>
      <li> <a href="http://www.xmlasp.net/c49.aspx">风格模板</a>
      </li>
      <li>
        <a href="http://www.xmlasp.net/c48.aspx">使用帮助</a>
      </li>
      <li>
        <a href="http://www.xmlasp.net/c36.aspx">使用技巧</a>
      </li>
    </ul>
  </li>
  <li> 技术文章
    <ul>
      <li>
        <a href="http://www.xmlasp.net/c17.aspx">脚本技术</a>
      </li>
      <li>
        <a href="http://www.xmlasp.net/c12.aspx">Asp.net</a>
        <ul>
          <li>
            <a href="http://www.xmlasp.net/c13.aspx">C#</a>
          </li>
          <li>
            <a href="http://www.xmlasp.net/c14.aspx">Ado.net</a>
          </li>
          <li>
            <a href="http://www.xmlasp.net/c37.aspx">Xml</a>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

TOP

这个是js代码的显示内容的,我只读到跟目录的那些名字,下一级的名字就不知道怎么来读取了。

TOP

小雨,请指教下哈。

TOP

学习一下

TOP

学习一下

TOP

大概这样不就行吗?

"........and Instr(sortPath,',"&sortID&",')>0"

TOP

老帖疑问,顶下。

有个问题不太明白,就是删除节点:
==================================
[3]删除节点
                     <1>选择节点
                     <2>如果parentID=0,报错,根节点不能删除。
                     <3>删除该节点及所有子节点。你可能想是不是很麻烦啊,哈哈,其实我只用了一个SQL语句就搞定啦。

(Access)sql="delete from T_Sort where Instr(sortPath,',"&parentID&",')>0"
==================================
parentID应该是指要删除的节点的父节点的ID对吧。
举个例子,如果一个根节点ID是1,那sortpath就是0,1,,一个子节点的parentID是1,自身ID是2,那sortpath就应该是0,1,2,对吧,假设还有一个平行的子节点parentID也是1,自身ID是3,那sortpath就应该是0,1,3,,假设现在我要删除ID为2的这个节点,那么按照你的数据就应该是delete from T_sort where Instr(sortpath,',1,')>0,那不是把这三个节点都删除了吗,也就是你这个语句的作用就变成了“删除目标节点的父节点及此父节点之下的所有子节点”,我觉得应该是用delete from T_sort where Instr(sortpath,',"&id&",')>0才对吧,这个ID是指要删除的节点的ID。

不知道是不是我自己绕进去了,呵呵,望指点。。
个人博客:http://www.iliawang.com

TOP

晕了
沉默寡言

TOP

哦,这里的parentID就是指它本身的ID,不是父的……

干脆,我把那个文件整个贴一下,可以瞅瞅。
复制内容到剪贴板
代码:
<%@ CODEPAGE="936"%>
<!--#include File="admin_session.asp" -->
<!--#include File="conn.asp" -->
<%
myAction=trim(request("myAction"))
parentID=trim(request("parentID")) '选择的类别
toParentID=trim(request("toParentID")) '移动到的类别
new_sortName=trim(request("new_sortName")) '新增的类别名称
edit_sortName=trim(request("edit_sortName")) '修改的类别名称
if parentID="" or NOT IsNumeric(parentID) then
    call showMsg("请选择要操作的类别。")
end if
'================执行操作================
select case myAction
case "addSort"
    if new_sortName="" or Len(new_sortName)>50 then
        call showMsg("请输入新增类别的名称(少于50字)。")
    end if
    '--查询父节点的路径
    set rs=server.CreateObject("adodb.recordset")
    sql="select * from TBL_Sort where sortID="&parentID
    rs.open sql,conn,1,3
    if parentID=0 then '根节点不存在,直接赋值。
        sortPath="0,"
    else
        if noRecord(rs) then
            call closeRs(rs)
            call showMsg("指定类别不存在。")
        end if
        sortPath=rs("sortPath") '父节点的路径
    end if
    '--新增记录
    rs.addNew
    rs("sortName")=new_sortName
    rs("parentID")=parentID
    rs("sortPath")=sortPath & rs("sortID") &"," '格式如:3,5,23,45,
    rs.update
    call closeRs(rs)
    call closeConn()
    backURL="admin_manageSort.asp"
case "editSort"
    if parentID="0" then
        call showMsg("根目录不能改名,请选择其他类别。")
    end if
    edit_sortName=replace(edit_sortName,"'","''")
    if edit_sortName="" or Len(edit_sortName)>50 then
        call showMsg("请输入类别新名称(少于50字)。")
    end if
    '--更新名称
    sql="update TBL_Sort set sortName='"&edit_sortName&"' where sortID="&parentID
    conn.execute sql
    call closeConn()
    backURL="admin_manageSort.asp"
case "deleteSort"
    if parentID="0" then
        call showMsg("根目录不能删除,请选择其他类别。")
    end if
    '--删除所有子分类,包括自己。
    sql="delete from TBL_Sort where Instr(sortPath,',"&parentID&",')>0"
    conn.execute sql
    '删除上述所有分类下的商品
    'sql="delete from TBL_Product where Instr(sortPath,',"&parentID&",')>0"
    'conn.execute sql
    call closeConn()
    backURL="admin_manageSort.asp"
case "moveSort"
    if toParentID="" or NOT IsNumeric(toParentID) then
        call showMsg("请选择目标类别。")
    end if
    if parentID="0" then
        call showMsg("根目录不能移动,请选择其他类别。")
    end if
    if toParentID=parentID then '移动到自己下,不可以
        call showMsg("目标类别与原类别相同,不能移动。")
    end if
    '--取原类别和目标类别的路径
    dim fromPath,toPath
    set rs=server.CreateObject("adodb.recordset")
    sql="select sortPath from TBL_Sort where sortID="&parentID
    rs.open sql,conn,0,1
    if noRecord(rs) then
        call closeRs(rs)
        call showMsg("要移动的类别不存在。")
    end if
    fromPath=rs("sortPath") '来源节点路径
    rs.close
    '--取目标路径
    if toParentID="0" then '移动到根目录下。
        toPath="0,"
    else
        sql="select sortPath from TBL_Sort where sortID="&toParentID
        rs.open sql,conn,0,1
        if noRecord(rs) then
            call closeRs(rs)
            call showMsg("目标类别不存在。")
        end if
        toPath=rs("sortPath") '目标节点路径
    end if
    call closeRs(rs)
    '--进行路径检查
    if toPath & parentID &"," =fromPath then '如把0,2,3,5,移动到0,2,3,下,无需移动。
        call showMsg("不必移动。"+fromPath+"|"+toPath)
    end if
    if Instr(toPath,fromPath)>0 then '如把0,2,3,5,移动到0,2,3,5,8,下。
        call showMsg("不能移动到当前类别的子类别下。")
    end if
    '--更新节点路径
    newPath=toPath & parentID &"," '如0,2,3,5,移动到0,1,下,那么新的前半部分path就是0,1,5,了。
    sql="update TBL_Sort set sortPath='"&newPath&"'+Mid(sortPath,Len('"&fromPath&"')+1) where Instr(sortPath,'"&fromPath&"')>0"
    conn.execute sql
    '--更新操作节点的父节点
    sql="update TBL_Sort set parentID="&toParentID&" where sortID="&parentID
    conn.execute sql
    '--更新对应商品路径
    sql="update TBL_Product set sortPath='"&newPath&"'+Mid(sortPath,Len('"&fromPath&"')+1) where Instr(sortPath,'"&fromPath&"')>0"
    conn.execute sql
    call closeConn()
    backURL="admin_manageSort.asp"
end select %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>处理结果</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<link href="css/wang.css" rel="stylesheet" type="text/css">
</head>
<body>
<p>&nbsp;</p>
<p align="center" class="font-14px"><b>操作成功,2秒后自动返回。</b></p>
<script language="JavaScript" type="text/JavaScript">
setTimeout("window.location='<%=backURL%>'",2000);
</script>
</body>
</html>

TOP