PHP無限極分類


一、參考資料

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM