文章標題中的多個詞語表達的其實是一個意思,就是遞歸分類數據,分級數據非常類似數據結構中的樹狀結構,即每個節點有自己的孩子節點,孩子結點本身也是父親節點。這是一個遞歸、分層形式。可以稱之為樹形層級數據。
層級數據結構是編程語言中非常普通的一種數據結構,它代表一系列的數據每一項都有一個父親節點(除了根節點)和其他多個孩子結點。WEB開發人員使用層級數據結構用于非常多的場景,包括內容管理系統CMS、論壇主題、郵件列表,還有電子商務網站的產品分類等。
本文章主要介紹了使用PHP和MYSQL來管理分級數據的方法,在其中將給出兩種最流行的分級數據模型:
鄰接表模型
嵌套集合模型
鄰接表模型用于分層數據
鄰接表模型是一種分級數據模型,其中每個節點有一個指向其父親的指針(根節點該指針為空值),使用下面的SQL語句將建立該結構并插入測試數據:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
表的結構
`
category
`
--
CREATE
TABLE
IF
NOT
EXISTS
`
category
`
(
`
category_id
`
int
(
10
)
NOT
NULL
AUTO_INCREMENT
,
`
category_name
`
varchar
(
50
)
NOT
NULL
,
`
parent_id
`
int
(
10
)
DEFAULT
NULL
,
PRIMARY
KEY
(
`
category_id
`
)
)
ENGINE
=
InnoDB
DEFAULT
CHARSET
=
utf8
AUTO_INCREMENT
=
15
;
--
--
轉存表中的數據
`
category
`
--
INSERT
INTO
`
category
`
(
`
category_id
`
,
`
category_name
`
,
`
parent_id
`
)
VALUES
(
1
,
'A'
,
NULL
)
,
(
2
,
'B'
,
1
)
,
(
3
,
'C'
,
1
)
,
(
4
,
'D'
,
1
)
,
(
5
,
'E'
,
2
)
,
(
6
,
'F'
,
2
)
,
(
7
,
'I'
,
4
)
,
(
8
,
'G'
,
5
)
,
(
9
,
'H'
,
5
)
,
(
10
,
'J'
,
7
)
,
(
11
,
'K'
,
10
)
,
(
12
,
'L'
,
10
)
;
建立完成后,數據庫中存在了數據,并且分類圖是每個節點為(關鍵字:數據庫ID)。
parent_id就是它的父節點的ID號,這種方法非常簡單,因為能很容易的看清楚父子關系。使用以下的簡單PHP函數代碼可以很容易的輸出樹狀路徑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function
get_path
(
$category_id
)
{
$con
=
mysql_connect
(
"localhost"
,
"root"
,
"123456"
)
;
if
(
!
$con
)
{
die
(
'數據庫連接失敗: '
.
mysql_error
(
)
)
;
}
mysql_select_db
(
'test'
,
$con
)
;
// 查找當前節點的父節點的ID,這里使用表自身與自身連接實現
$sql
=
"
SELECT c1.parent_id, c2.category_name AS parent_name
FROM category AS c1
LEFT JOIN category AS c2
ON c1.parent_id=c2.category_id
WHERE c1.category_id='$category_id' "
;
//echo $sql."
";//測試把SQL打印出來,拿到數據庫執行一下看看結果
$result
=
mysql_query
(
$sql
)
;
$row
=
mysql_fetch_array
(
$result
)
;
//現在$row數組存了父親節點的ID和名稱信息
// 將樹狀路徑保存在數組里面
$path
=
array
(
)
;
//如果父親節點不為空(根節點),就把父節點加到路徑里面
if
(
$row
[
'parent_id'
]
!=
NULL
)
{
//將父節點信息存入一個數組元素
$parent
[
0
]
[
'category_id'
]
=
$row
[
'parent_id'
]
;
$parent
[
0
]
[
'category_name'
]
=
$row
[
'parent_name'
]
;
//遞歸的將父節點加到路徑中
$path
=
array_merge
(
get_path
(
$row
[
'parent_id'
]
)
,
$parent
)
;
}
return
$path
;
}
//根據上面的圖可以看出,K的ID是11,我們就用它來測試路徑
$path
=
get_path
(
11
)
;
echo
"
路徑數組:
";
echo
"
"
;
print_r
(
$path
)
;
echo
"
";
//將路徑到根節點的路徑打印出來
//打印結果:J>I>D>A>
echo
"
向根節點打印路徑:
";
for
(
$i
=
count
(
$path
)
-
1
;
$i
>=
0
;
$i
--
)
{
echo
$path
[
$i
]
[
'category_name'
]
.
'>'
;
}
?>
由此可以知道怎樣找到一個葉子節點(沒有孩子的節點)到根節點的路徑,下面來看怎樣從根節點往下來遍歷層級結構,通過節點的層級關系來打印所有的節點:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function
display_children
(
$category_id
,
$level
)
{
$con
=
mysql_connect
(
"localhost"
,
"root"
,
"123456"
)
;
if
(
!
$con
)
{
die
(
'數據庫連接失敗: '
.
mysql_error
(
)
)
;
}
mysql_select_db
(
'test'
,
$con
)
;
// 獲得當前節點的所有孩子節點(直接孩子,沒有孫子)
$result
=
mysql_query
(
"SELECT * FROM category WHERE parent_id='$category_id'"
)
;
// 遍歷孩子節點,打印節點
while
(
$row
=
mysql_fetch_array
(
$result
)
)
{
// 根據層級,按照縮進格式打印節點的名字
// 這里只是打印,你可以將以下代碼改成其他,比如把節點信息存儲起來
echo
str_repeat
(
'--'
,
$level
)
.
$row
[
'category_name'
]
.
"
"
;
// 遞歸的打印所有的孩子節點
display_children
(
$row
[
'category_id'
]
,
$level
+
1
)
;
}
}
//根節點是A:1我們就用它來打印所有的節點
display_children
(
1
,
0
)
;
?>
然而,鄰接表模型(每個節點存儲父親節點信息)有它的劣勢,首先使用數據庫的查詢語句很難直接實現它,需要借助PHP代碼實現。SQL語句需要你知道節點位于哪一個層級。并且每個樹層是使用SQL的自我表連接實現的,這意味著樹的每一層處理都會降低數據庫的性能。
刪除節點的過程也會導致一些問題,如果只刪除了某個節點它卻有孩子,結果是它的孩子成了孤兒(就是沒有父親了),真正的體現就是,這些孩子節點從樹中相當于“消失了”。
嵌套集合模型用于樹形分層結構數據
嵌套集合模型,也叫做先根遍歷樹算法,也是一種處理樹形層級數據的方法。代替節點間的父子關系,層級使用嵌套的容器的集合來表示,其中每個節點具有兩個值,一個left,一個right。
決定left和right的值的過程是從左到右進行的,首先給left賦值,讓后向下遍歷節點的孩子們,最后才能得到節點的right的值。SQL語句如下所示:
1
2
3
4
5
6
CREATE
TABLE
category
(
category_id
INT
(
10
)
AUTO_INCREMENT
PRIMARY
KEY
,
category_name
VARCHAR
(
50
)
NOT
NULL
,
lft
INT
(
10
)
NOT
NULL
,
rgt
INT
(
10
)
NOT
NULL
)
;
現在可以用一句SQL查詢得到整個樹的節點:
SELECT * FROM category WHERE lft BETWEEN 1 AND 14 ORDER BY lft ASC
在本SQL中的兩個數字值1和14就是根節點的left和right值。類似的如果想得到某個節點的所有孩子節點,只需要將該SQL語句的1和14替換成本節點的left和right值就可以了。例如,如果想得到所有男人的衣服,可以用下面的SQL語句:
SELECT * FROM category WHERE lft BETWEEN 2 AND 7 ORDER BY lft ASC
想找到一條到某個節點的路徑,用一條SQL語句就可以搞定:
SELECT * FROM category WHERE lft < 9 AND rgt > 10 ORDER BY lft ASC
請仔細觀察一下一些葉子節點到根節點的路徑。就會發現所有的祖先都有更小的左值和更大的右值。本例子中一條到裙子分類的路徑被取出。觀察一下裙子的所有left值都小于9,right值都大于10,其他的非祖先節點都不滿足該要求。
盡管嵌套集合模型更加復雜并且有些難以理解,它有非常多的優勢。它不需要依賴其他資源(比如PHP代碼),也不需要遞歸。同時,數據庫查詢語句非常的簡單,大多數用一條SQL語句就可以搞定。這些特性都能夠顯著的增加應用程序的性能,使得它能夠用可接受的速度來處理復雜的層級結構。
然而萬事皆無完美,更新該層級結構(增加或刪除節點)卻更加的復雜,并且可能會非常慢。
增加一個節點到層級結構的方法:
將一個節點插入到層級數據中,需要整個樹很多節點的left和right值的更新。例如,如果你想將一個男士運動鞋的分類插入到男性衣服的短褲后面。那么所有你必須將大于6的所有left和right值都增加2。為什么呢?因為短褲的right值是6,那你就必須將你的新分類的left和right值設定為7和8,當然,以下兩條SQL就可以解決:
UPDATE category SET rgt=rgt+2 WHERE rgt>6
UPDATE category SET lft=lft+2 WHERE lft>6
現在樹中間已經有空隙用來插入新分類了,用一條SQL插入該節點:
INSERT INTO category (category_name,lft,rgt) VALUES (
'Sneakers'
,
'7'
,
'8'
)
樹形層級數據中刪除一個節點的方法:
在層級集合模型中刪除一個節點的方法,比在鄰接表中相同的操作稍微難一些。不同的操作的復雜程度是不同的,比如刪除一個葉子節點和一個帶孩子節點就很不同。
要刪除一個葉子節點,先將所有left和right大于該節點left和right值的節點的left和right減去2,然后再刪除該節點。以下使用SQL實現該過程:
UPDATE category SET lft=lft-2 WHERE lft>5
UPDATE category SET rgt=rgt-2 WHERE rgt>6
DELETE FROM category WHERE lft=
'5'
AND rgt=
'6'
該例子中短褲節點被刪除了。
如果要刪除的節點有孩子節點的話,刪除過程會多一個步驟:
比如我們刪除男性衣服分類的時候:
UPDATE category SET lft=lft-1, rgt=rgt-1 WHERE lft>2 AND rgt<7
UPDATE category SET lft=lft-2 WHERE lft>7
UPDATE category SET rgt=rgt-2 WHERE rgt>7
DELETE FROM category WHERE lft=
'2'
AND rgt=
'7'
哪種模型對于處理樹形分層數據更好?
哪種情況更好呢,看情況。如果需要一個更加靈活的模型,更容易更新,就用鄰接表模型吧。如果分類構成了一棵復雜的數,并且更新不需要很頻繁,用嵌套集合模型肯定是上上之選。
本文內容翻譯自:訪問,其中原文中的代碼有些問題,本人添加了測試數據并改正了代碼。