106
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ố: …………………..

Kieu Du Lieu Truu Tuong

Embed Size (px)

DESCRIPTION

CTDL

Citation preview

Page 1: Kieu Du Lieu Truu Tuong

SỞ GIÁO DỤC VÀ ĐÀO TẠO TỈNH BẮC GIANGTRƯỜ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-TinNăm học: 2011-2012Mã số: …………………..

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

Page 2: Kieu Du Lieu Truu Tuong

Phần thứ nhấtMỞ ĐẦ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.

Page 3: Kieu Du Lieu Truu Tuong

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ụng2. 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 2Năm học 2011-2012: Hoàn thành Chương 3 và Chương 4

Page 4: Kieu Du Lieu Truu Tuong

Phần thứ haiNỘI DUNG CHUYÊN ĐỀ

Chương ICƠ SỞ LÝ THUYẾTI.1. Các khái niệmI.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, XORNhậ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.

Page 5: Kieu Du Lieu Truu Tuong

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 -…,-1,0,1,…+ +,-,*, v.v…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ệuCó 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 đó.

Page 6: Kieu Du Lieu Truu Tuong

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.

Hình 1Phâ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,…

A B

Page 7: Kieu Du Lieu Truu Tuong

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 độngI.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ệmBiế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^

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

PNguyễn Linh Chi

9.5

Page 8: Kieu Du Lieu Truu Tuong

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ỏ:TYPETypepointer = ^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ớ đó);

Page 9: Kieu Du Lieu Truu Tuong

I.2.5.2. So sánh hai biến con trỏ cùng kiểuChỉ 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ỏ NILNIL 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;Beginwrite(‘Vào n =’); readln(n);new(a);for i:=1 to n do

beginwrite(‘a[‘,i, ‘]=’);readln(a^[i]);

end;for i:=1 to n do

beginnew(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.

Page 10: Kieu Du Lieu Truu Tuong

Chương IIDANH SÁCH TUYẾN TÍNH

II.1. Khái niệm và các phép toán cơ bảnII.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_typex : một đối tượng kiểu nàyp : 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ảnDướ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áchNế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 LKhô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à a1, 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)

Page 11: Kieu Du Lieu Truu Tuong

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ínhCó 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.

Page 12: Kieu Du Lieu Truu Tuong

Danh 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}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 Lprocedure 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

last

maxlength

list

rỗng

Page 13: Kieu Du Lieu Truu Tuong

beginfor 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 LProcedure DELETE(p: position; var L: LIST);Var q: position;beginif (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 doif 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ốiII.2.2.1. Lưu trữ móc nối đối với danh sách tuyến tính – Linked listLư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ỏ.

Page 14: Kieu Du Lieu Truu Tuong

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:

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útConst max = 1000; {Số phần tử cực đại}Elementtype = integer; {Kiểu dữ liệu của phần tử}TypePelem = record

Val : Elementtype;

header a1 a2 . . . an nil

list

Page 15: Kieu Du Lieu Truu Tuong

Next: integer; End;

List = array[1..max] of Pelem;VarNodes: List; Head: integer;

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

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

Elem = Recordval : 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 ĐƠNprogram 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

Page 16: Kieu Du Lieu Truu Tuong

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

Page 17: Kieu Du Lieu Truu Tuong

Thêm nút p vào đầu danh sách Lprocedure 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:= LNghĩa là L trỏ vào đâu thì con trỏ p^.Next trỏ vào đấyThao tác 2: Gán 2 con trỏ L:= pNghĩ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 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à cprocedure 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ỏ headProcedure Insert(p: Pelem; const v:Elementtype);Var newnode, q: Pelem;Begin

L

a1

a2

.

.

.

an

v

P

P^.Next

L

Page 18: Kieu Du Lieu Truu Tuong

New(newnode);Newnode^.val:= v;Newnode^.next:= p;If head = p then head:= newnodeElse

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

Page 19: Kieu Du Lieu Truu Tuong

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;

Page 20: Kieu Du Lieu Truu Tuong

Tách danh sách d thành hai danh sách mớiprocedure 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 Lprocedure 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 kprocedure 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 Lfunction 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

Page 21: Kieu Du Lieu Truu Tuong

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 đơnprocedure 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.

Page 22: Kieu Du Lieu Truu Tuong

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:

Hình 6Có 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à nilCá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.

B C A D

head tailprevv

next

Page 23: Kieu Du Lieu Truu Tuong

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 = Recordval : 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 ĐÔIprogram 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;

Page 24: Kieu Du Lieu Truu Tuong

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; 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ăngprocedure 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ôiprocedure 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ượcprocedure PrintList2(head,tail: PElem);begin write('[ '); while tail <> nil do begin write(tail^.Val,bl); tail:= tail^.prev; end; write(']');end;

Page 25: Kieu Du Lieu Truu Tuong

Test kết quả thực hiện một số thao thác cơ bản trên danh sách nối đôiprocedure 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 JosephusN 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.

Hình 7

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

1 2

3

4

567

8

9

10Người thắng cuộc

Page 26: Kieu Du Lieu Truu Tuong

program GiaiBaiJosephus;uses crt;constmaxn=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; beginCreateList(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;

Page 27: Kieu Du Lieu Truu Tuong

End;Write(‘Nguoi thang cuoc la:’,tail^.val);Dispose(tail); dispose(head);

end;BEGINWrite(‘Nhap vao n: ‘); readln(n);Randomize;m:= 1 + random(n);josephus(n,m);

END.II.2.4. Phân tích sử dụng linked listNhữ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 so

vớ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ápViệ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ậm trong cách cài đặt kia: Insert và Delete đòi hỏi thời gian hằng số trong cài đặt con 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ủa danh sách. Previous và End đòi hỏi thời gian hằng số trong cài đặt mảng, nhưng thờ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ỉ đòi hỏi bộ nhớ cho các phần tử đang có trong danh sách. Tuy nhiên cách cài đặt con trỏ lại đòi hỏi thêm bộ nhớ cho con trỏ.

Page 28: Kieu Du Lieu Truu Tuong

II.3. Ngăn xếp (Stack)II.3.1. Kiểu dữ liệu trừu tượng ngăn xếpNgă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 danh sá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ốiII.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ác

phầ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}Type

Telement = integer; {Kiểu phần tử của stack}TStack = record

Items: array[1..maxn] of Telement;Top: integer;

End;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 Pascal

như sau:program cacthaotaccoban;uses crt;const maxn = 1000;type TElement = integer; TStack = record items: array[1..maxn] of TElement; top: integer; end;var stack: TStack;Procedure init(var S: TStack); begin S.top:= 0; end;Function IsEmpty(var S: TStack): boolean;

Page 29: Kieu Du Lieu Truu Tuong

begin IsEmpty:= (S.top = 0); end;Function IsFull(var S: TStack): boolean; begin IsFull:= (S.top = maxn); end;Function Get(var S: TStack): TElement; begin if IsEmpty(S) then write('Stack rong!') else with S do Get:= items[top]; end;Procedure Push(x: TElement; var S: TStack); begin if IsFull(S) then write('Stack day!') else with S do begin top:= top + 1; items[top]:= x; end; end;Function Pop(var S: TStack): TElement; begin if IsEmpty(S) then write('Stack rong!') else with S do begin Pop:= items[top]; top:= top - 1; end; end;Procedure Test; {Test sự thực hiện một số thao tác cơ bản} const r = 100; max=100; var i,x,k,n: integer; a: array[1..maxn] of TElement; begin Init(Stack); randomize; n:= 1+random(max); For i:=1 to n do begin x:= random(r); Push(x,Stack); end; writeln; with Stack do for i:=1 to top do write(items[i], ' '); readln; k:= 0; While not IsEmpty(Stack) do begin x:= Pop(Stack); if x mod 2 = 0 then begin k:= k+1; a[k]:= x; end; end; writeln; writeln; for i:= 1 to k do write(a[i],' '); writeln;

Page 30: Kieu Du Lieu Truu Tuong

end; BEGIN clrscr; Test; END.

II.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 gian nhớ 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ớn nê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:Type

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

Elem = Recordval : Elementtype;next: ^PElem;

end;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:Program Caidatstackbangdanhsachnoidon;Uses crt;Type TElement = integer; PElem = ^Elem; Elem = record val: TElement; next: PElem; end;Var top: PElem;Procedure Init(var top: PElem); begin top:= Nil; end;Function IsEmpty(var top: PElem): boolean; begin IsEmpty:= (top=Nil); end;Function Get(var top: PElem): TElement; begin If IsEmpty(top) then write('Stack rong') else Get:= (top^.val); end;Procedure Push(x: TElement;var top: PElem); var p: PElem; begin

Page 31: Kieu Du Lieu Truu Tuong

New(p); p^.val:= x; p^.next:= top; top:= p; end;Function Pop(var top: PElem): TElement; var p: PElem; begin If IsEmpty(top) then write('Stack rong') else begin Pop:= top^.val; p:= top^.next; dispose(top); top:= p; end; end;Procedure Test; const r = 100; max=100; var i,x,k,n: integer; S,a: PElem; begin Init(top); randomize; n:= 1+random(max); For i:=1 to n do begin x:= random(r); Push(x,top); end; writeln('In cac phan tu cua Stack quan li boi top:'); S:= top; while S<> nil do begin write(S^.val, ' '); S:= S^.next; end; readln; Writeln('Day cac phan tu ra khoi Stack top, tao Stack S gom

cac ptu chan tu Stack top'); Init(S); While not IsEmpty(top) do begin x:= Pop(top); if x mod 2 = 0 then begin Push(x,S); end; end; writeln('In cac phan tu cua Stack S:'); while S<> nil do begin write(S^.val, ' '); S:= S^.next; end; writeln; writeln('Xem Stack top con phan tu khong:'); while top<> nil do begin write(top^.val, ' '); top:= top^.next;

Page 32: Kieu Du Lieu Truu Tuong

end; readln; end;BEGIN Test;END.

II.3.4. Một số ứng dụng của ngăn xếp (Applications of Stacks)

Ứng dụng trực tiếp: 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

Page 33: Kieu Du Lieu Truu Tuong

II.4. Hàng đợi (Queue)

II.4.1. Kiểu dữ liệu trừu tượng hàng đợiHàng đợi (Queue) là một kiểu danh sách mà việc bổ sung một phần tử được

thực hiện ở cuối danh sách và việc loại bỏ một phần tử được thực hiện ở đầu danh 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ơi phầ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ười nà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 đợiCác thuật ngữ liên quan đến hàng đợi được mô tả trong hình vẽ sau đây:

Hình 8II.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ử trong

hàng đợi, một biến nguyên front để lưu chỉ số phần tử đầu hàng đợi và một biến nguyê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}TypeTelement = integer; {Kiểu giá trị một phần tử}TQueue = record

Items: array[1..max] of TElement;Front: integer;Rear: integer;

End;Var Queue: TQueue;

Bổ sung/ Đưa vào

Front (Lối sau, đầu)Rear (Lối trước, cuối)

Loại bỏ/ Đưa ra

Page 34: Kieu Du Lieu Truu Tuong

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ình Free Pascal như sau:

uses crt;const max = 1000;type TElement = integer; TQueue = record items: array[1..max] of Telement; front: integer; rear: integer; end;var Queue: TQueue; Procedure init(var Q: TQueue); begin Q.front:= 1; Q.rear:= 0; end; Function IsEmpty(var Q: TQueue): Boolean; begin IsEmpty:= (Q.front > Q.rear); end; Function IsFull(var Q: TQueue): Boolean; begin IsFull:= (Q.rear = max); end; Function Get(var Q: TQueue): TElement; begin if IsEmpty(Q) then write('Queue rong!') else Get:= Q.items[Q.front]; end; Procedure Push(x: TElement; var Q: TQueue); begin if IsFull(Q) then write('Queue day!') else with Q do begin rear:= rear + 1; items[rear]:= x; end; end; Function Pop(var Q: TQueue): TElement; begin if IsEmpty(Q) then write('Queue rong!') else with Q do begin Pop:= items[front]; front:= front + 1; end; end; procedure Test; const r = 100; var i,x,k,n: integer; a: array[1..max] of TElement; begin Init(Queue); randomize;

Page 35: Kieu Du Lieu Truu Tuong

n:= 10; For i:=1 to n do begin x:= random(r); Push(x,Queue); end; writeln('Cac phan tu thuoc hang doi'); with Queue do for i:=front to rear do write(items[i], ' '); readln; k:= 0; While not IsEmpty(Queue) do begin x:= Pop(Queue); if x mod 2 = 0 then begin k:= k+1; a[k]:= x; end; end; writeln; writeln; for i:= 1 to k do write(a[i],' '); readln; end;BEGIN Test;END.

II.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 Stack như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ách vò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 đồng hồ). 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 đẩy thêm một phần tử vào hàng đợi tương đương với việc ta dịch chỉ số rear theo chiề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ử trong hà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ố front theo chiều vòng.

