- Cây đầy đủ: Cây m phâ nT được gọi là cây m phân đầy đủ nếu mỗi đỉnh trong đều có đúng m con.
R a: Cây nhị phân biểu diễn mã, nhánh phải là 1, trái là 0.
0 0 0 0 0 0 1 1 1 1 1 1 1 1 a e i k o p u
Bước 1: Lấy hai phần tử cuối bảng tần số xuất hiện ra khỏi bảng
Bước 2: Nếu phần tử nào chưa nằm trong cây nhị phân thì tạo ra một nút lá chứa phần tử đó, phần tử này chính là ký tự. Nối hai nút tương ứng với hai phần tử này với nhau thông qua việc tạo nút cha của chúng. Phần tử có tần số xuất hiện lớn hơn là nút trái, nhỏ hơn là nút phải.
Bước 3: Tính tổng tần số xuất hiện của 2 phần tử này chèn vào bảng sao cho phù hợp với nguyên tắc giảm dần của bảng. Phần tử mới của bảng sẽ tương ứng với nút vừa được tạo ra ở bước 2.
Bước 4: Quay trở lại bước 1 đến khi bảng chỉ còn lại 1 phần tử. Phần tử cuối cùng tương ứng với nút gốc của cây nhị phân.
Ví dụ: Ta có kết quả mã Huffman cho các ký tự ở bảng sau:
Ký tự Tần suất Mã nhị phân Chiều dài mã
a 0.3 00 2 b 0.2 10 2 c 0.2 11 2 d 0.1 011 3 e 0.1 0100 4 f 0.1 0101 4
Cây nhị phân biểu diễn như hình 1.3
Hình 1.3 59 a,e,f,d,b,c a,e,f,d b,c e,f,d a e,f d e f b c 0 0 0 0 0 1 1 1 1 1 0,3 0,6 0,2 0,1 0,1 0,1 0,3 0,4 0,2 0,2
Với thuật toán Huffman trường hợp xấu nhất là thời gian hình thành cây nhị phân là O(n) với n là số ký tự cần mã hoá.
Chương trình viết bằng ngôn ngữ Pascal minh hoạ thuật toán tạo mã Huffman:
Const n = 6; {Số ký tự cần mã hoá a, b, c, ...} Type Nod = record
S:integer; {tần suất} Code:String; {mã nhị phân} Name:char; {tên ký tự} end;
Var a:array[1..n] of Nod; i,m:integer;
Procedure InputData; {Khởi tạo bảng tần suất các ký tự} Var i:integer;
begin
for i:=1 to n do with A[i] do
begin S:=Round(exp(n/5)/exp(i/5))+1;Name:=Char(64+i);Code:=''; end; end;
Procedure FindCode(m:integer); {Sinh mã huffman} Var y,z:nod; k:integer; Begin if m=1 then exit; y:=a[m-1]; a[m-1].s:=a[m-1].s+a[m].s; k:=m-1;
While (k>1) and (a[k].s>a[k-1].s) do Begin z:=a[k]; a[k]:=a[k-1]; a[k-1]:=z; k:=k-1; End;
FindCode(m-1); z:=a[k];
for i:=k to m-2 do a[i]:=a[i+1]; a[m-1]:=y;
a[m-1].code:=z.code+'1'; a[m].code:=z.code+'0'; End;
BEGIN
InputData; FindCode(n);
END.
4.2 Cây biểu diễn biểu thức
Một biểu thức toán học có thể biểu diễn bằng cây, đây cũng là vấn đề hữu ích trong việc xử lý và lưu trữ biểu thức toán học trong máy tính
Xét biểu thức đại số sau:
Có thể vẽ 1 cây nhị phân như hình 1.4 biểu diễn biểu thức A, trong đó mỗi đỉnh trong mang dấu của một phép tính, gốc của cây mang phép tính sau cùng của A, ở đây là dấu nhân ký hiệu: *, mỗi lá mang 1 số hoặc một chữ đại diện cho số.
Hình 1.4
Một phép duyệt cây là tiền thứ tự nếu thăm gốc trước rồi sau đó thăm con trái như là một cây con với phương pháp thăm gốc trước và cuối cùng thăm con phải như là một cây con với phương pháp thăm gốc trước. Duyệt cây như vậy mang tính đệ quy.
Khi duyệt cây trên theo tiền thứ tự ta có: * + a b - c / d 2 Cách viết biểu thức theo tiền thứ tự là ký pháp Balan.
Bằng duyệt cây ta có thể tính được giá trị biểu thức, ngoài phương pháp tiền thứ tự còn có thể duyệt cây theo phương pháp pháp khác để tính giá trị biểu thức tùy vào yêu cầu và đặc điểm của từng bài toán.
4.3 Cây quyết định
Có những bài toán phụ thuộc vào các quyết định. Mỗi quyết định thì có thể có nhiều kết cục và những kết cục cuối cùng chính là lời giải của bài toán. Để giải những bài toán như vậy, người ta biểu diễn mỗi quyết định bằng một đỉnh của đồ thị và mỗi kết cục là 1 lá của quyết định. Một cây được xây dựng như trên gọi là cây quyết định. Trong nhiều bài toán Tin gặp phải, có thể dùng cây quyết định để mô hình hoá từ đó việc cài đặt sẽ dễ dàng thuận tiện hơn.
61( ) ( ) − × + = 2 d c b a A a * + - / b c d 2
Ví dụ:
hình 1.5 là cây quyết định biểu diễn việc sắp xếp 3 số khác nhau a, b, c
Hình 1.5
Đoạn chương trình sau thể hiện cho cây trên:
Var a, b, c: Integer;
Function Can(x,y: Integer): Boolean; Begin if x > y then Can:=True Else Can:=False; End; Begin Readln(a,b,c);
If Can(a,b) then Begin If Can(a,c) then Begin
if Can(b,c) then Writeln(c,' ',b,' ',a) Else Writeln(b,' ',c,' ',a);
End
Else Writeln(b,' ',a,' ',c); End
Else Begin
If Can(b,c) then Begin
if Can(a,c) then Writeln(c,' ',a,' ',b) Else Writeln(a,' ',c,' ',b); End a ? b a ? c b ? c b ? c a ? c c<a<b c<b<a b<c<a b<a<c a<c<b a<b<c b<a a<b c<a a<c c<b b<c c<b b<c c<a a<c
Else Writeln(a,' ',b,' ',c); End;
End.
4.4 Cây sắp xếp và tìm kiếm
Sắp xếp và tìm kiếm là một trong những vấn đề cơ bản trong kỹ thuật lập trình, cây nhị phân cũng có khá nhiều ứng dụng quan trọng trong vấn đề này. Ta có thể mô hình hoá việc sắp xếp và tìm kiếm bằng cây từ đó ta có thể đánh giá các kỹ thuật này từ góc độ về cây.
4.4.1 Sắp xếp chèn với tìm kiếm nhị phân
Ý tưởng được bắt đầu như sau, cho 1 danh sách chưa sắp xếp hãy tìm 1 phần tử x bất kỳ nào đó trong danh sách, rõ ràng để tìm ta phải lần lượt xét từng phần tử cho tới khi nào bắt gặp phần tử cần tìm, nếu danh sách lớn thì thời gian tìm rất lâu. Bây giờ với một danh sách đã sắp xếp, chia đôi danh sách và lấy phần tử là t ở vị trí chia đôi để so sánh. Nếu t = x thì dừng, nếu t < x vì danh sách đã sắp xếp nên x chỉ có thể nằm bên nửa phải danh sách nên ta chỉ việc tìm kiếm trong 1 nửa danh sách bên phải và giảm đi khá nhiều công việc tìm kiếm. Nếu x < t thì tương tự, ta chỉ việc tìm bên nửa trái, đối với việc tìm kiếm cho lần sau với các danh sách con nửa trái hoặc nửa phải ta thực hiện tương tự như vậy một cách đệ quy.
a) Từ những ý tưởng thuật toán ta xây dựng cây nhị phân tìm kiếm cho một danh sách như sau:
- Chọn 1 phần tử bất kỳ làm gốc
- Tất cả các phần tử có giá trị gốc thì thuộc cây con trái - Tất cả các phần tử có giá trị > gốc thì thuộc cây con phải - Đối với các cây con thì cũng có tính chất tương tự như vậy
Ví dụ cây nhị phân tìm kiếm cho danh sách 12, 10, 6, 11, 15, 13, 16, 19, 18 như hình 1.6: Hình 1.6 b) Sắp xếp chèn bằng việc tìm nhị phân vị trí đúng. 63 12 15 18 10 6 11 13 16 19
Có thể tạm gọi là phương pháp sắp xếp chèn nhị phân. Ý tưởng như sau: cho trước một danh sách đã sắp xếp A, cần chèn 1 phần tử mới x vào A sao cho danh sách luôn được sắp xếp. Đầu tiên ta tìm vị trí đúng của x trong A sau đó chèn x vào đúng vị trí này trong A, ta có danh sách A = A {x} luôn được sắp xếp. Để tìm được ví trí đúng cần chèn của x trong A ta sử dụng phương pháp tìm kiếm nhị phân, chèn theo cách này gọi là chèn nhị phân.
Ví dụ để sắp xếp B = x1, x2, x3, ... xn ta thực hiện như sau:
A :=
For i:=1 to n do Begin
- Tìm vị trí đúng của xi trong A theo phương pháp tìm nhị phân - Chèn xi vào A theo đúng vị trí vừa tìm được (A := A {xi}) End;
Kết quả A là danh sách sắp xếp của B.
Chương trình Pascal sau sắp xếp tăng dần theo phương pháp chèn nhị phân
Const n = 9;
Ds : Array[1..n] of Integer = (1,9,1,6,3,10,10,8,7); {Ham tra lai vi tri dung cua Pt trong danh sach} Function FindNp(l,r,Pt: Integer): Integer;
Var t: Integer; Begin
If Pt<=Ds[l] then FindNp:=l
Else If Pt>=Ds[r] then FindNp:= r + 1 Else Begin
Repeat
t:= (l + r) div 2;
If Pt = ds[t] then Begin FindNp:=t+1; Exit End Else If Pt<ds[t] then r:=t Else l:=t; Until r=l+1; FindNp:=l+1; End; End; Var i, j, vt, s: Integer; Begin
For i:=2 to n do Begin vt:= FindNp(1,i-1,ds[i]);
{Chen dung vi tri sao cho ds luon duoc sap xep} s:=ds[i];
For j:=i-1 Downto vt do ds[j+1]:=ds[j]; ds[vt]:=s;
End;
For i:=1 to n do Write(ds[i]:3); End.
4.4.2 Thuật toán sắp xếp hoà nhập
Giả sử ta có danh sách chưa được sắp 8, 2, 4, 6, 9, 7, 10, 1, 5 ,3 có thể dùng cây nhị phân mô tả quá trình sắp xếp danh sách theo thứ tự tăng dần như sau: Cây nhị phân với gốc được gán là chính là danh sách đó. Các con của gốc được gán theo nguyên tắc: Con bên trái gán nửa danh sách đầu, con bên phải gán nửa danh sách còn lại (danh sách gán ở gốc cây con trái và cây con phải hoặc bằng nhau về số lượng hoặc chênh lệch nhau 1 phần tử).
Cứ tiếp tục cho tới khi cây nhị phân có mỗi lá được gán 1 phần tử trong dãy. Đó là cây như hình 1.7
Hình 1.7
Đây là cây nhị phân đầy đủ chưa phải cây sắp xếp của dãy đã cho ở trên
658, 2, 4, 6, 9, 7, 10, 1, 5, 3 8, 2, 4, 6, 9, 7, 10, 1, 5, 3 8, 2, 4, 6, 9 7, 10, 1, 5, 3 8, 2, 4 6, 9 7, 10, 1 5, 3 8, 2 4 6 9 8 2 7, 10 1 7 10 5 3
Hình 1.8
Để có cây nhị phân sắp xếp của một dãy, trước hết cây được xây dựng tương tương tự như vậy, mỗi lá tương ứng với mỗi phần tử của dãy. Bước đầu tiên 2 phần tử được hoà nhập vào 1 danh sách theo thứ tự tăng dần, danh sách tương ứng này như một nút cha, 2 phần tử được hoà nhập là nút con. Sau đó ta tiếp tục hoà nhập các cặp nút tương tự như vậy cho tới toàn bộ danh sách được sắp xếp lại theo thứ tự tăng dần và cây biểu diễn cho dãy là cây nhị phân cân đối được mô tả như hình 1.8
Các bước thuật toán trên được mô tả trên là đã vận dụng thuật toán hoà nhập hai danh sách đã được sắp xếp thành một danh sách mới được sắp xếp, thuật toán này theo nguyên tắc sau:
- So sánh phần tử bé nhất trong danh sách trong danh sách thứ nhất với phần tử bé nhất trong danh sách thứ 2.
- Quá trình trên được lặp lại cho 2 danh sách nhận được ở bước trên - Sau một số bước sẽ gặp hai trường hợp sau:
a) Cả 2 danh sách trở thành rỗng. Trong trường hợp này, các phần tử có mặt trong danh sách hoà nhập chính là danh sách cần xác định
b) Một danh sách trở thành rỗng, còn danh sách kia khác rỗng. Trong trường hợp này đưa các phần tử còn lại (trong danh sách không rỗng) nối vào cuối của danh sách hoà nhập.
Độ phức tạp thuật toán của thuật toán trên là O(nlogn) trong đó n là số phần tử hoà nhập
Chương trình bằng ngôn ngữ Pascal thể hiện thuật toán hoà nhập như sau:
Const n = 10; 8 2 7 10 2, 8 4 6 9 7, 10 1 5 3 2, 4, 8 6, 9 1,7, 10 3, 5 2, 4, 6, 8,9 1, 3,5, 7, 10 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Type Vec = Array[1..n] of Integer; Const cm:Vec = (-3,3,-1,4,1,5,6,7,-8,-9); Var x,y:Vec; Procedure Viet; Var i:byte; Begin
For i:=1 to n do Write(x[i]:3); Writeln; End;
Procedure Hoa_nhap2p(x:Vec; p,m,n:Integer; Var z:Vec); Var i,j,k: Integer;
Begin
i:=p;j:=m+1;k:=p;
While (i<=m) and (j<=n) do Begin If x[i]<x[j] Then Begin
z[k]:=x[i];inc(i); End
Else Begin z[k]:=x[j];inc(j);End; Inc(k); End; If i>m Then for K:=j to n Do z[k]:=x[k] Else for k:=i to m Do z[n+k-m]:=x[k]; End; Procedure Merge(l,r:integer); Var i,m:integer; Begin If l<r Then Begin m:=(l+r) div 2; Merge(l,m); Merge(m+1,r); For i:=l to r do y[i]:=x[i]; Hoa_nhap2p(y,l,m,r,x); End; End; BEGIN X := cm; Viet; 67
Merge(1,n); Viet; END.
4.4.3 Thuật toán sắp xếp nhanh
Sắp xếp 1 danh sách có nhiều thuật toán, mỗi thuật toán đều có những ưu nhược điểm. Trong các thuật toán sắp xếp thì thuật sắp xếp nhanh (Quick sort) tỏ ra có nhiều ưu điểm được sử dụng phổ biến và rất hiệu quả. Nguyên tắc của thuật toán này có tính đệ quy có thể sử dụng cây nhị phân để mô tả thuật toán này. Thuật toán có thể được mô tả tóm tắt như sau:
Ví dụ để sắp xếp danh sách a1, a2, ..., an thuật toán bắt đầu bằng việc lấy ngẫu nhiên 1 phần tử làm chốt nhưng thường thì phần tử đầu tiên được chọn làm chốt. Như danh sách trên ta chọn a1 làm chốt khi đó danh sách được phân đoạn thành 2 danh sách con, một danh sách con gồm các phần tử nhỏ hơn a1 theo thứ tự xuất hiện, còn danh sách con khác gồm các phần tử lớn hơn a1 cũng theo thứ tự xuất hiện. Sau khi đã có hai danh sách con thì a1 được đặt vào sau cùng của danh sách con gồm các phần tử nhỏ hơn a1, như vậy sau a1 là danh sách con gồm các phần tử lớn hơn a1. Thủ tục này được lặp lại một cách đệ quy cho mỗi danh sách con cho tới khi nào mỗi danh sách con chỉ chứa một phần tử theo thứ tự xuất hiện của nó. Với kết quả này ta được một danh sách đã được sắp xếp. Ví dụ cho danh sách: 3, 5, 7, 8, 1, 9, 2, 6;
Có thể dùng cây nhị phân biểu diễn thuật toán sắp xếp nhanh để sắp xếp danh sách này như hình 1.9
Hình 1.9
Danh sách lúc chưa sắp xếp là gốc, danh sách được sắp xếp là danh sách mà mỗi phần tử của nó là lá của cây.
3, 5, 7, 8, 1, 9, 2, 4, 61, 2, 3 5, 7, 8, 9, 4, 6 1, 2, 3 5, 7, 8, 9, 4, 6 1 2, 3 4, 5 7, 8, 9, 6 2 3 4 5 6, 7 8, 9 9 8 6 7
Uses Crt; const
Max = 10; type
List = array[1..Max] of Integer; var
Data: List; I: Integer;
procedure QuickSort(var A: List; Lo, Hi: Integer); procedure Sort(l, r: Integer);
var i, j, x, y: integer; begin i := l; j := r; x := a[(l+r) DIV 2]; repeat while a[i] < x do i := i + 1; while x < a[j] do j := j - 1; if i <= j then begin
y := a[i]; a[i] := a[j]; a[j] := y; i := i + 1; j := j - 1; end; until i > j; if l < j then Sort(l, j); if i < r then Sort(i, r); end; Begin {QuickSort}; Sort(Lo,Hi); End; Begin {QSort}
{ Khởi tạo ngẫu nhiên 10 phần tử } Randomize;
for i := 1 to Max do Data[i] := Random(3000); Writeln;
{Sắp xếp các phần tử bằng Quick Sort} QuickSort(Data, 1, Max);
Writeln;
for i := 1 to 10 do Write(Data[i]:8); End.
Người ta đã chỉ ra rằng độ phức tạp của thuật toán là O(nlog2n)