前言
無限極分類是我很久前學(xué)到知識疯趟,今天在做一個項(xiàng)目時澎剥,發(fā)現(xiàn)對其概念有點(diǎn)模糊锡溯,所以今天就來說說無限極分類。
首先來說說什么是無限極分類。按照我的理解祭饭,就是對數(shù)據(jù)完成多次分類芜茵,如同一棵樹一樣,從根開始倡蝙,到主干九串、枝干、葉子……
完成無限極分類寺鸥,主要運(yùn)用了兩種方法猪钮,一是遞歸方式,二是迭代方式胆建。而主要運(yùn)用無限極分類的地方有地址解析烤低,面包屑導(dǎo)航等等。下面就來具體介紹兩種方法的原理及實(shí)現(xiàn)方法眼坏。
家譜樹是無限極分類的表現(xiàn)形式之一拂玻,另一個是子孫樹。一開始學(xué)習(xí)無限極分類時宰译,我時常弄混這兩棵樹檐蚜,現(xiàn)在看來自然是明白很多。從漢語的意思也能夠看出其中的區(qū)別沿侈。
家譜闯第,現(xiàn)在很多地方都流行起修家譜,那怎么修家譜缀拭,按照我理解咳短,就是給自己找一個祖宗,一代代找上去蛛淋,形成了一個體系咙好,這樣編篡而成的叫家譜。家譜樹就與之類似褐荷,從某個節(jié)點(diǎn)開始向上尋找其父節(jié)點(diǎn)勾效,再找父節(jié)點(diǎn)的父節(jié)點(diǎn),直到找不到為止叛甫。按照這種尋找层宫,形成的一個類似樹狀的結(jié)構(gòu),就叫做家譜樹其监。
而子孫樹與其相反萌腿,子孫樹類似于生物書中的遺傳圖,從某個節(jié)點(diǎn)開始尋找它的子節(jié)點(diǎn)抖苦,再找子節(jié)點(diǎn)的子節(jié)點(diǎn)毁菱,直到尋找完畢米死。這樣形成的樹狀結(jié)構(gòu)就叫做子孫樹。
從上面對家譜樹與子孫樹的描述鼎俘,將其轉(zhuǎn)換為代碼時哲身,我的第一印象就是利用遞歸方式,家譜樹贸伐,找父節(jié)點(diǎn)的父節(jié)點(diǎn)勘天,子孫樹,找子節(jié)點(diǎn)的子節(jié)點(diǎn)捉邢。完全符合遞歸思想脯丝。所以首先我們來說說利用遞歸方式完成家譜樹與子孫樹。
家譜樹的實(shí)現(xiàn)
為更清楚的講解伏伐,我先將即將分類的數(shù)據(jù)貼在下面宠进,是關(guān)于地址的數(shù)據(jù):
$address =array(
array('id'=>1,'address'=>'安徽','parent_id'=>0),
array('id'=>2,'address'=>'江蘇','parent_id'=>0),
array('id'=>3,'address'=>'合肥','parent_id'=>1),
array('id'=>4,'address'=>'廬陽區(qū)','parent_id'=>3),
array('id'=>5,'address'=>'大楊鎮(zhèn)','parent_id'=>4),
array('id'=>6,'address'=>'南京','parent_id'=>2),
array('id'=>7,'address'=>'玄武區(qū)','parent_id'=>6),
array('id'=>8,'address'=>'梅園新村街道','parent_id'=>7),
array('id'=>9,'address'=>'上海','parent_id'=>0),
array('id'=>10,'address'=>'黃浦區(qū)','parent_id'=>9),
array('id'=>11,'address'=>'外灘','parent_id'=>10)
array('id'=>12,'address'=>'安慶','parent_id'=>1)
? ? );
按照上文的介紹,對上面數(shù)據(jù)進(jìn)行家譜樹無限極分類藐翎,假設(shè)我們想要尋找大楊鎮(zhèn)的家譜樹材蹬,先找到與之相關(guān)的信息。
'id'=>5,'address'=>'大楊鎮(zhèn)','parent_id'=>4
可以看出它的父節(jié)點(diǎn)的id吝镣,即parent_id == 4堤器,那么id==4的節(jié)點(diǎn)就是其父節(jié)點(diǎn),由此找到廬陽區(qū):
'id'=>4,'address'=>'廬陽區(qū)','parent_id'=>3
與上面類似末贾,尋找id=3的節(jié)點(diǎn)闸溃,依次向上尋找,找到大楊鎮(zhèn)的家譜
大楊鎮(zhèn) -> 廬陽區(qū) -> 合肥 -> 安徽
那么怎么用代碼來完成它呢拱撵?其實(shí)很簡單辉川,只需要判斷尋找的父id是否與節(jié)點(diǎn)的id相等,即parent_id ?= id拴测,相等就是要尋找的父節(jié)點(diǎn)乓旗,并把該節(jié)點(diǎn)的parent_id作為尋找的id,遞歸進(jìn)行尋找集索。如下面的流程圖:
遞歸方法求家譜樹
下面就開始編寫代碼:
/**
* 獲取家譜樹
* @param? array? ? ? ? $data? 待分類的數(shù)據(jù)
* @param? int/string? $pid? ? 要找的祖先節(jié)點(diǎn)
*/
function Ancestry($data , $pid) {
static$ancestry =array();
foreach($dataas$key => $value) {
if($value['id'] == $pid) {
? ? ? ? ? ? $ancestry[] = $value;
Ancestry($data , $value['parent_id']);
? ? ? ? }
? ? }
return$ancestry;
}
根據(jù)流程圖屿愚,代碼編寫完成。注意上面存儲結(jié)點(diǎn)的數(shù)組抄谐,即$ancestry渺鹦,要添加靜態(tài)化關(guān)鍵字static扰法,否則每次遞歸都會將該數(shù)組初始化蛹含。當(dāng)然也可以使用array_merge將每次返回的數(shù)組與上一次的進(jìn)行合并。
尋找家譜的關(guān)鍵就是條件判斷烂完,尋找的parent_id等于某個節(jié)點(diǎn)的id值她我,顯然該節(jié)點(diǎn)就是要尋找的父節(jié)點(diǎn)。
代碼編寫完成掸绞,來看看是否符合我們的預(yù)期酷窥,來尋找大楊鎮(zhèn)的家譜:
Ancestry($address ,4);
結(jié)果:
Array
(
[0] =>Array
? ? ? ? (
[id] =>4
? ? ? ? ? ? [address] => 廬陽區(qū)
[parent_id] =>3
? ? ? ? )
[1] =>Array
? ? ? ? (
[id] =>3
? ? ? ? ? ? [address] => 合肥
[parent_id] =>1
? ? ? ? )
[2] =>Array
? ? ? ? (
[id] =>1
? ? ? ? ? ? [address] => 安徽
[parent_id] =>0
? ? ? ? )
)
可以看出結(jié)果與我們預(yù)期相符咽安。那么家譜樹的遞歸方法就完成了,下面來講子孫樹的實(shí)現(xiàn)蓬推。
子孫樹的實(shí)現(xiàn)
依然使用上面的數(shù)據(jù)妆棒,子孫樹是從父節(jié)點(diǎn)開始,向下尋找其子孫節(jié)點(diǎn)沸伏,而形成的一個樹狀圖形糕珊。
假設(shè)尋找id=0的子孫節(jié)點(diǎn),那么就要注意所有parent_id=0的節(jié)點(diǎn)毅糟,這些節(jié)點(diǎn)都是id=0的子節(jié)點(diǎn)红选。然后,把parent_id=0節(jié)點(diǎn)的id作為查詢id繼續(xù)向下查詢姆另,直到查不到任何子節(jié)點(diǎn)為止喇肋。如下:
子孫樹
流程圖如下:
子孫樹流程圖
其流程與家譜樹類似,不同點(diǎn)迹辐,也是關(guān)鍵點(diǎn)就是條件語句的執(zhí)行蝶防。家譜樹判斷的是當(dāng)前節(jié)點(diǎn)的id是否與上一個節(jié)點(diǎn)的parent_id相等;子孫樹判斷的是當(dāng)前節(jié)點(diǎn)的parent_id與上一個節(jié)點(diǎn)的id相等右核,按照這種條件判斷子孫樹能夠有多個子孫節(jié)點(diǎn)慧脱,而家譜樹只能存在一個祖先。代碼如下:
/**
* 獲取子孫樹
* @param? array? ? ? ? $data? 待分類的數(shù)據(jù)
* @param? int/string? $id? ? 要找的子節(jié)點(diǎn)id
* @param? int? ? ? ? ? $lev? ? 節(jié)點(diǎn)等級
*/
function getSubTree($data , $id = 0 , $lev = 0) {
static$son =array();
foreach($dataas$key => $value) {
if($value['parent_id'] == $id) {
$value['lev'] = $lev;
? ? ? ? ? ? $son[] = $value;
getSubTree($data , $value['id'] , $lev+1);
? ? ? ? }
? ? }
return$son;
}
在函數(shù)中我添加了一個變量lev贺喝,為的是給存入的節(jié)點(diǎn)標(biāo)注等級菱鸥,方便看出子孫樹的結(jié)構(gòu)。下面來測試結(jié)果:
getSubTree($data,0,0);
因篇幅有限躏鱼,將結(jié)果進(jìn)行部分處理:
foreach($treeas$k => $v) {
echostr_repeat('--', $v['lev']) . $v['address'] .'<br/>';
}
結(jié)果:
安徽
--合肥
----廬陽區(qū)
------大楊鎮(zhèn)
--安慶
江蘇
--南京
----玄武區(qū)
------梅園新村街道
上海
--黃浦區(qū)
----外灘
遞歸方式的家譜樹與子孫樹比較容易理解氮采,只要對遞歸思想比較了解,一步步寫下來不是很難染苛。比起遞歸方式鹊漠,迭代方式可能更加讓人難以理解。下面就來介紹迭代方式的家譜樹與子孫樹編寫茶行。
家譜樹
完成跌代方式的家譜樹之前躯概,首先說一下尋找祖先節(jié)點(diǎn)的終止條件。雖然叫無限極分類畔师,它不是絕對的無限娶靡,只是理論的無限。
如同我國上下五千年歷史看锉,任一個大的姓氏姿锭,向上找其祖先塔鳍,不是找到炎帝就是找到黃帝,在往前就沒有歷史記載了呻此。所以在家譜樹的尋找中也有終止條件轮纫,就是在分類數(shù)據(jù)中再也找不到它的父節(jié)點(diǎn)時,表現(xiàn)在實(shí)例數(shù)據(jù)上焚鲜,就是不存在parent_id < 0的節(jié)點(diǎn)掌唾。
這也是完成迭代的關(guān)鍵,以其作為迭代條件忿磅,對數(shù)據(jù)進(jìn)行循環(huán)判斷郑兴,并把每次找到的節(jié)點(diǎn)的parent_id再次作為迭代條件,直到不滿足迭代條件贝乎。流程圖如下:
家譜樹迭代流程
理清流程情连,現(xiàn)在開始完成代碼編寫:
function Ancestry($data , $pid) {
$ancestry =array();
while($pid >0) {
foreach($dataas$v) {
if($v['id'] == $pid) {
? ? ? ? ? ? ? ? $ancestry[] = $v;
$pid = $v['parent_id'];
? ? ? ? ? ? }
? ? ? ? }
? ? }
return$ancestry;
}
迭代條件$pid>0,當(dāng)pid>0時說明還有祖先存在览效,可以繼續(xù)迭代却舀,否則說明沒有祖先,迭代終止锤灿。$pid = $v['parent_id']是迭代繼續(xù)進(jìn)行的關(guān)鍵挽拔,每次找到祖先節(jié)點(diǎn),就將祖先節(jié)點(diǎn)的父id傳遞給pid但校,進(jìn)行下一次迭代螃诅。
運(yùn)行這個函數(shù),結(jié)果與使用遞歸方式的結(jié)果一致状囱。
子孫樹的實(shí)現(xiàn)
使用迭代方式完成子孫樹术裸,更為復(fù)雜,需要運(yùn)用的棧的思想亭枷。在進(jìn)行迭代的過程中袭艺,將每次尋找的id入棧,找到一個節(jié)點(diǎn)叨粘,就將該節(jié)點(diǎn)從原數(shù)據(jù)中刪除猾编,當(dāng)尋找到葉子節(jié)點(diǎn)時升敲,即不存在子孫節(jié)點(diǎn)時答倡,就將該葉子節(jié)點(diǎn)對應(yīng)的id從棧中彈出,再尋找棧頂id的子孫節(jié)點(diǎn)驴党,直到棧清空為止瘪撇,迭代結(jié)束。下面用一個例子來說明:
$address =array(
array('id'=>1,'address'=>'安徽','parent_id'=>0),
array('id'=>2,'address'=>'江蘇','parent_id'=>0),
array('id'=>3,'address'=>'合肥','parent_id'=>1),
array('id'=>4,'address'=>'廬陽區(qū)','parent_id'=>3),
array('id'=>5,'address'=>'大楊鎮(zhèn)','parent_id'=>4),
array('id'=>6,'address'=>'南京','parent_id'=>2),
array('id'=>7,'address'=>'玄武區(qū)','parent_id'=>6),
array('id'=>8,'address'=>'梅園新村街道','parent_id'=>7),
array('id'=>9,'address'=>'上海','parent_id'=>0),
array('id'=>10,'address'=>'黃浦區(qū)','parent_id'=>9),
array('id'=>11,'address'=>'外灘','parent_id'=>10)
array('id'=>12,'address'=>'安慶','parent_id'=>1)
? ? );
尋找id=0的子孫節(jié)點(diǎn),id=0入棧设江,尋找到該節(jié)點(diǎn),為
array('id'=>1,'address'=>'安徽','parent_id'=>0)
此時棧為[0]攘轩,并且將該節(jié)點(diǎn)從原數(shù)據(jù)中刪除叉存,再將id=1入棧,尋找id=1的子孫節(jié)點(diǎn)度帮,找到為:
array('id'=>3,'address'=>'合肥','parent_id'=>1),
此時棧[0][1],將該節(jié)點(diǎn)刪除歼捏,id=3入棧,尋找id=3的子孫節(jié)點(diǎn)笨篷,找到:
array('id'=>4,'address'=>'廬陽區(qū)','parent_id'=>3)
棧[0][1][3]瞳秽,將該節(jié)點(diǎn)刪除,id=4入棧率翅,尋找id=4的子孫節(jié)點(diǎn)练俐,找到:
array('id'=>5,'address'=>'大楊鎮(zhèn)','parent_id'=>4),
棧[0][1][3][4],將該節(jié)點(diǎn)刪除冕臭,id=5入棧腺晾,棧[0][1][3][4][5],并尋找id=5的子節(jié)點(diǎn)辜贵,遍歷后未找到悯蝉,于是將id=5出棧,再次尋找id=4的子孫節(jié)點(diǎn)托慨,依次進(jìn)行鼻由。最后完成整個迭代。
期間厚棵,棧的情況如下:
[0]
[0][1]
[0][1][3]
[0][1][3][4]
[0][1][3][4][5]
[0][1][3][4]
[0][1][3]
[0][1]
[0][1][12]
[0][1]
[0]
……
代碼如下:
function getSubTree($data , $id = 0) {
$task =array($id);# 棧 任務(wù)表
$son =array();
while(!empty($task)) {
$flag =false;# 是否找到節(jié)點(diǎn)標(biāo)志
foreach($dataas$k => $v) {
# 判斷是否是子孫節(jié)點(diǎn)的條件 與 遞歸方式一致
if($v['parent_id'] == $id) {
$son[] = $v;# 節(jié)點(diǎn)存入數(shù)組
array_push($task , $v['id']);# 節(jié)點(diǎn)id入棧
$id = $v['id'];# 判斷條件切換
unset($data[$k]);# 刪除節(jié)點(diǎn)
$flag =true;# 找到節(jié)點(diǎn)標(biāo)志
? ? ? ? ? ? }
? ? ? ? }
# flag == false說明已經(jīng)到了葉子節(jié)點(diǎn) 無子孫節(jié)點(diǎn)了
if($flag ==false) {
array_pop($task);# 出棧
$id = end($task);# 尋找棧頂id的子節(jié)點(diǎn)
? ? ? ? }
? ? }
return$son;
}
這里找到節(jié)點(diǎn)后必須把該節(jié)點(diǎn)從原數(shù)據(jù)中刪除蕉世,否則會造成每次都找到該節(jié)點(diǎn),形成無限迭代的bug婆硬。在這里利用數(shù)組函數(shù)array_push與array_pop模擬進(jìn)棧與出棧操作讨彼。
利用迭代完成子孫樹比較復(fù)雜,且我沒有測試過這個與遞歸方式誰的效率高柿祈,不過利用迭代完成家譜樹明顯比起遞歸方法效率高哈误。
面包屑導(dǎo)航
說完了無限極分類的實(shí)現(xiàn)原理與方法,現(xiàn)在來說說在網(wǎng)站中對無限極分類的應(yīng)用躏嚎。最常用的就是面包屑導(dǎo)航了蜜自。
什么是面包屑導(dǎo)航,這個稱呼來自于童話故事"漢賽爾和格萊特"卢佣,具體什么故事就不敘述了重荠,有興趣的可以去谷歌一下。面包屑導(dǎo)航的作用就是告訴訪問者他們目前在網(wǎng)站中的位置以及如何返回虚茶。下圖就是一個典型的面包屑導(dǎo)航戈鲁。
面包屑導(dǎo)航
面包屑是一個典型家譜樹的應(yīng)用仇参,不要看它是從左到右,分類級數(shù)越來越低婆殿,就認(rèn)為它是子孫樹應(yīng)用诈乒,要知道子孫樹是可能存在多個分支,而面包屑導(dǎo)航要求的是一條主干婆芦。
將上面家譜樹代碼做一定修改怕磨,就能夠完成面包屑導(dǎo)航。我們采用遞歸方式的家譜樹消约。代碼如下:
function Ancestry($data , $pid) {
static$ancestry =array();
foreach($dataas$key => $value) {
if($value['id'] == $pid) {
Ancestry($data , $value['parent_id']);
? ? ? ? ? ? $ancestry[] = $value;? ? ? ? ? ? ? ?
? ? ? ? }
? ? }
return$ancestry;
}
如果先進(jìn)行遞歸調(diào)用肠鲫,在遞歸結(jié)束再將找到的節(jié)點(diǎn)存入數(shù)組中,就能夠使祖先節(jié)點(diǎn)排列在數(shù)組前列或粮,子孫節(jié)點(diǎn)排列在數(shù)組后列导饲,方便進(jìn)行提取數(shù)據(jù)。
簡化演示步驟氯材,不從數(shù)據(jù)庫中取出數(shù)據(jù)帜消,改為模擬數(shù)據(jù):
$tmp =array(
array('cate_id'=1,'name'=>'首頁','parent_id'=>'0'),
array('cate_id'=2,'name'=>'新聞中心','parent_id'=>'1'),
array('cate_id'=3,'name'=>'娛樂新聞','parent_id'=>'2'),
array('cate_id'=4,'name'=>'軍事要聞','parent_id'=>'2'),
array('cate_id'=5,'name'=>'體育新聞','parent_id'=>'2'),
array('cate_id'=6,'name'=>'博客','parent_id'=>'1'),
array('cate_id'=7,'name'=>'旅游日志','parent_id'=>'6'),
array('cate_id'=8,'name'=>'心情','parent_id'=>'6'),
array('cate_id'=9,'name'=>'小小說','parent_id'=>'6'),
array('cate_id'=10,'name'=>'明星','parent_id'=>'3'),
array('cate_id'=11,'name'=>'網(wǎng)紅','parent_id'=>'3')
? ? );
假設(shè)用戶點(diǎn)進(jìn)明星導(dǎo)航,那么在網(wǎng)站顯示的導(dǎo)航為:
$tree = Ancestry($tmp ,10);
foreach($treeas$key => $value) {
echo$value['name'] .'>';
}
面包屑導(dǎo)航
防止設(shè)置父類為子類
在網(wǎng)站建立中浓体,可能會碰到用戶進(jìn)行編輯時出現(xiàn)誤操作泡挺,將某個欄目的父節(jié)點(diǎn)設(shè)置成了該欄目的子節(jié)點(diǎn),進(jìn)行這樣的設(shè)置后會導(dǎo)致數(shù)據(jù)庫中的數(shù)據(jù)丟失命浴,因此在進(jìn)行數(shù)據(jù)更新之前應(yīng)該注意這一點(diǎn)娄猫。
利用家譜樹,就能夠避免發(fā)生這種錯誤生闲。在用戶提交表單時媳溺,我們將即將修改欄目的父節(jié)點(diǎn)的家譜樹取出,并對家譜樹進(jìn)行遍歷碍讯,如果發(fā)現(xiàn)該家譜樹中發(fā)現(xiàn)了要修改的節(jié)點(diǎn)悬蔽,就說明是錯誤操作。有點(diǎn)繞捉兴,舉個例子來說明:
修改欄目新聞中心的父節(jié)點(diǎn)為娛樂新聞蝎困,就把娛樂新聞的家譜樹取出來:
娛樂新聞 新聞中心 首頁
在該家譜樹中發(fā)現(xiàn)要修改的節(jié)點(diǎn),新聞中心倍啥,那么說明出現(xiàn)了錯誤禾乘。具體代碼如下:
$data = Ancestry($tmp ,3);
foreach($dataas$key => $value) {
if($value['cate_id'] ==3) {
echo'Error';
break;
? ? }
}
對無限極分類的講解就寫到這兒,希望能夠給對無限極分類存在迷惑的同學(xué)一定的靈感虽缕。在下才疏學(xué)淺始藕,可能在描述中存在錯漏,希望看到的同學(xué)能夠指出。