Cấu trúc dữ liệu & Thuật toán - Chương 6: Cây (Tree) | Bài giảng HCMUT

2023

64
0
0

Phí lưu trữ

30 Point

Tóm tắt

I. Hướng dẫn toàn tập về cấu trúc dữ liệu cây và thuật toán

Trong lĩnh vực khoa học máy tính, cấu trúc dữ liệu và thuật toán là nền tảng cốt lõi, và cấu trúc dữ liệu cây (Tree) là một trong những khái niệm phi tuyến quan trọng nhất. Khác với các cấu trúc tuyến tính như mảng hay danh sách liên kết, cây được sử dụng để biểu diễn các mối quan hệ phân cấp, thứ bậc. Theo tài liệu Data Structure and Algorithms [CO2003], một cây bao gồm một tập hợp hữu hạn các phần tử gọi là nút (node) và các đường nối định hướng gọi là nhánh (branch). Cấu trúc này không chỉ giúp lưu trữ dữ liệu một cách có tổ chức mà còn cho phép thực hiện các thao tác tìm kiếm, chèn, xóa hiệu quả hơn trong nhiều trường hợp. Hiểu rõ về cây là bước đệm thiết yếu để tiếp cận các thuật toán phức tạp hơn, từ đó giải quyết các bài toán thực tế liên quan đến quản lý hệ thống tệp, thuật toán định tuyến mạng, hay tổ chức cơ sở dữ liệu. Bài viết này sẽ cung cấp cái nhìn tổng quan, từ các khái niệm cơ bản đến các loại cây chuyên biệt và những thuật toán duyệt cây phổ biến.

1.1. Khám phá các thuật ngữ cốt lõi Nút Gốc Lá và Nhánh

Để làm việc với cấu trúc dữ liệu cây, việc nắm vững các thuật ngữ cơ bản là điều kiện tiên quyết. Một cây được cấu thành từ các nút (node), trong đó nút đầu tiên, không có nhánh nào đi vào, được gọi là nút gốc (root). Các nút không có nhánh đi ra được gọi là nút lá (leaf). Các nút còn lại, không phải gốc cũng không phải lá, là các nút trung gian (internal node). Mối quan hệ giữa các nút được định nghĩa rõ ràng: một nút cha (parent) là nút có nhánh đi ra, và nút con (child) là nút có một nhánh đi vào từ nút cha. Các nút có cùng một nút cha được gọi là anh em (siblings). Chiều sâu hay mức (level) của một nút là khoảng cách từ nó đến nút gốc. Chiều cao của cây (height) được định nghĩa là mức của nút lá xa nhất cộng với một. Một cây con (subtree) là bất kỳ cấu trúc được kết nối nào bên dưới nút gốc. Những khái niệm này, được định nghĩa trong tài liệu của TS. Nguyễn Đức Dũng, tạo nên bộ khung để phân tích và triển khai mọi loại cây trong lập trình.

1.2. Các phương pháp biểu diễn cấu trúc cây trong thực tế

Có nhiều cách để biểu diễn một cấu trúc dữ liệu cây. Lựa chọn phương pháp nào phụ thuộc vào bản chất của bài toán và sự tiện lợi trong cài đặt. Tài liệu [CO2003] giới thiệu ba phương pháp phổ biến. Thứ nhất là biểu diễn dưới dạng sơ đồ tổ chức, đây là cách trực quan nhất, tương tự như cây phả hệ, giúp dễ dàng hình dung mối quan hệ cha-con. Thứ hai là danh sách trong ngoặc (parenthetical listing), nơi cây con được đặt trong cặp dấu ngoặc đơn ngay sau nút cha của nó, ví dụ: a (b (e f) c d (g h i)). Phương pháp này súc tích và thường được dùng trong các biểu diễn văn bản. Cuối cùng là danh sách thụt đầu dòng (indented list), trong đó các nút con được thụt vào so với nút cha. Cách này rất phổ biến trong việc hiển thị cấu trúc thư mục của hệ điều hành. Mỗi phương pháp biểu diễn đều có ưu và nhược điểm riêng, nhưng tất cả đều nhằm mục đích mô tả chính xác mối quan hệ phân cấp của dữ liệu.

