一、前言
很多情况下二级分类已经不能满足需要了,而网上可用的多级分类的例子实在是不好找,故有此文。
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]图一
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_sort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
图二
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_product.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
[/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]
图三
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_sort2.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/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]
图四
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/addsort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
图五
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/addsort_mssql.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
[2]修改节点
节点的属性只有一个名字而已,直接update就可以了,就不说了。
[3]删除节点
<1>选择节点
<2>如果parentID=0,报错,根节点不能删除。
<3>删除该节点及所有子节点。你可能想是不是很麻烦啊,哈哈,其实我只用了一个SQL语句就搞定啦。
本算法的精华就在这里啦,仔细想想吧,sortPath最后那个逗号的作用也在这里啦。
<4>删除上述所有节点下的商品。同上,表名不同而已。
<5>范例代码见图6。MS SQL的代码就不贴了。
[center]
图六
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/delsort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/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好像没有内置repalce函数,所以麻烦了一些。
<10>更新要移动节点的parentID。直接update就行啦。
<11>商品是跟随分类走的,所以商品的parentID不用更新,只要更新它的sortPath就行了。语句同<9>,只是表名换成T_Product。
<12>范例代码见图7。MS SQL的代码只有上面2个SQL语句不同,不贴了。
[center]
图七
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" style="WIDTH: 735px; CURSOR: hand; HEIGHT: 738px" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" height=738 alt="Click here to open new window CTRL+Mouse wheel to zoom in/out" src="http://mytju.com/temp/tech/movesort.gif" width=716 onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0 resized="true">[/center]
[5]前台分类浏览商品
前台一般都不会把所有类别一下子都列出来,都是分级浏览的,一层一层的看。我们要做的只是浏览一个分类时,把它的下级分类列出来。
<1>取直接子类别,很简单啦。
就行啦。
<2>一般我们都会显示一个当前位置,就是分类所在的路径,怎么办呢?难道去递归查询吗?当然不,我这里用了一个小技巧。
浏览某一个分类的时候,我们会有一个sortID,可以根据它从T_Sort取得sortPath....不说了,大家看示范代码吧,不懂问我。
范例代码见图8。
[center]
图八
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/browersort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
显示的时候用
就OK了。getValueByID是我写的一个Function,见后。
<3>显示该类别及其所有子类别下的所有商品。
如果只显示该类别下的商品,那么就用parentID判断就行了。
[6]前台检索商品
如果你的检索表单不包含商品类别,那么没有什么特殊的。如果有商品类别的话,也很简单,SQL的where条件里加一个
就行了。
[7]后台商品添加、修改
添加是要选择所在的分类,这样就可以得到sortID,并能取得它的sortPath,保存到商品记录就行了。
修改类似。
[8]后台商品删除
和分类不相关,直接根据productID删除就可。
五、附加信息
目前还没有拆分出来的代码给大家(太麻烦),主要的东西都在上面啦。
师傅领进门,修行在个人啦。
很多情况下二级分类已经不能满足需要了,而网上可用的多级分类的例子实在是不好找,故有此文。
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]图一
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_sort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
图二
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_product.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
[/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]
图三
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/t_sort2.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/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]
图四
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/addsort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>
图五
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/addsort_mssql.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
[2]修改节点
节点的属性只有一个名字而已,直接update就可以了,就不说了。
[3]删除节点
<1>选择节点
<2>如果parentID=0,报错,根节点不能删除。
<3>删除该节点及所有子节点。你可能想是不是很麻烦啊,哈哈,其实我只用了一个SQL语句就搞定啦。
CODE:
(Access)sql="delete from T_Sort where Instr(sortPath,',"&parentID&",')>0"
CODE:
(MS SQL)sql="delete from T_Sort where CHARINDEX(',"&parentID&",',sortPath)>0"
本算法的精华就在这里啦,仔细想想吧,sortPath最后那个逗号的作用也在这里啦。
<4>删除上述所有节点下的商品。同上,表名不同而已。
CODE:
(Access)sql="delete from T_Product where Instr(sortPath,',"&parentID&",')>0"
CODE:
(MS SQL)sql="delete from T_Product where CHARINDEX(',"&parentID&",',sortPath)>0"
<5>范例代码见图6。MS SQL的代码就不贴了。
[center]
图六
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/delsort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/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,"就行了。
CODE:
(Access)sql="update T_Sort set sortPath='"&newPath&"'+Mid(sortPath,Len('"&fromPath&"')+1) where Instr(sortPath,'"&fromPath&"')>0"
CODE:
(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]
图七
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" style="WIDTH: 735px; CURSOR: hand; HEIGHT: 738px" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" height=738 alt="Click here to open new window CTRL+Mouse wheel to zoom in/out" src="http://mytju.com/temp/tech/movesort.gif" width=716 onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0 resized="true">[/center]
[5]前台分类浏览商品
前台一般都不会把所有类别一下子都列出来,都是分级浏览的,一层一层的看。我们要做的只是浏览一个分类时,把它的下级分类列出来。
<1>取直接子类别,很简单啦。
CODE:
sql="select sortID,sortName from T_Sort where parentID="&sortID
就行啦。
<2>一般我们都会显示一个当前位置,就是分类所在的路径,怎么办呢?难道去递归查询吗?当然不,我这里用了一个小技巧。
浏览某一个分类的时候,我们会有一个sortID,可以根据它从T_Sort取得sortPath....不说了,大家看示范代码吧,不懂问我。
范例代码见图8。
[center]
图八
screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open(this.src);}" alt="" src="http://mytju.com/temp/tech/browersort.gif" onload="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL+Mouse wheel to zoom in/out';}" border=0>[/center]
显示的时候用
CODE:
<%for i=1 to UBound(myArray)
response.write "-> <a href='product.asp?sortID="&myArray(i)&"'>"&getValueByID(myArray(i),nameArray)&"</a>"
next%>
response.write "-> <a href='product.asp?sortID="&myArray(i)&"'>"&getValueByID(myArray(i),nameArray)&"</a>"
next%>
就OK了。getValueByID是我写的一个Function,见后。
<3>显示该类别及其所有子类别下的所有商品。
CODE:
sql="select * from T_Product where Instr(sortPath,',"&sortID&",')>0"
如果只显示该类别下的商品,那么就用parentID判断就行了。
[6]前台检索商品
如果你的检索表单不包含商品类别,那么没有什么特殊的。如果有商品类别的话,也很简单,SQL的where条件里加一个
CODE:
"and Instr(sortPath,',"&sortID&",')>0"
就行了。
[7]后台商品添加、修改
添加是要选择所在的分类,这样就可以得到sortID,并能取得它的sortPath,保存到商品记录就行了。
修改类似。
[8]后台商品删除
和分类不相关,直接根据productID删除就可。
五、附加信息
目前还没有拆分出来的代码给大家(太麻烦),主要的东西都在上面啦。
师傅领进门,修行在个人啦。
CODE:
'-----根据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
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