Các cách cài đặt danh sách tuyến tính Có các cách cài đặt danh sách tuyến tính cơ bản sau đây: • Dùng mảng Array-based: Cất giữ các phần tử của danh sách vào các ô liên tiếp của mảng.. D
Trang 1SỞ GIÁO DỤC VÀ ĐÀO TẠO TỈNH BẮC GIANG
TRƯỜNG THPT CHUYÊN BẮC GIANG
Trang 2Do đó, trong hai năm học 2010-2011 và 2011-2012, tôi chọn chuyên đề
nghiên cứu là: “Một số vấn đề về kiểu dữ liệu trừu tượng” để bước đầu có thể
đọc và hiểu một phần nho nhỏ về kiểu dữ liệu trừu tượng
Đây là một trong những nội dung rất khó nhưng tôi thấy rất cần thiết chocông việc giảng dạy của tôi cũng như của nhóm Tin Trường THPT Chuyên BắcGiang
II ĐỐI TƯỢNG NGHIÊN CỨU
Đối tượng nghiên cứu chủ yếu là các kiểu dữ liệu trừu tượng và các bàitoán nâng cao trong lập trình
III LỊCH SỬ VẤN ĐỀ
Tài liệu bồi dưỡng HSG Tin rất ít không như một số bộ môn khác Do đó,tất cả các chuyên đề chuyên sâu của nhóm Tin được hoàn thành trong nhữngnăm vừa qua đều là những tài liệu quí giá không những cho học sinh và giáoviên trong trường Chuyên mà còn cần thiết cho giáo viên các trường THPTkhác
Để giải một bài toán tin thành công ngoài việc bạn phải tìm ra một thuậtgiải tốt, bạn cần thiết phải trả lời được câu hỏi: “Thuật giải đó tác động lên dữliệu gì? Nó được tổ chức như thế nào?” Nếu dữ liệu không phù hợp thì thuậttoán dù tốt cũng khó đáp ứng được yêu cầu đặt ra của bài toán Việc lựa chọncấu trúc dữ liệu phù hợp với thuật giải tốt chính là nghệ thuật lập trình
Việc chọn vấn đề này không phải là ngẫu nhiên hay vì dễ viết mà vì tôithấy mình còn hiểu quá ít về nó – đó có khi là thử thách Ngay trong quá trìnhdạy đội tuyển HSG, tôi và học sinh cũng còn nhiều lúng túng về cấu trúc dữ liệutrừu tượng vì thế kết quả đạt được chưa cao
Từ những khó khăn gặp phải tôi đã tìm tài liệu, đọc tài liệu, thực hành càiđặt và tôi chỉ mạnh dạn viết nên những điều mình đã hiểu và làm được
Trang 3Rất mong được sự góp ý của các đồng nghiệp để tài liệu được cải tiến tốthơn trong các lần cải biên sau!
IV MỤC ĐÍCH NGHIÊN CỨU, ĐÓNG GÓP MỚI CỦA CHUYÊN ĐỀ
Xuất phát từ yêu cầu giảng dạy nâng cao cho đội tuyển HSG Tin dự thichọn HSG QG và giảng dạy cho các lớp chuyên Tin đòi hỏi cần thiết có thêmmột số chuyên đề chuyên sâu mới, một trong những chuyên đề cần thiết đó là:
“Một số vấn đề về kiểu dữ liệu trừu tượng”
Chuyên đề giúp học sinh và giáo viên hiểu được:
1 Các khái niệm về kiểu dữ liệu, kiểu dữ liệu trừu tượng, cấu trúc dữ liệu
2 Tập các giá trị và tập các phép toán có thể thực hiện trên một số kiểu dữ liệutrừu tượng
3 Các bài tập ứng dụng từ cơ bản đến nâng cao
V PHƯƠNG PHÁP NGHIÊN CỨU
Để hoàn thành nhiệm vụ của chuyên đề, tôi đã sử dụng các phương phápnghiên cứu như: Đối sánh, Phân tích, Thực nghiệm, Tổng hợp
VI CẤU TRÚC CỦA CHUYÊN ĐỀ
1 Ngoài phần mở đầu chuyên đề được chia thành bốn chương như sau:
- Chương 1: Cơ sở lý thuyết
- Chương 2: Danh sách tuyến tính
- Chương 3: Cây nhị phân
- Chương 4: Bài tập áp dụng
2 Với mỗi cấu trúc dữ liệu trừu tượng tôi đều triển khai theo cấu trúc 3phần như sau:
- Phần 1: Hệ thống các khái niệm liên quan
- Phần 2: Biểu diễn kiểu dữ liệu và tập các phép toán áp dụng kiểu dữ liệu
- Phần 3: Cài đặt các phép toán bằng một ngôn ngữ lập trình cụ thể
VII KẾ HOẠCH VIẾT CHUYÊN ĐỀ
Năm học 2010-2011: Hoàn thành Chương 1 và Chương 2
Năm học 2011-2012: Hoàn thành Chương 3 và Chương 4
Trang 4Phần thứ hai NỘI DUNG CHUYÊN ĐỀ Chương I
CƠ SỞ LÝ THUYẾT
I.1 Các khái niệm
I.1.1 Kiểu dữ liệu (Data types)
Kiểu dữ liệu (data type) được đặc trưng bởi:
- Tập các giá trị (a set of values);
- Cách biểu diễn dữ liệu (data representation) được sử dụng chung cho tất
cả các giá trị này;
- Tập các phép toán (set of operations) có thể thực hiện trên tất cả các giátrị;
Các kiểu dữ liệu dựng sẵn (Built-in data types)
Trong các ngôn ngữ lập trình thường có một số kiểu dữ liệu nguyên thuỷ đãđược xây dựng sẵn Ví dụ:
- Kiểu số nguyên (Integer numeric types) Chẳng hạn, byte, shortint,integer, word, longint, int64
- Kiểu số thực dấu phẩy động (floating point numeric types) Chẳng hạn,real, single, double, extended, comp
- Kiểu lôgíc (logical type): Boolean
- Kiểu kí tự (Character type): Char
- Kiểu mảng (Array type) Chẳng hạn mảng các phần tử cùng kiểu
Phép toán đối với một số kiểu dữ liệu nguyên thuỷ
- Đối với kiểu số nguyên: +, -, *, DIV, MOD
- Đối với kiểu số thực: +, -, *, /
- Đối với kiểu kí tự: so sánh
- Đối với kiểu lôgíc: so sánh, AND, OR, NOT, XOR
Nhận thấy rằng: Các ngôn ngữ lập trình khác nhau có thể sử dụng mô tảkiểu dữ liệu khác nhau Chẳng hạn, PASCAL và C có những mô tả các kiểu dữliệu số khác nhau
I.1.2 Kiểu dữ liệu trừu tượng (Abstract Data Types)
Kiểu dữ liệu trừu tượng (Abstract Data Type – ADT) bao gồm:
- Tập các giá trị (set of values) và
- Tập các phép toán (set of operations) có thể thực hiện với tất cả các giá trịnày
Cách biểu diễn dữ liệu trong định nghĩa của kiểu dữ liệu (Data Type) đã bị
bỏ qua trong định nghĩa ADT Cách biểu diễn dữ liệu (data representation) được
Trang 5sử dụng chung cho tất cả các giá trị này Việc làm này có ý nghĩa làm trừu tượnghoá khái niệm kiểu dữ liệu ADT không còn phụ thuộc vào cài đặt, không phụthuộc ngôn ngữ lập trình.
Ví dụ:
Đồ thị (Graphs) đỉnh, cạnh duyệt, đường đi, …
Ngăn xếp (Stack) các phần tử pop, push, isEmpty,…Hàng đợi (Queue) các phần tử pop, push, isEmpty,…
Điều dễ hiểu là các kiểu dữ liệu nguyên thuỷ mà các ngôn ngữ lập trình càiđặt sẵn cũng được coi là thuộc vào kiểu dữ liệu trừu tượng Trên thực tế chúng
là cài đặt của kiểu dữ liệu trừu tượng trên ngôn ngữ lập trình cụ thể
Định nghĩa:(Xem [3] Trang 47) Ta gọi việc cài đặt (implementation) một
ADT là việc diễn tả bởi các câu lệnh của một ngôn ngữ lập trình để mô tả cácbiến trong ADT và các thủ tục trong ngôn ngữ lập trình để thực hiện các phéptoán của ADT, hoặc trong các ngôn ngữ hướng đối tượng, là các lớp (class) baogồm cả dữ liệu (data) và các phương thức xử lý (methods)
I.1.3 Cấu trúc dữ liệu
Có thể nói những thuật ngữ: kiểu dữ liệu, kiểu dữ liệu trừu tượng và cấutrúc dữ liệu (Data Types, Abstract Data Types, Data Structures) nghe rất giốngnhau, nhưng thực ra chúng có ý nghĩa khác nhau
Trong ngôn ngữ lập trình, kiểu dữ liệu của biến là tập các giá trị mà biếnnày có thể nhận Ví dụ, biến kiểu boolean chỉ có thể nhận giá trị true hoặc false(đúng hoặc sai) Các kiểu dữ liệu cơ bản có thể thay đổi từ ngôn ngữ lập trình(NNLT) này sang NNLT khác Ta có thể tạo những kiểu dữ liệu phức hợp từnhững kiểu dữ liệu cơ bản Cách tạo cũng phụ thuộc vào ngôn ngữ lập trình.Kiểu dữ liệu trừu tượng là mô hình toán học cùng với những phép toán xácđịnh trên mô hình này Nó không phụ thuộc vào ngôn ngữ lập trình
Để biểu diễn mô hình toán học trong ADT ta sử dụng cấu trúc dữ liệu
Cấu trúc dữ liệu (Data Structures) là một họ các biến, có thể có kiểu dữ
liệu khác nhau, được liên kết lại theo một cách thức nào đó
Việc cài đặt ADT đòi hỏi lựa chọn cấu trúc dữ liệu để biểu diễn ADT Ta
sẽ xét xem việc làm đó được tiến hành như thế nào?
Ô (cell) là đơn vị cơ sở cấu thành cấu trúc dữ liệu Có thể hình dung ô như
là cái hộp đựng giá trị phát sinh từ một kiểu dữ liệu cơ bản hay phức hợp
Trang 6Cấu trúc dữ liệu được tạo nhờ đặt tên cho một nhóm các ô và đặt giá trị chomột số ô để mô tả sự liên kết giữa các ô Ta xét một số cách tạo nhóm.
Một trong những cách tạo nhóm đơn giản nhất trong các ngôn ngữ lập trình
đó là mảng (array) Mảng là một dãy các ô có cùng kiểu xác định nào đó
Ví dụ: Khai báo trong Pascal sau đây:
Var name: array[1 10] of integer;
Khai báo biến name gồm 10 phần tử kiểu integer
Có thể truy xuất đến phần tử của mảng nhờ chỉ ra tên mảng cùng với chỉ sốcủa nó
Một phương pháp chung nữa hay dùng để nhóm các ô là cấu trúc bản ghi(record structure)
Bản ghi (record) là ô được tạo bởi một họ các ô (gọi là các trường) có thể
có kiểu rất khác nhau Các bản ghi lại thường được nhóm lại thành mảng; kiểuđược xác định bởi việc nhóm các trường của bản ghi trở thành kiểu của phần tửcủa mảng
Phương pháp thứ ba để nhóm các ô là file File, cũng giống như mảng mộtchiều, là một dãy các giá trị cùng kiểu nào đó
Khi lựa chọn cấu trúc dữ liệu cài đặt ADT một vấn đề cần được quan tâm
là thời gian thực hiện các phép toán đối với ADT sẽ như thế nào Bởi vì, cáccách cài đặt khác nhau có thể dẫn đến thời gian thực hiện phép toán khác nhau
Con trỏ (Pointer)
Một trong những ưu thế của phương pháp nhóm các ô trong các ngôn ngữlập trình là ta có thể biểu diễn mối quan hệ giữa các ô nhờ sử dụng con trỏ
Định nghĩa Con trỏ (pointer) là ô mà giá trị của nó chỉ ra một ô khác.
Khi vẽ các cấu trúc dữ liệu, để thể hiện ô A là con trỏ trỏ đến ô B, ta sẽ sửdụng mũi tên hướng từ A đến B
Hình 1
Phân loại các cấu trúc dữ liệu:
Trong nhiều tài liệu về CTDL thường sử dụng phân loại cấu trúc dữ liệusau đây:
• Cấu trúc dữ liệu cơ sở (Base data structures) Ví dụ, trong Pascal: integer, char, real, boolean,…; trong C: int, char, float, double,…
• Cấu trúc dữ liệu tuyến tính (Linear data structure) Ví dụ, Mảng (Array), Danh sách liên kết (Linked list), Ngăn xếp (Stack), Hàng đợi (Queue),…
• Cấu trúc dữ liệu phi tuyến (Non linear data structures) Ví dụ, Cây
(Trees), Đồ thị (Graphs), bảng băm (hash table),…
Trang 7I.2 Sơ lược giới thiệu về con trỏ và biến động
I.2.1 Biến tĩnh (Static variable)
Biến tĩnh là biến được khai báo ngay trong mục var của chương trình.Chúng được xác định rõ ràng khi khai báo và sau đó được dùng thông qua têncủa nó
Thời gian tồn tại của các biến tĩnh cũng là thời gian tồn tại của khối chươngtrình có chứa khai báo các biến này
I.2.2 Biến động (Dynamic variable)
I.2.2.1 Khái niệm
Biến động là biến không được khai báo trước trong chương trình Khi nàocần dùng ta mới yêu cầu máy cấp phát bộ nhớ cho biến động đó Khi nào khôngcần dùng có thể xoá biến động đi để giải phonga bộ nhớ
Vùng bộ nhớ lưu trữ các biến động là HEAP (kích thước rất lớn)
Biến động không có tên, nó được truy nhập thông qua biến con trỏ (pointervariable)
Ví dụ: Để truy nhập vào biến động do con trỏ p trỏ tới ta viết là p^
P^
Hình 2
I.2.2.2 Cấp phát vùng nhớ cho biến động
Để cấp phát vùng nhớ cho các biến động do con trỏ p trỏ tới, ta dùng thủtục NEW như sau: NEW(p);
Khi đó máy sẽ tạo ra một vùng nhớ có kiểu và kích thước do p qui định,hướng con trỏ p trỏ tới byte đầu tiên của vùng biến động trên
Ta chỉ được dùng biến động p^ khi đã có lệnh New(p);
P Nguyễn Linh Chi
9.5
Trang 8I.2.2.3 Giải phóng hay thu hồi ô nhớ của biến động
Khi một biến động không được dùng tới nữa ở trong chương trình, ta có thểthu hồi lại ô nhớ nó chiếm để dùng vào việc khác bằng thủ tục DISPOSE
Để giải phóng ô nhớ của biến động p^, ta dùng lệnh: DISPOSE(p);
I.2.3 Khai báo kiểu con trỏ (Xem [4], Trang 122)
Kiểu con trỏ là một kiểu dữ liệu đặc biệt dùng để biểu diễn địa chỉ của cácđối tượng (biến, mảng, bản ghi, …), có bao nhiêu đối tượng thì có bấy nhiêukiểu con trỏ Kiểu con trỏ nguyên dùng để biểu thị địa chỉ của biến nguyên, kiểucon trỏ bản ghi dùng để biểu diễn địa chỉ của bản ghi…
Cách khai báo một kiểu con trỏ:
I.2.4 Khai báo biến con trỏ (Pointer)
Biến con trỏ được khai báo thông qua các kiểu con trỏ đã được định nghĩatrong phần TYPE hoặc có thể khai báo trực tiếp
Trang 9I.2.5.2 So sánh hai biến con trỏ cùng kiểu
Chỉ có hai phép so sánh là bằng (=) và khác (<>) với hai biến con trỏ cùngkiểu
Chú ý: Các giá trị của biến con trỏ không thể đọc vào từ bàn phím hay in
trực tiếp trên màn hình, máy in được, tức là không thể dùng với các thủ tụcRead/Write
I.2.5.3 Hằng con trỏ NIL
NIL là một giá trị hằng đặc biệt dành cho các biến con trỏ để báo con trỏkhông trỏ vào đâu cả Ta có thể gán NIL cho bất kỳ biến con trỏ nào Chẳng hạnkhi gán p:=NIL thì p không trỏ đến dữ liệu nào cả
I.2.6 Con trỏ kiểu mảng và mảng các con trỏ
Dùng để cấp phát động các mảng
Ví dụ minh hoạ cách nhập, in biến a là biến con trỏ kiểu mảng, biến b làmảng các con trỏ Đối với mảng các bản ghi cùng làm tương tự
Const maxn = 100;
Type mang = array[1 maxn] of integer;
Var a: ^mang; {con trỏ kiểu mảng}
b: array[1 maxn] of ^integer; {mảng các con trỏ}
for i:=1 to n do write(a^[i], ‘ ’); writeln;
for i:=1 to n do write(b[i]^, ‘ ’);
End
Trang 10Chương II
DANH SÁCH TUYẾN TÍNH
II.1 Khái niệm và các phép toán cơ bản
II.1.1 Khái niệm
Danh sách tuyến tính (Linear List) là dãy gồm 0 hoặc nhiều hơn các phần
tử cùng kiểu cho trước: (a1, a2, …, an), n > 0
• ai là phần tử của danh sách
• a1 là phần tử đầu tiên và an là phần tử cuối cùng
• n là độ dài của danh sách
Khi n = 0, ta có danh sách rỗng (empty list) Các phần tử được sắp thứ tựtuyến tính theo vị trí của chúng trong danh sách Ta nói ai đi trước ai+1, ai+1 đi sau
ai và ai ở vị trí i
Ví dụ:
• Danh sách các sinh viên được sắp thứ tự theo tên
• Danh sách điểm thi sắp xếp theo thứ tự giảm dần
Đưa vào ký hiệu:
L : danh sách các đối tượng có kiểu element_type
x : một đối tượng kiểu này
p : kiểu vị trí
END(L) : hàm trả lại vị trí đi sau vị trí cuối cùng trong danh sách L
II.1.2 Các phép toán cơ bản
Dưới đây ta kể ra một số phép toán đối với danh sách tuyến tính
1 Insert(x,p,L)
• Chèn x vào vị trí p trong danh sách L
• Nếu p = END(L), chèn x vào cuối danh sách
Nếu L không có vị trí p, kết quả là không xác định
2 Locate(x,L)
• Trả lại vị trí của x trong L
• Trả lại END(L) nếu x không xuất hiện
Trang 115 Next(p,L)
• Trả lại vị trí đi ngay sau vị trí p
• Nếu p là vị trí cuối cùng trong L thì Next(p,L)=END(L)
• Kết quả không xác định nếu p là END(L) hoặc p không tồn tại
• In ra danh sách các phần tử của L theo thứ tự xuất hiện
II.2 Các cách cài đặt danh sách tuyến tính
Có các cách cài đặt danh sách tuyến tính cơ bản sau đây:
• Dùng mảng (Array-based): Cất giữ các phần tử của danh sách vào các ô liên tiếp của mảng
• Danh sách liên kết (Linked list / Pointer-based): Các phần tử của danh sách có thể cất giữ ở các chỗ tuỳ ý trong bộ nhớ Mỗi phần tử có con trỏ (hoặc móc nối-link) đến phần tử tiếp theo
• Địa chỉ không trực tiếp (Indirect addressing): Các phần tử của danh sách
có thể cất giữ ở các chỗ tuỳ ý trong bộ nhớ Tạo bảng trong đó phần tử thứ i của bảng cho biết nơi lưu trữ phần tử thứ i của danh sách
II.2.1 Biểu diễn dưới dạng mảng
(Array-based Representation of Linear List).
Ta cất giữ các phần tử của danh sách tuyến tính vào các ô liên tiếp của mảng (array)
Danh sách sẽ là cấu trúc gồm hai thành phần:
• Thành phần 1: là mảng các phần tử
• Thành phần 2: last – cho biết vị trí của phần tử cuối cùng trong danh sách
Vị trí có kiểu nguyên (integer chẳng hạn) và chạy trong khoảng từ 0 đến maxlength-1 Hàm END(L) trả lại giá trị last-1
Trang 12Danh sách có thể được mô tả bằng hình vẽ sau:
Phần tử đầu tiên Phần tử thứ hai
Phần tử cuối cùng
Hình 3Cấu trúc dữ liệu mô tả danh sách dưới dạng mảng:
Const maxlength = 1000; {giá trị thích hợp}
Insert(x,p,L): chèn x vào vị trí p trong danh sách L
procedure INSERT(x: elementtype; p: position; var L: LIST); var q: position;
L.last:= L.last + 1;
L.elemenst[p]:= x;
end;
end;
Delete(p,L): loại phần tử ở vị trí p trong danh sách L
Procedure DELETE(p: position; var L: LIST);
Trang 13(maxlength) Điều này dẫn đến lãng phí bộ nhớ.
• Các thao tác chèn một phần tử vào danh sách và xoá bỏ một phần tử khỏidanh sách được thực hiện chậm (với thời gian tuyến tính đối với kích thướcdanh sách)
II.2.2 Danh sách móc nối
II.2.2.1 Lưu trữ móc nối đối với danh sách tuyến tính – Linked list
Lưu trữ kế tiếp có những nhược điểm cơ bản đã được phân tích ở trên: đó
là việc bổ sung và loại bỏ phần tử là rất tốn kém thời gian, ngoài ra phải kể đến
việc sử dụng một không gian liên tục trong bộ nhớ
Việc tổ chức con trỏ (hoặc mối nối) để tổ chức danh sách tuyến tính – mà
ta gọi là danh sách móc nối là giải pháp khắc phục nhược điểm này, tuy nhiêncái giá mà ta phải trả là bộ nhớ dành cho con trỏ
Một số cách tổ chức danh sách móc nối:
• Danh sách móc nối đơn (Singly linked list)
• Danh sách nối vòng (Circulary linked list)
• Danh sách nối kép (Doubly linked list)
Khi nào dùng danh sách móc nối:
• Khi không biết kích thước của dữ liệu – hãy dùng con trỏ và bộ nhớ động
(Unknown data size – use pointer & dynamic storage).
• Khi không biết kiểu dữ liệu – hãy dùng con trỏ void
Trang 14(Unknown data type – use void pointers)
• Khi không biết số lượng dữ liệu – hãy dùng danh sách móc nối (Unknown
number of data – linked structure)
II.2.2.2 Danh sách móc nối đơn - (Singly linked list)
Trong cách biểu diễn này, danh sách bao gồm các ô (các nút – node), mỗi ôchứa một phần tử của danh sách và con trỏ trỏ đến ô tiếp theo của danh sách.Nếu danh sách là a1, a2, …, an thì ô lưu trữ ai có con trỏ (mối nối) đến ô lưu
trữ ai+1 với i = 1,2, …, n-1 Ô lưu trữ an có con trỏ rỗng, mà ta sẽ ký hiệu là nil.
Như vậy mỗi ô có cấu trúc:
Element Link/Pointer
Có một ô đặc biệt gọi là ô header để trỏ ra ô chứa phần tử đầu tiên trong
danh sách (a1); Ô header không lưu trữ phần tử nào cả Trong trường hợp danhsách rỗng, con trỏ của header là nil (hoặc null), và không có ô nào khác
Các ô có thể nằm ở vị trí bất kỳ trong bộ nhớ
Danh sách móc nối được tổ chức như trong hình vẽ sau:
Hình 4Mối nối chỉ ra địa chỉ bộ nhớ của nút tiếp theo trong danh sách
Danh sách nối đơn là một kiểu dữ liệu trừu tượng Để cài đặt kiểu dữ liệutrừu tượng này, chúng ta có thể dùng mảng các nút (trường next chứa chỉ số củanút kế tiếp) hoặc biến cấp phát động (trường next chứa con trỏ tới nút kế tiếp).Cấu trúc dữ liệu mô tả danh sách dưới dạng danh sách móc nối:
Cách 1: Dùng mảng các nút
Const max = 1000; {Số phần tử cực đại}
Elementtype = integer; {Kiểu dữ liệu của phần tử}
Trang 15var header: PElem;
Danh sách nối đơn gồm các nút được nối với nhau theo một chiều Mỗi nút
là một bản ghi (record) gồm hai trường:
• Trường val chứa giá trị lưu trong nút đó
• Trường next chứa liên kết (con trỏ) tới nút kế tiếp, tức là chứa một thôngtin đủ để biết nút kế tiếp nút đó trong danh sách là nút nào, trong trường hợp lànút cuối cùng (không có nút kế tiếp), trường liên kết này được gán một giá trịđặc biệt, chẳng hạn con trỏ nil
Như vậy việc quản lý một danh sách móc nối dù có ít hay có rất nhiều phần
tử chỉ thông qua một con trỏ header duy nhất Hình ảnh danh sách móc nối với
việc quản lý từ một đầu giống như một cậu bé đang cầm đầu dây của một chiếcdiều đang bay trên bầu trời hay một cô gái cầm dải lụa rất dài múa dẻo trên sânkhấu
Để duyệt danh sách nối đơn, ta bắt đầu từ nút đầu tiên, dựa vào trường liênkết để đi sang nút kế tiếp, đến khi gặp giá trị đặc biệt (duyệt qua nút cuối) thìdừng lại
Ta có thể cài đặt các phép toán cơ bản trên danh sách móc nối đơn bằngngôn ngữ lập trình Free Pascal
DANH SÁCH VÀ MỘT SỐ GIẢI THUẬT XỬ LÝ TRÊN DANH SÁCH NỐI ĐƠN
Trang 16Thêm nút p vào đầu danh sách L
procedure AddFirst(p: PElem; var L: PElem);
begin
p^.Next:= L;
L:= p;
end;
Muốn thêm nút p vào đầu danh sách L ta thực hiện 2 thao tác:
Thao tác 1: Gán 2 con trỏ p^.Next:= L
Nghĩa là L trỏ vào đâu thì con trỏ p^.Next trỏ vào đấy
Thao tác 2: Gán 2 con trỏ L:= p
Nghĩa là p trỏ vào đâu thì L trỏ vào đấy
Các thao tác mô tả bằng Hình 5 sau:
Hình 5
Để dễ hiểu ta có có thể coi mỗi ô là một tấm bìa cứng, mỗi tấm bìa đều códây nối với tấm bìa kế tiếp Khi đó, hai thao tác gán trên tương đương với việc:buộc sợi dây của tấm bìa p vào đầu danh sách L, sau đó ta nắm lấy đầu tấm bìa plàm đầu danh sách L sau khi thêm p
Thêm nút p vào cuối danh sách có con trỏ đầu là d, con trỏ cuối là c
procedure Add(p: PElem; var d,c: PElem);
Procedure Insert(p: Pelem; const v: Elementtype);
Var newnode, q: Pelem;
an
v P
P^.Next
L
Trang 17while q^.next <> p then q:= q^.next;
Đếm số phần tử của danh sách L bằng đệ qui
function RCard(d: PElem): integer;
begin
if d = nil then RCard:= 0
else RCard:= RCard(d^.Next) + 1;
end;
Lật ngược danh sách L không đệ qui
function Rev(L: PElem): PElem;
Trang 18Rev:= s;
end;
Lật ngược danh sách L bằng đệ qui
function RRev(L: PElem): PElem;
var t,u: PElem;
begin
RRev:= L;
if L = nil then exit;
if L^.Next = nil then exit;
Kiểm tra hai danh sách p và q có giống nhau hay không?
function IsEqual(p,q: PElem): Boolean;
Đếm số phần tử của danh sách L không đệ qui
function Card(L: PElem): integer;
Tính tổng các phần tử chia hết cho k của danh sách L
function SumDivk(L: PElem; k: integer): integer; var s: integer;
Trang 19Tách danh sách d thành hai danh sách mới
procedure Split(var d,c,l: PElem);
t:= d; d:= d^.Next; { Lay the dau tien }
if Odd(t^.Val) then Add(t,l,cl) else Add(t,c,cc); end;
end;
Xoá nút t khỏi danh sách L
procedure DelElem(var L: PElem; s: PElem; var t: PElem); begin
if t = L then { xoa dau danh sach t }
Xoá khỏi danh sách L những phần tử chia hết cho k
procedure Del(var L: PElem; k: integer);
Tìm con trỏ cuối của danh sách L
function FindLast(L: PElem): PElem;
Gộp hai danh sách a và b thành một danh sách
function Merge(a, b: PElem): PElem;
Trang 20writeln(nl,' Tong cac pt chan: ', Sumdivk(d,2));
writeln(nl,' Tong toan bo: ', SumDivk(d,1));
Trang 21- Việc chỉnh lại liên kết trong phép chèn phần tử vào danh sách nối đơn mấtthời gian O(1), tuy nhiên việc tìm nút đứng liền trước nút p yêu cầu phải duyệt
từ đầu danh sách, việc này mất thời gian trung bình O(n) Vậy phép chèn mộtphần tử vào danh sách nối đơn mất thời gian trung bình O(n) để thực hiện
II.2.2.3 Danh sách nối đôi - (Doubly linked list)
Trong nhiều ứng dụng ta muốn duyệt danh sách theo cả hai chiều một cáchhiệu quả Hoặc cho một phần tử, ta cần xác định cả phần tử đi trước lẫn phần tử
đi sau nó trong danh sách một cách nhanh chóng Trong tình huống như vậy ta
có thể gán cho mỗi ô trong danh sách con trỏ đến cả phần tử đi trước lẫn phần tử
đi sau nó trong danh sách Cách tổ chức này được gọi là danh sách nối đôi
Cách tổ chức danh sách nối đôi được minh hoạ trong hình vẽ sau:
Hình 6
Có hai nút đặc biệt: tail (đuôi) và head (đầu)
• head có con trỏ trái prev là nil
• tail có con trỏ phải next là nil
Các phép toán cơ bản được xét tương tự như danh sách nối đơn
Danh sách nối kép gồm các nút được nối với nhau theo hai chiều Mỗi nút
là một bản ghi (record) gồm ba trường:
• Trường val chứa giá trị lưu trong nút đó
• Trường next chứa liên kết (con trỏ) tới nút kế tiếp, tức là chứa một thôngtin đủ để biết nút kế tiếp nút đó trong danh sách là nút nào, trong trường hợp lànút cuối cùng (không có nút kế tiếp), trường liên kết này được gán một giá trịđặc biệt, chẳng hạn con trỏ nil
• Trường prev chứa liên kết (con trỏ) tới nút liền trước, tức là chứa mộtthông tin đủ để biết nút liền trước nút đó trong danh sách là nút nào, trongtrường hợp là nút đầu tiên trong danh sách (không có nút liền trước), trường liênkết này được gán một giá trị đặc biệt, chẳng hạn con trỏ nil
B C
next
Trang 22Cách mô tả danh sách nối đôi trên NNLT Free Pascal:
vad head, tail: PElem;
Có thể cài đặt các phép toán cơ bản trên danh sách móc nối đôi bằng ngônngữ lập trình Free Pascal:
DANH SÁCH VÀ MỘT SỐ GIẢI THUẬT XỬ LÝ TRÊN DANH SÁCH NỐI ĐÔI
var head,tail: PElem;
Tạo một nút mới có giá trị v và con trỏ prev là prv, con trỏ next là nxt
function NewElem(v: integer; var prv,nxt: PElem): PElem;
Trang 23Tạo một danh sách nối đôi có n phần tử sắp xếp tăng dần
procedure Gentang(n: integer; var head, tail: PElem);
Hiển thị danh sách nối đôi theo chiều xuôi
procedure PrintList1(head,tail: PElem);
Hiển thị danh sách nối đôi theo chiều ngược
procedure PrintList2(head,tail: PElem);
Trang 24Test kết quả thực hiện một số thao thác cơ bản trên danh sách nối đôi
II.2.3 Bài toán Josephus
N khách hàng tham gia vào vòng quay trúng thưởng của Công ty X Cáckhách hàng được xếp thành một vòng tròn đánh số 1,2,…,n Giám đốc Công tylựa chọn ngẫu nhiên một số m (m < n) Bắt đầu từ một người được chọn ngẫunhiên trong số khách hàng, Giám đốc đếm theo chiều kim đồng hồ và dừng lạimỗi khi đếm đến m Khách hàng ở vị trí này sẽ dời khỏi cuộc chơi Quá trìnhđược lặp lại cho đến khi chỉ còn một người Người cuối cùng còn lại là ngườitrúng thưởng!
Hình vẽ dưới đây mô tả trò chơi với n=10, m=5
7
8 9 10
Người thắng cuộc
Trang 25var head,tail: PElem; n,m: integer;
function NewElem(v: integer; var prv,nxt: PElem): PElem; var e: PElem;
procedure josephus(n: integer; m: integer);
var i,j : integer;
Trang 26II.2.4 Phân tích sử dụng linked list
Những ưu điểm của việc dùng linked list:
• Không xảy ra vượt mảng, ngoại trừ hết bộ nhớ
• Chèn và xoá được thực hiện dễ dàng hơn là cài đặt mảng
• Với những bản ghi lớn, thực hiện di chuyển con trỏ là nhanh hơn nhiều sovới thực hiện di chuyển các phần tử của danh sách
Những bất lợi khi sử dụng linked list:
• Dùng con trỏ đòi hỏi bộ nhớ phụ
• Linked list không cho phép trực truy
• Tốn thời gian cho cho việc duyệt và biến đổi con trỏ
• Lập trình với con trỏ là khá rắc rối.
So sánh các phương pháp
Việc lựa chọn cách cài đặt mảng hay cài đặt con trỏ để biểu diễn danh sách
là tuỳ thuộc vào việc thao tác nào là thao tác thường phải dùng nhất Dưới đây làmột số nhận xét về hai cách cài đặt:
• Cài đặt mảng phải khai báo kích thước tối đa Nếu ta không lường trướcđược giá trị này thì nên dùng cài đặt con trỏ
• Có một số thao tác có thể thực hiện nhanh trong cách này nhưng lại chậmtrong cách cài đặt kia: Insert và Delete đòi hỏi thời gian hằng số trong cài đặtcon trỏ nhưng trong cài đặt mảng đòi hỏi thời gian O(n) với n là số phần tử củadanh sách Previous và End đòi hỏi thời gian hằng số trong cài đặt mảng, nhưngthời gian đó là O(n) trong cài đặt con trỏ
• Cách cài đặt mảng đòi hỏi dành không gian nhớ định trước không phụthuộc vào số phần tử thực tế của danh sách Trong khi đó cài đặt con trỏ chỉ đòihỏi bộ nhớ cho các phần tử đang có trong danh sách Tuy nhiên cách cài đặt contrỏ lại đòi hỏi thêm bộ nhớ cho con trỏ
Trang 27II.3 Ngăn xếp (Stack)
II.3.1 Kiểu dữ liệu trừu tượng ngăn xếp
Ngăn xếp là dạng đặc biệt của danh sách tuyến tính trong đó các đối tượngđược nạp vào (push) và lấy ra (pop) chỉ từ một đầu gọi là đỉnh (top) của danhsách
Nguyên tắc hoạt động: Vào sau-ra trước, Last in – first out (LIFO)
Các phép toán cơ bản (stack operations):
• Init(): Khởi tạo một ngăn xếp rỗng
• IsEmpty(): Cho biết ngăn xếp có rỗng không?
• IsFull():Cho biết ngăn xếp có đầy không?
• Get(): Lấy ra một phần tử từ ngăn xếp mà không loại nó khỏi ngăn xếp
• Push (object): Bổ sung vào ngăn xếp một phần tử, gọi tắt là đẩy vào
• Pop(): loại bỏ và trả lại phần tử nạp vào sau cùng, gọi tắt là lấy ra
Có hai cách tổ chức ngăn xếp:
• Sử dụng mảng
• Sử dụng danh sách móc nối
II.3.2 Tổ chức ngăn xếp bằng mảng (Array-based Stack)
Cách biểu diễn ngăn xếp bằng mảng cần có một mảng Items để lưu cácphần tử trong ngăn xếp và một biến nguyên top để lưu chỉ số của phần tử tạiđỉnh ngăn xếp
Const maxn = 1000; {Dung lượng cực đại của ngăn xếp}
Var Stack: TStack;
Sáu thao tác cơ bản của ngăn xếp có thể cài đặt bằng ngôn ngữ Free Pascalnhư sau:
Trang 28if IsEmpty(S) then write('Stack rong!')
else with S do Get:= items[top];
Trang 29II.3.3 Tổ chức cài đặt ngăn xếp bằng danh sách móc nối (Linked Stack)
Ta sẽ trình bày cách cài đặt ngăn xếp bằng danh sách nối đơn các biến động
và con trỏ Trong cách cài đặt này, ngăn xếp sẽ bị đầy nếu như vùng không giannhớ dùng cho các biến động không còn đủ để thêm một phần tử mới Tuy nhiên,việc kiểm tra điều này phụ thuộc vào máy tính, chương trình dịch và ngôn ngữlập trình Mặt khác, không gian bộ nhớ dùng cho các biến động thường rất lớnnên ta sẽ không viết mã cho hàm IsFull: kiểm tra ngăn xếp tràn
Các khai báo dữ liệu:
Var top: PElem; {Con trỏ tới phần tử đỉnh ngăn xếp}
Khi đó ta quản lí danh sách thông qua một con trỏ top trỏ vào phần tử ở đỉnh ngăn xếp
Các thao tác trên ngăn xếp được cài đặt bằng ngôn ngữ lập trình Free
Pascal như sau:
If IsEmpty(top) then write('Stack rong')
else Get:= (top^.val);
end;
Procedure Push(x: TElement;var top: PElem);
var p: PElem;
begin
Trang 30writeln('Xem Stack top con phan tu khong:');
while top<> nil do
begin
write(top^.val, ' ');
top:= top^.next;
Trang 31• Lịch sử duyệt trang trong trình duyệt Web
• Dãy Undo trong bộ soạn thảo văn bản
• Kiểm tra tính hợp lệ của các dấu ngoặc trong biểu thức
• Đổi cơ số
• Ứng dụng trong cài đặt chương trình dịch (Compiler implementation)
• Tính giá trị biểu thức (Evaluation of expression)
• Quay lui (Backtracking)
• Khử đệ qui
• …
Các ứng dụng khác (Indirect applications):
• Cấu trúc dữ liệu hỗ trợ cho các thuật toán
• Thành phần của các cấu trúc dữ liệu khác
Trang 32II.4 Hàng đợi (Queue)
II.4.1 Kiểu dữ liệu trừu tượng hàng đợi
Hàng đợi (Queue) là một kiểu danh sách mà việc bổ sung một phần tử đượcthực hiện ở cuối danh sách và việc loại bỏ một phần tử được thực hiện ở đầudanh sách
Khi cài đặt hàng đợi, có hai vị trí quan trọng là vị trí đầu danh sách (front
or head), nơi các phần tử được lấy ra, và vị trí cuối danh sách (rear or back), nơiphần tử cuối cùng được đưa vào
Có thể hình dung hàng đợi như một đoàn người xếp hàng mua vé: Ngườinào xếp hàng trước sẽ được mua vé trước
Các phần tử được lấy ra khỏi hàng đợi theo qui tắc: Vào trước – Ra trước,
vì thế hàng đợi còn còn có tên gọi là danh sách kiểu FIFO (First In First Out).Một số thao tác cơ bản trên hàng đợi:
• Init: Khởi tạo một hàng đợi rỗng
• IsEmpty: Cho biết hàng đợi có rỗng không?
• IsFull: Cho biết hàng đợi có đầy không?
• Get: Đọc giá trị phần tử ở đầu hàng đợi
• Push: Đẩy một phần tử vào cuối hàng đợi
• Pop: Lấy ra một phần tử từ hàng đợi
Các thuật ngữ liên quan đến hàng đợi được mô tả trong hình vẽ sau đây:
Hình 8
II.4.2 Tổ chức hàng đợi bằng mảng (Array-based Queue)
Ta có thể biểu diễn hàng đợi bằng một mảng items để lưu các phần tử tronghàng đợi, một biến nguyên front để lưu chỉ số phần tử đầu hàng đợi và một biếnnguyên rear để lưu chỉ số phần tử cuối hàng đợi Chỉ một phần của mảng items
từ vị trí front tới rear được sử dụng lưu trữ các phần tử trong hàng đợi
Const max = 1000; {Dung lượng cực đại}
Trang 33Các thao tác cơ bản trên hàng đợi được cài đặt bằng ngôn ngữ lập trình Free Pascal như sau:
if IsEmpty(Q) then write('Queue rong!')
else Get:= Q.items[Q.front];
Trang 34II.4.3 Tổ chức hàng đợi bằng danh sách vòng (Xem [1], Trang 21)
Xét việc biểu diễn ngăn xếp và hàng đợi bằng mảng, giả sử mảng có tối đa
10 phần tử, ta thấy rằng nếu thực hiện 6 lần thao tác Push, rồi 4 lần thao tác Pop,rồi tiếp tục 8 lần thao tác Push nữa thì không có vấn đề gì xảy ra với Stacknhưng sẽ gặp thông báo lỗi tràn mảng đối với Queue
Lý do dẫn đến lỗi trên là do chỉ số cuối hàng đợi rear luôn tăng lên vàkhông bao giờ giảm đi cả Đó chính là nhược điểm mà ta nói tới khi cài đặt: Chỉ
có các phần tử từ vị trí front tới vị trí rear là thuộc hàng đợi, các phần tử từ vị trí
1 tới front-1 là vô nghĩa
Để khắc phục điều này, ta có thể biểu diễn hàng đợi bằng một danh sáchvòng (dùng mảng hoặc danh sách nối vòng đơn): coi như các phần tử của hàngđợi được xếp quanh vòng tròn theo một chiều nào đó (chẳng hạn chiều kim đồnghồ) Các phần tử nằm trên phần cung tròn từ vị trí front tới vị trí rear là các phần
tử của hàng đợi Có thêm một biến n lưu số phần tử trong hàng đợi Việc đẩythêm một phần tử vào hàng đợi tương đương với việc ta dịch chỉ số rear theochiều vòng một vị trí rồi đặt giá trị mới vào đó Việc lấy ra một phần tử tronghàng đợi tương đương với việc lấy ra phần tử tại vị trí front rồi dịch chỉ số fronttheo chiều vòng
Trang 35Hình vẽ sau mô tả cách dùng danh sách vòng để mô tả hàng đợi:
Hình 9
Để tiện cho việc dịch chỉ số theo vòng, khi cài đặt danh sách vòng bằngmảng, người ta thường dùng cách đánh chỉ số từ 0 để tiện sử dụng phép lấy dư(modulus – mod)
Const max = 1000; {Dung lượng cực đại}
Var Queue: TQueue;
Các thao tác cơ bản trên hàng đợi được cài đặt bằng ngôn ngữ lập trìnhFree Pascal như sau:
Trang 36if IsEmpty(Q) then write('Queue rong!')
else Get:= Q.items[Q.front];
Trang 37II.4.4 Tổ chức hàng đợi bằng danh sách nối đơn kiểu FIFO
Tương tự như cài đặt ngăn xếp bằng biến động và con trỏ trong một danhsách nối đơn, ta cũng không viết hàm IsFull để kiểm tra hàng đợi đầy
If IsEmpty then write('Stack rong')
else Get:= (front^.val);
Trang 38Chương III
CÂY NHỊ PHÂN
III.1 Định nghĩa, tính chất và phân loại
III.1.1 Định nghĩa (Xem [3], trang 82)
Cây nhị phân là cây mà mỗi nút có nhiều nhất là hai con
Vì mỗi nút chỉ có không quá hai con, nên ta sẽ gọi chúng là con trái và conphải (left and right child) Như vậy mỗi nút của cây nhị phân hoặc là không cócon, hoặc chỉ có con trái, hoặc chỉ có con phải, hoặc có cả con trái và con phải
Hình 10
Vì ta phân biệt con trái và con phải nên khái niệm cây nhị phân không
trùng với cây có thứ tự (Xem 3 trang 109) Vì thế, chúng ta sẽ không so sánh cây
nhị phân với cây tổng quát
III.1.2 Tính chất của cây nhị phân
Bổ đề 1:
(i) Số đỉnh lớn nhất ở trên mức i của cây nhị phân là 2i-1, i >1
(ii) Một cây nhị phân với chiều cao k có không quá 2k-1 nút, k >1
(iii) Một cây nhị phân có n nút có chiều cao tối thiểu là [log2(n+1)]
Chứng minh:
(i) Bằng qui nạp theo i:
Cơ sở: Gốc là nút duy nhất trên mức i=1 Như vậy số đỉnh lớn nhất trên
mức i=1 là 20=2i-1
Chuyển qui nạp: Giả sử với mọi nút j, 1 < j < i-1, số đỉnh lớn nhất trên mức
j là 2j-1 Do số đỉnh trên mức i-1 là 2i-2, mặt khác theo định nghĩa mỗi đỉnh trêncây nhị phân có không quá 2 con, ta suy ra số lượng nút lớn nhất trên mức i làkhông vượt quá 2 lần số lượng nút trên mức i-1, nghĩa là không vượt quá 2*2i-
1=2i nút