Page 36: Kieu Du Lieu Truu Tuong

Hì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ằng

mả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}TypeTelement = integer; {Kiểu giá trị một phần tử}TQueue = record

Items: array[0..max-1] of TElement;Front: integer;Rear: integer;N : integer;

End;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ình

Free Pascal như sau:program Caidathangdoibangdsvong;uses crt;const max = 1000;type TElement = integer; TQueue = record items: array[0..max-1] of Telement; front: integer; rear: integer; n : integer; end;var Queue: TQueue; Procedure init(var Q: TQueue); begin Q.front:= 1; Q.rear:= max-1; Q.n:=0; end; Function IsEmpty(var Q: TQueue): Boolean; begin IsEmpty:= (Q.n = 0);

Page 37: Kieu Du Lieu Truu Tuong

end; Function IsFull(var Q: TQueue): Boolean; begin IsFull:= (Q.n = max); end; Function Get(var Q: TQueue): TElement; begin if IsEmpty(Q) then write('Queue rong!') else Get:= Q.items[Q.front]; end; Procedure Push(x: TElement; var Q: TQueue); begin if IsFull(Q) then write('Queue day!') else with Q do begin rear:= (rear + 1) mod max; items[rear]:= x; inc(n); end; end; Function Pop(var Q: TQueue): TElement; begin if IsEmpty(Q) then write('Queue rong!') else with Q do begin Pop:= items[front]; front:= (front + 1) mod max; end; end;procedure Test; const r = 100; var i,x,nn,k: integer; a: array[1..max] of TElement; begin Init(Queue); randomize; nn:= 10; For i:=1 to nn do begin x:= random(r); Push(x,Queue); end; writeln('Cac phan tu thuoc hang doi'); with Queue do for i:=front to rear do write(items[i], ' '); readln; k:= 0; While not IsEmpty(Queue) do begin x:= Pop(Queue); if x mod 2 = 0 then begin k:= k+1; a[k]:= x; end; end; writeln; writeln; for i:= 1 to k do write(a[i],' '); writeln;

Page 38: Kieu Du Lieu Truu Tuong

end;

BEGIN clrscr; Test; END.

II.4.4. Tổ chức hàng đợi bằng danh sách nối đơn kiểu FIFOTương tự như cài đặt ngăn xếp bằng biến động và con trỏ trong một danh

sách nối đơn, ta cũng không viết hàm IsFull để kiểm tra hàng đợi đầy. type TElement = integer; PElem = ^Elem; Elem = record val: TElement; next: PElem; end;var front, rear: PElem; {Con trỏ tới phần tử đầu và cuối hàng

đợi} procedure Init; begin front:= Nil; end; Function IsEmpty: boolean; begin IsEmpty:= (front=Nil); end; Function Get: TElement; begin If IsEmpty then write('Stack rong') else Get:= (front^.val); end; Procedure Push(x: Telement); var p: PElem; begin New(p); p^.val:= x; p^.next:= nil; if front = nil then front:= p

else rear^.next:= p; rear:= p; end; Function Pop: TElement; var p: PElem; begin If IsEmpty(top) then write('Stack rong') else begin Pop:= front^.val; p: front^.next; dispose(front); front := p; end; end;

Page 39: Kieu Du Lieu Truu Tuong

Chương IIICÂY NHỊ PHÂNIII.1. Định nghĩa, tính chất và phân loạiIII.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à con

phả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 10Vì 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ânBổ đề 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ên câ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*2 i-

1=2i nút.

Left child Right child

Page 40: Kieu Du Lieu Truu Tuong

(ii) Số lượng nút lớn nhất của cây nhị phân chiều cao k là không vượt quá tổng số lượng nút lớn nhất trên các mức i = 1, 2, …, k, theo (i) của bổ đề 1, số này không vượt quá 1+2+4+ …+2k-1+2k=2k – 1.

(iii) Cây nhị phân n nút có chiều cao thấp nhất k khi số lượng nút ở các mức i = 1,2,…,k đều là lớn nhất có thể được. Từ đó ta có:

n = 2k – 1, suy ra 2k = n+1, hay k = [log2(n+1)].

III.1.3. Phân loạiCây nhị phân đầy đủ (full binary tree): là cây nhị phân thỏa

mãn:. mỗi nút lá đều có cùng độ sâu và. các nút trong có đúng 2 conVí dụ: Cây nhị phân đầy đủ được cho trong hình vẽ sau:

Hình 11Bổ đề 2: Cây nhị phân đầy đủ với độ sâu n có 2n – 1 nút.Chứng minh: Suy trực tiếp từ bổ đề 1.Cây nhị phân hoàn chỉnh (Complete Binary Tree): là cây nhị phân độ

sâu n thỏa mãn:. là cây nhị phân đầy đủ nếu không tính đến các nút ở độ sâu n, và. tất cả các nút ở độ sâu n là lệch sang trái nhất có thể được.Bổ đề 3: Cây nhị phân hoàn chỉnh độ sâu n có số lượng nút nằm trong

khoảng từ 2n-1 đến 2n – 1.Chứng minh: Suy trực tiếp từ định nghĩa và bổ đề 1.Ví dụ: Cây nhị phân hoàn chỉnh được cho trong hình vẽ sau:

Hình 12Cây nhị phân cân đối (balanced binary tree): là cây nhị phân mà chiều

cao của cây con trái và chiều cao của cây con phải chênh lệch nhau không quá 1 đơn vị.

Page 41: Kieu Du Lieu Truu Tuong

Ví dụ: Cây nhị phân cân đối được cho trong hình vẽ sau:

Hình 13Nhận xét: . Nếu cây nhị phân là đầy đủ thì nó là hoàn chỉnh.. Nếu cây nhị phân là hoàn chỉnh thì nó là cân đối.

III.2. Biểu diễn cây nhị phânTa xét hai phương pháp:. Biểu diễn sử dụng mảng. Biểu diễn sử dụng con trỏIII.2.1. Biểu diễn cây dùng mảngGiả sử T là cây với các nút đặt tên là 1, 2, …, n. Cách đơn giản để biểu

diễn T là hỗ trợ thao tác parent(i, T) (trả lại nút cha của nút i trong cây T) bởi danh sách tuyến tính A, trong đó mỗi phần tử A[i] chứa con trỏ đến cha của nút i. Riêng gốc của T có thể phân biệt bởi con trỏ rỗng.

Khi dùng mảng, ta đặt a[i] = j nếu j là nút cha của nút i, và A[i] = 0 nếu nút i là gốc.

TypeTree = Array[1..maxn] of longint;

VarA : Tree;

Ví dụ: Cây trong hình 14 được biểu diễn bởi mảng A

Hình 14

1

2 3

4 5

7

6

8 9

Page 42: Kieu Du Lieu Truu Tuong

A: 0 1 1 2 2 3 4 4 6cs: 1 2 3 4 5 6 7 8 9

Nhận xét: Trong trường hợp cây nhị phân hoàn chỉnh, sử dụng cách biểu diễn này ta có thể cài đặt nhiều phép toán với cây hiệu quả.

Xét cây nhị phân hoàn chỉnh T có n nút, trong đó mỗi nút chứa một giá trị. Gán nhãn cho các nút của cây hoàn chỉnh T từ trên xuống dưới và từ trái qua phải bằng các số 1, 2, …, n. Đặt tương ứng cây T với mảng A, trong đó phần tử thứ i của A là giá trị cất giữ trong nút thứ i của cây T, i=1, 2, …,n.

Ví dụ: Cây nhị phân hoàn chỉnh sau được biểu diễn bằng mảng A.

Hình 15A: H D K B F J L A C Ecs: 1 2 3 4 5 6 7 8 9 10

Một số thao tác trên cây được biểu diễn bởi mảng A như sau:Để tìm Sử dụng Hạn chếCon trái của A[i] A[2*i] 2*i <= nCon phải của A[i] A[2*i+1] 2*i+1 <= nCha của A[i] A[i/2] i>1Gốc A[1] A khác rỗngNút A[i] là lá? True 2*i > n

III.2.2. Biểu diễn cây nhị phân dùng con trỏMỗi nút của cây, ngoài trường Val để lưu giá trị của nút đó còn có con trỏ

đến con trái và con trỏ đến con phải:

