Tải bản đầy đủ (.doc) (76 trang)

MỘT số vấn đề về KIỂU dữ LIỆU TRỪU TƯỢNG

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (473.67 KB, 76 trang )

SỞ GIÁO DỤC VÀ ĐÀO TẠO TỈNH BẮC GIANG
TRƯỜNG THPT CHUYÊN BẮC GIANG

NGUYỄN THỊ HỢP

MỘT SỐ VẤN ĐỀ VỀ
KIỂU DỮ LIỆU TRỪU TƯỢNG

Tổ: Toán-Tin
Năm học: 2011-2012
Mã số: …………………..

Bắc Giang, tháng 4 năm 2012


Phần thứ nhất

MỞ ĐẦU
I. LÝ DO CHỌN ĐỀ TÀI
Những năm gần đây đề thi chọn HSG QG môn Tin học được phân chia
theo ba cấp độ khó dần. Mỗi đề thi gồm có ba bài, trong đó Bài 2 và Bài 3 đòi
hỏi học sinh không những giỏi về thuật toán mà còn phải biết vận dụng linh hoạt
các kiến thức về cấu trúc dữ liệu. Mặt khác các tài liệu viết về cấu trúc dữ liệu
còn rất trừu tượng, tổng quát và khó có thể áp dụng để giảng dạy cho học sinh
chuyên Tin cũng như đội tuyển HSG.
Do đó, 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 cho
cô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ắc
Giang.


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ài
toá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ững
năm vừa qua đều là những tài liệu quí giá không những cho học sinh và giáo
viên trong trường Chuyên mà còn cần thiết cho giáo viên các trường THPT
khá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ật
giả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ật
toá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ọn
cấ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ôi
thấy mình còn hiểu quá ít về nó – đó có khi là thử thách. Ngay trong quá trình
dạ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ệu
trừ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.


Rất mong được sự góp ý của các đồng nghiệp để tài liệu được cải tiến tốt
hơ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ự thi
chọ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êm
mộ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ệu
trừ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áp
nghiê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 3
phầ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


Phầ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


sử 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ượng
hoá 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ụ:
ADT
Đối tượng (Object)
Phép toán (Operations)
Danh sách (List)
các nút
chèn, xoá, tìm, …
Đồ thị (Graphs)
đỉnh, cạnh
duyệt, đường đi, …
Integer
+,-,*, v.v…
-∞…,-1,0,1,…+∞
Real
+,-,*, v.v…
-∞,…,+∞
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,…
Cây nhị phân

các nút
traversal, find,…
Đ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ác
biến trong ADT và các thủ tục trong ngôn ngữ lập trình để thực hiện các phép
toán của ADT, hoặc trong các ngôn ngữ hướng đối tượng, là các lớp (class) bao
gồ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ấu
trúc dữ liệu (Data Types, Abstract Data Types, Data Structures) nghe rất giống
nhau, 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ến
nà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.



Cấu trúc dữ liệu được tạo nhờ đặt tên cho một nhóm các ô và đặt giá trị cho
mộ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ột
chiề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ác
cá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.
B

A


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ệu
sau đâ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),…


I.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ên
củ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ương
trình có chứa khai báo các biến này.
Ví dụ:
- Các biến tĩnh được khai báo trong chương trình chính sẽ tồn tại trong suốt
thời gian chương trình đó chạy.
- Các biến tĩnh được khai báo trong chương trình con sẽ tồn tại mỗi khi
chương trình con đó được gọi.

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ào
cầ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ông

cầ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ỏ (pointer
variable).
Ví dụ: Để truy nhập vào biến động do con trỏ p trỏ tới ta viết là p^
P^
Nguyễn Linh Chi
9.5

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);


I.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êu
kiểu con trỏ. Kiểu con trỏ nguyên dùng để biểu thị địa chỉ của biến nguyên, kiểu

con 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ỏ:
TYPE
Typepointer = ^Kiểu_đối_tượng;
Ví dụ:
Type
IntPtr = ^integer;
hsPtr = ^hs;
hs
= record
Ho_ten: string[27];
Dtb
: real;
End;

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ĩa
trong phần TYPE hoặc có thể khai báo trực tiếp.
Cách khai báo:
VAR
Namepointer1: Typepointer;
Namepointer2: ^Kiểu_đối_tượng;

