Trong khuôn khổ tài liệu này, tác giả sẽ giới thiệu về một số cấu trúc dữ liệu động đặc biệt như danh sách liên kết, ngăn xếp vàhàng đợi và một số ứng dụng của chúng đặc biệt trong việc
Trang 1ra các giải pháp hiệu quả giải quyết các bài toán tin.
Với các cấu trúc dữ liệu được xây dựng từ các kiểu cơ sở như: kiểu thực,kiểu nguyên, kiểu ký tự hoặc từ các cấu trúc đơn giản như mẩu tin, tập hợp,mảng lập trình viên có thể giải quyết hầu hết các bài toán đặt ra Các đối tượng
dữ liệu được xác định thuộc những kiểu dữ liệu này có đặc điểm chung là khôngthay đổi được kích thước, cấu trúc trong quá trình sống, do vậy thường cứngnhắc, gò bó khiến đôi khi khó diễn tả được thực tế vốn sinh động, phong phú.Các kiểu dữ liệu kể trên được gọi là các kiểu dữ liệu tĩnh
Một số đối tượng dữ liệu trong chu kỳ sống của nó có thể thay đổi về cấutrúc, độ lớn, như danh sách các học viên trong một lớp học có thể tăng thêm,giảm đi Khi đó nếu cố tình dùng những cấu trúc dữ liệu tĩnh đã biết như mảng
để biểu diễn những đối tượng đó lập trình viên phải sử dụng những thao tác phứctạp, kém tự nhiên khiến chương trình trở nên khó đọc, do đó khó bảo trì và nhất
là khó có thể sử dụng bộ nhớ một cách có hiệu quả
Do bản chất của các dữ liệu tĩnh, chúng sẽ chiếm vùng nhớ đã dành chochúng suốt quá trình hoạt động của chương trình Tuy nhiên, trong thực tế, cóthể xảy ra trường hợp một dữ liệu nào đó chỉ tồn tại nhất thời hay không thườngxuyên trong quá trình hoạt động của chương trình Vì vậy việc dùng các CTDLtĩnh sẽ không cho phép sử dụng hiệu quả bộ nhớ
Do vậy, nhằm đáp ứng nhu cầu thể hiện sát thực bản chất của dữ liệu cũngnhư xây dựng các thao tác hiệu quả trên dữ liệu, cần phải tìm cách tổ chức kếthợp dữ liệu với những hình thức mới linh động hơn, có thể thay đổi kích thước,cấu trúc trong suốt thời gian sống Các hình thức tổ chức dữ liệu như vậy được
gọi là cấu trúc dữ liệu động Trong khuôn khổ tài liệu này, tác giả sẽ giới thiệu
về một số cấu trúc dữ liệu động đặc biệt như danh sách liên kết, ngăn xếp vàhàng đợi và một số ứng dụng của chúng đặc biệt trong việc khử đệ qui
Tác giả rất mong nhận được những đóng góp của các thầy, cô giáo và họcsinh để chuyên đề này được hoàn thiện hơn
Xin trân trọng cảm ơn.!
Trang 2MỤC LỤC
1 DANH SÁCH LIÊN KẾT 3
1.1 Khái niệm 3
1 2 Cấu tạo của danh sách liên kết 4
1.3 Các thao tác cơ bản với danh sách liên kết 4
1 3 1 Khai báo 4
1.3.2 Khởi tạo danh sách 4
1.3.3 Bổ sung một nút vào đầu danh sách 4
1.3.4 Bổ sung một nút vào cuối danh sách 5
1.3.5 Duyệt danh sách 5
1.3.6 Bổ sung một nút vào sau nút được trỏ bởi p 5
1.3.7 Xoá một nút khỏi danh sách 6
1.4 Mảng hay danh sách liên kết? 6
1.5 Ví dụ làm việc với danh sách liên kết 7
2 NGĂN XẾP – STACK 14
2.1 Khái niệm 14
2.2 Các thao tác cơ bản của ngăn xếp 14
2.4 Ứng dụng Stack 15
2.4.1 Ứng dụng Stack khử đệ quy 15
2.4.2 Tính giá trị một biểu thức dạng hậu tố 22
2.4.3 Chuyển biểu thức từ trung tố sang hậu tố 25
3 HÀNG ĐỢI – Queue 27
3.1 Khái niệm 27
3.2 Các thao tác cơ bản của hàng đợi 28
3.3 So sánh việc cài đặt Queue bằng mảng và danh sách liên kết 31
3.4 Ứng dụng 31
Trang 31 DANH SÁCH LIÊN KẾT
Danh sách là một dãy hữu hạn các phần tử thuộc cùng một lớp đối tượngnào đó Ví dụ : danh sách sinh viên, danh sách vật tư, danh sách các hoá đơn,
danh sách các số thực Chúng ta đã dùng mảng để biểu thị một danh sách, cách
này có các nhược điểm: kích thước của mảng phải định trước nên tốn bộ nhớ (sốphần tử thực tế dùng nhiều khi rất ít so với khai báo), khi thêm một phần tử vàomảng hoặc xoá một phần tử ra khỏi mảng ta phải mất nhiều thời gian để dồnmảng
Danh sách liên kết dùng để cài đặt một danh sách sẽ khắc phục được cácnhược điểm trên của mảng
1.1 Khái niệm
R Sedgewick (Alogrithms in Java – 2002) định nghĩa danh sách liên kếtnhư sau: Danh sách liên kết là một cấu trúc dữ liệu bao gồm một tập các phần tử,trong đó mỗi phần tử là một phần của một nút có chứa liên kết đến phần tử kếtiếp
“Mối phần tử là một phần của một nút” vì mỗi nút có ít nhất hai trường,một trường gọi là dữ liệu (data), trường còn lại là trường liên kết trỏ đến nút tiếptheo trong danh sách
Trường liên kết của phần tử thứ i của danh sách sẽ trỏ tới phần tử thứ(i+1) của danh sách Tuy nhiên nó cũng có thể trỏ đến chính nó
Có thể nói danh sách liên kết là một cấu trúc dữ liệu được định nghĩa kiểu
đệ quy, vì trong định nghĩa một nút của danh sách có tham chiếu tới khái niệmnút
Có nhiều loại danh sách liên kết khác nhau tùy thuộc vào cấu trúc của mỗiphần tử trong danh sách (số trường liên kết với các phần tử khác trong danhsách) nhưng cơ bản nhất là danh sách liên kết đơn (single linked list), mỗi phần
tử có một trường liên kết như trên hình vẽ minh họa, và khi chúng ta nói đếndanh sách liên kết, nếu không có các chú giải đi kèm thì ngầm hiểu đó là danhsách liên kết đơn
Danh sách liên kết kép: mỗi phần tử liên kết với các phần tử đứng trước và sau
nó trong danh sách:
Danh sách liên kết vòng : phần tử cuối danh sách liên kết với phần tử đầu danhsách:
Trang 41 2 Cấu tạo của danh sách liên kết
Cấu tạo của danh sách liên kết: có một con trỏ First chứa địa chỉ của phần
tử đầu tiên của danh sách, phần tử đầu có phần dữ liệu và một con trỏ Next để
chứa địa chỉ của phần tử thứ hai, tổng quát phần tử thứ i có phần dữ liệu và mộtcon trỏ next để chứa địa chỉ của phần tử thứ i+1, đối với phần tử cuối cùng giátrị của con trỏ next bằng NIL Để thuận tiện khi thêm phần tử mới vào cuối danh
sách liên kết ta dùng một con trỏ Last chứa địa chỉ của phần tử cuối cùng Khởi
tạo một danh sách rỗng first = NIL
Nhắc lại 2 thủ tục cơ bản với biển trỏ:
+ Cấp phát vùng nhớ cho biến trỏ p: New(p)
+ Giải phóng vùng nhớ cho biến trỏ p: Dispose(p)
1.3 Các thao tác cơ bản với danh sách liên kết
1.3.2 Khởi tạo danh sách
Trang 5First :=p;
1.3.4 Bổ sung một nút vào cuối danh sách
Xuất phát danh sách không có nút nào cả Nút mới thêm vào sẽ nằm cuối danh sách Khi đó ta cần hai biến con trỏ First và Last lần lượt trỏ đến các nút đầu và cuối danh sách
Procedure Khoitao;
var p: TroNut;
Begin
First := nil; Last:= nil;
While <còn thêm nút mới vào danh sách> do Begin
1.3.6 Bổ sung một nút vào sau nút được trỏ bởi p
Thủ tục sau thực hiện việc bổ sung một nút có nội dung x vào sau nút được trỏ bởi p
Trang 61.3.7 Xoá một nút khỏi danh sách
Thủ tục sau thực hiện việc xóa một nút trỏ bởi p ra khỏi danh sách
1.4 Mảng hay danh sách liên kết?
Chúng ta đã từng làm quen với kiểu mảng, lưu danh sách gồm nhiều thànhphần có cùng kiểu Mỗi thành phần là một biến tĩnh và số lượng thành phần củadanh sách là cố định Tuy nhiên việc sử dụng mảng có các nhược điểm: kíchthước của mảng phải định trước nên tốn bộ nhớ (số phần tử thực tế dùng nhiềukhi rất ít so với khai báo), khi thêm một phần tử vào mảng hoặc xoá một phần tử
để cài đặt một danh sách sẽ khắc phục được các nhược điểm trên của mảng
Tuy nhiên không thể kết luận phương pháp cài đặt nào hiệu quả hơn, mà
nó hoàn toàn tuỳ thuộc vào từng ứng dụng hay tuỳ thuộc vào các phép toán trêndanh sách Tuy nhiên ta có thể tổng kết một số ưu nhược điểm của từng phươngpháp làm cơ sở để lựa chọn phương pháp cài đặt thích hợp cho từng ứng dụng:
Cài đặt bằng mảng đòi hỏi phải xác định số phần tử của mảng, do đó nếukhông thể ước lượng được số phần tử trong danh sách thì khó áp dụng cách càiđặt này một cách hiệu quả vì nếu khai báo thiếu chỗ thì mảng thường xuyên bịđầy, không thể làm việc được còn nếu khai báo quá thừa thì lãng phí bộ nhớ
Trang 7Cài đặt bằng con trỏ thích hợp cho sự biến động của danh sách, danh sách
có thể rỗng hoặc lớn tuỳ ý chỉ phụ thuộc vào bộ nhớ tối đa của máy Tuy nhiên
ta phải tốn thêm vùng nhớ cho các con trỏ (trường next)
Cài đặt bằng mảng thì thời gian xen hoặc xoá một phần tử tỉ lệ với số phần
tử đi sau vị trí xen/ xóa Trong khi cài đặt bằng con trỏ các phép toán này mấtchỉ một hằng thời gian
Phép truy nhập vào một phần tử trong danh sách chỉ tốn một hằng thờigian đối với cài đặt bằng mảng, trong khi đối với danh sách cài đặt bằng con trỏ
ta phải tìm từ đầu danh sách cho đến vị trí trước vị trí của phần tử hiện hành Nóichung danh sách liên kết thích hợp với danh sách có nhiều biến động, tức là tathường xuyên thêm, xoá các phần tử
1.5 Ví dụ làm việc với danh sách liên kết
Xét danh sách liên kết đơn biểu diễn một dãy số nguyên Nút đầu tiên trong danh sách được trỏ bởi First Cho khai báo mỗi nút trong danh sách như sau:
Type TroNut = ^ Nut;
Nut = Record
Data: Integer;
Next: TroNut;
End;
Var First: TroNut;
Viết chương trình thực hiện các yêu cầu sau:
a Nhập dãy các số nguyên và lưu vào danh sách có nút đầu trỏ bởi First, quá trình nhập dừng khi dữ liệu đưa vào không phải là số nguyên.
Trang 8write(‘Nhap gia tri mot nut – Ket thuc bang ky tu Q: ‘);
Trang 9Procedure Nut_duong(var dem: word; tb:real);
Procedure Insert(var first: TroNut; m: integer) thực hiện việc bổ sung một
nút vào danh sách sao cho tính tăng dần được bảo toàn
Procedure Insert(var first: TroNut; m: integer);Var
Trang 10p^.Next:= q^.Next;
q^.Next:= p;
end;
end;
Procedure InitList thực hiện việc tạo danh sách có tính chất trên bằng cách
nhập dữ liệu từ bàn phím và quá trinh nhập dừng khi nhấn phím ESC (yêu cầu:
Procedure List(First: TroNut) in dãy giá trị các nút trong danh sách.
Procedure List(First: Tronut);
Procedure DeleteZero( Var First: TroNut) thực hiện việc xoá tất cả các nút có
giá trị 0 trong danh sách
Procedure DeleteZero(Var First: TroNut);
Trang 11Function TroMax(First: TroNut): TroNut trả về địa chỉ của nút đầu tiên đạt
giá trị lớn nhất (tính từ đầu danh sách, nếu có, ngược lại hàm trả về giá trị Nil)
Function Tromax(First: TroNut);
e Giả sử danh sách khác rỗng Viết các thủ tục và hàm sau:
Function DataMax(First: TroNut): integer trả về giá trị lớn nhất của nút có
Trang 12Function Search(First: TroNut; k: word): TroNut trả về địa chỉ của nút thứ k
(nếu có, ngược lại, hàm trả về giá trị Nil)
Function Search(First: TroNut; k: word): Tronut;Var
d: word;
Trang 13Procedure Delete_K(Var First: TroNut; k: word) thực hiện việc xoá nút thứ k
trong danh sách (nếu có)
Procedure Delete_K(Var first: Tronut; k:word);var
Trang 142.2 Các thao tác cơ bản của ngăn xếp
Đối với một ngăn xếp chỉ có hai thao tác cơ bản: thao tác thêm một phần
tử vào Stack và thao tác loại bỏ một phần tử khỏi Stack Trong khuôn khổ tàiliệu này tác giả chỉ đưa ra cách cài đặt Stack bằng DSLK
Stack dùng danh sách liên kết hoàn toàn giống danh sách liên kết thuận,nhưng chỉ có điều khác là khi thêm phần tử mới hay huỷ một phần tử ta luônluôn làm ở đầu danh sách Do đó ta phải duy trì một con trỏ Top để trỏ vào phần
tử đầu tiên của danh sách (đỉnh của stack)
Khởi tạo Stack
procedure Khoitao(var s:stack);
Trang 15Thêm một phần tử vào Stack
procedure Push(var x:Datatype; var s:stack);
Lấy một phần tử ra khỏi Stack
Procedure Pop(var x:Datatype; var s:stack);
Nếu một chương trình con đệ quy P được gọi từ chương trình chính ta nóichương trình con được thực hiện ở mức 1 Chương trình con này gọi chính nó, tanói nó đi sâu vào mức 2 cho đến một mức k Rõ ràng mức k phải thực hiệnxong thì mức k-1 mới được thực hiện tiếp tục, hay ta còn nói là chương trình conquay về mức k-1
Trang 16Trong khi một chương trình con từ mức i đi vào mức i+1 thì các biến cục
bộ của mức i và địa chỉ của mã lệnh còn dang dở phải được lưu trữ, địa chỉ nàygọi là địa chỉ trở về Khi từ mức i+1 quay về mức i các giá trị đó được sử dụng.Như vậy những biến cục bộ và địa chỉ lưu sau được dùng trước Tính chất nàygợi ý cho ta dùng một ngăn xếp để lưu giữ các giá trị cần thiết của mỗi lần gọitới chương trình con Mỗi khi lùi về một mức thì các giá trị này được lấy ra đểtiếp tục thực hiện mức này
a) Bài toán tính giai thừa:
While N <> 1 DoBegin
b) Bài toán tính số Fibonaci
Dãy số Fibonaci xác định như sau: Bắt đầu từ 2 số 0 và 1 tiếp sau đó là các số Fibonaci sau bằng tổng của 2 số Fibonaci trước đó
F1=0; F2=1; FN = FN-1+ FN-2 ∀N≥2
Ví dụ dãy số Fibonaci: 0 1 1 2 3 5 8…
Đệ quy hàm Fibonaci như sau:
Function Fibo(N:Integer): Longint;
Trang 17If N=1 then Fibo:=0 Else If N=2 then Fibo:=1Else Fibo:=Fibo(N-1) * Fibo(N-2);
Begin
Push(N,S);
While N<>0 DoBegin
N:=N-1;
Push(N,S);
End;
If Top(S) <> 2 thenBegin
c) Khử đệ quy thuật toán sắp xếp Quicksort
B1: Khởi tạo một stack rỗng
B2: Mảng A ta đang xét từ phần tử thứ 1 đến phần tử thứ N, gán L:=1, R:=N, đẩy hai giá trị 1 và N vào Stack
B3: Lấy lại L, R từ Stack ra
B4: Phân hoạch dãy A[L] A[R] thành hai dãy: A[L] A[j-1] và dãy
A[j+1] A[R]
B5: Nếu j+1 < R thì đẩy (j+1) và R vào Stack
Trang 18B6: Nếu L< j-1 thì quay lại bước 4 để phân hoạch dãy A[L] A[j-1] nếu không chuyển sang bước 7.
B7: Nếu Stack khác rỗng thì quay lại B3 để phân hoạch tiếp, nếu Stack rỗng thì kết thúc
Trang 19Cho đơn đồ thị vô hướng G=(V,E) gồm N đỉnh, M cạnh Các đỉnh đượcđánh số thứ tự từ 1 đến N Xuất phát tại đỉnh S, Tìm đường đi từ đỉnh xuất phát
S đến đỉnh kết thúc F;
Tư tưởng dùng đệ quy của thuật toán tìm kiếm theo chiều sâu (DFS):Trước hết, mọi đỉnh x kề với S tất nhiên sẽ đến được từ S Với mỗi đỉnh x kề với
S đó thì tất nhiên những đỉnh y kề với x cũng đến được từ S Điều đó gợi ý cho
ta viết một thủ tục đệ quy DFS(u) mô tả việc duyệt từ đỉnh u bằng cách thôngbáo thăm đỉnh u và tiếp tục quá trình duyệt DFS(v), với v là một đỉnh chưa thăm
kề với u Để không một đinh nào bị liệt kê tới 2 lần, ta sử dụng kĩ thuật đánhdấu, mỗi lần thăm một đỉnh, ta đánh dẫu đỉnh đó lại để các bước duyệt đệ quy kếtiếp không duyệt lại đỉnh đó nữa Để lưu lại đường đi từ đỉnh xuất phát S, trongthủ tục DFS(u), trước khi gọi đệ quy DFS(v), với v kề với u mà chưa đánh dấu,
ta lưu lại vết đường đi từ u->v bằng cách Trace[v]:=u, tức là trước khi đến đỉnh
v là đỉnh u
Procedure DFS(u:integer);
Var v: Integer;
Begin
<Thông báo thăm đỉnh u>
<Đánh dấu đã thăm u>
<Xét tất cả đỉnh v kề với u, v chưa thăm >
Khi mô tả quá trình đệ quy bằng ngăn xếp, ta luôn luôn để cho ngăn xếp
để lưu lại dây chuyền duyệt sâu từ nút gốc (đỉnh xuất phát S)
< Thăm S, đánh dấu S đã thăm>
< Đẩy S vào ngăn xếp>
Repeat
< Lấy u khỏi ngăn xếp>
If < u có đỉnh kề chưa thăm> thenBegin
< Chỉ chọn một đỉnh v, là đỉnh đầu tiên
kề với u mà chưa thăm>
< Thông báo thăm v>
< Đẩy u trở lại ngăn xếp>
Trang 20< Đẩy tiếp v vào ngăn xếp>
a: array[1 max, 1 max] of Boolean;
Free: array[1 max] of Boolean;
Trace: array[1 max] of Integer;
Stack: array[1 max] of Integer;
Trang 22Assign(Input, 'GRAPH.INP'); Reset(Input);
Assign(Output, 'GRAPH.OUT'); Rewrite(Output); Enter;
2.4.2 Tính giá trị một biểu thức dạng hậu tố
Biểu thức trung tố, hiểu đơn giản tức là toán tử sẽ được đặt giữa hai toánhạng, dĩ nhiên đây phải là toán tử hai ngôi Vậy dựa vào vị trí của của toán tử,liệu ta có thể biểu diễn biểu thức đại số dưới dạng khác? Câu trả lời là được, vànhư đã nói, ta có ba cách là biểu thức tiền tố (prefix), trung tố (infix) và hậu tố(postfix) Hãy xem một chút giới thiệu về cách biểu diễn biểu thức tiền tố và hậu
tố để hiểu rõ hơn về chúng
Prefix: Biểu thức tiền tố được biểu diễn bằng cách đặt toán tử lên trước
các toán hạng Cách biểu diễn này còn được biết đến với tên gọi “ký pháp Ba
Lan” do nhà toán học Ba Lan Jan Łukasiewicz phát minh năm 1920 Với cách
biểu diễn này, thay vì viết x+y như dạng trung tố, ta sẽ viết +xy Tùy theo độ ưutiên của toán tử mà chúng sẽ được sắp xếp khác nhau, bạn có thể xem một số ví
dụ ở phía sau phần giới thiệu này
Postfix: Ngược lại với cách Prefix, tức là các toán tử sẽ được đặt sau các
toán hạng Cách biểu diễn này được gọi là “ký pháp nghịch đảo Ba Lan” hoặc được viết tắt là RPN (Reverse Polish notation), được phát minh vào khoảng giữa
thập kỷ 1950 bởi một triết học gia và nhà khoa học máy tính Charles Hamblinngười Úc