Val

Hình 16

H

D K

B F J L

A C ER

1

2 3

4 5 6 7

8 9 10

Pointer to left child Pointer to right child

Page 43: Kieu Du Lieu Truu Tuong

Kiểu dữ liệu được mô tả như sau:Type PNode = ^Node; Node = record Val: integer; Left,Right: PNode; end;Var root : Pnode;

III.3. Phép duyệt cây nhị phân (Xem [6], trang 74)Phép xử lý các nút trên cây mà ta gọi chung là phép thăm (Visit) các nút

một cách hệ thống sao cho mỗi nút chỉ được thăm một lần gọi là phép duyệt cây.Giả sử rằng nếu như một nút không có nút con trái (hoặc nút con phải) thì

liên kết Left (Right) của nút đó được liên kết thẳng tới một nút đặc biệt mà ta gọi là NIL (hay NULL), nếu cây rỗng thì nút gốc của cây đó cũng được gán bằng NIL. Khi đó có ba cách duyệt cây hay được sử dụng:

III.3.1. Duyệt theo thứ tự trước (preorder traversal)Trong phép duyệt theo thứ tự trước thì giá trị trong mỗi nút bất kì sẽ được

liệt kê trước giá trị lưu trong hai nút con của nó, có thể mô tả bằng thủ tục đệ qui sau:

Procedure NLR(N : Pnode); {Duyệt nhánh cây nhận N là gốc của nhánh đó}Begin

<Output trường Val của nút N>NLR(N^.Left);

NLR(N^.Right);End;Quá trình duyệt theo thứ tự trước bắt đầu bằng lời gọi NLR(root), với root

là nút gốc của cây.

Hình 17Nếu ta duyệt cây trong hình 17 theo thứ tự trước thì các giá trị sẽ lần lượt

được liệt kê theo thứ tự: H D B A C F E K J L.

Page 44: Kieu Du Lieu Truu Tuong

III.3.2. Duyệt theo thứ tự giữa (inorder traversal)Trong phép duyệt theo thứ tự giữa thì giá trị trong mỗi nút bất kì sẽ được

liệt kê sau giá trị lưu ở nút con trái và được liệt kê trước giá trị lưu ở nút con phải của nút đó, có thể mô tả bằng thủ tục đệ qui sau:

Procedure LNR(N : Pnode); {Duyệt nhánh cây nhận N là gốc của nhánh đó}Begin

LNR(N^.Left);<Output trường Val của nút N>LNR(N^.Right);

End;Quá trình duyệt theo thứ tự giữa bắt đầu bằng lời gọi LNR(root), với root là

nút gốc của cây.Nếu ta duyệt cây trong hình 17 theo thứ tự giữa thì các giá trị sẽ lần lượt

được liệt kê theo thứ tự: A B C D E F H J K L.III.3.3. Duyệt theo thứ tự sau (postorder traversal)Trong phép duyệt theo thứ tự sau thì giá trị trong mỗi nút bất kì sẽ được liệt

kê sau giá trị lưu ở hai nút con của nút đó, có thể mô tả bằng thủ tục đệ qui sau:Procedure LRN(N : Pnode); {Duyệt nhánh cây nhận N là gốc của nhánh đó}Begin

LRN(N^.Left);LRN(N^.Right);<Output trường Val của nút N>

End;Quá trình duyệt theo thứ tự sau bắt đầu bằng lời gọi LRN(root), với root là

nút gốc của cây.Nếu ta duyệt cây trong hình 17 theo thứ tự sau thì các giá trị sẽ lần lượt

được liệt kê theo thứ tự: A C B E F D J L K H.

Dưới đây là chương trình cài đặt một số thao tác cơ bản trên cây nhị phân: Program Thaotactrencay;uses crt;const nl = #13#10; bl = #32;type PNode = ^Node; Node = record Val: integer; L,R: PNode; end;(*---------------------------------------

Page 45: Kieu Du Lieu Truu Tuong

Đếm số phần tử chia hết cho k(*---------------------------------------function Count(t: PNode; k: integer): integer; var d: integer;begin if t = nil then d:= 0 else begin if t^.Val mod k = 0 then d:= 1 else d:= 0; d:= d + Count(t^.L,k) + Count(t^.R,k); end; Count:= d;end;(*---------------------------------------Tạo một nút mới có giá trị v và 2 con trỏ là lp và rp--------------------------------------- *)function NewElem(v: integer; lp,rp: PNode): PNode;var e: PNode;begin new(e); e^.Val:= v; e^.L:= lp; e^.R:= rp; NewElem:= e;end;(*----------------------------------------Tìm vị trí xuất hiện của trị v trên cây T 1: v có xuất hiện tại node u,0: v ko xuất hiện---------------------------------------*)function Find(t: PNode; v: integer; var u: PNode): integer;begin u:= nil; while t <> nil do begin u:= t; if v <= t^.Val then t:= t^.L else t:= t^.R; end; if v <= u^.Val then Find:= 0 else Find:= 1;end;(*------------------------------------------------ Tìm node chứa trị v trên cây nhị phân tìm kiếm T ----------------------------------------------*)function PFind(t: PNode; v: integer): PNode; var u: PNode;begin u:= nil; while t <> nil do begin u:= t; if v <= t^.Val then t:= t^.L else t:= t^.R; end; if v = u^.Val then PFind:= u else PFind:= nil;end;

Page 46: Kieu Du Lieu Truu Tuong

(*------------------------------------------------ Thêm một nút p vào cây T ----------------------------------------------*)procedure Add(var t: PNode; p: PNode); var u: PNode;begin if t = nil then t:= p else if Find(t,p^.Val, u) = 0 then u^.L:= p else u^.R:= p;end;(*------------------------------------------------ Sinh một cây có n nút ----------------------------------------------*)function GenTree(n: integer): PNode;const r = 100;var i: integer; t, p: PNode;begin GenTree:= nil; if n <= 0 then exit; t:= nil; for i:= 1 to n do begin p:= NewElem(random(r),nil,nil); Add(t,p); end; GenTree:= t;end;(* Hien thi tang *)procedure LNRPrint(t: PNode);begin if t = nil then exit; LNRPrint(t^.L); write(t^.Val,bl); LNRPrint(t^.R);end;(* Hien thi giam *)procedure RNLPrint(t: PNode);begin if t = nil then exit; RNLPrint(t^.R); write(t^.Val,bl); RNLPrint(t^.L);end;{Dem so nut tren cay T}function Card(t: PNode): integer;begin if t = nil then Card:= 0 else Card:= Card(t^.L) + Card(t^.R) + 1;end;{Tinh tong cac nut tren cay T}function Sum(t: PNode): integer;begin

Page 47: Kieu Du Lieu Truu Tuong

if t = nil then Sum:= 0 else Sum:= Sum(t^.L) + Sum(t^.R) + t^.Val;end;{Xoa cay T}procedure DelTree(var t: PNode);begin if t = nil then exit; DelTree(t^.L); DelTree(t^.R); Dispose(t); t:= nil;end;procedure Test; var t: PNode;begin randomize; t:= GenTree(20); write(nl, ' Day tang: '); LNRPrint(t); write(nl, ' Day giam: '); RNLPrint(t); write(nl,' Card = ',Card(t)); write(nl,' Sum = ',Sum(t)); writeln(nl,Count(t,1)); DelTree(t); LNRPrint(t);end;BEGIN writeln(nl,'==========================',nl); Test; writeln(nl,' Fini'); readln;END.

III.4. Các ví dụ ứng dụng

III.4.1. Cây nhị phân biểu thức (Binary Expression trees)

Cây biểu thức là cây nhị phân trong đó:1. Mỗi nút lá chứa một toán hạng2. Mỗi nút trong chứa một phép toán hai ngôi3. Các cây con trái và phải của nút phép toán biểu diễn các biểu thức con

(subexpressions) cần được thực hiện trước khi thực hiện phép toán ở gốc của các cây con.

Ví dụ: Cây nhị phân 4 mức:

*

-

8 5

/

+ 3

4 2

Page 48: Kieu Du Lieu Truu Tuong

Hình 18Hình 18 mô tả cây nhị phân biểu thức (8-5)*((4+2)/3)Dạng tiền tố (prefix) của biểu thức: là kí pháp của biểu thức khi duyệt

cây theo thứ tự trước.Dạng trung tố (infix) của biểu thức: là kí pháp của biểu thức khi duyệt

cây theo thứ tự giữa.Dạng hậu tố (postfix) của biểu thức: là kí pháp của biểu thức khi duyệt

cây theo thứ tự sau.Nếu duyệt cây biểu thức trong hình 18 theo thứ tự sau thì ta sẽ được 8 5 – 4

2 + 3 / *Trong những năm đầu 1950, nhà lô-gic học người Balan Jan Lukasiewicz

đã chứng minh rằng biểu thức hậu tố không cần phải có dẫu ngoặc vẫn có thể tính được một cách đúng đắn bằng cách đọc lần lượt biểu thức từ trái qua phải và dùng một Stack để lưu các kết quả trung gian:

Bước 1: Khởi tạo một Stack rỗngBước 2: Đọc lần lượt các phần tử của biểu thức LRN từ trái qua phải (phần

tử này có thể là hằng, biến hay toán tử) với mỗi phần tử đó, ta kiểm tra: Nếu phần tử này là một toán hạng thì đẩy giá trị đó vào Stack. Nếu phần tử này là một toán tử ®, ta lấy từ Stack ra hai giá trị (y và x) sau đó áp

dụng toán tử ® đó vào hai giá trị vừa lấy ra, đẩy kết quả tìm được (x ® y) vào Stack.

Bước 3: Sau khi kết thúc bước 2 thì toàn bộ biểu thức đã được đọc xong, trong Stack chỉ còn duy nhất một phần tử, phần tử đó chính là giá trị của biểu thức.

Chương trình minh họa cho cách tính giá trị của biểu thức bạn đọc có thể tham khảo tại trang 81 - Cấu trúc dữ liệu và giải thuật - tác giả Lê Minh Hoàng.

III.4.2. Chuyển từ dạng trung tố sang dạng hậu tố (Xem [6], trang 83)

Thuật toán sử dụng một Stack để chứa các toán tử và dấu ngoặc mở. Thủ tục push(V) để đẩy một phần tử vào Stack, hàm Pop để lấy ra một phần tử từ Stack, hàm Get ddeerr đọc giá trị phần tử nằm ở đỉnh Stack mà không lấy phần tử đó ra. Ngoài ra mức độ ưu tiên của các toán tử được qui định bằng hàm Priority như sau: Ưu tiên cao nhất là dấu “*” và “/” với Priority là 2, tiếp theo là dấu “+” và “-” với Priority là 1, ưu tiên thấp nhất là dấu ngoặc mở “(” với Priority là 0.

Stack := ;For <Phần tử T đọc được từ biểu thức Infix> do

Page 49: Kieu Du Lieu Truu Tuong

{T có thể là hằng, biến, toán tử hoặc dấu ngoặc được đọc từ biểu thức infix theo thứ tự từ trái qua phải}

Case T of‘(‘: Push(T);‘)’:

RepeatX := Pop;If x <> ‘(‘ then Output(x);

Until x=’(‘; ‘+’, ‘-“, ‘*’, ‘/’:

BeginWhile (Stack <> ) and (Priority(T) <= Priority(Get)) do Output(Pop);Push(T);

End; Else Output(T);End;While (Stack <> ) do Output(Pop);

Chương trình minh họa cho thuật toán bạn đọc có thể tham khảo tại trang 84 - Cấu trúc dữ liệu và giải thuật - tác giả Lê Minh Hoàng.

III.4.3. Thuật toán sắp xếp kiểu vun đống (HeapSort)

(Xem [6], trang101]

HeapSort được đề xuất bởi J. W. J. Williams năm 1981, thuật toán không những đóng góp một phương pháp sắp xếp hiệu quả mà còn xây dựng một cấu trúc dữ liệu quan trọng để biểu diễn hàng đợi có độ ưu tiên: Cấu trúc dữ liệu Heap.

III.4.3.1. Đống (Heap)Đống là một dạng cây nhị phân hoàn chỉnh đặc biệt mà giá trị lưu tại mọi

nút có độ ưu tiên cao hơn hay bằng giá trị lưu trong hai nút con của nó. Trong thuật toán sắp xếp kiểu vun đống, ta coi quan hệ “ưu tiên hơn hay bằng” là quan hệ “lớn hơn hay bằng”: >

Hình 19: Heap

10

9 6

7 8 4 1

3 2 5

Page 50: Kieu Du Lieu Truu Tuong

III.4.3.2. Vun đốngMột dãy khóa k[1..n] là biểu diễn của một cây nhị phân hoàn chỉnh mà k[i]

là giá trị lưu trong nút thứ i, nút con của nút thứ i là nút 2i và nút 2i+1, nút cha của nút thứ j là nút j div 2. Vấn đề đặt ra là sắp lại dãy khóa đã cho để nó biểu diễn một đống.

Vì cây nhị phân chỉ gồm có một nút hiển nhiên là đống, nên để vun một nhánh cây gốc r thành đống,ta có thể coi hai nhánh con của nó đã là đống rồi và thực hiện vun đống từ dưới lên (bottom-up) đối với cây: Gọi h là chiều cao của cây, ở mức h (nút lá) đã là gốc một đống, ta vun lên để những nút ở mức h-1 cũng là gốc của đống, …, cứ như vậy cho tới nút ở mức 1 (nút gốc) cũng là gốc của đống.

Thuật toán vun thành đống đối với cây gốc r, hai nhánh con của r đã là đống rồi:

Giả sử ở nút r chứa giá trị V. Từ r, ta cứ đi tới nút con chứa giá trị lớn nhất trong 2 nút con, cho tới khi gặp phải một nút c mà mọi nút con của c đều chứa giá trị u < V. Dọc trên đường đi từ r tới c, ta đẩy giá trị chứa ở nút con lên nút cha và đặt giá trị V vào nút c.

Hình 20: Vun đốngIII.4.3. Tư tưởng của HeapSortBước 1: Dãy khóa k[1..n] được vun từ dưới lên để nó biểu diễn một đống,

khi đó có khóa k[1] tương ứng với nút gốc của đống là khóa lớn nhất.Bước 2: Đảo giá trị khóa đó cho k[n] và không tính tới k[n] nữa. Dãy khóa

k[1..n-1] biểu diễn cây nhị phân hoàn chỉnh mà hai nhánh cây con của nút 1 đã là đống rồi.

Bước 3: Vun đống cho cây nhị phân tương ứng với dãy khóa k[1..n-1], đảo giá trị k[1] cho k[n-1].

Tiếp tục như vậy cho tới khi đống chỉ còn lại 1 nút.

4

10

9

7 8 6 1

3 5 2

10

8 9

7 4 6 1

3 5 2

Page 51: Kieu Du Lieu Truu Tuong

Hình 21: Đảo giá trị k[1] cho k[n] và xét phần còn lại

Hình 22: Vun phần còn lại thành đống rồi lại đảo trị k[1] cho k[n-1]Thuật toán HeapSort có hai thủ tục chính:

Thủ tục Ajust(root,endnode) vun cây gốc root thành đống trong điều kiện hai cây gốc 2.root và 2.root+1 đã là đống rồi. Các nút từ endnode+1 tới n đã nằm ở vị trí dúng và không được tính nữa.

Thủ tục HeapSort mô tả lại quá trình vun đống và chọn khóa theo ý tưởng trên:Procedure HeapSort;Varr, i : integer;procedure Ajust(root, endnode: integer);

{Vun cây gốc root thành đống}var

c : integer; key : Tkey; {Biến lưu giá trị khóa ở nút Root}begin

Key := k[root];While root*2 < endnode do {Chừng nào root chưa là lá} Begin

C := root*2;If (c<endnode) and (k[c] < k[c+1] then c:=c+1;If k[c] < key then break;K[root] := k[c]; root := c;

End; K[root] := key; End;BeginFor r := n div 2 downto 1 do Ajust(r,n);For i:=n downto 2 do

Begin<Đảo giá trị k[1] và k[i]>;

10

8 9

7 4 6 1

3 5 2

2

8 9

7 4 6 1

3 5 10

9

8 6

7 4 2 1

3 5

5

8 9

7 4 6 1

3 9

Page 52: Kieu Du Lieu Truu Tuong

Ajust(1,i-1);End;

End;Độ phức tạp trung bình của thuật toán HeapSort là O(nlgn).

Page 53: Kieu Du Lieu Truu Tuong

Chương IVBÀI TẬP ỨNG DỤNGBài 1: Mass of Molecule (Duyên hải Bắc Bộ 2005)Bạn có thể Test bài này trên VNOI.INFO với mã bài là: MMASSHóa chất chỉ gồm các nguyên tố C, H, O có trọng lượng 12,1,16 tương ứng. Nó được biểu diễn dạng "nén", ví dụ COOHHH là CO2H3 hay CH

(COOH) (COOH) (COOH) là CH(CO2H)3. Nếu ở dạng nén thì số lần lặp >=2 và <=9.

Yêu cầu: Tính khối lượng hóa chất.Dữ liệu vào: Gồm một dòng mô tả hóa chất không quá 1000 kí tự chỉ gồm

C, H, O, (, ), 2,..,9. Kết quả: Khối lượng của hóa chất, luôn <=100000. Ví dụ:

MASS.INP MASS.OUT MASS.INP MASS.OUT MASS.INP MASS.OUTCOOH 45 CH(CO2H)3 148 ((CH)2(OH2H)(C(H))O)3 222

Hướng dẫn:Đây là một bài khá dễ giải bằng Stack đơn thuần chỉ cần chú ý một chút là

được.Vì đây có cả dấu ‘(‘ và ‘)’ nên ta để Element có kiểu AnsiString;Ta sẽ làm như sau:+ Đọc INPUT vào chuỗi kí tự S.+ Duyệt i lần lượt từ 1 đến Length(S) với mỗi S[i] ta xét như sau:+ Nếu S[i] {C,H,O} thì ta đẩy {12,1,16} vào Stack

+ Nếu S[i] =2,3..9 thì ta làm như sau:. Đổi S[i] ra số gán vào y.. Lấy một phần ra khỏi Stack giả sử gán vào x.. Đổi x ra số gán vào k sau đó gán k:=y*k;. Đổi k ra chuỗi gán vào x. Đẩy x vào Stack.

+Nếu S[i] = ‘)’ thì:. k:=0;. Lấy một phần ra khỏi Stack giả sử gán vào x.. Đổi x ra số gán vào y sau đó k:=k+y;. Lặp đi lặp lại các công việc trên đến khi nào phần tử lấy ra là ‘(‘ thì

thôi.. Đổi k ra chuỗi gán vào x rồi đẩy x vào Stack.

+ Nếu S[i]=’(‘ thì đẩy S[i] vào Stack.

Page 54: Kieu Du Lieu Truu Tuong

+ Việc đơn giản còn lại chỉ là lần lượt lấy các phần tử ra khỏi Stack đến khi nào Stack rỗng thì thôi và tính tổng của chúng, tổng đó chính là đáp án của bài toán.

VD: S=’C(OH)2’;i:=1; Stack=( );S[1]=’C’ đẩy ’12’ vào Stack.i:=2; Stack=(‘12’);S[2]=’(‘ đẩy ‘(‘ vào Stack.i:=3; Stack=(‘12’,’(‘ );S[3]=’O’ đẩy ‘16’ vào Stack.i:=4; Stack=(‘12’,’(‘,’16’); S[4]=’H’ đẩy ‘1’ vào Stack.i:=5; Stack=(‘12’, ‘(‘,’16’,’1’); S[5]=’)’ thực hiện:

+ sum:=0;+ Lấy Stack[Top] ra khỏi Stack: y:=1;Top:=Top-1;+ k:=k+y=1;+ Lấy Stack[Top] ra khỏi Stack gán vào x, x<>’ (‘ nên a:=16;+ k:=k+y=17; Top:=Top-1;+ Lấy Stack[Top] ra khỏi Stack gán vào x, x=’(‘ nên dừng lại.+ Đẩy k vào Stack.i=6; Stack(‘12’,17’);S[6]=’2’; lấy Stack[Top] ra khỏi Stack gán vào x; Đổi

x,Stack[Top] ra số gán vào k và y ; k:=y*k; Đẩy k vào Stack.Stack=(‘12’,’34’);Kq=12+34=46;Vậy kết quả cần tìm là 46.Thực ra bài toán trên chỉ khó ở phần xử lý chuỗi ‘(…)’ và ‘…A’) với

A=2..9;Nếu chuỗi có dạng ‘(…)’ thì ta chỉ việc tính tổng khối lượng các phần tử

trong dấu ngoặc rồi đẩy nó vào trong Stack làm như vậy khi trường ‘(…)A’ với A=2..9 chúng ta coi ‘(…)’ như là một nguyên tố có khối lượng là Stack[Top] và xử lý chung là lấy một phần tử ở Top của Stack ra và nhân với hệ số A rồi lại đẩy vào Stack.

Cài đặt chương trình:

Const fi='MASS.INP'; fo='MASS.OUT'; Maxn = 1000; Number =['2','3','4','5','6','7','8','9'];Type Element =AnsiString; TStack =Array[0..Maxn] of Element;Var Stack :TStack; Top :longint; kq :longint; s :Ansistring;

Page 55: Kieu Du Lieu Truu Tuong

f :text;Procedure Nhap;begin assign(f,fi); reset(f); read(f,s); close(f);end;Procedure Init;begin Top:=0;end;Function Is_Empty:boolean;begin Is_Empty:=Top=0;end;Function Is_Full:boolean;begin Is_Full:=Top=Maxn;end;Procedure PushS(x:Ansistring);begin if not Is_Full then begin inc(Top); Stack[Top]:=x; end;end;Procedure PopS(var x:Ansistring);begin if not Is_Empty then begin x:=Stack[Top]; dec(Top); end;end;Procedure Solution;var i :longint; x :string; y :longint; k :longint;begin kq:=0; for i:=1 to length(s) do begin if s[i]='H' then PushS('1'); if s[i]='O' then PushS('16'); if s[i]='C' then PushS('12'); if s[i]='(' then PushS(s[i]); if s[i]=')' then begin k:=0; while Stack[Top]<>'(' do begin PopS(x); val(x,y); k:=k+y; end; PopS(x); str(k,x);

Page 56: Kieu Du Lieu Truu Tuong

PushS(x); end; if s[i] in Number then begin val(s[i],y); PopS(x); val(x,k); k:=k*y; str(k,x); PushS(x); end; end; kq:=0; While Top>0 do begin PopS(x); val(x,y); kq:=kq+y; end;end;Procedure Xuat;begin assign(f,fo); rewrite(f); write(f,kq); close(f);end;begin Nhap; Init; Solution; Xuat;end.

Bài 2: Những con đường quanh nông trang Bạn có thể Test bài này trên VNOI.INFO với mã bài là: VRATF

Các con bò của nông dân John có sở thích là hay đi khám phá những vùng xung quanh nông trang. Ban đầu, tất cả N (1 <= N <= 1,000,000,000) con bò tập trung thành 1 nhóm và cùng bắt đầu chuyến đi trên 1 con đường. Cho tới khi gặp một ngã ba đường thì chúng đôi khi chọn cách chia làm 2 nhóm nhỏ hơn ( mỗi nhóm ít nhất 1 bò ) và mỗi nhóm lại tiếp tục hành trình trên con đường của nhóm chúng. Khi một trong những nhóm này gặp 1 ngã ba khác thì nhóm này lại có thể tách ra tiếp, và cứ như vậy.

Các con bò đã hình thành nên 1 quy tắc về việc chia nhóm như sau: nếu chúng có thể chia thành 2 nhóm mà chênh lệch số bò của 2 nhóm là đúng bằng K (1 <= K <= 1000) thì tại ngã ba đó chúng sẽ chia làm 2; nếu không thì chúng sẽ dừng cuộc hành trình và đứng ở đó nhấm nháp cỏ non.

Giả sử rằng luôn có những ngã ba mới trên các con đường, hãy tính xem cuối cùng có bao nhiêu nhóm bò tất cả.

Dữ liệu vào: 2 số nguyên N và K cách nhau bởi dấu cách.

Page 57: Kieu Du Lieu Truu Tuong

Kết quả: Một số nguyên cho biết số lượng nhóm bò sau cùng.Ví dụ:

VRATF.INP VRATF.OUT6 2 3

Giải thích:Cuối cùng có 3 nhóm bò (1 nhóm có 2 bò, 1 nhóm có 1 và 1 nhóm có 3 ). 6 / \ 2 4 / \ 1 3

Hướng dẫn:Bài trên các bạn có thể dùng đệ quy để giải một cách hữu hiệu, nhưng

chúng ta có thể sử dụng Stack và Queue để giải quyết.Sau đây là ý tưởng sử dụng hàng đợi để giải quyết bài này:Gọi kq là kết quả cần tìm. Ban đầu khởi tạo Queue có duy nhất một phần tử

đó là số bò ban đầu và kq:=0;Thực hiên các công việc sau chừng nào Queue rỗng thì thôi: + Lấy một phần tử ra khỏi hàng đợi gán vào n.

+ Nếu ((n-k) mod 2=1) or (n<=k) thì tăng kq lên 1: kq:=kq+1;{ Khi n<=k thì có nghĩa không thể chia được nữa, (n-k) mod 2 =1 có nghĩa là không thể chia thành hai nhóm mà chênh lệch số lượng đúng bằng k, khi đó chúng sẽ dừng lại và gặm cỏ, khi đó ta có thêm một nhóm mới nên kq:=kq+1}

+ Trái lại thì ta có thể chia thành hai nhóm mà chênh lệch số lượng đúng bằng k, như vậy nhóm một sẽ có a=(n-k) div 2 con bò, nhóm 2 sẽ có b=n-a con bò.Ta đẩy a và b vào hàng đợi.

Note: Tuy n<= nhưng ta chỉ cần khai báo số phần tử tối đa của Queue là

10000 là quá đủ rồi. Cài đặt chương trình:Đây là một bài toán khá đơn giản vì vậy tôi chỉ viết thủ tục chính các thủ

tục còn lại các bạn tự viết.Procedure solution;

Var a,b :longint;Begin

PushQ(n);kq:=0;{kq va n la bien khai bao o chuong trinh chinh}While not Empty do

BeginPopQ(n);if (n mod 2=1) or (n<=k) then inc(kq)

else

Page 58: Kieu Du Lieu Truu Tuong

Begin a:=(n-k) div 2; b:=n-a; PushQ(a); PushQ(b); End; End;End;

Tôi xin giới thiệu các bạn thủ tục đệ quy như sau:Hàm Find(n:longint):longint trả về số nhóm bò cuối cùng khi có n con bò:Function find(n:longint):longint;Begin

if (n mod 2=1) or (n<=k) then exit(1) else

find:=find((n-k) div 2))+find((n-k) div 2+k);End;

Ta thấy thủ tục đệ quy trên khá ngắn gọn tuy với n<=1000000000 nó chạy tương đối nhanh nhưng khi mở rộng ra nó tỏ ra chậm hơn nhiều so với cách sử dụng hàng đợi. Tuy nhiên khi đi thi tôi khuyên nên sử dụng một cách hợp lý các thuật toán, không nhưng tốc độ nhanh mà còn đòi hỏi cài đặt càng đơn giản càng tốt. Nếu là tôi tôi sẽ chọn cách đệ quy.

Bài 3: Huyền thoại Lục Vân TiênBạn có thể Test bài này trên VNOI.INFO với mã bài là: MINK

Lục Vân Tiên cũng giống Samurai Jack, bị Quan Thái Sư đẩy vào vòng xoáy thời gian và bị chuyển tới tương lai của những năm 2193.

Ở thời đại này, Tráng sỹ phải là người thông thạo máy tính, gõ bàn phím lia lịa như đấu sỹ thời xưa múa kiếm ấy và phải qua một cuộc thi lập trình mới được phong danh hiệu.

Để vượt qua vòng loại, Vân Tiên cần tham gia cuộc thi sát hạch. Ban Giám Khảo cuộc thi sát hạch gồm có N người, họ đều là các cao thủ trong giới IT. Các thành viên trong Ban Giám Khảo được đánh số từ 1 -> N và mỗi người lại có một chỉ số sức mạnh gọi là APM ( Actions Per Minute ). Các giám khảo sẽ xếp hàng lần lượt từ 1 -> N. Mỗi thí sinh sẽ phải đấu với K vị giám khảo và K vị giám khảo này phải đứng liền thành 1 đoạn ( Tức là i, i+1, i+2,... i+K-1 ), chỉ cần thắng 1 vị giám khảo thì sẽ vượt qua vòng loại.

Tuy nhiên thí sinh không được chọn xem những giám khảo nào sẽ đấu với mình.

Vân Tiên rất lo vì lỡ may đụng độ với những vị giám khảo nào "khó nhằn" thì sẽ tiêu mất. Nên chiến thuật của Vân Tiên là tập trung hạ vị giám khảo có chỉ số APM thấp nhất trong số K vị. Bạn hãy lập trình để giúp Lục Vân Tiên xác

Page 59: Kieu Du Lieu Truu Tuong

định được ở tất cả các phương án thì chỉ số APM của vị giám khảo thấp nhất sẽ là bao nhiêu (Có tất cả N-k+1 phương án:

Phương án 1: Vân Tiên phải đấu với vị 1 -> vị k Phương án 2: Vân Tiên phải đấu với vị 2 -> vị k+1 … Phương án N-k+1: Vân Tiên phải đấu với vị N-k+1 -> vị N.(1 < N < 17000, chỉ số APM của 1 giám khảo > 1 và < 2 tỉ, 1 < K < N ). Dữ liệu vào:Dòng 1: số T là số test.Tiếp theo là T bộ test, mỗi bộ test có format như sau:Dòng 1: N k Dòng 2: N số nguyên dương A[1], … A[N]. Kết quả:Kết quả mỗi test ghi ra trên dòng, dòng thứ i gồm N-k+1 số, số thứ j tương

ứng là chỉ số APM của vị giám khảo yếu nhất trong phương án j. Ví dụ:

MINK.INP MINK.OUT24 23 2 4 13 31 2 3

2 2 11

Hướng dẫn:Ta có thể phát biểu bài toán trên ngắn gọn như sau:Cho một dãy số gồm n phần tử a[1..n] và một số nguyên dương k. Tìm và

in ra theo cầu sau: Min(a[1],a[2],a[3]..a[k]);

Min(a[2],a[3],a[4]..a[k+1]); Min(a[3],a[4],a[5]..a[k+2]); … Min(a[n-k+1],a[n-k+2],..a[n]);

Và tất nhiên cũng có thể là tìm Max. Có thể nói đây là một bài toán rất hay về việc sử dụng Queue và Stack để tìm Min Max trong một đoạn tịnh tiến với thời gian là O(n).

Lời giản quen thuộc và đơn giản nhất:For i:=1 to n-k+1 do Begin

min:=a[i]; for j:=i+1 to i+k-1 do

if min>a[j] then min:=a[j];

Page 60: Kieu Du Lieu Truu Tuong

writeln(f,min);{Min(a[i],a[i+1],..,a[i+k-1])} End;

Với thuật toán trên ta có độ phức tạp O(n.k) không thể chạy với đề bài trên. Tôi xin trình bày ngắn gọn tư tưởng thuật toán sử dụng DQueue (Double

Ended Queue) như chúng ta đã trình bày ở trên như sau: Ở đây nếu chỉ nói là đẩy một phần tử vào Queue ta hiểu là thao tác PushR tức là đẩy vào cuối Queue.

Trước tiên ta khai báo kiểu dữ liệu cho một phần tử là một bản ghi (record) gồm 2 trường: value: giá trị của một phần tử của Queue, cs:Chỉ số của phần tử ở mảng a được đưa vào Queue.

Ta duy trì Queue như sau: Giả sử xét đến phần tử thứ i là a[i]:+ Lấy ra một phần tử của Queue gán vào x.Nếu x<=a[i] thì ta đẩy x và a[i]

vào Queue. + Trái lại tức là x>a[i] ta lặp lại công việc lấy ra một phần tử của Queue

đến khi nào Queue rỗng hoặc gặp một phần tử nhỏ hơn hoặc bằng a[i] thì thôi.Sau đó chúng ta lại đẩy a[i] vào Queue.

+ Tại sao chúng ta lại làm vậy: Ta nhận xét nếu làm như trên thì Queue luôn duy trì ở trạng thái là một dãy tăng dần xét các phần tử từ Head đến Top về giá trị (value), mặt khác vẫn đảm bảo trong Queue các phần tử luôn có chỉ số tăng dần từ Head đến Top. Như vậy phần tử nhỏ nhất trong Queue chính là phần tử ở đầu Queue hay chính là Queue[Head].value.Và Min(a[i-k+1],a[i-k+1]..a[i]) là phần tử Queue[Head].cs của mảng a.

+ Nếu i>=k có nghĩa là i là phần tử cuối của đoạn [(i-k+1)..i] ta suy ra: Min(a[i-k+1],a[i-k+2],..,a[i]) =Queue[Head].value.

Ta cần chú ý: Vì kết quả cần tìm nằm trong đoạn a[i-k+1],a[i-k+2]..a[i] nên phần tử đầu của Queue tức là Head luôn phải thỏa mãn Head>=i-k+1 thì Queue[Head] mới là kết quả cần tìm. Vì vậy để Head>=i-k+1 khi xét đến a[i] thì khi lấy Queue[Head] ra khỏi Queue để đưa ra kết quả nếu y=Queue[head].cs<=i-k+1 thì ta không đẩy lại x=Queue[head] vào đầu của Queue nữa (PushL) trong trường hợp y>i-k+1 có nghĩa là i-k+2<=y<=i thì ta lại đẩy lại x vào Queue vì đoạn tiếp theo cần tìm là:Min(a[i-k+2],a[i-k+3]…a[i+1]), nên ta phải giữ lại và tất nhiên các phần tử còn lại trong Queue cũng có chỉ số trong đoạn đó.

Cài đặt chương trình:Const fi='MINK.INP'; fo='MINK.OUT'; maxn=17000;Type Element =record value:longint;

Page 61: Kieu Du Lieu Truu Tuong

cs :longint; end; arr1 =array[1..maxn] of longint; DQueue =array[1..maxn] of Element;Var T :longint; n,k :longint; a :arr1; Queue :DQueue; Head,Top:longint; f,f1 :text;Procedure nhap;var i :longint;begin readln(f,n,k); for i:=1 to n do read(f,a[i]);end;Procedure InitQ;begin Top:=0; Head:=1;end;Procedure PushR(x,y:longint);{Day phan tu x co chi so trong mang a

la y vao cuoi Queue}begin inc(Top); Queue[Top].value:=x; Queue[Top].cs:=y;end;Procedure PushL(x,y:longint);{Day phan tu x co chi so trong mang a

la y vao dau Queue}begin dec(Head); Queue[Head].value:=x; Queue[Head].cs:=y;end;Procedure PopR(var x,y:longint);{Lay ra phan tu o cuoi Queue gan

gia tri vao x, chi so vao y}begin x:=Queue[Top].value; y:=Queue[Top].cs; dec(Top);end;Procedure PopL(var x,y:longint);{Lay ra phan tu o dau Queue gan gia

tri vao x, chi so vao y}begin x:=Queue[Head].value; y:=Queue[Head].cs; inc(Head);end;Procedure Solution;var

Page 62: Kieu Du Lieu Truu Tuong

i,j :longint; x :longint; y :longint; cuoi :longint;begin InitQ; for i:=1 to n do If Head>Top then begin PushR(a[i],i); If i>=k then write(f1,a[i],' '); if k=1 then PopL(x,y); end else begin PopR(x,y); if x<=a[i] then begin PushR(x,y); PushR(a[i],i); end else begin while (x>a[i]) and (Top>=head) do PopR(x,y); If x<=a[i] then PushR(x,y); PushR(a[i],i); end; if i>=k then begin PopL(x,y); write(f1,x,' ');{x=Min(a[i-k+1]..a[i])} if y>i-k+1 then PushL(x,y); end; end;end;Procedure Run;var i :longint;begin assign(f,fi); reset(f); Readln(f,T); assign(f1,fo); rewrite(f1); for i:=1 to T do begin nhap; solution; writeln(f1); end; close(f); close(f1);end;

Page 63: Kieu Du Lieu Truu Tuong

begin run;end.

Bài 4: Bán dừaBạn có thể Test bài này trên VNOI.INFO với mã bài là: KPLANK

Nếu các bạn biết câu chuyện thương tâm "ăn dưa leo trả vàng" của Pirate hẳn đã phải khóc hết nước mắt khi anh ấy vì lòng thương chim, đã bán rẻ trái dưa leo siêu bự của mình.

Dưa leo cũng đã bị chim to lấy đi rồi, Pirate giờ chuyển sang nghề bán dừa để bù lỗ. Bất đắc dĩ thôi, vì trên đảo toàn là dừa...

Nhưng mà bán cái gì thì đầu tiên cũng phải có biển hiệu đã. Pirate quyết định lùng sục trên đảo các mảnh ván còn sót lại của những con tàu đắm để ghép lại thành tấm biển. Cuối cùng anh cũng tìm được N tấm ván hình chữ nhật, tấm thứ i có chiều rộng là 1 đơn vị và chiều dài là a i đơn vị. Pirate dựng đứng chúng trên mặt đất và dán lại với nhau để được một mảnh ván to hơn (xem hình minh họa).

Việc cuối cùng chỉ là đem mảnh ván này đi cưa thành tấm biển thôi. Nhưng hóa ra đây lại là công việc khó khăn nhất. Pirate rất thích hình vuông và muốn tấm biển của mình càng to càng tốt, nhưng khổ nỗi trên đảo lại không có nhiều dụng cụ đo đạc. Không êke, không thước đo độ, nên Pirate chỉ còn cách dựa vào cạnh của N tấm ván ban đầu để cưa cho thẳng thôi. Pirate chỉ có thể cưa theo những đoạn thẳng chứa một cạnh nào đó (dọc hoặc ngang) của các tấm ván.

Hãy giúp anh ấy cưa được tấm biển lớn nhất có thể.Dữ liệu vào: Dòng thứ nhất: ghi số nguyên N - số tấm ván. N dòng tiếp theo: mô tả độ cao của các tấm ván theo thứ tự trái sang phải

sau khi đã dán lại.Kết quả: Một số nguyên duy nhất là độ dài cạnh của tấm biển lớn nhất có thể cưa

được. Giới hạn Độ cao của các tấm ván là các số nguyên dương không vượt quá 109. 1 ≤ N ≤ 106.

Page 64: Kieu Du Lieu Truu Tuong

60% số test có 1 ≤ N ≤ 2000. 80% số test có 1 ≤ N ≤ 105.Ví dụ:

KPLANK KPLANK75243314

3

Giải thích: Hình dưới đây minh họa phương án tối ưu.

Chú ý:+ Phân biệt tấm ván và tấm biển.+ Các tấm ván cắt ra làm tấm biển phải trong một đoạn liên tiếp các tấm

ván từ i đến j. (1.1)+ Vì không có thước nên muốn Pirate cắt được theo chiều ngang thì phải

có một tấm ván làm mẫu có nghĩa là trong tấm biển cần tìm phải có ít nhất một tấm ván giữ nguyên không bị cắt. (1.2)

+ Để đạt được 60% đến 80% số test chỉ việc xét i là cạnh của tấm biển cần tìm sau đó tìm left nhỏ nhất <=i và right lớn nhất >=i thỏa mãn a[left..right]>=a[i] nếu right-left>=a[i] thì cập nhật a[i] với Max. Chú ý: Nếu a[i]<=Max thì ta không cần xét tấm ván i nữa.

Hướng dẫn:Gọi left[i] và right[i] lần lượt là gần i nhất thỏa mãn: + left[i]<=i; right[i]>=i.+ Min(a[left[i]..right[i]])>=a[i]).+ a[left[i-1]]<a[i] và a[right[i]+1]>a[i].Như vậy ta dễ dàng thấy nếu right[i]-left[i]+1>=a[i] thì ta sẽ cắt được tấm

biển có cạnh là a[i] và chứa tấm ván i.Bây giờ ta tính lần lượt left[i] và right[i] bằng Stack.

Page 65: Kieu Du Lieu Truu Tuong

Ta có nhận xét: Nếu a[i-1]<a[i] thì suy ra left[i]:=i trái lại nếu a[i-1]>=a[i] thì left[i] luôn nhỏ hơn hoặc bằng left[i-1] vì trong đoạn left[i-1]..i-1 các phần tử luôn lớn hơn hoặc bằng a[i-1] mà a[i-1]>=a[i] nên các phần tử trong đoạn

left[i-1]..i luôn lớn hơn hoặc bằng a[i].Tương tự ta có nhận xét trên đối với right.Ta dùng Stack lưu lại left[i], left[left[i]], và i để thu hẹp phạm vi tìm kiếm

left[i], thay vì phải duyệt từ i-1 đến khi nào gặp phần tử nhỏ hơn, thì với những phần tử có chiều cao lớn hơn hoặc bằng a[i] thì nếu a[i+1]<=a[i] thì các phần tử đó cũng thỏa mãn i+1 nên ta chỉ cần tìm tiếp từ left[i-1] trở xuống,và ta chỉ cần lưu i vào Stack để so sánh a[i+1] với a[i] thôi thay vì phải lưu đoạn từ left[i] đến i. Còn nếu a[i+1]<a[i] thì left[i] luôn bằng i.

Nếu Stack rỗng thì ta có luôn có left[i] := i và đẩy i vào Stack.Nếu Stack khác rỗng thì ta làm như sau:Khởi tạo y:=i;Chừng nào Stack chưa rỗng và a[Stack[Top]]>=a[i] thì Pop(x); (x chính

là Stack[Top]) ta có y=left[x].Khi đó left[i]:=y; Đẩy i vào Stack. Ta thấy trong cả hai trường hợp i đều

được đẩy vào Stack để phục vụ cho việc tìm left[i+1].Tính right[i] tương tự như tính left[i] chỉ khác duyệt các phần tử theo thứ tự

ngược lại.Cài đặt chương trình:Const tfi='KPLANK.INP'; tfo='KPLANK.OUT'; maxn=1000000;Type arr1 =array[1..maxn] of longint; TStack =array[0..maxn+1] of longint;Var n :longint; a :arr1; left :arr1; right :arr1; Stack :TStack; Top :longint; kq :longint; fi,fo :text;Procedure nhap;var i :longint;begin assign(fi,tfi); reset(fi); readln(fi,n);

Page 66: Kieu Du Lieu Truu Tuong

for i:=1 to n do readln(fi,a[i]); close(fi);end;procedure Init;begin Top:=0;end;Procedure Push(x:longint);begin inc(Top); Stack[Top]:=x;end;Procedure Pop(var x:longint);begin x:=Stack[Top]; dec(Top);end;Procedure solution;var i :longint; x,y :longint;begin {Tim left[i]} Init; for i:=1 to N do begin if Top=0 then begin Push(i); left[i]:=i; end else begin y:=i; While (Top>0) and (a[Stack[Top]]>=a[i]) do begin Pop(x);

{Neu a[x]>=a[i] thi a[left[x]..x]>=a[i]} y:=left[x]; end; left[i]:=y; Push(i); end; end; {Tim right[i]} Init; for i:=N downto 1 do begin if Top=0 then begin Push(i); right[i]:=i; end

Page 67: Kieu Du Lieu Truu Tuong

else begin y:=i; While (Top>0) and (a[Stack[Top]]>=a[i]) do begin Pop(x); {Neu a[x]>=a[i] thi a[x..right[x]]>=a[i]}

y:=right[x]; end; right[i]:=y; Push(i); end; end; kq:=0; for i:=1 to n do if (right[i]-left[i]+1>=a[i]) and (a[i]>kq) then kq:=a[i];end;Procedure xuat;begin assign(fo,tfo); rewrite(fo); write(fo,kq); close(fo);end;Begin Nhap; Solution; Xuat;End.

Bài 5: Giải mã văn tự MAYA (IOI 2006)Các bạn có thể Test bài này trên VNOI.INFO với mã bài là PBCWRI

Giải mã văn tự MayA là một nhiệm vụ phức tạp hơn so với các nghiên cứu trước đây. Trên thực tế, sau gần 200 năm người ta chưa làm sáng tỏ gì nhiều lắm trong lĩnh vực này. Chỉ trong phạm vi ba thập niên cuối này mới có những tiến bộ đáng kể trong nghiên cứu.

Văn tự MayA đặt cơ sở dựa vào các hình vẽ nhỏ, được biết dưới dạng các nét vạch biểu diễn âm tiết. Từ trong tiếng May A thường được viết dưới dạng ô vuông chứa một một số các nét vạch. Đôi khi một từ bị bổ dọc thành nhiều ô hoặc một ô lại chứa nhiều nét vạch hơn số nét cần thiết cho một từ.

Một trong số các vấn đề liên quan tới giải mã văn tự May A nảy sinh khi xác định trình tự đọc âm tiết. Khi điền các vạch vào ô vuông, đôi khi người May A lại quy trình trình tự đọc dựa trên các tiêu chuẩn thẩm mỹ riêng chứ không theo một quy luật chung. Điều này dẫn đến việc, ngay cả khi đã biết rõ âm tiết của nhiều nét vạch, các nhà khảo cổ học cũng không dám khẳng định chắc chắn cách phát âm cả từ.

Page 68: Kieu Du Lieu Truu Tuong

Các nhà khảo cổ đang khảo sát một từ W cụ thể. Họ biết những nét gạch tạo thành từ đó, nhưng không biết hết các cách vẽ chúng. Biết bạn đến tham dự IOI-06, họ đề nghị bạn giúp đỡ. Các nhà khảo cổ sẽ chọ bạn biết g nét gạch tạo thành từ W và dãy S các nét vạch (theo trình tự xuất hiện) của câu đang khảo sát. Hãy xác định các khả năng xuất hiện từ W trong câu được khảo sát.

Yêu cầu: Cho các nét vạch tạo thành từ W và dãy S các nét vạch trong bản văn tự chạm trổ. Hãy lập trình xác định số khả năng xuất hiện từ W trong S. Vì mọi trình tự xuất hiện các nét vạch trong W đều là chấp nhận được, các nhà khảo cổ yêu cầu bạn tìm số lượng dãy các nét vạch liên tiếp trong S, mỗi dãy tương ứng với hoán vị g nét vạch trong W.

Giới hạn:1 ≤ g ≤ 3000 số lượng vạch trong Wg ≤ |S| ≤ 3 000 000 Số nét vạch trong dãy SDữ liệu vào: Đọc từ file văn bản writing.inp

WRITING.INP Ý nghĩa4 11cAdaAbrAcadAbRa

Dòng 1: Chứa 2 số nguyên g và |S| viết rời nhau. Dòng 2: Chứa g nét vạch liên tiếp nhau tạo thành W. Mỗi nét vạch tươn ứng với một ký tự. Các ký tự hợp lệ là ‘a’-‘z’ và ‘A’-‘Z’; ký tự hoa và thường là khác nhau.Dòng 3: Chứa |S| ký tự liên tiếp biểu diễn S. Valid characters are ‘a’-‘z’ and ‘A’-‘Z’; Các ký tự hợp lệ là ‘a’-‘z’ và ‘A’-‘Z’; ký tự hoa và thường là khác nhau..

Kết quả: Ghi kết quả ra file văn writing.out WRITING.OUT Ý nghĩa2 Dòng 1: Phải chứa số khả năng xuất hiện W trong S.

Chấm điểm: Có một bộ phận Tests được đánh giá tổng cộng 50 điểm, mỗi test thoả mãn ràng buộc g ≤ 10.

Chú ý:Các kí tự xét trong hang đá xuất hiện W phải liên tiếp nhau.Hướng dẫn:Bài này thực chất không khó, có khá nhiều cách giải và cách đơn giản nhất

là xét tất cả các từ có độ dài g trong S rồi sắp xếp từ đó và so sánh với từ W (cũng đã sắp xếp), vì các từ trong khoảng ‘a’..’z’ và ‘A’..’Z’ ta có thể dùng thuật toán đếm phân phối tuy nhiên thuật toán trên chỉ giải quyết được 75% bài toán.

Sau đây tôi xin trình bày cách dùng Queue để giải quyết triệt để bài toán trên: Queue lưu chỉ số các kí tự trong hang đá.

Page 69: Kieu Du Lieu Truu Tuong

Gọi dd[i] là số kí tự có mã ASCII là i trong từ W. d[i] là số kí tự có mã ASCII là i trong Queue.

Giả sử xét đến kí tự thứ i trong từ S, tất nhiên đã xét hết các kí tự từ 1 đến i-1 ta có: Gọi k=ord[S[i]] (Mã ASCII kí tự S[i])

Nếu d[k]+1>dd[k] (số kí tự S[i] trong Queue cộng thêm S[i] nhiều hơn trong từ W) thì ta phải lấy ra trong Queue (tất nhiên là lấy ở front) đến khi nào d[k]+1=dd[k] thì thôi.Và nếu dd[k]>0(Có kí tự S[i] trong W) thì đẩy i vào Queue.

Nếu số kí tự trong Queue đúng bằng g (Queue[rear]-Queue[front]+1=g tăng kq lên 1 (1))thì có nghĩa trong hang đá các kí tự thứ Queue[front] đến Queue[rear] tạo thành từ W.

Chứng minh:Nếu S[i] không xuất hiên trong W thì cũng không được đưa vào Queue và

khi đó vì S[i] không xuất hiện trong W nên dd[k]=0 và d[k]=0(k=ord[S[i]]) nên d[k]+1 luôn lớn hơn dd[k] ta lấy đến khi nào Queue rỗng thì thôi.

Nếu S[i] xuất hiện trong W mà d[k]+1<=dd[k] có nghĩa là trong Queue chưa đủ dd[k] kí tự S[i] thì ta đẩy i vào Queue khi đó d[k] tăng nên 1.

Nếu S[i] xuất hiện trong W mà d[k]+1>dd[k] có nghĩa là trong Queue có số kí tự S[i] lớn hơn dd[k](nói thế thôi chứ trong Queue chỉ chứa tối đa dd[k] kí tự S[i] thôi nha) thì ta lấy bớt phần tử ra khỏi Queue để thỏa mãn d[k]=dd[k], ta thấy các phần tử được lấy ra ở front thỏa mãn các kí tự trong Queue là các kí tự liên tiếp trong S.

Vậy ta thấy trong Queue luôn chứa các kí tự có trong W và số lượng các kí tự này luôn <= số kí tự trong W (1) luôn đúng

Note: Cài đặt Queue bằng danh sách vòng.Cài đặt chương trình:Const tfi='DMAYA.INP'; tfo='DMAYA.OUT'; maxs=3000000; maxg=3000;Type arr1 =array[1..maxs] of char; TQueue =array[0..maxg-1] of longint; arr3 =array[1..maxg] of char; arr2 =array[60..200] of longint;Var m,n :longint; w :arr3; S :arr1; Queue :TQueue; front :longint;

Page 70: Kieu Du Lieu Truu Tuong

rear :longint; dem :longint; kq :longint; d :arr2; dd :arr2; fi,fo :text;Procedure Init;var i :longint;begin for i:=60 to 200 do begin d[i]:=0; dd[i]:=0; end; kq:=0; dem:=0; Front:=0; rear:=Maxg-1;end;Procedure nhap;var i :longint;begin assign(fi,tfi); reset(fi); readln(fi,m,n); for i:=1 to m do begin read(fi,w[i]); inc(dd[ord(w[i])]); end; readln(fi); for i:=1 to n do read(fi,s[i]); close(fi);end;Procedure Push(x:longint);begin rear:=(rear+1) mod maxg; Queue[rear]:=x; inc(d[ord(S[x])]); inc(dem);end;Procedure Pop(x:longint);begin x:=Queue[front]; front:=(front+1) mod maxg; dec(d[ord(S[x])]); dec(dem);end;Procedure Solution;var i,j :longint;

Page 71: Kieu Du Lieu Truu Tuong

x :longint; k :longint;begin For i:=1 to n do begin k:=ord(S[i]); while (d[k]>dd[k]-1) and (dem>0) do Pop(x); if dd[k]>0 then Push(i); if Queue[rear]-Queue[front]+1=m then inc(kq); end;end;Procedure xuat;begin assign(fo,tfo); rewrite(fo); write(fo,kq); close(fo);end;Begin Init; Nhap; Solution; Xuat;End.

Độ phức tạp: O(n)Bài 6: Chiến trường Ô qua (Mã bài: KAGAIN VNOI.INFO)Lại nói về Lục Vân Tiên, sau khi vượt qua vòng loại để trở thành Tráng Sỹ,

anh đã gặp được Đôrêmon và được chú mèo máy cho đi quá giang về thế kỷ 19. Trở lại quê hương sau nhiều năm xa cách, với tấm bằng Tráng Sỹ hạng 1 do Liên Đoàn Type Thuật cấp, anh đã được Đức Vua cử làm đại tướng thống lãnh 3 quân chống lại giặc Ô Qua xâm lăng. Đoàn quân của anh sẽ gồm N đại đội, đại đội i có A[i] ( > 0 ) người. Quân sỹ trong 1 đại đội sẽ đứng thành 1 cột từ người 1 -> người A[i], như vậy binh sỹ sẽ đứng thành N cột. Vì Vân Tiên quyết 1 trận sẽ đánh bại quân Ô Qua nên đã cử ra 1 quân đoàn hùng mạnh nhất. Trong sử cũ chép rằng, quân đoàn của Vân Tiên cử ra lúc đó là một nhóm các đại đội có chỉ số liên tiếp nhau ( tức là đại đội i, i + 1, … j ). Vì sử sách thì mối mọt hết cả nên chỉ biết được mỗi thế. Ngoài ra theo giang hồ đồn đại thì sức mạnh của 1 quân đoàn = số người của đại đội ít người nhất * số đại đội được chọn. Nhiệm vụ của bạn là dựa trên các thông số của các nhà khảo cổ có được, hãy cho biết quân đoàn mà Vân Tiên đã chọn ra là từ đại đội nào đến đại đội nào. Chú ý nếu có nhiều phương án thì ghi ra phương án mà chỉ số của đại đội đầu tiên được chọn là nhỏ nhất.

InputDòng 1: Số T là số bộ test.

Page 72: Kieu Du Lieu Truu Tuong

T nhóm dòng tiếp theo, mỗi nhóm dòng mô tả 1 bộ test. Nhóm dòng thứ i: Dòng 1: N ( <= 30000 ) Dòng 2: N số nguyên mô tả N số A[1], A[2], … A[N] ( các số nguyên

dương <= 30000 ).OutputKết quả mỗi test ghi ra trên 1 dòng, gồm 3 số: sức mạnh quân đoàn mạnh

nhất, chỉ số của đại đội đầu tiên và chỉ số của đại đội cuối cùng được chọn. Example

Input Output243 4 3 141 2 1 3

9 1 34 1 4

Gợi ý: Tương tự bài KPLANK.Bài 7: Hình vuông 01 (Mã bài: QBSQUARE)Cho một bảng kích thước MxN, được chia thành lưới ô vuông đơn vị M

dòng N cột ( 1 <= M, N <= 1000 ). Trên các ô của bảng ghi số 0 hoặc 1. Các dòng của bảng được đánh số 1,

2... M theo thứ tự từ trên xuống dưới và các cột của bảng được đánh số 1, 2..., N theo thứ tự từ trái qua phải

Yêu cầu: Hãy tìm một hình vuông gồm các ô của bảng thoả mãn các điều kiện sau:

1 - Hình vuông là đồng nhất: tức là các ô thuộc hình vuông đó phải ghi các số giống nhau (0 hoặc 1),

2 - Cạnh hình vuông song song với cạnh bảng,3 - Kích thước hình vuông là lớn nhất có thể.InputDòng 1: Ghi hai số m, n M dòng tiếp theo, dòng thứ i ghi N số mà số thứ j là số ghi trên ô (i, j) của

bảng

OutputGồm 1 dòng duy nhất ghi kích thước cạnh của hình vuông tìm được ExampleInput:

Page 73: Kieu Du Lieu Truu Tuong

11 130 0 0 0 0 1 0 0 0 0 0 0 00 0 0 0 1 1 1 0 0 0 0 0 00 0 1 1 1 1 1 1 1 0 0 0 00 0 1 1 1 1 1 1 1 0 0 0 00 1 1 1 1 1 1 1 1 1 0 0 01 1 1 1 1 1 1 1 1 1 1 0 00 1 1 1 1 1 1 1 1 1 0 0 00 0 1 1 1 1 1 1 1 0 0 0 00 0 1 1 1 1 1 1 1 0 0 0 00 0 0 0 1 1 1 0 0 0 0 1 10 0 0 0 0 1 0 0 0 0 0 1 1Output:7

Page 74: Kieu Du Lieu Truu Tuong

Phần thứ baKẾT LUẬN

Các kiến thức liên quan đến kiểu dữ liệu trừu tượng (Abstract Data Type – ADT) rất phong phú và đa dạng. Tuy nhiên, đối với công tác bồi dưỡng HSG Tin, tôi thấy cần thiết phải tìm hiểu ADT, cài đặt các phép toán trên mỗi ADT bằng một ngôn ngữ lập trình cụ thể và sau đó là biết áp dụng vào giải các bài toán

Lần đầu đọc, thực hành và suy ngẫm về vấn đề này nên chắc chắn còn nhiều thiếu sót, tác giả rất mong nhận được ý kiến đóng góp của bạn đọc, đồng nghiệp, học sinh, sinh viên để tài liệu được ngày càng hoàn thiện hơn.

Tác giảNguyễn Thị Hợp

Page 75: Kieu Du Lieu Truu Tuong

Mục lụcTrang

Phần thứ nhất: MỞ ĐẦU...........................................................................2Phần thứ hai: NỘI DUNG CHUYÊN ĐỀ.................................................4

Chương I: CƠ SỞ LÝ THUYẾT.......................................................................4I.1. Các khái niệm......................................................................................4

I.1.1. Kiểu dữ liệu..............................................................................4I.1.2. Kiểu dữ liệu trừu tượng............................................................4I.1.3. Cấu trúc dữ liệu........................................................................5

I.2. Sơ lược về con trỏ và biến động.........................................................7I.2.1. Biến tĩnh...................................................................................7I.2.2. Biến động.................................................................................7I.2.3. Khai báo kiểu con trỏ...............................................................8I.2.4. Khai báo biến con trỏ...............................................................8I.2.5. Các thao tác đối với biến con trỏ.............................................8

Chương II: DANH SÁCH TUYẾN TÍNH......................................................10II.1. Khái niệm và các phép toán cơ bản.................................................10

II.1.1. Khái niệm.............................................................................10II.1.2. Các phép toán cơ bản............................................................10

II.2. Các cách cài đặt danh sách tuyến tính.............................................11II.2.1. Biểu diễn dưới dạng mảng....................................................11II.2.2. Danh sách móc nối...............................................................13II.2.3. Bài toán Josephus.................................................................24II.2.4. Phân tích sử dụng linked list.................................................26

II.3. Ngăn xếp..........................................................................................27II.3.1. Kiểu dữ liệu trừu tượng ngăn xếp.........................................27II.3.2. Tổ chức ngăn xếp bằng mảng...............................................27II.3.3. Tổ chức cài đặt ngăn xếp bằng danh sách móc nối..............29II.3.4. Một số ứng dụng của ngăn xếp.............................................31

II.4. Hàng đợi..........................................................................................32II.4.1. Kiểu dữ liệu trừu tượng hàng đợi.........................................32II.4.2. Tổ chức hàng đợi bằng mảng...............................................32II.4.3. Tổ chức hàng đợi bằng danh sách vòng...............................34II.4.4. Tổ chức hàng đợi bằng danh sách móc nối đơn kiểu FIFO..37

Chương III: CÂY NHỊ PHÂN.........................................................................38III.1. Định nghĩa, tính chất và phân loại..................................................38

Page 76: Kieu Du Lieu Truu Tuong

III.1.1. Định nghĩa...........................................................................38III.1.2. Tính chất của cây nhị phân..................................................38III.1.3. Phân loại..............................................................................39

III.2. Biểu diễn cây nhị phân...................................................................40III.2.1. Biểu diễn cây dùng mảng....................................................40III.2.2. Biểu diễn cây nhị phân dùng con trỏ...................................41

III.3. Phép duyệt cây nhị phân.................................................................42III.3.1. Duyệt theo thứ tự trước.......................................................42III.3.2. Duyệt theo thứ tự giữa.........................................................43III.3.3. Duyệt theo thứ tự sau..........................................................43

III.4. Các ví dụ ứng dụng........................................................................46III.4.1. Cây nhị phân biểu thức........................................................46III.4.2. Chuyển từ dạng trung tố sang dạng hậu tố..........................47III.4.3. Thuật toán sắp xếp kiểu vun đống.......................................48

Chương IV: BÀI TẬP ỨNG DỤNG...............................................................52Bài 1: Mas of Molecule...........................................................................52Bài 2: Những con đường quanh nông trang............................................55Bài 3: Huyền thoại Lục Vân Tiên...........................................................57Bài 4: Bán dừa.........................................................................................62Bài 5: Giải mã văn tự MayA...................................................................66Bài 6: Chiến trường Ô Qua.....................................................................70Bài 7: Hình vuông 01..............................................................................71

Phần thứ ba: KẾT LUẬN................................................................................73

Page 77: Kieu Du Lieu Truu Tuong

TÀI LIỆU THAM KHẢO

[1]. Hồ Sĩ Đàm – Tài liệu giáo khoa Chuyên Tin – Nxb Giáo dục, 2008.

[2]. Đỗ Xuân Lôi - Cấu trúc dữ liệu và Giải thuật – Nxb ĐHQG Hà Nội, 2007.

[3]. Nguyễn Đức Nghĩa - Cấu trúc dữ liệu và Giải thuật – Trung tâm TBVP 123

Lương Thế Vinh, 2008.

[4]. Bùi Thế Tâm – Turbo Pascal 7.0 – Nxb Giao thông vận tải, 2000.

[5]. Lê Minh Hoàng - Cấu trúc dữ liệu và giải thuật – Nxb ĐHSPHN 1999-

2004.

[6]. Trang Web: www.wikimedia.org

[7]. Trang Web: www.vmoi.info.vn