I.2.5. Các thao tác đối với biến con trỏ
I.2.5.1. Phép gán (:=)
Ta có thể gán hai con trỏ cùng kiểu cho nhau.
Ví dụ:
Var
p,q: ^integer;
Ta có thể thực hiện phép gán: p:= q; (ý nghĩa: con trỏ q trỏ đến vùng nhớ

nào thì con trỏ p trỏ đến vùng nhớ đó);


I.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ùng
kiể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ục
Read/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ạn
khi 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ỏ}
i,n: integer;
Begin
write(‘Vào n =’); readln(n);
new(a);
for i:=1 to n do
begin

write(‘a[‘,i, ‘]=’);
readln(a^[i]);
end;
for i:=1 to n do
begin
new(b[i]);
write(‘b[‘,i, ‘]=’);
readln(b[i]^);
end;
for i:=1 to n do write(a^[i], ‘ ’); writeln;
for i:=1 to n do write(b[i]^, ‘ ’);
End.


Chươ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 a i đ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
3. Retrieve(p,L)
•Trả lại phần tử ở vị trí p trong L
•Không xác định nếu p không tồn tại hoặc p=END(L)
4. Delete(p,L)
• Xoá phần tử ở vị trí p trong L. Nếu L là a 1, a2, …, an, thì L sẽ là a1, a2, …,
ap-1, ap+1,…, an.
• Kết quả là không xác định nếu L không có vị trí p hoặc p=END(L)


5. 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.
6. Prev(p,L)
• Trả lại vị trí trước vị trí p
• Kết quả không xác định nếu p là 1 hoặc p không tồn tại.
7. Makenull(L)
• Hàm này biến L trở thành danh sách rỗng và trả lại vị trí END(L).
8. First(L)
• Trả lại vị trí đầu tiên trong L. Nếu L là rỗng hàm này trả lại END(L).
9. Printlist(L)
• 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.


Danh sách có thể được mô tả bằng hình vẽ sau:
Phần tử đầu
tiên
list

Phần tử thứ hai

Phần tử cuối
cùng

last
rỗng
maxlength

Hình 3
Cấ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}
Type
Elementtype = enteger; {kiểu của phần tử là nguyên}
LIST = Record
elements: array[1..maxlength] of Elementtype;
last: integer;
end;
position = integer; {vị trí có kiểu nguyên}
var L: LIST;

Cài đặt các phép toán cơ bản bằng ngôn ngữ lập trình Pascal:

Hàm END(L)
function END(var L: LIST): position;
begin
exit(L.last+1)
end;

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;
begin
if L.last >= maxlength then write(‘Error: List is full’)
else
if (p>L.last+1) or (p<1) then write(‘Vị trí là không tồn tại’)
else
begin
for q:=L.last downto p do
L.elements[q+1]:=L.element[q];
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);
Var q: position;
begin
if (p>L.last) or (p<1) then write(‘Vị trí là không tồn tại’)


else

begin
L.last:= L.last – 1;
for q:=p to L.last do L.elements[q]:= L.elements[q+1];
end;
end;

Hàm Locate(x,L): trả lại vị trí của phần tử x trong danh sách L, nếu
không tìm thấy x trong danh sách trả lại vị trí last+1.
Function LOCATE(x: elementtype; var L: LIST): position;
var q: position;
begin
for q:=1 to L.last do
if L.elements[q] = x then exit(q);
exit(L.last+1);
end;

Có thể nhận thấy một số ưu - khuyết điểm sau đây của cách tổ chức lưu trữ
này:


Cách biểu diễn này rất tiện cho việc truy xuất đến các phần tử của danh

sách.
Do danh sách là biến động, số phần tử trong danh sách là không biết
trước. Nên ta thường phải khai báo kích thước tối đa cho mảng để dự phòng
(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ỏi
danh sách được thực hiện chậm (với thời gian tuyến tính đối với kích thước
danh 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ên
cá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


(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 danh
sá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:
header

a1

a2

. . .

an

nil

list

Hình 4
Mố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ệu
trừ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ủa
nú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ử}
Type
Pelem = record

Val : Elementtype;
Next: integer;
End;
List = array[1..max] of Pelem;
Var
Nodes: List;
Head: integer;

Cách 2: Biến cấp phát động
Type

Elementtype = integer; {Kiểu dữ liệu của phần tử}
PElem = ^Elem;
Elem = Record
val : Elementtype;
next: ^PElem;
end;


var

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ông
tin đủ để 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ếc
diề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ân
khấ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ên
kế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ằng
ngô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

program danhsachnoidon;
uses crt;
const
maxlength = 100;
nl = #13#10; bl = #32;
type
Elementtype = integer; {Elementtype có thể gồm nhiều thành
phần}
PElem = ^Elem;
Elem = record
Val: Elementtype;
Next: PElem;
end;

Tạo một ô (hay nút) mới chứa phần tử v của danh sách và con trỏ là
nxt
function NewElem(v: Elementtype; nxt: PElem): PElem;
var e: PElem;

begin
new(e); e^.Val:= v; e^.Next:= nxt;
NewElem:= e;
end;


Thê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:
P

L
a1

a2

L

v


P^.Next

.
.
.

an

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 p
là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);
begin
p^.Next:= nil;
if c = nil then d:= p else c^.Next:= p;
c:= p;
end;

Thêm nút có giá trị v vào vị trí nút p của danh sách được quản lý bởi
con trỏ head
Procedure Insert(p: Pelem; const v:Elementtype);
Var newnode, q: Pelem;
Begin


New(newnode);
Newnode^.val:= v;

Newnode^.next:= p;
If head = p then head:= newnode
Else
Begin
q:= head;
while q^.next <> p then q:= q^.next;
q^.next:= newnode;
end;
end;

Tạo ra nột danh sách có n phần tử, giá trị các phần tử trong danh sách
được sinh ngẫu nhiên bằng hàm Random
function GenList(n: integer): PElem;
var i: integer;
t,head: PElem;
begin
GenList:= nil;
if n <= 0 then exit;
head:= nil;
for i:= 1 to n do
begin
t:= NewElem(random(100),nil);
AddFirst(t,head);
end;
GenList:= head;
end;

In danh sách L
procedure PrintList(L: PElem);
begin

write('[ ');
while L <> nil do
begin
write(L^.Val,,’ ‘); L:= L^.Next;
end;
write(']');
end;

Đế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;
var t,s: PElem;
begin
if L = nil then exit;
s:= nil;
while L <> nil do
begin
t:= L;
L:= L^.Next;
AddFirst(t,s);
end;


Rev:= 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;
t:= L^.Next;
u:= RRev(t);
t^.Next:= L;
L^.Next:= nil;
RRev:= u;
end;

Kiểm tra hai danh sách p và q có giống nhau hay không?
function IsEqual(p,q: PElem): Boolean;
begin
IsEqual:= false;
while (p <> nil) and (q <> nil) do
begin
if p^.Val <> q^.Val then exit;
end;
IsEqual:= (p = nil) and (q = nil);
end;

Đếm số phần tử của danh sách L không đệ qui
function Card(L: PElem): integer;
var n: integer;

begin
n:= 0;
while (L <> nil) do
begin
n:= n+1; L:= L^.Next;
end;
Card:= n;
end;

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;
begin
s:= 0;
while (L <> nil) do
begin
if (L^.Val mod k = 0) then s:= s + L^.Val;
L:= L^.Next;
end;
SumDivk:= s;
end;

Tính tổng các phần tử của danh sách L
function Sum(L: PElem): integer;
begin
Sum:= SumDivk(L,1);
end;


Tách danh sách d thành hai danh sách mới

procedure Split(var d,c,l: PElem);
var cc, cl, t: PElem;
begin
c:= nil; l:= nil; cc:= nil; cl:= nil;
while (d <> nil) do
begin
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 }
begin
L:= L^.Next; dispose(t); t:= nil;
end else
begin
s^.Next:= t^.Next; dispose(t); t:= s;
end;
end;

Xoá khỏi danh sách L những phần tử chia hết cho k
procedure Del(var L: PElem; k: integer);
var s, t: PElem;
begin
t:= L; s:= nil;
while (t <> nil) do
begin

if (t^.Val mod k = 0) then
DelElem(d, s, t);
if t = nil then t:= L else
begin
s:= t;
t:= t^.Next;
end;
end;
end;

Tìm con trỏ cuối của danh sách L
function FindLast(L: PElem): PElem;
var s: PElem;
begin
s:= nil;
while (L <> nil) do
begin
s:= L; L:= L^.Next;
end;
FindLast:= s;
end;

Gộp hai danh sách a và b thành một danh sách
function Merge(a, b: PElem): PElem;
var c, cc, t: PElem;
begin
c:= nil; cc:= nil;
while (a <> nil) and (b <> nil) do



begin
if a^.Val < b^.Val then
begin
t:= a;
a:= a^.Next;
end else
begin
t:= b;
b:= b^.Next;
end;
Add(t,c,cc);
end;
t:= FindLast(a);
cc^.Next:= a;
cc:= t; t:= FindLast(b);
cc^.Next:= b;
Merge:= c;
end;

Test kết quả thực hiện một số thao thác cơ bản trên danh sách nối đơn
procedure Test;
var d, c, l: PElem;
begin
randomize;
d:= GenList(15);
PrintList(d);
writeln(nl,' So phan tu = ',RCard(d));
c:= Rev(d);
write(nl,' c = Lat d = ');
PrintList(c);

d:= RRev(c);
write(nl,' Lat de quy d = ');
PrintList(d);
writeln(nl,' Tong cac pt chan: ', Sumdivk(d,2));
writeln(nl,' Tong toan bo: ', SumDivk(d,1));
Split(d,c,l);
write(nl,' chan c = '); PrintList(c);
write(nl,' le l = '); PrintList(l);
write(nl,' d = '); PrintList(d);
write(nl,' d = c+l = '); PrintList(d);
Del(d,3);
write(nl,' Xoa pt chia het cho 3, d = '); PrintList(d);
Del(d,1);
write(nl,' Xoa toan bo danh sach, d = '); PrintList(d);
c:= GenTang(10); l:= GenTang(8);
write(nl,' c = '); PrintList(c);
write(nl,' l = '); PrintList(l);
d:= Merge(c, l);
write(nl,' Merge a, b = '); PrintList(d);
end;
BEGIN
writeln('==========================');
Test;
writeln('Finish'); readln;
END.


Nhận xét:
- Việc xác định phần tử đứng thứ p trong danh sách nối đơn bắt buộc phải
duyệt từ đầu danh sách qua p nút, việc này mất thời gian trung bình O(n), và tỏ

ra không hiệu quả như thao tác trên mảng. Nói cách khác, danh sách nối đơn tiện
lợi cho việc truy nhập tuần tự nhưng không hiệu quả nếu chúng ta thực hiện
nhiều phép truy cập ngẫu nhiên.
- 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ất
thờ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ột
phầ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ách
hiệ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:
next

A
head

B
prev
v

C

D
tail

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ông
tin đủ để 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ột
thông tin đủ để biết nút liền trước nút đó trong danh sách là nút nào, trong
trườ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ên
kết này được gán một giá trị đặc biệt, chẳng hạn con trỏ nil.


Cách mô tả danh sách nối đôi trên NNLT Free Pascal:
Type

Elementtype = integer; {Kiểu dữ liệu của phần tử}
PElem = ^Elem;
Elem = Record
val : Elementtype;
next : ^PElem;
prev : ^PElem;
end;
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ô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 ĐÔI

program danhsachnoidoi;
uses crt;
const
maxlength = 100;
nl = #13#10; bl = #32;
type
PElem = ^Elem;
Elem = Record
val: integer;
prev: PElem;
next: PElem;
End;
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;
var e: PElem;
begin
new(e); e^.Val:= v; e^.prev:= prv; e^.Next:= nxt;
NewElem:= e;
end;

Thêm một nút mới p vào cuối danh sách nối đôi có đầu là con trỏ head,
cuối là con trỏ tail
procedure Add(p: PElem; var head,tail: PElem);
begin
if head = nil then begin head:= p; p^.prev:= nil; end
else

begin
tail^.Next:= p;
p^.prev:= tail;
end;
tail:= p;
tail^.next:= nil;
end;

Tạo một danh sách nối đôi có n phần tử nguyên, các phần tử được sinh
ngẫu nhiên.
procedure GenTang(n: integer; var head, tail: PElem);
const r = 100;
var i,v: integer;
t,prv,nxt: PElem;
begin
if n = 0 then exit;


end;

prv:= nil; nxt:= nil; v:= 0;
for i:= 1 to n do
begin
v:= random(r)+random(r);
t:= NewElem(v,prv,nxt);
Add(t,head,tail);
end;

Tạ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);

const r = 10;
var i,v: integer;
t,prv,nxt: PElem;
begin
if n = 0 then exit;
prv:= nil; nxt:= nil; v:= 0;
for i:= 1 to n do
begin
v:= v + random(r);
t:= NewElem(v,prv,nxt);
Add(t,head,tail);
end;
end;

Chèn một phần tử có giá trị bằng x vào danh sách tăng sao cho danh
sách sau khi chèn vẫn được sắp xếp tăng
procedure Insertx(x: integer; var head1, tail1: PElem);
var p,k,prv,nxt,tr: PElem;
begin
prv:= nil; nxt:= nil;
p:= NewElem(x,prv,nxt);
k:= head1;
while (k^.val < p^.val) and (k <> nil) do
begin
tr:= k; k:= k^.next;
end;
Insert(p,tr,head1,tail1);
end;

Hiển thị danh sách nối đôi theo chiều xuôi

procedure PrintList1(head,tail: PElem);
begin
write('[ ');
while head <> nil do
begin
write(head^.Val,bl); head:= head^.Next;
end;
write(']');
writeln;
end;

Hiển thị danh sách nối đôi theo chiều ngược
procedure PrintList2(head,tail: PElem);
begin
write('[ ');
while tail <> nil do
begin
write(tail^.Val,bl); tail:= tail^.prev;
end;
write(']');
end;


Test kết quả thực hiện một số thao thác cơ bản trên danh sách nối đôi
procedure Test;
const r=100;
var d, c, l: PElem;
x: integer;
begin
head:= nil;

tail:= nil;
randomize;
Genlist(15,head,tail);
writeln('In danh sach xuoi');
PrintList1(head,tail);
writeln('In danh sach nguoc');
PrintList2(head,tail);
writeln;
writeln('Danh sach tang');
Gentang(15,head1,tail1);
x:= random(r);
writeln('x= ',x);
PrintList1(head1,tail1);
Insertx(x,head1,tail1);
writeln('Sau khi chen them ',x);
PrintList1(head1,tail1);
end;

BEGIN
clrscr;
writeln(nl,'==========================',nl);
Test;
writeln(nl,' Fini'); readln;
END.

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ác
khách hàng được xếp thành một vòng tròn đánh số 1,2,…,n. Giám đốc Công ty
lự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ẫu
nhiên trong số khách hàng, Giám đốc đếm theo chiều kim đồng hồ và dừng lại

mỗ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ười
trúng thưởng!
Hình vẽ dưới đây mô tả trò chơi với n=10, m=5.
1

10

Người thắng cuộc

2
3

9
4
8

7

5
6

Hình 7

Có thể giải bài toán này nhờ danh sách nối đôi.


program GiaiBaiJosephus;
uses crt;
const

maxn=1000;
type
PElem = ^Elem;
Elem = Record
val: integer;
prev: PElem;
next: PElem;
End;
var head,tail: PElem; n,m: integer;
function NewElem(v: integer; var prv,nxt: PElem): PElem;
var e: PElem;
begin
new(e); e^.Val:= v; e^.prev:= prv; e^.Next:= nxt;
NewElem:= e;
end;
procedure Add(p: PElem; var head,tail: PElem);
begin
if head = nil then begin head:= p; p^.prev:= nil; end
else
begin
tail^.Next:= p;
p^.prev:= tail;
end;
tail:= p;
tail^.next:= nil;
end;
procedure CreateList(n: integer; var head, tail: PElem);
var i,v: integer;
t,prv,nxt: PElem;
begin

if n = 0 then exit;
prv:= nil; nxt:= nil;
for i:= 1 to n do
begin
v:= i;
t:= NewElem(v,prv,nxt);
Add(t,head,tail);
end;
end;
procedure josephus(n: integer; m: integer);
var i,j : integer;
begin
CreateList(n,head,tail);
Tail:= head^.next;
For i:=1 to n-1 do
Begin
For j:=1 to m-1 do
Begin
Tail:= tail^.next;
If if tail = head then tail:= tail^.next;
End;
Tail:= tail^.next;
Erase(tail^.prev);
If tail = head then tail:= tail^.next;


×