II. Cấu trúc cây nhị phân Nền tảng thuật toán tìm kiếm

Cây nhị phân (Binary Tree) là một dạng đặc biệt và phổ biến nhất của cấu trúc dữ liệu cây. Điểm đặc trưng của nó là mỗi nút chỉ có thể có tối đa hai nút con, thường được gọi là con trái và con phải. Do tính đơn giản và hiệu quả, cây nhị phân trở thành nền tảng cho nhiều thuật toán và cấu trúc dữ liệu phức tạp hơn, đặc biệt là các thuật toán tìm kiếm. Một định nghĩa chính thức nêu rằng: "Một cây nhị phân có thể là rỗng, hoặc bao gồm một nút gốc cùng với hai cây nhị phân con được gọi là cây con trái và cây con phải". Sự cân bằng là một yếu tố quan trọng ảnh hưởng trực tiếp đến hiệu suất của cây nhị phân. Một cây được xem là cân bằng khi chênh lệch chiều cao giữa cây con trái và cây con phải của mọi nút không quá một. Việc duy trì tính cân bằng giúp đảm bảo các thao tác trên cây có độ phức tạp tính toán ở mức tối ưu, thường là O(log N).

2.1. Phân tích các đặc tính quan trọng của cây nhị phân

Một cây nhị phân sở hữu những đặc tính toán học quan trọng quyết định hiệu suất của nó. Với N nút, chiều cao tối thiểu của cây là H_min = log₂(N) + 1, đạt được ở cây nhị phân gần hoàn chỉnh (nearly complete tree). Ngược lại, chiều cao tối đa có thể là H_max = N, xảy ra khi cây bị suy biến thành một danh sách liên kết. Với chiều cao H, số nút tối thiểu là N_min = H, và số nút tối đa là N_max = 2^H - 1, đạt được ở cây nhị phân hoàn chỉnh (complete tree), nơi mọi mức đều được lấp đầy. Yếu tố cân bằng (Balance Factor), được tính bằng B = H_L - H_R (chiều cao cây con trái trừ chiều cao cây con phải), là một chỉ số quan trọng. Một cây được coi là cây cân bằng nếu hệ số cân bằng của mọi nút là -1, 0, hoặc 1 và tất cả các cây con của nó cũng cân bằng. Việc hiểu rõ các đặc tính này giúp lập trình viên lựa chọn và thiết kế cấu trúc dữ liệu và thuật toán phù hợp.

2.2. So sánh 2 phương pháp cài đặt cây nhị phân phổ biến

Việc cài đặt một cây nhị phân có thể được thực hiện bằng hai phương pháp chính: sử dụng cấu trúc liên kết (linked implementation) hoặc sử dụng mảng (array-based implementation). Cài đặt dựa trên liên kết là phương pháp linh hoạt nhất. Mỗi nút là một cấu trúc dữ liệu chứa dữ liệu và hai con trỏ, một trỏ đến nút con trái và một trỏ đến nút con phải. Phương pháp này hiệu quả về mặt bộ nhớ khi cây thưa thớt (không gần hoàn chỉnh) và giúp các thao tác chèn, xóa dễ dàng hơn. Ngược lại, cài đặt dựa trên mảng đặc biệt phù hợp cho các cây nhị phân hoàn chỉnh hoặc gần hoàn chỉnh. Trong cách này, gốc được lưu ở chỉ số 0 (hoặc 1), con trái của nút i được lưu ở 2i + 1, và con phải ở 2i + 2. Ưu điểm của phương pháp này là truy cập nhanh và tiết kiệm không gian con trỏ, nhưng lại lãng phí bộ nhớ nếu cây không đầy đủ và việc thay đổi cấu trúc cây rất tốn kém.

III. Top 4 phương pháp duyệt cây Tree Traversal hiệu quả

