Cây là một cấu trúc dữ liệu phổ biến và có ứng dụng rất nhiều để việc giải quyết các vấn đề trong máy tính. Để biểu diễn và cài đặt cây trong bộ nhớ trong khá đơn giản. Vậy việc biểu diễn cây trong cơ sở dữ liệu như thế nào, có khác biệt gì so với khi cài đặt trong bộ nhớ trong, chúng ta cùng tìm hiểu nhé.
Sau đây là định nghĩa về cây theo Wikipedia
Trong khoa học máy tính, cây là một cấu trúc dữ liệu được sử dụng rộng rãi gồm một tập hợp các nút (tiếng Anh: node) được liên kết với nhau theo quan hệ cha-con.
Cây có rất nhiều loại, tuỳ thuộc vào đặc điểm cũng như những vấn đề mà nó giải quyết như
Như hình vẽ, ta có thể dễ dàng mô phỏng một cấu trúc cây như sau
Nút {
khoá
liên kết tới nút cha
dữ liệu
}
hoặc
Nút {
khoá
danh sách các nút con
dữ liệu
}
Chắc hẳn các bạn thường cài đặt theo cách thứ 2 :smile:
Bây giờ, mình cùng nói qua các toán tử chính với cấu trúc cây phổ biến - cây k phân
Lưu ý: tất cả đoạn code dưới đây mình sử dụng Python nhé
class Node:
def __init__(self, data: Any, children: List[Node], key: int):
self.data = data
self.children = children
self.key = key
Bây giờ mình sẽ biểu diễn cấu trúc hình bên dưới bằng class Node bên trên nhé
store
├─ book
│ ├─ novel
│ ├─ science
├─ computer/
│ ├─ macbook
│ ├─ dell
store = Node(
data = "store",
key = 1,
children = [
Node(
data = "book",
key = 2,
children = [
Node("novel", None, 3),
Node("science", None, 4),
]
),
Node(
data = "computer",
key = 5,
children = [
Node("macbook", None, 6),
Node("dell", None, 7),
]
)
]
)
Như các bạn thấy, với cấu trúc này khi xoá một nút tương ứng với việc bạn xoá toàn bộ cây với gốc là nút đó, nên ta có thể rút gọn các toán tử tìm một nút từ cây, xoá một nút hay thêm một nút từ cây chỉ là toán tử tìm kiếm một nút.
Với toán tử này, bạn có thể cài đặt một hàm duyệt cây đơn thuần, dfs hoặc bfs để tìm ra nút đó dựa vào key
.
Lưu ý, phần này là trọng tâm của bài viết :smile:
Như bạn biết rằng, cơ sở dữ liệu quan hệ (SQL) biểu diễn dữ liệu một cách tuyến tính, tức là theo dạng danh sách, để biểu diễn các quan hệ phức tạp, ta cần dùng tới các liên kết được gọi là các quan hệ (khoá ngoại).
Sau đây là ERD cho cấu trúc nút của cây muốn biểu diễn trong SQL
Do trong một số loại cơ sở dữ liệu quan hệ không có cấu trúc danh sách nên mình không thể biểu diễn danh sách của một nút dưới dạng danh sách con của nó mà mình sẽ làm ngược lại, trỏ nút đó tới cha của nó (cách biểu diễn thứ 1)
Sự khác nhau ở đây là gì? Khi ta có một nút trong tay
OK, đánh giá những khó khăn với các biểu diễn cây trong SQL nhé
Tuy nhiên, có một vài hệ quản trị cơ sở dữ liệu như Postgres hỗ trợ truy vấn đệ quy và bạn có thể truy vấn như sau để có được một subtree của một nút
WITH RECURSIVE nodes_t AS (
SELECT * FROM nodes WHERE key = :key
UNION ALL
SELECT parent_.key, parent.data FROM nodes AS parent
INNER JOIN nodes_t AS child
ON parent.id = child.parent_key
) SELECT key, parent_id, data FROM nodes_t;
còn MySQL thì không có chức năng đó :smile:, ok giờ mình ở đây để giúp các bạn vấn đề này
Vậy làm sao ta có thể tìm tất cả các nút con của một nút cho trước trong 1 truy vấn
Mình có một thủ thuật nhỏ ở đây, ta sẽ thêm một trường hỗ trợ việc này là path
Cấu trúc trường path
như sau:
key ông/key cha/key nút hiện tại
Lúc truy vấn, ta sẽ truy vấn subtree của một nút có key là store
như sau:
SELECT * FROM nodes WHERE path LIKE '%/store/%' OR path LIKE 'store/%';
store
không phải là gốc của toàn bộ câystore
là nút gốc của toàn bộ câyYep, chúng ta đã có toàn bộ cây trong subtree, giờ làm sau để đưa nó về dạng 2. Lưu ý, kết quả ta nhận được có cấu trúc như sau
[
{ "key": 2, "data": "book", "path": "store/book", "parent_key": 1 },
{
"key": 4,
"data": "science",
"path": "store/book/science",
"parent_key": 2
},
{ "key": 3, "data": "novel", "path": "store/book/novel", "parent_key": 2 },
{
"key": 6,
"data": "macbook",
"path": "store/computer/macbook",
"parent_key": 5
},
{ "key": 1, "data": "store", "path": "store", "parent_key": null },
{ "key": 5, "data": "computer", "path": "store/computer", "parent_key": 1 },
{ "key": 7, "data": "dell", "path": "store/computer/dell", "parent_key": 5 }
]
OK, giờ các bạn hãy đọc đoạn code sau và suy ngẫm điểm tương đồng nhé
class Node:
def __init__(self, data, children, key, path):
self.data = data
self.children = children
self.key = key
self.path = path
store = Node(
data="store",
key=1,
path="store",
children=[
Node(
data="book",
key=2,
path="store/book",
children=[
Node("novel", None, 3, "store/book/novel"),
Node("science", None, 4, "store/book/novel"),
],
),
Node(
data="computer",
key=5,
path="store/computer",
children=[
Node("macbook", None, 6, "store/computer/macbook"),
Node("dell", None, 7, "store/computer/dell"),
],
),
],
)
def dfs(root):
print(root.path)
if root.children:
for child in root.children:
dfs(child)
print("prefix order")
dfs(store)
print("")
print("sort path by ascend")
paths = [
"store/computer/macbook",
"store/computer",
"store/computer/dell",
"store/book/novel",
"store/book",
"store/book/science",
"store",
]
for path in sorted(paths):
print(path)
Bạn có thể thấy rằng, cách duyệt cây theo thứ tự prefix có kết quả tương tự như cách sắp xếp trường path
theo thứ tự tăng dần, và dựa vào đặc điểm này, ta sẽ có hàm xây dựng cấu trúc thứ 2 như sau
Đầu tiên, mình sẽ thêm truy vấn sắp xếp như sau
SELECT * FROM nodes WHERE path LIKE '%/store/%' OR path LIKE 'store/%' ORDER BY path;
Ta sẽ nhận được kết quả
[
{ "key": 1, "data": "store", "path": "store", "parent_key": null },
{ "key": 2, "data": "book", "path": "store/book", "parent_key": 1 },
{ "key": 3, "data": "novel", "path": "store/book/novel", "parent_key": 2 },
{
"key": 4,
"data": "science",
"path": "store/book/science",
"parent_key": 2
},
{ "key": 5, "data": "computer", "path": "store/computer", "parent_key": 1 },
{
"key": 6,
"data": "macbook",
"path": "store/computer/macbook",
"parent_key": 5
},
{ "key": 7, "data": "dell", "path": "store/computer/dell", "parent_key": 5 }
]
def build_tree(flat):
tree = {}
flat.sort(key=lambda x: x["path"])
visited = {}
for item in flat:
key = item["key"]
node = Node(key, item["data"])
visited[key] = node
parent_key = item["parent_key"]
if parent_key not in visited:
tree = node
else:
parent = visited[parent_key]
parent.children.append(node)
return tree
Và đây là kết quả khi cây được build xong
store
book
novel
science
computer
dell
macbook
Bạn có thể tham khảo đoạn code full ở đây
Các bài toán trong thực tế từ vấn đề này như bạn có thể phải trả về cho frontend cấu trúc cây danh mục sản phẩm chẳng hạn, hay một hệ thống quán lí file trên cloud,...
OK, phương pháp này có một hạn chế là phá vỡ lợi ích của SQL đó là chúng ta đã lưu quan hệ của nó vào hai trường (path và parent_key) nên khi chúng ta chuyển nút từ nút cha này sang nút cha khác đòi hỏi chúng ta phải có cơ chế để cập nhật lại trường path
. Trong bài viết sai mình sẽ nói về vấn đề này sau.
Mình hi vọng các bạn sẽ thích bài viết này, hẹn gặp lại các bạn trong các bài viết lần sau :smile: