Cây K_phân là một dạng cấu trúc cây mà mỗi nút trên cây có tối đa K nút con (có tính đến thứ tự của các nút con).
1. Biểu diễn cây K_phân bằng mảng
Cũng tương tự như việc biểu diễn cây nhị phân, người ta có thể thêm vào cây K_phân một số nút giả để cho mỗi nút nhánh của cây K_phân đều có đúng K nút con, các nút con được xếp thứ tự từ nút con thứ nhất tới nút con thứ K, sau đó đánh số các nút trên cây K_phân bắt đầu từ 0 trở đi, bắt đầu từ mức 1, hết mức này đến mức khác và từ "trái qua phải" ở mỗi mức:
A B F J C D E M K L I G H 0 1 2 3 4 5 6 7 8 9 10 11 12
Theo cách đánh số này, nút con thứ j của nút i là: i * K + j. Nút cha của nút x là nút (x - 1) div K. Ta có thể dùng một mảng T đánh số từ 0 để lưu các giá trị trên các nút: Giá trị tại nút thứ i được lưu trữ ở phần tử T[i].
A B F J C D E G H I K L M
0 1 2 3 4 5 6 7 8 9 10 11 12
2. Biểu diễn cây K_phân bằng cấu trúc liên kết
Khi biểu diễn cây K_phân bằng cấu trúc liên kết, mỗi nút của cây là một bản ghi (record) gồm hai trường:
• Trường Info: Chứa giá trị lưu trong nút đó.
• Trường Links: Là một mảng gồm K phần tử, phần tử thứ i chứa liên kết (con trỏ) tới nút con thứ i, trong trường hợp không có nút con thứ i thì Links[i] được gán một giá trị đặc biệt.
Đối với cây K_ phân, ta cũng chỉ cần giữ lại nút gốc, bởi từ nút gốc, đi theo các hướng liên kết có thể đi tới mọi nút khác.
VỊ CÂY TỔNG QUÁT
Trong thực tế, có một số ứng dụng đòi hỏi một cấu trúc dữ liệu dạng cây nhưng không có ràng buộc gì về số con của một nút trên cây, ví dụ như cấu trúc thư mục trên đĩa hay hệ thống đề mục của một cuốn sách. Khi đó, ta phải tìm cách mô tả một cách khoa học cấu trúc dữ liệu dạng cây tổng quát. Cũng như trường hợp cây nhị phân, người ta thường biểu diễn cây tổng quát bằng hai cách: Lưu trữ kế tiếp bằng mảng và lưu trữ bằng cấu trúc liên kết.
1. Lưu trữ cây tổng quát bằng mảng
Để lưu trữ cây tổng quát bằng mảng, trước hết, ta đánh số các nút trên cây bắt đầu từ 1 theo một thứ tự tuỳ ý. Giả sử cây có n nút thì ta sử dụng:
• Một mảng Info[1..n], trong đó Info[i] là giá trị lưu trong nút thứ ị
• Một mảng Children được chia làm n đoạn, đoạn thứ i gồm một dãy liên tiếp các phần tử là chỉ số các nút con của nút ị Như vậy mảng Children sẽ chứa tất cả chỉ số của mọi nút con trên cây (ngoại trừ nút gốc) nên nó sẽ gồm n - 1 phần tử, lưu ý rằng khi chia mảng Children làm n đoạn thì sẽ có những đoạn rỗng (tương ứng với danh sách các nút con của một nút lá)
• Một mảng Head[1..n + 1], để đánh dấu vị trí cắt đoạn trong mảng Children: Head[i] là vị trí đầu đoạn thứ i, hay nói chính xác hơn: Các phần tử trong mảng Children từ vị trí Head[i] đến Head[i+1] - 1 là chỉ số các nút con của nút thứ ị Khi Head[i] = Head[i+1] có nghĩa là đoạn thứ i rỗng. Quy ước: Head[n+1] = n.
• Giữ lại chỉ số của nút gốc. Ví dụ: Với cây dưới đâỵ
A B F J C D E M K L G H 9 1 2 4 3 5 6 7 8 10 11 12 Mảng Info: Info[i] B F C J D E G H A K L M
i 1 2 3 4 5 6 7 8 9 10 11 12
Mảng Children:
Children[i] 3 5 6 7 8 10 11 12 1 2 4
i 1 2 3 4 5 6 7 8 9 10 11
Đoạn 1 Đoạn 2 Đoạn 4 Đoạn 9
(Các đoạn 3, 5, 6, 7, 8, 10, 11, 12 là rỗng)
Mảng Head:
Head[i] 1 4 6 6 9 9 9 9 9 12 12 12 12
i 1 2 3 4 5 6 7 8 9 10 11 12 13
2. Lưu trữ cây tổng quát bằng cấu trúc liên kết
Khi lưu trữ cây tổng quát bằng cấu trúc liên kết, mỗi nút là một bản ghi (record) gồm ba trường: • Trường Info: Chứa giá trị lưu trong nút đó.
• Trường FirstChild: Chứa liên kết (con trỏ) tới nút con đầu tiên của nút đó (con cả), trong trường hợp là nút lá (không có nút con), trường này được gán một giá trị đặc biệt.
• Trường Sibling: Chứa liên kết (con trỏ) tới nút em kế cận bên phải (nút cùng cha với nút đang xét, khi sắp thứ tự các con thì nút đó đứng liền sau nút đang xét). Trong trường hợp không có nút em kế cận bên phải, trường này được gán một giá trị đặc biệt.
Dễ thấy được tính đúng đắn của phương pháp biểu diễn, bởi từ một nút N bất kỳ, ta có thể đi theo liên kết FirstChild để đến nút con cả, nút này chính là chốt của một danh sách nối đơn các nút con của nút N: từ nút con cả, đi theo liên kết Sibling, ta có thể duyệt tất cả các nút con của nút N.
Bài tập
1. Viết chương trình mô tả cây nhị phân dùng cấu trúc liên kết, mỗi nút chứa một số nguyên, và viết các thủ tục duyệt trước, giữa, saụ
2. Chứng minh rằng nếu cây nhị phân có x nút lá và y nút cấp 2 thì x = y + 1
3. Chứng minh rằng nếu ta biết dãy các nút được thăm của một cây nhị phân khi duyệt theo thứ tự trước và thứ tự giữa thì có thể dựng được cây nhị phân đó. Điều này con đúng nữa không đối với thứ tự trước và thứ tự saủ Với thứ tự giữa và thứ tự saụ
§6. KÝ PHÁP TIỀN TỐ, TRUNG TỐ VÀ HẬU TỐ
Ị BIỂU THỨC DƯỚI DẠNG CÂY NHỊ PHÂN
Chúng ta có thể biểu diễn các biểu thức số học gồm các phép toán cộng, trừ, nhân, chia bằng một cây nhị phân, trong đó các nút lá biểu thị các hằng hay các biến (các toán hạng), các nút không phải là lá biểu thị các toán tử (phép toán số học chẳng hạn). Mỗi phép toán trong một nút sẽ tác động lên hai biểu thức con nằm ở cây con bên trái và cây con bên phải của nút đó. Ví dụ: Cây biểu diễn biểu thức (6 / 2 + 3) * (7 - 4) * L R + L R - L R / L R 3 L R 7 L R 4 L R 6 L R 2 L R
IỊ CÁC KÝ PHÁP CHO CÙNG MỘT BIỂU THỨC
Với cây nhị phân biểu diễn biểu thức,
• Nếu ta duyệt theo thứ tự trước, ta sẽ được dạng tiền tố (prefix) của biểu thức: * + / 6 2 3 - 7 4. Trong ký pháp này, toán tử được viết trước hai toán hạng tương ứng, người ta còn gọi ký pháp này là ký pháp Ba lan.
• Nếu ta duyệt theo thứ tự giữa, ta sẽ được: 6 / 2 + 3 * 7 - 4. Ký pháp này hơi mập mờ vì thiếu dấu ngoặc. Nếu ta thêm vào thủ tục duyệt inorder việc bổ sung các cặp dấu ngoặc vào mỗi biểu thức con thì ta sẽ được biểu thức (((6 / 2) + 3) * (7 - 4)). Ký pháp này gọi là dạng trung tố
(infix) của một biểu thức
• Nếu ta duyệt theo thứ tự sau, ta sẽ được dạng hậu tố (postfix) của biểu thức 6 2 / 3 + 7 4 - *. Trong ký pháp này toán tử được viết sau hai toán hạng, người ta còn gọi ký pháp này là ký pháp nghịch đảo Balan (Reverse Polish Notation - RPN)
• Chỉ có dạng trung tố mới cần có dấu ngoặc, dạng tiền tố và hậu tố không cần phải có dấu ngoặc.
IIỊ CÁCH TÍNH GIÁ TRỊ BIỂU THỨC
Có một vấn đề cần lưu ý là khi máy tính giá trị một biểu thức số học gồm các toán tử hai ngôi (toán tử gồm hai toán hạng như +, -, *, /) thì máy chỉ thực hiện được phép toán đó với hai toán hạng, nếu biểu thức phức tạp thì máy phải chia nhỏ và tính riêng từng biểu thức trung gian, sau đó mới lấy giá trị tìm được để tính tiếp. Ví dụ như biểu thức 1 + 2 + 4 máy sẽ phải tính 1 + 2 trước được kết quả là 3 sau đó mới đem 3 cộng với 4 chứ không thể thực hiện phép cộng một lúc ba số được.
Khi lưu trữ biểu thức dưới dạng cây nhị phân thì ta có thể coi mỗi nhánh con của cây đó mô tả một biểu thức trung gian mà máy cần tính khi xử lý biểu thức lớn. Như ví dụ trên, máy sẽ phải
tính hai biểu thức 6 / 2 + 3 và 7 - 4 trước khi làm phép tính nhân cuối cùng. Để tính biểu thức 6 / 2 + 3 thì máy lại phải tính biểu thức 6 / 2 trước khi đem cộng với 3.
Vậy để tính một biểu thức lưu trữ trong một nhánh cây nhị phân gốc ở nút n, máy sẽ tính gần giống như hàm đệ quy sau:
function Calculate(n): Value; {Tính biểu thức con trong nhánh cây gốc n}
begin
if <Nút n chứa không phải là một toán tử> then Calculate := <Giá trị chứa trong nút n>
else {Nút n chứa một toán tử R}
begin
x := Calculate(nút con trái của n); y := Calculate(nút con phải của n); Calculate := x R y;
end; end.
(Trong trường hợp lập trình trên các hệ thống song song, việc tính giá trị biểu thức ở cây con trái và cây con phải có thể tiến hành đồng thời làm giảm đáng kể thời gian tính toán biểu thức).
Để ý rằng khi tính toán biểu thức, máy sẽ phải quan tâm tới việc tính biểu thức ở hai nhánh con trước, rồi mới xét đến toán tử ở nút gốc. Điều đó làm ta nghĩ tới phép duyệt hậu thứ tự và ký pháp hậu tố. Trong những năm đầu 1950, nhà lô-gic học người Balan Jan Lukasiewicz đã chứng minh rằng biểu thức hậu tố không cần phải có dấu ngoặc vẫn có thể tính được một cách đúng đắn bằng cách đọc lần lượt biểu thức từ trái qua phải và dùng một Stack để lưu các kết quả trung gian:
Bước 1: Khởi động một Stack rỗng
Bước 2: Đọc lần lượt các phần tử của biểu thức RPN từ trái qua phải (phần tử này có thể là hằng, biến hay toán tử) với mỗi phần tử đó, ta kiểm tra:
• Nếu phần tử này là một toán hạng thì đẩy giá trị của nó vào Stack.
• Nếu phần tử này là một toán tử, ta lấy từ Stack ra hai giá trị (y và x) sau đó áp dụng toán tử (R) đó vào hai giá trị vừa lấy ra, đẩy kết quả tìm được (x R y) vào Stack (ra hai vào một).
Bước 3: Sau khi kết thúc bước 2 thì toàn bộ biểu thức đã được đọc xong, trong Stack chỉ còn duy nhất một phần tử, phần tử đó chính là giá trị của biểu thức.
Ví dụ: Tính biểu thức 10 2 / 3 + 7 4 - * (tương ứng với biểu thức (10 / 2 + 3) * (7 - 4)
Đọc Xử lý Stack
10 Đẩy vào Stack 10
2 Đẩy vào Stack 10, 2
/ Lấy 2 và 10 khỏi Stack, Tính được 10 / 2 = 5, đẩy 5 vào Stack 5
3 Đẩy vào Stack 5, 3
+ Lấy 3 và 5 khỏi Stack, tính được 5 + 3 = 8, đẩy 8 vào Stack 8
7 Đẩy vào Stack 8, 7
4 Đẩy vào Stack 8, 7, 4
- Lấy 4 và 7 khỏi Stack, tính được 7 - 4 = 3, đẩy 3 vào Stack 8, 3 * Lấy 3 và 8 khỏi Stack, tính được 8 * 3 = 24, đẩy 24 vào Stack 24
Ta được kết quả là 24
Dưới đây ta sẽ viết một chương trình đơn giản tính giá trị biểu thức RPN. Chương trình sẽ nhận Input là biểu thức RPN gồm các số thực và các toán tử + - * / và cho Output là kết quả biểu thức đó. Quy định khuôn dạng bắt buộc là hai số liền nhau trong biểu thức RPN phải viết cách nhau ít nhất một dấu cách. Để quá trình đọc một phần tử trong biểu thức RPN được dễ dàng hơn, sau bước nhập liệu, ta có thể hiệu chỉnh đôi chút biểu thức RPN về khuôn dạng dễ đọc nhất. Chẳng hạn như thêm và bớt một số dấu cách trong Input để mỗi phần tử (toán hạng, toán tử) đều cách nhau đúng một dấu
cách, thêm một dấu cách vào cuối biểu thức RPN. Khi đó quá trình đọc lần lượt các phần tử trong biểu thức RPN có thể làm như sau:
T := '';
for p := 1 to Length(RPN) do {Xét các ký tự trong biểu thức RPN từ trái qua phải}
if RPN[p] ≠ ' ' then T := T + RPN[p] {Nếu RPN[p] không phải dấu cách thì nối ký tự đó vào T}
else {Nếu RPN[p] là dấu cách thì phần tử đang đọc đã đọc xong, tiếp theo sẽ là phần tử khác}
begin
<Xử lý phần tử T>;
T := ''; {Chuẩn bị đọc phần tử mới}
end;
Để đơn giản, chương trình không kiểm tra lỗi viết sai biểu thức RPN, việc đó chỉ là thao tác tỉ mỉ chứ không phức tạp lắm, chỉ cần xem lại thuật toán và cài thêm các mô-đun bắt lỗi tại mỗi bước.
Ví dụ về Input / Output của chương trình:
Enter RPN Expression: 10 2/3 + 4 7 -* 10 2 / 3 + 4 7 - * = 24.0000 {$N+,E+} program CalculateRPNExpression; const Opt = ['+', '-', '*', '/']; var T, RPN: String;
Stack: array[1..255] of Extended; p, Last: Integer;
{- - - Các thao tác đối với Stack - - - }
procedure StackInit; begin
Last := 0; end;
procedure Push(V: Extended); begin
Inc(Last); Stack[Last] := V; end;
function Pop: Extended; begin
Pop := Stack[Last]; Dec(Last); end;
{- - - -- - - - - - - - - - -- - - -}
procedure Refine(var S: String); {Hiệu chỉnh biểu thức RPN về khuôn dạng dễ đọc nhất}
var
i: Integer; begin
S := S + ' ';
for i := Length(S) - 1 downto 1 do {Thêm những dấu cách giữa toán hạng và toán tử}
if (S[i] in Opt) or (S[i + 1] in Opt) then Insert(' ', S, i + 1);
for i := Length(S) - 1 downto 1 do {Xoá những dấu cách thừa}
if (S[i] = ' ') and (S[i + 1] = ' ') then Delete(S, i + 1, 1); end;
procedure Process(T: String); {Xử lý phần tử T đọc được từ biểu thức RPN}
var
x, y: Extended; e: Integer;
begin
if not (T[1] in Opt) then {T là toán hạng}
begin
Val(T, x, e); Push(x); {Đổi T thành số và đẩy giá trị đó vào Stack}
end
else {T là toán tử}
begin
y := Pop; x := Pop; {Ra hai}
case T[1] of '+': x := x + y; '-': x := x - y; '*': x := x * y; '/': x := x / y; end; Push(x); {Vào một} end; end; begin
Write('Enter RPN Expression: '); Readln(RPN); Refine(RPN);
StackInit; T := '';
for p := 1 to Length(RPN) do {Xét các ký tự của biểu thức RPN từ trái qua phải}
if RPN[p] <> ' ' then T := T + RPN[p] {nếu không phải dấu cách thì nối nó vào sau xâu T}
else {Nếu gặp dấu cách}
begin
Process(T); {Xử lý phần tử vừa đọc xong}
T := ''; {Đặt lại T để chuẩn bị đọc phần tử mới}
end;
Writeln(RPN, ' = ', Pop:0:4); {In giá trị biểu thức RPN được lưu trong Stack}
end.