Duyệt cây (Tree Traversal) là quá trình truy cập (thăm) mỗi nút trong cây đúng một lần theo một thứ tự có hệ thống. Đây là một thao tác cơ bản và cực kỳ quan trọng, làm nền tảng cho nhiều thuật toán phức tạp khác như tìm kiếm, sao chép, hoặc xóa cây. Có hai chiến lược duyệt cây chính được đề cập trong tài liệu [CO2003]. Chiến lược thứ nhất là duyệt theo chiều sâu (Depth-First Traversal), ưu tiên đi sâu vào một nhánh cho đến khi tới nút lá, sau đó mới quay lại và duyệt các nhánh khác. Chiến lược thứ hai là duyệt theo chiều rộng (Breadth-First Traversal), tiến hành duyệt tất cả các nút ở cùng một mức trước khi chuyển sang mức tiếp theo. Mỗi chiến lược có các biến thể riêng và được ứng dụng cho những mục đích khác nhau. Việc lựa chọn đúng phương pháp duyệt cây là chìa khóa để xử lý dữ liệu một cách hiệu quả và chính xác theo yêu cầu của bài toán.

3.1. Hướng dẫn kỹ thuật duyệt theo chiều sâu Depth First

Duyệt theo chiều sâu (Depth-First Traversal) là một kỹ thuật duyệt cây ưu tiên đi theo một đường dẫn từ gốc đến nút lá xa nhất có thể trước khi quay lui. Có ba biến thể chính của kỹ thuật này, khác nhau ở thứ tự xử lý nút gốc so với các cây con của nó. Cụ thể: Preorder (NLR) xử lý nút gốc trước, sau đó duyệt cây con trái, rồi đến cây con phải. Inorder (LNR) duyệt cây con trái trước, sau đó xử lý nút gốc, và cuối cùng là duyệt cây con phải. Postorder (LRN) duyệt cây con trái, rồi cây con phải, và xử lý nút gốc sau cùng. Các thuật toán duyệt này thường được cài đặt một cách tự nhiên và thanh lịch bằng phương pháp đệ quy, vì bản chất của cây là một cấu trúc đệ quy. Mỗi kiểu duyệt có một ứng dụng riêng biệt; ví dụ, duyệt Inorder một cây tìm kiếm nhị phân sẽ cho ra một danh sách các phần tử đã được sắp xếp.

3.2. Phân tích 3 thuật toán duyệt cây Preorder Inorder Postorder

Ba thuật toán duyệt cây theo chiều sâu có những ứng dụng thực tiễn khác nhau. Preorder (Node-Left-Right) thường được sử dụng để tạo một bản sao của cây, vì nó tái tạo lại cấu trúc cây một cách chính xác khi chèn các nút theo đúng thứ tự duyệt. Nó cũng được dùng để lấy biểu thức tiền tố (Prefix) từ một cây biểu thức. Inorder (Left-Node-Right) là phương pháp quan trọng nhất đối với cây tìm kiếm nhị phân (BST), vì kết quả duyệt luôn là một dãy các giá trị được sắp xếp theo thứ tự tăng dần. Điều này làm cho việc kiểm tra tính hợp lệ của một BST trở nên đơn giản. Postorder (Left-Right-Node) rất hữu ích trong việc xóa một cây, vì nó đảm bảo rằng tất cả các nút con đã được xử lý (và có thể đã được giải phóng bộ nhớ) trước khi xử lý nút cha. Ngoài ra, nó còn được dùng để lấy biểu thức hậu tố (Postfix) từ một cây biểu thức, rất tiện lợi cho việc tính toán bằng stack.

3.3. Tìm hiểu về thuật toán duyệt cây theo chiều rộng BFS

Duyệt theo chiều rộng (Breadth-First Traversal - BFS) là phương pháp duyệt cây theo từng mức, từ trái sang phải. Quá trình bắt đầu từ nút gốc. Sau khi xử lý nút gốc, tất cả các nút con trực tiếp của nó (mức 1) sẽ được xử lý. Tiếp theo, tất cả các nút con của các nút ở mức 1 (tức là các nút ở mức 2) sẽ được xử lý, và quá trình này tiếp tục cho đến khi tất cả các nút đã được duyệt. Để cài đặt BFS, người ta thường sử dụng một cấu trúc dữ liệu hàng đợi (Queue). Nút gốc được thêm vào hàng đợi. Trong một vòng lặp, nút ở đầu hàng đợi được lấy ra để xử lý, và các nút con của nó (nếu có) được thêm vào cuối hàng đợi. Vòng lặp kết thúc khi hàng đợi rỗng. BFS rất hữu ích trong việc tìm đường đi ngắn nhất từ một nút đến các nút khác trong các đồ thị không có trọng số, hoặc trong các bài toán cần tìm kiếm một phần tử ở gần gốc nhất có thể.

