Professional Documents
Culture Documents
Chương 7 DDT
Chương 7 DDT
Các chuyên gia về năng suất nói rằng những đột phá đến từ tư duy “phi tuyến
tính”. TRONG chương này, chúng ta thảo luận về một trong những cấu trúc dữ liệu phi
tuyến quan trọng nhất trong máy tính—cây. Cấu trúc cây thực sự là một bước đột phá
trong tổ chức dữ liệu, vì chúng cho phép chúng tôi triển khai một loạt thuật toán nhanh
hơn nhiều so với khi sử dụng cấu trúc dữ liệu tuyến tính, chẳng hạn như danh sách, vectơ
và chuỗi. Cây cối cũng cung cấp một tổ chức tự nhiên cho dữ liệu và do đó đã trở thành
cấu trúc phổ biến trong hệ thống tập tin, giao diện đồ họa người dùng, cơ sở dữ liệu, trang
Không phải lúc nào các chuyên gia về năng suất cũng hiểu rõ ý nghĩa của tư duy
“phi tuyến tính”, nhưng khi chúng ta nói rằng cây cối là “phi tuyến tính”, chúng ta đang
đề cập đến một tổ chức mối quan hệ phong phú hơn các mối quan hệ “trước” và “sau”
đơn giản giữa các đối tượng theo trình tự. Các mối quan hệ trong cây có tính phân cấp,
với một số các đối tượng “ở trên” và một số đối tượng “ở dưới” những đối tượng khác.
Trên thực tế, thuật ngữ chính đối với cấu trúc dữ liệu cây xuất phát từ cây gia phả, với
các thuật ngữ “cha”, “con”,“tổ tiên” và “con cháu” là những từ phổ biến nhất được sử
dụng để mô tả mối quan hệ tàu thuyền. Chúng tôi đưa ra một ví dụ về cây gia đình trong
Hình 7.1.
Hình 7.1: Cây gia phả cho thấy một số hậu duệ của Abraham, như được ghi lại trong
phần tử trên cùng, mỗi phần tử trong cây có một phần tử cha và bằng 0 hoặc nhiều yếu tố
trẻ em hơn. Một cái cây thường được hình dung bằng cách đặt các phần tử bên trong
hình bầu dục hoặc hình chữ nhật và bằng cách vẽ ra mối liên hệ giữa cha mẹ và con cái
với những đường thẳng. Chúng ta thường gọi phần tử trên cùng là gốc của cây, nhưng nó
được coi là phần tử cao nhất, với các phần tử khác là được kết nối bên dưới (ngay đối
trong phần cha-con mối quan hệ với các tính chất sau:
-Nếu T khác rỗng thì nó có một nút đặc biệt, gọi là gốc của T, không có cha mẹ.
-Mỗi nút v của T khác với nút gốc có một nút cha duy nhất w; mọi nút có cha w là con
của w.
--Lưu ý: rằng theo định nghĩa của chúng tôi, một cây có thể trống, nghĩa là nó không có
bất kỳ nút nào.Quy ước này cũng cho phép chúng ta định nghĩa một cây theo cách đệ
quy, chẳng hạn như rằng cây T trống hoặc chứa nút r, được gọi là gốc của T và một tập
Hai nút là con của cùng một cha mẹ là anh em. Nút v là ở ngoài nếu v
không có con.
Nút v là nút nội nếu nó có một hoặc nhiều nút con. Bên ngoài các nút còn
Ví dụ 7.1: Trong hầu hết các hệ điều hành, các tệp được sắp xếp theo thứ bậc thành thư
mục lồng nhau (còn gọi là thư mục), được trình bày cho người dùng dưới dạng của một
cái cây. (Xem Hình 7.3.) Cụ thể hơn, các nút bên trong của cây là được liên kết với các
thư mục và các nút bên ngoài được liên kết với các tệp thông thường. Trong hệ điều hành
UNIX và Linux, gốc của cây được đặt một cách thích hợp được gọi là “thư mục gốc” và
Nút u là tổ tiên của nút v nếu u = v hoặc u là tổ tiên của nút cha của v.
Ngược lại, chúng ta nói rằng nút v là hậu duệ của nút u nếu u là nút tổ tiên của v.
Ví dụ, trong Hình 7.3, cs252/ là tổ tiên của các giấy tờ/, và pr3 là một hậu duệ của
cs016/. Cây con của T có gốc tại nút v là cây gồm tất cả các con cháu của v trong T (kể
cả chính v).
Trong Hình 7.3, cây con có gốc tại cs016/ bao gồm các nút cs016/, điểm, bài tập về nhà/,
- Đường đi của T là một chuỗi các nút sao cho hai nút liên tiếp bất kỳ trong dãy tạo
của mỗi nút; nghĩa là, chúng ta có thể xác định các nút con của một nút là nút thứ nhất,
thứ hai, thứ ba, v.v. Thứ tự như vậy được xác định bởi cách sử dụng cây và thường được
biểu thị bằng cách vẽ cây có các cây anh em được sắp xếp từ trái sang phải, tương ứng
với mối quan hệ tuyến tính của chúng. Cây có thứ tự thường biểu thị mối quan hệ thứ tự
tuyến tính tồn tại giữa các cây anh em bằng cách liệt kê chúng theo trình tự hoặc trình
• Hàm cây
Cây ADT lưu trữ các phần tử tại các nút của cây.
Thay vì, mỗi nút của cây được liên kết với một đối tượng vị trí, cung cấp công khai truy
cập vào các nút. Vì lý do này, khi thảo luận về giao diện chung của các chức năng của
ADT, chúng tôi sử dụng ký hiệu p (chứ không phải v) để làm rõ rằng đối số cho chức
năng là một vị trí chứ không phải một nút. Tuy nhiên, do mối liên hệ chặt chẽ giữa hai
đối tượng này, chúng ta thường làm mờ đi sự khác biệt giữa chúng và sử dụng các thuật
Chương 6, chúng ta khai thác khả năng của C++ trong việc nạp chồng toán tử
hội thảo (“*”) để truy cập phần tử được liên kết với một chức vụ. Cho một biến vị trí p,
phần tử liên quan được truy cập bởi *p.Điều này có thể được sử dụng cho cả việc đọc và
sửa đổi giá trị của phần tử. Nó rất hữu ích để lưu trữ các bộ sưu tập các vị trí.
Ví dụ: các nút con của một nút trong cây có thể được hiển thị cho người dùng dưới dạng
danh sách như vậy. Ta xác định danh sách vị trí là một danh sách có các thành phần là
vị trí cây.
Sức mạnh thực sự của phát hiện cây trí tuệ từ khả năng tiếp cận các vị trí lân cận
các yếu tố của cây. Cho vị trí p của cây T, ta xác định như sau:
Nếu p là một nút bên ngoài thì p.children() trả về một giá trị trống danh sách.
- Hai hàm đầu tiên, size(kích thước) và empty(trống), chỉ là các hàm tiêu chuẩn mà
chúng ta đã xác định cho các loại vùng chứa khác mà chúng ta đã thấy.
- Hàm root mang lại vị trí của gốc và các vị trí tạo ra một danh sách chứa tất cả các
-root(nguồn gốc): Trả về vị trí cho gốc của cây, lỗi xảy ra nếu cây trống rỗng.
-positions(vị trí): Trả về danh sách vị trí của tất cả các nút của cây.
Thay vì, muốn mô tả các chức năng cập nhật cây khác nhau kết hợp với các ứng
dụng của cây ở các chương tiếp theo. Trên thực tế, chúng ta có thể tưởng tượng ra nhiều
loại các hoạt động cập nhật cây ngoài những hoạt động được đưa ra trong cuốn sách này.
7.1.3 giao diện cây C++
template <typename E> // loại phần tử cơ sở
};
Đoạn mã 7.1: Giao diện không chính thức cho một vị trí trong cây (không phải
class Tree<E> {
Đoạn mã 7.2: Giao diện không chính thức cho cây ADT (không phải là một lớp
hoàn chỉnh).
7.1.4 Cấu trúc liên kết cho cây thông thường
Chúng ta biểu diễn mỗi nút của T bởi một đối tượng vị trí p (xem Hình 7.5(a))
với các trường sau: một tham chiếu đến phần tử của nút, một liên kết tới nút cha của nút
và một số loại bộ sưu tập (ví dụ: danh sách hoặc mảng) để lưu trữ các liên kết đến nút con
của nút. Nếu p là gốc của T thì trường cha của p là NULL. T và số nút của T trong các
Hình 7.5: Cấu trúc liên kết của cây tổng quát: (a) cấu trúc nút; (b) cái phần cấu
trúc dữ liệu được liên kết với một nút và các nút con của nó.
Bảng 7.1 tóm tắt hiệu suất triển khai cấu trúc liên kết
của một cái cây. Phân tích này được để lại dưới dạng bài tập (C-7.27), nhưng chúng tôi
lưu ý rằng, bằng cách sử dụng container để lưu trữ các nút con của mỗi nút p, chúng ta có
thể triển khai các nút con (p) bằng cách sử dụng trình vòng lặp cho vùng chứa để liệt kê
tải trên một cây bằng cách truy cập nó thông qua các hàm ADT của cây.
Cho p là một nút của cây T. Độ sâu của p là số tổ tiên của p, không bao gồm chính p.Lưu
ý rằng định nghĩa này hàm ý rằng độ sâu của nghiệm của T là 0. Độ sâu của nút p cũng
Ngược lại, độ sâu của p bằng một cộng với độ sâu của phần tử cha của p
Dựa trên định nghĩa trên, độ sâu thuật toán đệ quy (T, p) được hiển thị trong đoạn mã 7.3,
tính toán độ sâu của nút được tham chiếu bởi vị trí p của T bằng gọi chính nó một cách đệ
quy trên phần tử cha của p và thêm 1 vào giá trị được trả về.
if (p.isRoot())
else
Ngược lại, chiều cao của p bằng một cộng với chiều cao tối đa của con của p
int h = 0;
if (q−>isExternal())
return h;
Ta thấy thuật toán chiều cao không hiệu quả lắm. Vì thuật toán gọi cao
độ sâu nhịp điệu (p) trên mỗi nút bên ngoài p của T, thời gian chạy của chiều cao1 được
tính bằng O(n+∑p(1+dp)), trong đó n là số nút của T, dp là độ sâu của nút p và E là tập
hợp các nút bên ngoài của T. Trong trường hợp xấu nhất, tổng sum ∑p(1+ dp) tỷ lệ thuận
Chiều cao thuật toán 2, được hiển thị trong Đoạn mã 7.8, tính toán chiều cao của
cây T theo cách hiệu quả hơn bằng cách sử dụng định nghĩa đệ quy về chiều cao.
int height2(const Tree& T, const Position& p) {
int h = 0;
Đoạn mã 7.8: Phương thức chiều cao 2 được viết bằng C++.
Thuật toán chiều cao 2 hiệu quả hơn chiều cao 1 (từ Đoạn mã 7.5).Thuật toán có
tính đệ quy và nếu ban đầu nó được gọi trên gốc của T, nó sẽ cuối cùng được gọi trên mỗi
nút của T. Do đó, chúng ta có thể xác định thời gian chạy của phương pháp này bằng
cách tính tổng, trên tất cả các nút, lượng thời gian dành cho mỗi nút (ở phần không đệ
quy). Xử lý từng nút ở trẻ em (p) mất O(cp) thời gian, trong đó cp biểu thị số nút con của
nút p. Ngoài ra, vòng lặp while có các lần lặp cp và mỗi lần lặp của vòng lặp mất O(1)
thời gian cộng với thời gian cho lệnh gọi đệ quy lên phần tử con của p. Do đó, thuật toán
có chiều cao 2 tiêu tốn thời gian O(1+ cp) tại mỗi nút p và thời gian chạy của nó là
O(∑p(1 + cp)). Để hoàn thành việc phân tích, ta sử dụng tính chất sau.
Mệnh đề 7.5: Cho T là một cây có n nút và cp là số lượng con của nút p của T.
Giải thích: Mỗi nút của T, ngoại trừ nút gốc, là con của một nút khác nút và do đó đóng
góp một đơn vị vào tổng trên. Theo Mệnh đề 7.5, thời gian chạy của thuật toán có độ cao
2, khi được gọi trên nghiệm của T, là O(n), trong đó n là số nút của T.
Duyệt cây T là một cách có hệ thống để truy cập hoặc “thăm” tất cả các nút của
T. Trong phần này, chúng tôi trình bày một sơ đồ duyệt cơ bản cho cây, được gọi là duyệt
theo thứ tự trước. Trong phần tiếp theo, chúng ta nghiên cứu một sơ đồ truyền tải cơ bản
Trong phép duyệt cây T theo thứ tự trước, gốc của T được thăm trước và sau đó
mới đến các cây con có gốc tại các cây con của nó được duyệt đệ quy.
Nếu cây được đặt hàng thì các cây con được duyệt theo thứ tự của các cây con.Hành
động cụ thể liên quan đến “lượt truy cập” của một nút phụ thuộc vào ứng dụng của quá
trình truyền tải này và có thể liên quan đến bất cứ điều gì từ việc tăng bộ đếm đến thực
hiện một số thao tác phức tạp tính toán cho nút này. Mã giả cho việc duyệt thứ tự trước
của cây con có gốc tại nút được tham chiếu bởi vị trí p được hiển thị trong Đoạn mã 7.9.
Ban đầu gọi thủ tục này bằng lệnh gọi trước (T,T.root()).
Đoạn mã 7.9: Thứ tự trước của thuật toán để thực hiện việc duyệt theo thứ tự
trước của cây con của cây T có gốc tại nút p.Thuật toán duyệt theo thứ tự trước rất hữu
ích trong việc tạo ra thứ tự tuyến tính của các nút của cây trong đó nút cha luôn phải đứng
trước con của họ trong thứ tự. Thứ tự như vậy có một số ứng dụng khác nhau. Chúng ta
khám phá một ví dụ đơn giản của ứng dụng như vậy trong ví dụ tiếp theo.
Hình 7.6: Duyệt cây theo thứ tự trước, trong đó các nút con của mỗi nút được sắp xếp từ
Việc duyệt theo thứ tự trước cũng là một cách hiệu quả để truy cập vào tất cả các
nút của cây. Để chứng minh điều này, chúng ta hãy xem xét thời gian chạy của quá trình
duyệt cây theo thứ tự trước T với n nút với giả định rằng việc truy cập một nút mất O(1)
thời gian. Các phân tích thuật toán duyệt theo thứ tự trước thực sự tương tự như thuật
toán chiều cao 2 (Đoạn mã 7.8), được nêu trong Phần 7.2.1. Tại mỗi nút p, điểm không
phần chữ thảo của thuật toán duyệt thứ tự trước yêu cầu thời gian O(1+ cp), trong đó cp
là số con của p. Do đó, theo Dự luật 7.5, tổng thời gian chạyviệc duyệt thứ tự trước của T
là O(n).
Thuật toán preorderPrint(T, p), được triển khai trong C++ trong Đoạn mã 7.10,
thực hiện việc in thứ tự trước cây con của nút p của T, nghĩa là nó thực hiện duyệt cây
con có gốc tại p và in ra phần tử được lưu trữ tại nút khi nút được truy cập. Hãy nhớ lại
rằng, đối với cây có thứ tự T, hàm T.children(p) trả về một trình vòng lặp truy cập các
phần tử con của p theo thứ tự. Chúng tôi giả định rằng Iterator đây là loại vòng lặp. Cho
preorderPrint(T, *q);
Đoạn mã 7.10: Phương thức preorderPrint(T, p) thực hiện việc in thứ tự trước
của các phần tử trong cây con tương ứng với vị trí p của T.
Có một biến thể thú vị của hàm preorderPrint tạo ra một biểu diễn khác nhau của toàn bộ
cây. Biểu diễn chuỗi ngoặc đơn P(T) của cây T được định nghĩa đệ quy như sau. Nếu T
bao gồm một nút duy nhất được tham chiếu bởi vị trí p, sau đó
P(T) = *p.
trong đó p là vị trí gốc của T và T1,T2,...,Tk là các cây con có gốc tại con của p, được sắp
Lưu ý rằng định nghĩa của P(T) là đệ quy. Ngoài ra, chúng tôi đang sử dụng “+”
ở đây để biểu thị nối chuỗi. (Nhắc lại kiểu chuỗi ở Phần1.1.3.)biểu diễn trong dấu ngoặc
đơn của cây Hình 7.2 được thể hiện trong Hình 7.7. Biểu diễn cây của Hình 7.2 được thể
R&D
Sales (
Domestic
International (
Canada
S.America
Purchasing
Manufacturing ( TV CD Tuner ) )
Hình 7.7: Biểu diễn trong ngoặc đơn của cây Hình 7.2. Thụt lề, dòng dấu ngắt và khoảng
Lưu ý rằng, về mặt kỹ thuật, có một số tính toán xảy ra giữa và sau các lệnh gọi
đệ quy tại các nút con trong thuật toán trên. Tuy nhiên, ta vẫn coi thuật toán này là truyền
tải theo thứ tự trước, vì thuật toán chính hành động in nội dung của nút xảy ra trước các
Hàm parenPrint trong C++, được trình bày trong Đoạn Mã 7.11, là một biến thể
của chức năng preorderPrint (Đoạn mã 7.10). Nó thực hiện định nghĩa được đưa ra ở trên
để xuất ra một biểu diễn chuỗi ngoặc đơn của cây T. Đầu tiên, nó in phần tử liên kết với
mỗi nút. Đối với mỗi nút bên trong, trước tiên chúng tôi in “(”, theo sau bằng cách biểu
diễn trong ngoặc đơn của mỗi phần tử con của nó, theo sau là “)”.
void parenPrint(const Tree& T, const Position& p) {
if (!p.isExternal()) {
này có thể được coi là đối lập với việc duyệt theo thứ tự trước, bởi vì nó đệ quy duyệt qua
các cây con có gốc tại các cây con trước tiên, sau đó mới đến thăm gốc.Tuy nhiên, nó
tương tự như việc duyệt thứ tự trước ở chỗ chúng ta sử dụng nó để giải quyết một vấn đề
cụ thể. vấn đề bằng cách chuyên biệt hóa một hành động liên quan đến “lượt truy cập”
của nút p.
Tuy nhiên, giống như việc duyệt theo thứ tự trước, nếu cây được sắp thứ tự,
chúng ta thực hiện các lệnh gọi đệ quy cho các nút con của nút p theo thứ tự được chỉ
định của chúng. Mã giả cho truyền tải sau thứ tự được đưa ra trong Đoạn mã 7.12.
Đoạn mã 7.12: Thuật toán postorder để thực hiện việc duyệt postorder cây con của cây T
Tên của quá trình duyệt thứ tự sau xuất phát từ thực tế là phương thức truyền tải
này truy cập vào nút p sau khi nó đã truy cập tất cả các nút khác trong cây con có gốc tại
Hình 7.8: Duyệt thứ tự sau của cây có thứ tự trong Hình 7.6.
Việc phân tích thời gian chạy của việc duyệt theo thứ tự sau cũng tương tự như
việc phân tích thời gian chạy của việc duyệt theo thứ tự trước. (Xem Phần 7.2.2.) Tổng
thời gian dành cho các phần không đệ quy của thuật toán tỷ lệ thuận với thời gian dành
cho việc truy cập các nút con của mỗi nút trong cây. Do đó, việc duyệt cây T với n nút sẽ
mất O(n) thời gian, giả sử rằng việc truy cập mỗi nút mất O(1) thời gian. Nghĩa là, quá
trình duyệt thứ tự sau chạy theo thời gian tuyến tính. Trong Đoạn mã 7.13, chúng tôi
trình bày một hàm C++ postorderPrint thực hiện việc duyệt thứ tự sau của cây T. Hàm
này in phần tử được lưu trữ tại một nút khi nó được truy cập.
postorderPrint(T, *q);
Đoạn mã 7.13: Hàm postorderPrint(T, p), in ra các phần tử của cây con ở vị trí p của T.
Phương pháp duyệt thứ tự sau rất hữu ích để giải quyết các bài toán trong đó
chúng ta muốn tính một số thuộc tính cho mỗi nút p trong cây, nhưng việc tính thuộc tính
đó cho p yêu cầu chúng ta phải tính cùng thuộc tính đó cho các nút con của p. Một ứng
Ví dụ 7.7: Xét cây hệ thống tệp T, trong đó các nút bên ngoài biểu thị các tệp và
các nút bên trong biểu thị các thư mục (Ví dụ 7.1). Giả sử chúng ta muốn tính toán dung
lượng ổ đĩa được sử dụng bởi một thư mục, được tính đệ quy bằng tổng của các giá trị
tệp/thư mục được liên kết bên trong mỗi nút và không gian đĩa được sử dụng bởi thư mục
Sau khi duyệt qua các cây con của nút bên trong p, chúng ta tính toán không
gian được sử dụng bởi p bằng cách cộng các kích thước của chính thư mục p và của các
tệp chứa trong p, để không gian được sử dụng bởi mỗi phần tử con bên trong của p, được
tính bằng cách duyệt thứ tự hậu đệ quy của các phần tử con của p.
Thuật toán diskSpace, được trình bày trong Đoạn Mã 7.14, thực hiện duyệt theo
thứ tự sau của cây hệ thống tệp T, in tên và không gian đĩa được sử dụng bởi thư mục liên
kết với mỗi nút bên trong của T. Khi được gọi trên gốc của cây T, diskSpace chạy trong
thời gian O(n), trong đó n là số nút của cây, với điều kiện là các hàm phụ trợ name(p) và
cout << name(p) << ": " << s << endl; // in tóm tắt
return s;
Đoạn mã 7.14: Hàm diskSpace, in tên và dung lượng ổ đĩa được thư mục liên kết
với p sử dụng, cho mỗi nút bên trong p của cây hệ thống tệp T. Hàm này gọi tên và kích
thước của các hàm phụ trợ, cần được xác định để trả về tên và kích thước của tệp/thư mục
Truyền tải theo thứ tự trước rất hữu ích khi chúng ta muốn thực hiện một hành
động cho một nút và sau đó thực hiện đệ quy hành động đó cho các nút con của nó, và
truyền tải theo thứ tự sau rất hữu ích khi chúng ta muốn thực hiện một hành động trước
tiên trên các nút con của một nút và sau đó thực hiện hành động đó trên nút con của nút.
Mặc dù việc duyệt thứ tự trước và thứ tự sau là những cách phổ biến để truy cập
các nút của cây, chúng ta cũng có thể hình dung ra các cách duyệt khác. Ví dụ: chúng ta
có thể duyệt một cây để truy cập tất cả các nút ở độ sâu d trước khi truy cập các nút ở độ
sâu d+1. Việc duyệt như vậy, được gọi là duyệt theo chiều rộng, có thể được triển khai
bằng cách sử dụng hàng đợi, trong khi việc duyệt thứ tự trước và thứ tự sau sử dụng ngăn
xếp. (Ngăn xếp này tiềm ẩn trong việc chúng ta sử dụng đệ quy để mô tả các hàm này,
nhưng chúng ta cũng có thể làm cho việc sử dụng này trở nên rõ ràng để tránh đệ quy.)
Ngoài ra, cây nhị phân mà chúng ta sẽ thảo luận tiếp theo, hỗ trợ một phương pháp truyền
2. Mỗi nút con được gắn nhãn là nút con trái hoặc nút con phải.
3. Con trái đứng trước con phải theo thứ tự các con của một nút.
Cây con có gốc ở nút con trái hoặc phải của nút bên trong được gọi tương ứng là
cây con trái hoặc cây con phải của nút đó. Cây nhị phân là phù hợp nếu mỗi nút có 0
hoặc 2 nút con. Một số người cũng coi những cây như vậy là cây nhị phân đầy đủ. Vì
vậy, trong một cây nhị phân thích hợp, mỗi nút bên trong có đúng hai nút con. Cây nhị
Ví dụ 7.8: Một lớp cây nhị phân quan trọng xuất hiện trong các bối cảnh mà
chúng ta muốn biểu diễn một số kết quả khác nhau có thể xảy ra từ việc trả lời một loạt
câu hỏi có hoặc không. Mỗi nút nội bộ được liên kết với một câu hỏi Bắt đầu từ gốc,
chúng ta đi đến nút con trái hoặc phải của nút hiện tại, tùy thuộc vào câu trả lời cho câu
hỏi là “Có” hay “Không”. Với mỗi quyết định, chúng ta đi theo một cạnh từ nút cha đến
nút con, cuối cùng dò tìm một đường dẫn trong cây từ gốc đến nút bên ngoài. Những cây
nhị phân như vậy được gọi là cây quyết định, bởi vì mỗi nút p bên ngoài trong cây như
vậy thể hiện một quyết định phải làm gì nếu các câu hỏi liên quan đến tổ tiên của p được
trả lời theo cách dẫn đến p. Cây quyết định là cây nhị phân thích hợp.
Hình 7.10 minh họa cây quyết định cung cấp các khuyến nghị cho nhà đầu tư tiềm năng.
Hình 7.10: Cây quyết định cung cấp lời khuyên đầu tư.
Ví dụ 7.9: Một biểu thức số học có thể được biểu diễn bằng một cây có các nút
bên ngoài được liên kết với các biến hoặc hằng và các nút bên trong của nó được liên kết
với một trong các toán tử +, −, × và /. (Xem Hình 7.11.) Mỗi nút trong cây như vậy có
• Nếu một nút là bên ngoài thì giá trị của nó là biến hoặc hằng.
• Nếu một nút là bên trong thì giá trị của nó được xác định bằng cách áp dụng phép toán
của nó cho các giá trị của các nút con của nó.
Cây biểu thức số học như vậy là cây nhị phân thực sự, vì mỗi toán tử +, −, × và /
lấy chính xác hai toán hạng. Tất nhiên, nếu chúng ta cho phép các toán tử một ngôi, như
phủ định (-), như trong “−x,” thì chúng ta có thể có một cây nhị phân không đúng.
Hình 7.11: Cây nhị phân biểu diễn một biểu thức số học. Cây này biểu thị biểu
thức ((((3+1)×3)/((9−5)+2))−((3×(7−4))+6)). Giá trị được liên kết với nút bên trong có
nhãn “/” là 2.
Ngẫu nhiên, chúng ta cũng có thể định nghĩa cây nhị phân theo cách đệ quy sao cho cây
Giống như ADT cây trước đây của chúng ta, mỗi nút của cây lưu trữ một phần tử và được
liên kết với đối tượng vị trí, cung cấp quyền truy cập công cộng vào các nút. Bằng cách
nạp chồng toán tử tham chiếu, phần tử được liên kết với vị trí p có thể được truy cập bởi
*p. Ngoài ra, vị trí p còn hỗ trợ các thao tác sau.
p.left(): Trả về con trái của p; một tình trạng lỗi xảy ra nếu p là một nút bên
ngoài.
p.right(): Trả về con phải của p; một tình trạng lỗi xảy ra nếu p là một nút bên
ngoài.
Cây cũng cung cấp các hoạt động tương tự như cây tiêu chuẩn ADT. Như size(),
đầu trong Đoạn mã 7.15 bằng cách trình bày một giao diện C++ không chính thức cho
lớp Vị trí, đại diện cho một vị trí trong cây. Nó khác với giao diện cây ở Phần 7.1.3 ở chỗ
thay thế hàm thành viên cây con bằng hai hàm left và right.
public:
};
Đoạn mã 7.15: Giao diện không chính thức cho cây nhị phân ADT (không phải là lớp C+
+ hoàn chỉnh).
Tiếp theo, trong Đoạn mã 7.16, trình bày giao diện C++ không chính thức cho
cây nhị phân. Để giữ cho giao diện đơn giản nhất có thể, ta bỏ qua việc xử lý lỗi và do đó
};
Cây nhị phân có một số thuộc tính thú vị liên quan đến mối quan hệ giữa chiều
cao và số nút của chúng. Chúng ta biểu thị tập hợp tất cả các nút của cây T, ở cùng độ sâu
d, là mức d của T. Trong cây nhị phân, mức 0 có một nút (gốc), cấp 1 có nhiều nhất hai
nút (con của nút gốc), cấp 2 có nhiều nhất là bốn nút, v.v. (Xem Hình 7.12.) Nói chung,
Mệnh đề 7.10: Cho T là cây nhị phân khác rỗng và gọi n, nE, nI và h lần lượt là số
nút, số nút ngoài, số nút trong và chiều cao của T. Khi đó T có các tính chất sau:
1. h+1 ≤ n ≤ 2h+1 −1
2. 1 ≤ nE ≤ 2h
3. h ≤ nI ≤ 2h −1
4. log(n+1)−1 ≤ h ≤ n−1
1. 2h+1 ≤ n ≤ 2h+1 −1
2. h+1 ≤ nE ≤ 2h
3. h ≤ nI ≤ 2h −1
4. log(n+1)−1 ≤ h ≤ (n−1)/2
Hình 7.12: Số nút tối đa ở các cấp của cây nhị phân.
Ngoài các thuộc tính của cây nhị phân ở trên, chúng ta còn có mối quan hệ sau
đây giữa số lượng nút bên trong và nút bên ngoài trong một cây nhị phân thích hợp.
Mệnh đề 7.11: Trong cây nhị phân T khác rỗng, số nút bên ngoài nhiều hơn số nút bên
trong một.
Giải thích: Chúng ta có thể thấy điều này bằng cách sử dụng lập luận dựa trên quy nạp.
Nếu cây bao gồm một nút gốc duy nhất thì rõ ràng chúng ta có một nút bên ngoài và
không có nút bên trong nào, do đó mệnh đề đúng. Mặt khác, nếu chúng ta có hai hoặc
nhiều hơn thì gốc có hai cây con. Vì các cây con nhỏ hơn cây ban đầu nên chúng ta có thể
giả định rằng chúng thỏa mãn mệnh đề. Như vậy, mỗi cây con có nhiều hơn một nút bên
ngoài so với các nút bên trong. Giữa hai nút này có nhiều nút bên ngoài hơn hai nút bên
trong. Tuy nhiên, gốc của cây là một nút bên trong. Khi chúng ta xem xét gốc và cả hai
cây con cùng nhau, sự khác biệt giữa số lượng nút bên ngoài và bên trong là 2−1 = 1, đây
Lưu ý rằng mối quan hệ trên không đúng, nói chung, đối với cây nhị phân và cây
không nhị phân không đúng, mặc dù có những mối quan hệ thú vị khác có thể xảy ra khi
Trong phần này, cách triển khai cây nhị phân T dưới dạng cấu trúc được liên kết,
được gọi là LinkedBinaryTree. Chúng ta biểu diễn mỗi nút v của T bằng một đối tượng
nút lưu trữ phần tử liên quan và các con trỏ tới nút cha và hai nút con của nó. (Xem Hình
7.13.) Để đơn giản, chúng ta giả sử cây là đúng, nghĩa là mỗi nút có 0 hoặc 2 nút con.
Hình 7.13: Một nút trong cấu trúc dữ liệu được liên kết để biểu diễn cây nhị phân.
Trong Hình 7.14, chúng tôi trình bày biểu diễn cấu trúc liên kết của cây nhị phân. Cấu
trúc lưu trữ kích thước của cây, nghĩa là số lượng nút trong cây và một con trỏ tới gốc của
cây. Phần còn lại của cấu trúc bao gồm các nút được liên kết với nhau một cách thích
hợp. Nếu v là nút gốc của T thì con trỏ tới nút cha là NULL và nếu v là nút bên ngoài thì
Hình 7.14: Một ví dụ về cấu trúc dữ liệu được liên kết để biểu diễn cây nhị phân.
Chúng ta bắt đầu bằng việc xác định các thành phần cơ bản tạo nên lớp
LinkedBinaryTree. Thực thể cơ bản nhất là cấu trúc Nút, được hiển thị trong Đoạn mã
};
Đoạn mã 7.17: Nút cấu trúc thực hiện một nút của cây nhị phân. Nó được lồng trong
private:
public:
{ return v−>elt; }
{ return Position(v−>left); }
{ return Position(v−>right); }
{ return Position(v−>par); }
};
Đoạn Mã 7.18: Lớp Vị trí thực hiện một vị trí trong cây nhị phân. Nó được lồng trong
phần công khai của lớp Cây nhị phân được liên kết.
Hầu hết các hàm của lớp Vị trí chỉ đơn giản liên quan đến việc truy cập các
thành viên thích hợp của cấu trúc Nút. Chúng tôi cũng đã bao gồm một khai báo về lớp
Danh sách vị trí, dưới dạng danh sách các vị trí STL. Danh sách này được sử dụng để thể
hiện các tập hợp các nút. Để giữ cho mã đơn giản, chúng tôi đã bỏ qua việc kiểm tra lỗi
và thay vì sử dụng các mẫu, chúng tôi chỉ cung cấp định nghĩa kiểu cho loại phần tử cơ
class LinkedBinaryTree {
protected:
public:
public:
LinkedBinaryTree(); // constructor
private:
};
Đoạn mã 7.19: Triển khai lớp Cây nhị phân được liên kết.
Dữ liệu riêng cho lớp LinkedBinaryTree bao gồm một con trỏ gốc tới nút gốc và
một biến n, chứa số lượng nút trong cây.Ngoài các chức năng của ADT, còn có một số
hàm cập nhật, addRoot, ExpandExternal và RemoveAboveExternal, cung cấp các phương
LinkedBinaryTree::LinkedBinaryTree() // constructor
:_root(NULL), n(0) { }
{ return n; }
{ return size() == 0; }
Đoạn mã 7.20: Các hàm thành viên đơn giản cho lớp LinkedBinaryTree.
Ngoài các hàm giao diện BinaryTree và addRoot, lớp LinkedBi-naryTree còn bao gồm
các hàm cập nhật sau đây với vị trí p. Cái đầu tiên được sử dụng để thêm các nút vào cây
bằng cách tạo hai nút bên ngoài mới và biến chúng lần lượt là con trái và con phải của p;
một điều kiện lỗi xảy ra nếu p là một nút bên trong.
RemoveAboveExternal(p): Loại bỏ nút bên ngoài p cùng với nút cha q của nó,
thay thế q bằng nút anh em của p (xem Hình 7.15, trong đó nút của p là w và nút của q là
v); tình trạng lỗi xảy ra nếu p là nút bên trong hoặc p là nút gốc.
của p, nó tạo ra hai nút mới. Một đứa trở thành con trái của v và đứa kia trở thành con
phải của v. Hàm tạo của Node khởi tạo các con trỏ của nút thành NULL, vì vậy chúng ta
chỉ cần cập nhật các liên kết chính của nút mới.
Phân tích thời gian chạy của các hàm của lớp LinkedBinaryTree, lớp này sử
• Mỗi hàm vị trí trái, phải, cha, isRoot và isExternal mất O(1) thời gian.
• Bằng cách truy cập vào biến thành viên n, biến này lưu trữ số lượng nút của T, kích
thước hàm và làm trống mỗi lần chạy trong thời gian O(1).
• Các hàm cập nhật ExpandExternal và RemoveAboveExternal chỉ truy cập một số nút
không đổi, vì vậy cả hai đều chạy trong thời gian O(1).
• Vị trí hàm được thực hiện bằng cách thực hiện duyệt theo thứ tự trước, mất O(n) thời
gian. Các nút được duyệt qua cây nhị phân được thêm vào danh sách STL trong thời gian
7.3.5 Cấu trúc dựa trên vectơ cho cây nhị phân
Cấu trúc đơn giản để biểu diễn cây nhị phân T dựa trên cách đánh số các nút của
T. Với mọi nút v của T, cho f(v) là số nguyên được xác định như sau:
• Nếu v là nút con bên phải của nút u thì f(v) = 2 f(u) +1
Hình 7.17: Biểu diễn cây nhị phân T bằng vectơ S.
Bảng 7.3: Thời gian chạy của cây nhị phân T được triển khai bằng vectơ S.
Chúng ta biểu thị số nút của T bằng n và N biểu thị kích thước của S. Khoảng cách
sử dụng là O(N), tức là O(2n) trong trường hợp xấu nhất.
Hình 7.19: Cây tìm kiếm nhị phân lưu trữ các số nguyên. Đường nét đứt màu xanh
lam được đi ngang khi tìm kiếm (thành công) cho 36. Đường nét đứt màu xanh lam được
đi ngang khi tìm kiếm (không thành công) cho 70.
Sử dụng phương pháp truyền tải thứ tự để vẽ cây
Việc duyệt theo thứ tự cũng có thể được áp dụng cho bài toán tính toán bản vẽ cây
nhị phân. Chúng ta có thể vẽ cây nhị phân T bằng thuật toán gán tọa độ x và y cho nút p
của T bằng hai quy tắc sau (xem Hình 7.20).
• x(p) là số nút được truy cập trước p trong lần duyệt thứ tự của T.
• y(p) là độ sâu của p trong T.
Hình 7.22: Biểu diễn cây bằng cây nhị phân: (a) cây T; (b) cây nhị phân T’ liên kết
với T. Các cạnh nét đứt kết nối các nút của T’ liên kết với các nút anh em của T.
Dễ dàng duy trì sự tương ứng giữa T và T', và biểu diễn các phép toán trong T
dưới dạng các phép toán tương ứng trong T'. Một cách trực quan, chúng ta có thể nghĩ về
sự tương ứng dưới dạng chuyển đổi T thành T′ lấy mỗi tập hợp anh chị em {v1,v2,...,vk}
trong T với cha mẹ v và thay thế nó bằng một chuỗi con bên phải có gốc tại v1, sau đó trở
thành con trái của v.