一、參考資料
http://www.php.cn/php-weizijiaocheng-360446.html
http://www.php.cn/keywords-無限極分類.html
本文博客部分內容是上述網上內容搬運過來的。
二、場景
無限極分類在web網站中應用很多,比如無限極菜單,無限極文件夾展開。因為最近的項目中有用到樹的結構,其實就是無限極菜單的存儲。
在某次面試中也有提及,所以這里集合上述網上的資料總結一下。
使用場景:
1、需要獲取所有的節點,也就是無限極菜單的樹形結構分類。
2、獲取某個節點的所有葉子節點。
3、獲取某個節點的所有子孫節點。
4、非葉子節點的分頁展示。
三、數據庫存儲
無限極分類其實就是一棵樹,所有的節點作為一個存儲元素。每個節點可以有任意個孩子節點,且只有一個父節點。
MySQL數據存儲,結合項目的數據表存儲結構如下:
CREATE TABLE t_tree_info (
`Fid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '標簽自增ID',
`Fname` varchar(255) NOT NULL DEFAULT '' COMMENT '節點名稱',
`Fpid` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '父節點id',
`Fadd_time` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '創建時間',
`Fmodify_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
PRIMARY KEY (`Fid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='無限極分類菜單存儲表';
核心字段就是節點自身唯一標識Fid
,和對應的父節點標識Fpid
。
四、技術實現
全局數據存儲節點
節點數據在項目中是MySQL存儲的,現在為了測試,把MySQL數據直接存放到數組里面,全局使用。數據存儲如下:
private static $listData = [
[
"Fid" => 1,
"Fpid" => 8,
"Fname" => '首頁',
],
[
"Fid" => 2,
"Fpid" => 8,
"Fname" => '博客',
],
[
"Fid" => 3,
"Fpid" => 8,
"Fname" => '官網',
],
[
"Fid" => 4,
"Fpid" => 2,
"Fname" => '個人博客',
],
[
"Fid" => 5,
"Fpid" => 2,
"Fname" => '他人博客',
],
[
"Fid" => 6,
"Fpid" => 8,
"Fname" => '測試1',
],
[
"Fid" => 7,
"Fpid" => 8,
"Fname" => '測試2',
],
[
"Fid" => 8,
"Fpid" => 0,
"Fname" => '無限極分類',
],
[
"Fid" => 9,
"Fpid" => 5,
"Fname" => '女性欄目',
],
[
"Fid" => 10,
"Fpid" => 5,
"Fname" => '男性欄目',
],
];
引用方式實現無限極分類
思路:
1、即所有待處理的數據進行包裝成下標為主鍵Fid(pk)的數組,便於由Fpid獲取對應的父欄目。
2、對包裝的數據進行循環,如果為根節點,則將其引用添加到tree中,否則,將其引用添加到其父類的子元素中。這樣雖然tree中,只是添加了根節點,但是每個根節點如果有子元素,其中包含了子元素的引用。故能形成樹型。
個人覺得引用的設計思路相比遞歸的思路更容易理解,更直觀一些。
代碼如下:
/**
* 把返回的數據集轉換成Tree
* @param array $list 要轉換的數據集
* @param string $pk 自增字段(欄目Fid)
* @param string $pid parent標記字段
* @return array
*/
public static function quote_make_tree($list, $pk = 'Fid', $pid = 'Fpid',$child = '_child', $root = 0)
{
$tree = $packData = [];
foreach ($list as $data) {
$packData[$data[$pk]] = $data;
}
foreach ($packData as $key =>$val) {
if ($val[$pid] == $root) {//代表跟節點
$tree[] = & $packData[$key];
} else {
//找到其父類
$packData[$val[$pid]][$child][] = & $packData[$key];
}
}
return $tree;
}
引用調用樹:
/**
* 引用生成樹
* @return
*/
public static function quote_tree_test()
{
$tree = self::make_tree(self::$listData, $pk = 'id', $pid = 'pid', $child = '_child', $root = 0);
echo json_encode($tree);
}
返回結果如下:
[
{
"Fid": 8,
"Fpid": 0,
"Fname": "無限極分類",
"_child": [
{
"Fid": 1,
"Fpid": 8,
"Fname": "首頁"
},
{
"Fid": 2,
"Fpid": 8,
"Fname": "博客",
"_child": [
{
"Fid": 4,
"Fpid": 2,
"Fname": "個人博客"
},
{
"Fid": 5,
"Fpid": 2,
"Fname": "他人博客",
"_child": [
{
"Fid": 9,
"Fpid": 5,
"Fname": "女性欄目"
},
{
"Fid": 10,
"Fpid": 5,
"Fname": "男性欄目"
}
]
}
]
},
{
"Fid": 3,
"Fpid": 8,
"Fname": "官網"
},
{
"Fid": 6,
"Fpid": 8,
"Fname": "測試1"
},
{
"Fid": 7,
"Fpid": 8,
"Fname": "測試2"
}
]
}
]
遞歸方式實現無限分類
思路:
1、使用循環,分別獲取所有的根節點。
2、在獲取每個節點的時候,將該節點從原數據中移除,並遞歸方式獲取其所有的子節點,一直原數據為空。
代碼如下:
/**
* 遞歸生成樹
* @param array $list 要轉換的數據集
* @param string $pk 自增字段(欄目id)
* @param string $pid parent標記字段
* @param string $child 孩子節點key
* @param integer $root 根節點標識
* @return array
*/
public static function recursive_make_tree($list, $pk = 'Fid', $pid = 'Fpid', $child = '_child', $root = 0)
{
$tree = [];
foreach ($list as $key => $val) {
if ($val[$pid] == $root) {
//獲取當前$pid所有子類
unset($list[$key]);
if (!empty($list)) {
$child = self::recursive_make_tree($list, $pk, $pid, $child, $val[$pk]);
if (!empty($child)) {
$val['_child'] = $child;
}
}
$tree[] = $val;
}
}
return $tree;
}
遞歸調用樹:
public static function recursive_tree_test()
{
$tree = self::recursive_make_tree(self::$listData, $pk = 'Fid', $pid = 'Fpid', $child = '_child', $root = 0);
echo json_encode($tree);
}
獲取某個節點的所有葉子節點
思路:
1、如果當前節點不是某個節點的父節點,則說明當前節點是葉子節點。則返回當前節點。同時也是遞歸出口。
2、如果當前節點是某個節點的父節點,則說明當前節點不是葉子節點。針對當前節點的孩子節點進行同樣遞歸查詢。
代碼如下:
/**
* 獲取某節點的所有葉子節點
* 如果當前節點是葉子節點,則返回本身
* @param $pid
* @return array
*/
public static function getAllLeafNodeByFatherIds($pid )
{
$allLeafTagIds = [];
$sql = "SELECT GROUP_CONCAT(Fid) AS Fid FROM t_tree_info WHERE Fpid={$pid}";
$res = $this->db->fetchRow($sql);
if ($res && $res['Fid']) {
$leafTagIds = explode(',', $res['Fid']);
foreach ($leafTagIds as $nodeTagId) {
$curLeafTagIds = $this->getAllLeafNodeByFatherIds($nodeTagId);
if ($curLeafTagIds) {
$allLeafTagIds = array_merge($allLeafTagIds, $curLeafTagIds);
}
}
return $allLeafTagIds;
} else {
// 當前節點是葉子節點,返回該節點
return [$tagId];
}
}
獲取某個節點的所有子孫節點
思路:
1、如果當前節點不是某個節點的父節點,說明當前節點是葉子節點,則返回空。同時也是遞歸出口。
2、如果當前節點是某個節點的父節點,則合並當前節點到返回數組中;然后對該節點的所有孩子節點進行同樣的遞歸查詢。
代碼如下:
/**
* 根據父節點獲取所有的子節點
* @param $pids
* @return array
*/
public static function getAllChildNodeByFatherIds($pids)
{
if (!is_array($pids) || empty($pids)) {
return [];
}
$allChildTagIds = [];
$tagIdsStr = implode(',', array_unique($pids));
$sql = "SELECT GROUP_CONCAT(Fid) AS Fid FROM t_tree_info WHERE Fpid IN({$tagIdsStr})";
$res = $this->db->fetchRow($sql);
if ($res && $res['Fid']) {
$childTagIds = explode(',', $res['Fid']);
// 合並當前查詢出的子節點
$allChildTagIds = array_merge($allChildTagIds, $childTagIds);
$curChildTagIds = $this->getAllChildNodeByFatherIds($childTagIds);
// 如果再查詢出子節點
$allChildTagIds = array_merge($allChildTagIds, $curChildTagIds);
return $allChildTagIds;
} else {
return [];
}
}
非葉子節點的分頁展示
思路:
1、獲取非葉子節點,只要節點不是其他節點的父節點即可。
代碼如下:
# MySQL分頁查詢語句
SELECT SQL_CALC_FOUND_ROWS Fid, Fname, Fpid, Fadd_time, Fmodify_time FROM t_tree_info WHERE Fid IN(SELECT DISTINCT Fpid FROM t_tree_info) ORDER BY Fadd_time DESC LIMIT 0, 10