IV. Bí quyết tối ưu tìm kiếm với cây tìm kiếm nhị phân BST

Cây tìm kiếm nhị phân (Binary Search Tree - BST) là một trong những ứng dụng mạnh mẽ nhất của cấu trúc dữ liệu cây. Nó kết hợp những ưu điểm tốt nhất của hai cấu trúc dữ liệu khác: tốc độ tìm kiếm nhanh của mảng đã sắp xếp và tốc độ chèn/xóa nhanh của danh sách liên kết. Theo định nghĩa, một BST là một cây nhị phân tuân thủ một thuộc tính quan trọng: "Với mọi nút, tất cả các giá trị trong cây con trái đều nhỏ hơn giá trị của nút đó, và tất cả các giá trị trong cây con phải đều lớn hơn hoặc bằng giá trị của nút đó". Thuộc tính này phải đúng cho tất cả các nút trong cây. Nhờ đặc tính này, việc tìm kiếm một phần tử trong BST trở nên cực kỳ hiệu quả. Thay vì phải duyệt qua toàn bộ cây, thuật toán có thể loại bỏ một nửa số nút còn lại ở mỗi bước so sánh, tương tự như thuật toán tìm kiếm nhị phân trên mảng. Điều này giúp độ phức tạp thời gian trung bình của các thao tác tìm kiếm, chèn và xóa chỉ còn O(log N).

4.1. Đặc điểm cốt lõi của một cây tìm kiếm nhị phân hợp lệ

Để một cây nhị phân được coi là một cây tìm kiếm nhị phân (BST) hợp lệ, nó phải thỏa mãn nghiêm ngặt thuộc tính tìm kiếm tại mọi nút. Điều này có nghĩa là, không chỉ giá trị của nút con trái trực tiếp phải nhỏ hơn nút cha, mà tất cả các hậu duệ trong cây con trái đều phải nhỏ hơn. Tương tự, tất cả các hậu duệ trong cây con phải phải lớn hơn hoặc bằng giá trị của nút cha. Một cách hiệu quả để kiểm tra tính hợp lệ của một BST là thực hiện phép duyệt Inorder (LNR). Nếu kết quả của phép duyệt là một dãy các giá trị được sắp xếp theo thứ tự không giảm (tăng dần), thì cây đó là một BST hợp lệ. Bất kỳ sự lộn xộn nào trong thứ tự này đều cho thấy cây đã vi phạm thuộc tính BST tại một điểm nào đó. Việc đảm bảo tính hợp lệ là cực kỳ quan trọng vì tất cả các thuật toán tối ưu trên BST đều dựa trên giả định rằng thuộc tính này được duy trì.

4.2. So sánh thuật toán tìm kiếm đệ quy và lặp trong BST

Việc tìm kiếm một giá trị trong cây tìm kiếm nhị phân có thể được cài đặt bằng hai phương pháp chính: đệ quy (recursive) và lặp (iterative). Thuật toán tìm kiếm đệ quy, như searchBST trong tài liệu [CO2003], tận dụng bản chất đệ quy của cây. Nó so sánh giá trị cần tìm với nút hiện tại. Nếu khớp, trả về nút. Nếu nhỏ hơn, nó gọi lại chính nó trên cây con trái. Nếu lớn hơn, nó gọi lại trên cây con phải. Quá trình dừng lại khi tìm thấy giá trị hoặc khi gặp một cây con rỗng. Thuật toán tìm kiếm lặp, như iterativeSearchBST, sử dụng một vòng lặp và một biến con trỏ để di chuyển xuống cây. Bắt đầu từ gốc, vòng lặp tiếp tục miễn là con trỏ chưa rỗng. Trong mỗi lần lặp, nó di chuyển con trỏ sang trái hoặc phải tùy thuộc vào kết quả so sánh. Cả hai phương pháp đều có cùng độ phức tạp thời gian O(log N) trong trường hợp trung bình và O(N) trong trường hợp xấu nhất. Phương pháp lặp thường hiệu quả hơn một chút về mặt bộ nhớ vì không tốn không gian cho các ngăn xếp lời gọi hàm đệ quy.

V. Thách thức với cây Tree và tầm quan trọng của cây cân bằng

Mặc dù cây tìm kiếm nhị phân (BST) mang lại hiệu suất vượt trội trong trường hợp trung bình, nó tồn tại một thách thức lớn: sự suy biến. Khi dữ liệu được chèn vào cây theo thứ tự đã được sắp xếp (tăng dần hoặc giảm dần), BST sẽ mất đi tính phân nhánh và biến thành một danh sách liên kết. Trong kịch bản này, chiều cao của cây trở thành N, và độ phức tạp của các thao tác tìm kiếm, chèn, xóa tăng vọt lên O(N), làm mất đi hoàn toàn lợi thế của cấu trúc dữ liệu cây. Vấn đề này nhấn mạnh tầm quan trọng của việc duy trì sự cân bằng của cây. Một cây cân bằng (Balanced Tree) là một cây tìm kiếm nhị phân tự động điều chỉnh cấu trúc của mình sau mỗi lần chèn hoặc xóa để đảm bảo chiều cao luôn ở mức xấp xỉ O(log N). Việc này đảm bảo hiệu suất của các thao tác luôn ở mức tối ưu, ngay cả trong những trường hợp xấu nhất.

5.1. Vấn đề suy biến trong cây tìm kiếm nhị phân BST

Sự suy biến (degeneration) là điểm yếu cố hữu của một cây tìm kiếm nhị phân đơn giản. Nó xảy ra khi chuỗi các thao tác chèn và xóa dẫn đến một cây mất cân bằng nghiêm trọng. Trường hợp điển hình nhất là chèn một chuỗi các khóa đã được sắp xếp. Ví dụ, chèn các số 1, 2, 3, 4, 5 theo thứ tự sẽ tạo ra một cây mà mỗi nút chỉ có con phải, trông giống hệt một danh sách liên kết. Khi đó, thao tác tìm kiếm số 5 sẽ yêu cầu duyệt qua tất cả 5 nút, tương đương với tìm kiếm tuyến tính. Hiệu suất của thuật toán trên cây suy biến không tốt hơn một mảng chưa sắp xếp. Để giải quyết vấn đề này, các nhà khoa học máy tính đã phát triển các cấu trúc dữ liệu tiên tiến hơn, được gọi là các cây tìm kiếm nhị phân tự cân bằng.

5.2. Giải pháp tương lai Giới thiệu cây AVL và cây B Tree

Để khắc phục nhược điểm của BST, các cấu trúc cây cân bằng đã ra đời. Cây AVL là một trong những cây tìm kiếm nhị phân tự cân bằng đầu tiên. Nó duy trì hệ số cân bằng (balance factor) của mọi nút trong khoảng [-1, 1]. Nếu một thao tác chèn hoặc xóa làm vi phạm điều kiện này, cây sẽ thực hiện các phép xoay (rotation) để tái cân bằng cấu trúc. Một loại cây quan trọng khác là B-Tree, đây là cây tìm kiếm đa đường (multi-way tree) được tối ưu hóa cho các hệ thống lưu trữ đĩa. Thay vì chỉ có hai con, mỗi nút trong B-Tree có thể có nhiều con, giúp giảm chiều cao của cây một cách đáng kể. Điều này giảm số lần truy cập đĩa cần thiết để tìm kiếm dữ liệu, một yếu tố then chốt trong hiệu suất của cơ sở dữ liệu và hệ thống tệp. Cả cây AVLB-Tree đều là những cải tiến quan trọng, đảm bảo hiệu suất ổn định và hiệu quả của cấu trúc dữ liệu cây trong các ứng dụng thực tế quy mô lớn.

16/07/2025