You are on page 1of 208

CHƯƠNG 1

TỔNG QUAN

1. 1. VAI TRÒ CỦA THUẬT TOÁN VÀ CẤU TRÚC DỮ LIỆU


Để giải một bài toán trong thực tế bằng chương trình máy tính ta
phải bắt đầu từ việc xác định bài toán. Cần nhiều thời gian và công
sức để xác định chính xác bài toán cần giải quyết, tức là phải trả lời rõ
ràng câu hỏi phải làm gì và như thế nào, vì thông thường hầu hết các
bài toán là chưa rõ ràng và có cảm giác phức tạp khi bắt đầu xem xét,
tìm hiểu. Để giảm bớt sự phức tạp của các bài toán thực tế, cũng như
làm rõ hơn các yêu cầu cần giải quyết, phải hình thức hóa hoặc mô
hình hóa bài toán, nghĩa là phát biểu lại bài toán thực tế thành một bài
toán hình thức. Khi một bài toán đã được hình thức hoá, ta có thể tìm
kiếm cách giải và xác định có hay không một lời giải có sẵn để giải
bài toán ấy. Trong trường hợp chưa có lời giải cho bài toán, vẫn có thể
dựa vào cách thức ta đã mô hình hóa và các thông tin có liên quan đến
bài toán để xây dựng một cách thức giải cho bài toán. Như thế, thông
qua thao tác mô hình hóa, sẽ dễ hơn trong việc tìm lời giải cho bài
toán ấy – đó chính là giải thuật.
Có thể thấy rằng, lời giải là tập hợp các bước hữu hạn nối tiếp
nhau để thực hiện các yêu cầu nào đó mà tập hợp của các kết quả
thông qua từng bước thực hiện giúp ta giải được bài toán. Có thể thấy
rằng các xử lý chi tiết trong từng bước phản ánh các thao tác trên các
đối tượng mang thông tin trong bài toán, đó chính là dữ liệu, bao gồm
các dữ liệu có sẵn, các dữ liệu nhập vào thêm cho bài toán và các dữ
liệu trung gian được tạo ra trong quá trình xử lý. Cách thức dữ liệu lưu
trữ và biểu diễn các thông tin cần thiết cho bài toán tạo thành các cấu
trúc lưu trữ thông tin – cấu trúc dữ liệu.
Như thế khi nói đến lời giải hay nói cách khác là giải thuật thì
không thể không nói đến dữ liệu và cách thức xử lý trên dữ liệu ấy.
Hơn thế nữa giữa cách thức lưu trữ dữ liệu và giải thuật có mối quan
hệ mật thiết với nhau khi giải quyết một bài toán trong tin học.
1
Điều này cũng được Niklaus Wirth - một nhà khoa học uy tín
trong lĩnh vực công nghệ thông tin, khẳng định qua một công thức n i
tiếng hương trình (Program) = Cấu trúc dữ liệu (Data Structure) +
Giải thuật (Algorithm). Điều này nói lên bản chất của việc tìm lời giải
cho một bài toán trong tin học là đi tìm một cấu trúc dữ liệu phù hợp
để biểu diễn dữ liệu của bài toán và từ đó x y dựng giải thuật phù hợp
với cấu trúc dữ liệu đã chọn. Ngày nay, với sự phát triển của các k
thuật lập trình, công thức của Wirth không hẳn còn đúng tuyệt đối
nữa, nhưng nó vẫn phản ánh sự gắn kết và thể hiện tầm quan trọng của
các cấu trúc dữ liệu và giải thuật.

1.2. THUẬT TOÁN

1.2.1. Định nghĩa thuật t n


ó rất nhiều định nghĩa cũng như cách phát biểu khác nhau về
định nghĩa của thuật toán. Theo Thomas H . Cormen trong cuốn ách
n i tiếng viết về thuật toán ntroduction to lgorithm , thuật toán
được định nghĩa là một thủ tục tính toán xác định; nhận vào các giá
tri hoặc một tập các giá trị gọi là giá trị đầu vào và inh ra các giá
tri hoặc một tập giá trị mới được gọi là giá trị đầu ra. Nói một cách
khác, thuật toán giống như là quy trình để hoàn thành một công việc
cu thể xác định nào đó.

Đ t ng ủa thuật toán
Thuật toán có các đặc trưng như sau :
- Tính đúng đắn: thuật toán cần phải đảm bảo cho một kết quả
đúng au khi thực hiện đối với các bộ dữ liệu đầu vào. Đ y có thể nói
là đặc trưng quan trọng nhất đối với một thuật toán.
- Tính dừng: thuật toán cần phải đảm bảo ẽ dừng sau một ố hữu
hạn bước.
- Tính xác định: các bước của thuật toán phải được phát biểu rõ
ràng, cụ thể, tránh gây nhầm lẫn.

2
- Tính hiệu quả: thuật toán được x m là hiêụ quả nếu có khả n ng
giải quyết hiệu qủa bài toán đặt ra, đáp ứng yêu cầu về mặt thời gian
xử lý hoặc không gian lưu trữ trong bộ nhớ.
- Tính h á : thuật toán được gọi là có tính phố quát ph
biến) nếu có thể giải quyết được một lớp các bài toán tương tự.

1.2.3. u ễn thuật t n
Có nhiều cách khác nhau để biểu diễn thuật toán, gồm: biểu diễn
bằng ngôn ngữ tự nhiên, lưu đồ giải và mã giả.

Biểu diễn bằng ngôn ngữ tự nhiên


Ví dụ:
Thuật toán tính t ng tất cả các số nguyên dương nhỏ hơn N bằng
ngôn ngữ tự nhiên, gồm các bước sau:
- Bước 1: Khởi gán giá trị biến S = 0;
Khởi gán giá trị biến i = 1;
- Bước 2: Nếu i< N thì
S = S + i;
Ngược lại:
Chuyển qua bước 4;
- Bước 3: Gán i = i + 1;
Quay lại bước 2;
- Bước 4: T ng cần tìm là S.
Nhận xét:
Với cách biểu diễn này, có ưu điểm là đơn giản, dễ hiểu. Tuy
nhiên, nhược điểm là hơi dài dòng, đôi lúc khó hiểu, không diễn đạt
được rõ ràng một cách trực quan nhất từng bước của thuật toán và mối
quan hệ giữa chúng.

3
Biểu diễn bằng lưu đồ giải thuật
Trong cách biểu diễn này sử dụng, hệ thống các ký hiệu để mô tả
chức n ng, ý nghĩa của từng bước thực hiện trong thuật toán.
Hệ thống các ký hiệu và chức n ng của các ký hiệu sử dụng để
biểu diễn thuật toán :

Hình 1.1 Hệ thống các ký hiệu để biểu diễn thuật toán


Ví dụ
Thuật toán tìm phần tử giá trị lớn nhất trong mảng một chiều a
gồm n phần tử được biểu diễn bằng lưu đồ như au

4
Nhận xét:
Với cách biểu diễn này, có ưu điểm là t ng tính trực quan, rõ
ràng trong các bước thực hiện thuật toán, trong đó có đi u vào
từng chi tiết xử lý cụ thể trong từng bước của thuật toán. Tuy
nhiên, hạn chế của cách biểu diễn này là cần nhiều thời gian hơn
trong việc xây dựng các bước thực hiện so với biểu diễn bằng ngôn
ngữ tự nhiên.

Biểu diễn bằng mã giả


Cách biểu diễn này kết hợp giữa sử dụng ngôn ngữ tự nhiên và
một ngôn ngữ lập trình nào đó trong biểu diễn các bước thuật toán.
Ví dụ:
Thuật toán tìm phần tử lớn nhất trong mảng một chiều a gồm n
phần tử bằng mã giả được thể hiện như au
- Bước 1: Khởi gán giá trị biến a =a ;
max 0

- Bước 2: Khởi gán giá trị biến i=1;


- Bước 3: while (i<n)
{ if (a <a )
max i

a = a;
max i

i= i +1;
}
end while;
Nhận xét :
Với cách biểu diễn này, có ưu điểm là đỡ cồng kềnh so với cách
biểu diễn bằng lưu đồ giải thuật, rõ ràng hơn o với cách biểu diễn
bằng ngôn ngữ tự nhiên. Tuy nhiên, hạn chế của cách biểu diễn này là

5
đòi hỏi phải có kiến thức cơ bản về ngôn ngữ lập trình nào đó để sử
dụng biểu diễn các bước thuật toán1.
Thông thường hai cách biểu diễn thuật toán sử dụng ph biến là
ngôn ngữ tự nhiên và lưu đồ giải thuật.

4 Độ phức tạp thuật toán


Định nghĩa về độ phức tạp thuật toán:
ho hai hàm f và g có miền xác định trong tập ố tự nhiên. Ta
viết f n) = O g n)) và nói f n) có cấp cao nhất là g n) khi tồn tại hằng
ố và k ao cho| f n) | ≤ .g n) với mọi n > k
Thông thường, để đánh giá mức độ tốt, xấu và so sánh các thuật
toán c ng loại, có thể dựa trên hai tiêu chu n:
 Tiêu chu n thuật toán đơn giản, dễ hiểu, dễ cài đặt.
 Hoặc tiêu chu n dựa vào thời gian thực hiện và tài nguyên ử
dụng để thực hiện trên dữ liệu.
Trên thực tế các thuật toán hiệu quả thì thường không dễ hiểu,
cách cài đặt hiệu quả cũng không dễ dàng thực hiện và hiểu được một
cách nhanh chóng và như thế đôi khi thuật toán càng hiệu quả thì càng
khó hiểu, cài đặt càng phức tạp. Tuy nhiên, điều này không phải lúc
nào cũng đúng trong mọi trường hợp. Do đó, để đánh giá và o ánh
các thuật toán, ta thường dựa trên độ phức tạp về thời gian thực hiện
của thuật toán, gọi là độ phức tạp thuật toán.
Về bản chất độ phức tạp thuật toán là một hàm ước lượng số phép
tính mà thuật toán cần thực hiện, trên một bộ dữ liệu đầu vào kích
thước N trong bài toán. hi x m x t o ánh các thuật toán c ng loại,
ta thường x t độ phức tạp của thuật toán trong các trường hợp trung
bình, xấu nhất và tốt nhất.
Phân tích thuật toán là một công việc rất khó kh n, đòi hỏi phải
có những hiểu biết sâu sắc về thuật toán và nhiều kiến thức toán học

1
Trong những phần sau của cuốn giáo trình này sử dụng ngôn ngữ lập trình C/
++ để biểu diễn các thuật toán
6
khác. Một công việc mà không phải bất cứ người nào cũng làm được,
vì phải hiểu được các khái niệm liên quan.
Ðánh giá về thời gian của thuật toán không phải là xác định thời
gian tuyệt đối chạy thuật toán mất bao nhiêu gi y, bao nhiêu phút,...)
để thực hiện thuật toán mà là xác định mối liên quan giữa dữ liệu đầu
vào input) của thuật toán và chi phí ố thao tác, ố ph p tính cộng,
trừ, nh n, chia, rút c n,...) để thực hiện thuật toán. Sở dĩ người ta
không quan t m đến thời gian tuyệt đối của thuật toán, vì yếu tố này
phụ thuộc vào tốc độ của máy tính, mà các máy tính khác nhau thì có
tốc độ rất khác nhau. Như thế, một cách t ng quát, chi phí thực hiện
thuật toán là một hàm ố phụ thuộc vào dữ liệu đầu vào
T = f(input)
Tuy vậy, khi ph n tích thuật toán, người ta thường chỉ chú ý đến
mối liên quan giữa độ lớn của dữ liệu đầu vào và chi phí. Trong các
thuật toán, độ lớn của dữ liệu đầu vào thường được thể hiện bằng
một con ố nguyên n. hẳng hạn ắp xếp N con ố nguyên, tìm con
ố lớn nhất trong N ố, tính điểm trung bình của N học inh, ... Lúc
này, người ta thể hiện chi phí thực hiện thuật toán bằng một hàm ố
phụ thuộc vào N:
T = f(N)
Việc x y dựng một hàm T t ng quát như trên trong mọi trường
hợp của thuật toán là một việc rất khó kh n, nhiều lúc không thể thực
hiện được. hính vì vậy mà ta chỉ x y dựng hàm T cho một ố trường
hợp đáng chú ý nhất của thuật toán, thường là trường hợp tốt nhất và
xấu nhất.
Ta x t lại ví dụ thuật toán tìm phần tử giá trị lớn nhất được nêu
ở trên:
- Bước 1: Ghi nhớ amax = a1
- Bước 2: hởi gán giá trị biến i = 2
- Bước 3: Nếu i < N) thì thực hiện :

7
+ Bước 3.1: Nếu ai > amax ) thì
Ghi nhớ amax = ai
+ Bước 3.2 : Gán i = i+1 // ăng i lên mộ đơn vị
Ngược lại :
huyển ang bước 5.
- Bước 4. Trở lại bước 3.
- Bước 5. Phần tử lớn nhất dãy a chính là amax. ết thúc.
Nhận x t :
- Nếu mảng chỉ có 1 phần tử thì phần tử đó là ố lớn nhất.
- Giả ử mảng có N phần tử và ta đã xác định được phần tử lớn
nhất là amax . Nếu b ung thêm phần tử thứ an+1 vào dãy mà an+1 > amax
thì an+1 chính là phần tử lớn nhất của mảng có N+1 phần tử. Trường
hợp ngược lại, nghĩa là an+1 < amax thì amax vẫn là phần tử lớn nhất của
mảng có N+1 phần tử.
Trong thuật toán trên, để đơn giản, ta chỉ xem chi phí là số lần so
sánh ở bước 3.1 và số lần ghi nhớ trong bước 3.1, như thế :
- Trường hợp tốt nhất của thuật toán này xảy ra khi con số lớn
nhất nằm đầu dãy (amax = a1)
- Trường hợp xấu nhất xảy ra khi con số lớn nhất nằm ở cuối dãy
(amax = aN) và dãy được sắp xếp theo thứ tự t ng dần.
Dựa theo ơ đồ khối của thuật toán, ta nhận thấy rằng, trong mọi
trường hợp của bài toán, thao tác ở bước 3.1 luôn được thực hiện và số
lần thực hiện là N - 1 (ứng với việc xét từ phần tử a2 đến aN). Ta gọi
đ y là chi phí cố định hoặc bất biến của thuật toán.
Trường hợ ố nhấ :
Do amax = a1 , suy ra với mọi i >2, ai< amax. Do đó, điều kiện ai >
amax ở bước 3.1 luôn không thỏa mãn nên thao tác ghi nhớ không bao

8
giờ được thực hiện. Như vậy, chi phí chung cho trường hợp này chính
là chi phí cố định của bài toán.
T = f(N) = N-1
Trường hợ xấ nhấ :
Ta có với mọi i>1, ai-1< ai do định nghĩa mảng được ắp xếp
t ng dần) nên điều kiện ai>amax ở bước 3.1 luôn thỏa mãn, thao tác ghi
nhớ luôn được thực hiện. Như vậy, ngoài chi phí chung là N-1 phép so
ánh, ta cần phải d ng thêm N-1 thao tác ghi nhớ ở bước 3.1. Như
vậy, t ng chi phí của trường hợp này
T = f(N) = 2(N-1) = 2N-2
Tuy chi phí của thuật toán trong trường hợp tốt nhất và xấu nhất
có thể nói lên nhiều điều, nhưng vẫn chưa đưa ra được một hình dung
tốt nhất về độ phức tạp của thuật toán. Ðể có thể hình dung chính xác
về độ phức tạp của thuật toán, ta x t đến một yếu tố khác là độ t ng
của chi phí khi độ lớn N của dữ liệu đầu vào t ng.
Th o định nghĩa ở trên, ta nhận thấy chi phí thấp nhất và lớn nhất
của thuật toán tìm ố lớn nhất đều bị chặn bởi O N) tồn tại hằng ố
C = 10, k = 1 để 2N - 2 < 10N với mọi N>1).
Một cách t ng quát, nếu hàm chi phí của thuật toán x t trong
một trường hợp nào đó) bị chặn bởi O f N)) thì ta nói rằng thuật toán
có độ phức tạp là O f N)) trong trường hợp đó.
Như vậy, thuật toán tìm ố lớn nhất có độ phức tạp trong trường
hợp tốt nhất và xấu nhất đều là O N). Người ta gọi các thuật toán có
độ phức tạp O N) là các thuật toán có độ phức tạp tuyến tính.
Bảng dưới đ y thể hiện độ phức tạp của thuật toán được sử dụng
rộng rãi. ác độ phức tạp được sắp xếp theo thứ tự t ng dần.
Tên Ký hiệu
Độ phức tạp hằng số O(C)
Độ phức tạp logarith O(log(N))
Độ phức tạp tuyến tính O(N)
Độ phức tạp NlogN O(N*log(N))
9
Độ phức tạp đa thức O(Nk)
Độ phức tạp lũy thừa O(aN)
Độ phức tạp giai thừa O(N!)

1.2.5. Các chiến th ết kế thuật toán


Có thể thấy rằng, không có một phương pháp nào có thể giúp ta
thiết kế nên các thuật toán cho tất cả các loại bài toán. Tuy nhiên, các
nhà khoa học máy tính đã nghiên cứu và đưa ra các chiến lược thiết kế
các giải thuật chung nhất áp dụng cho các loại bài toán khác nhau như
là các phương pháp tiếp cận chung trong thiết kế thuật toán.

1.2.5.1. Duyệt toàn bộ


Chiến lược duyệt toàn bộ là chiến lược mà m i người lập trình
nên nghĩ đến đầu tiên khi giải quyết bất cứ bài toán nào. Trong
phương pháp này, ta ẽ x m x t tất cả các ứng cử viên thuộc một
không gian có thể có của bài toán để x m đó có phải là nghiệm của
bài toán hay không. Phương pháp này yêu cầu có một hàm kiểm tra
xem một ứng cử viên nào đó có phải là nghiệm của bài toán hay
không. Phương pháp này dễ hiểu, ong không dễ thực hiện và đặc
biệt là không hiệu quả đối với các bài toán mà kích thước dữ liệu
đầu vào lớn.

1.2.5.2. Đệ qui quay lui


Chiến lược đệ qui quay lui là chiến lược xây dựng thuật toán dựa
trên quan hệ đệ qui. Nghiệm của bài toán được mô hình hóa dưới dạng
một véc-tơ, m i thành phần của véc-tơ nghiệm sẽ có một tập giá trị có
thể nhận và thuật toán sẽ tiến hành các bước gán các giá trị có thể cho
các thành phần của nghiệm để xác định đúng nghiệm của bài toán.
Mặc d , không phải bài toán nào cũng có thể áp dụng, song các thuật
toán dựa trên phương pháp đệ qui quay lui luôn có ự ngắn gọn, súc
tích.

10
1.2.5.3. hia để trị
hiến lươc chia để tri là chiến lược quan trọng trong thiết kế các
thuật toán. tưởng của chiến lươc này rất đơn giản, khi cần giải quyết
mô bài toán, ta ẽ tiến hành chia bài toán đó thành các bài toán nhỏ
hơn. Tiến hành thực hiện giải các bài toán nhỏ hơn đó và au đó kết
hợp nghiệm của các bài toán nhỏ hơn đã giải lại thành nghiệm của bài
toán ban đầu.

1.2.5.4. hiến lược tham lam


Chiến lược tham lam là chiến lược xây dựng thuật toán tìm
nghiệm tối ưu cục bộ cho các bài toán tối ưu nhằm đạt được
nghiệm tối ưu toàn cục cho cả bài toán. Trong trường hợp cho
nghiệm đúng, lời giải của chiến lược tham lam thường rất dễ cài đặt
và có hiệu n ng cao.
1.2.5.5. Qui hoạch động
Qui hoạch động là chiến lược xây dựng thuật toán để giải quyết
các bài toán tối ưu. Trong chiến lược này, ta sẽ xây dựng các quan hệ
đệ qui của bài toán, bài toán gốc sẽ có lời giải dựa trên các bài toán
con dựa trên quan hệ đệ qui. Các thuật toán qui hoạch động thường ử
dụng các cấu trúc bảng để lưu lại giá trị nghiệm của các bài toán con
qua từng bước và có hai cách tiếp cận: đi từ dưới lên trên (bottom up)
và đi từ trên xuống dưới (top down) để giải quyết bài toán ban đầu.

1.3. KIỂU DỮ LIỆ C C Ữ LIỆU

1.3.1. Ki u dữ liệu

1.3.1.1. Định nghĩa


Theo Peter Brass trong Advanced Data Structures, Cambridge
University Press, 2008 [1], kiểu dữ liệu là một tập hợp các giá trị và
một tập hợp các phép toán trên các giá trị đó.

11
1.3.1.2. Các kiểu dữ liệu
Có hai kiểu dữ liệu là kiểu dữ liệu cơ bản và kiểu dữ liệu có
cấu trúc.
Kiểu dữ liệu cơ bản thường là các loại dữ liệu đơn giản và không
có cấu trúc, bao gồm: các giá trị kiểu số như ố nguyên, số thực, các
giá trị logic,… ác ngôn ngữ lập trình thường cung cấp sẵn kiểu dữ
liệu này cho người lập trình và thường được gọi là các kiểu dữ liệu cơ
sở. Tùy theo các ngôn ngữ lập trình khác nhau mà các kiểu dữ liệu cơ
sở có thể khác nhau về cách thức khai báo, sử dụng, miền giá trị, cũng
như các ph p toán trên đó.
Ví dụ :
Kiểu số nguyên, không dấu trong ngôn ngữ được khai báo sử
dụng bằng từ khóa un ign int, có kích thước 2 bytes và có miền giá trị
từ giá trị 0 đến 65535.
Trong thực tế, khi giải quyết các bài toán, bản thân các kiểu dữ
liệu cơ ở không đủ để biểu diễn các dữ liệu đa dạng, do đó nhu
cầu xây dựng các kiểu dữ liệu mới là cần thiết. Các kiểu dữ liệu
mới thường được xây dựng dựa trên các kiểu dữ liệu cơ ở của
ngôn ngữ lập trình. Những kiểu dữ liệu mới này được gọi là các
kiểu dữ liệu cấu trúc.
Ví dụ:
Kiểu dữ liệu nhân viên, bao gồm các thông tin: mã nhân viên, tên
nhân viên, ngày inh, địa chỉ thường trú, ngày tuyển dụng, chức vụ
được tạo ra từ các kiểu dữ liệu cơ ở, như au
 Mã nhân viên: kiểu số
 Tên nhân viên: kiểu chu i ký tự
 Ngày sinh: kiểu ngày tháng n m
 Địa chỉ thường trú: kiểu chu i ký tự
 Ngày tuyển dụng: kiểu ngày tháng n m

12
 Chức vụ: kiểu chu i ký tự
Các kiểu dữ liệu cấu trúc ph biến thường sử dụng:
 Kiểu dữ liệu danh sách liên kết
 Kiểu dữ liệu bản ghi
 Kiểu dữ liệu cây
 Kiểu dữ liệu bảng b m

1.3.2. ut ữ liệu
Theo và Donald Knuth trong The Art of Computer Programming,
1997 [2], cấu trúc dữ liệu là cách lưu dữ liệu trong máy tính sao cho
dữ liệu có thể được sử dụng một cách hiệu quả.
Tính hiệu quả được thể hiện qua các đặc điểm: chính xác, tối ưu
về bộ nhớ, khả n ng h trợ các thao tác tìm kiếm, truy xuất, khả n ng
cập nhật, thêm hoặc xóa trên dữ liệu và cuối c ng là tính đơn giản và
dễ hiểu.
Trong thiết kế và xây dựng một chương trình máy tính, việc chọn
cấu trúc dữ liệu là vấn đề quan trọng, đặc biệt với các chương trình
lớn lại càng có tầm quan trọng cao, vì tính hiệu quả phụ thuộc rất
nhiều vào cách thức t chức và lưu trữ dữ liệu.
M i loại cấu trúc dữ liệu phù hợp với một vài loại ứng dụng khác
nhau, ví dụ cách t chức dữ liệu theo cấu trúc cây đặc biệt phù hợp với
các yêu cầu cần phân nhóm dữ liệu. Thông thường, sau khi cấu trúc
dữ liệu được chọn phù hợp, ta sẽ x m x t đến thuật toán cần sử dụng.
Nhưng đôi khi trình tự công việc diễn ra theo thứ tự ngược lại, khi đó
cấu trúc dữ liệu được chọn phụ thuộc vào những bài toán đã có trước
thuật toán và cần cải tiến để chạy tốt nhất với cách t chức dữ liệu
khác. Rõ ràng, trong cả hai trường hợp, việc lựa chọn cấu trúc dữ liệu
là rất quan trọng.
Thông thường, một cấu trúc dữ liệu được chọn c n thận ẽ cho
ph p thực hiện thuật toán hiệu quả hơn. Việc chọn cấu trúc dữ liệu
thường bắt đầu từ chọn một cấu trúc dữ liệu trừu tượng. Một cấu trúc
13
dữ liệu được thiết kế tốt cho ph p thực hiện nhiều ph p toán, ử dụng
càng ít tài nguyên, thời gian xử lý và không gian bộ nhớ càng tốt.
ác cấu trúc dữ liệu được x y dựng bằng cách ử dụng các kiểu
dữ liệu cơ bản và các ph p toán trên đó được cung cấp bởi một ngôn
ngữ lập trình cụ thể.Tri thức đó đã dẫn đến ự phát triển của
nhiều ngôn ngữ lập trình và phương pháp thiết kế được hình thức hóa,
mà trong đó, nh n tố t chức quan trọng là các cấu trúc dữ liệu chứ
không phải các thuật toán. Đa ố ngôn ngữ lập trình có một tính n ng
thuộc dạng hệ thống modul hóa cho ph p các cấu trúc dữ liệu được
tái ử dụng an toàn trong các ứng dụng khác nhau.

BÀI TẬP CHƯƠNG 1


1. Vì sao các ngôn ngữ lập trình thường cung cấp trước một số kiểu
dữ liệu cơ bản? Giải thích mục đích của việc này?
2. ó nên định nghĩa thêm các kiểu dữ liệu mới từ các kiểu dữ liệu
cơ bản? Khi nào ta cần làm việc này?
3. Cho 3 ví dụ minh họa về mối quan hệ giữa cấu trúc dữ liệu và
thuật toán.
4. Cho ví dụ minh họa để giải thích cho câu phát biểu sau:“Một cấu
trúc dữ liệu tồi, không thể cứ vãn được một thuật toán tố ”
5. Khi tiếp cận để giải quyết một vấn đề hoặc bài toán, bạn thường
quan t m đến xây dựng cách thức t chức, quản lý dữ liệu trước
hay cách thức giải quyết vấn đề hoặc bài toán trước? Vì sao? Cho
ví dụ minh họa để giải thích.

14
CHƯƠNG 2
SẮP XẾP VÀ TÌM KIẾM

2.1. MỐI QUAN HỆ GIỮA NHU CẦU SẮP XẾP VÀ TÌM KIẾM
DỮ LIỆU
Ngày nay, với sự bùng n của dữ liệu, các công cụ tìm kiếm trên
mạng ngày càng trở nên quan trọng hơn bao giờ hết. Chính nhờ các
công cụ tìm kiếm này mà người dùng có cơ hội tìm được nhanh chóng
các thông tin và dữ liệu cần thiết thông qua thao tác nhập vào các từ
khóa nội dung để truy xuất đến các hệ thống lưu trữ và quản lý dữ
liệu. Các công cụ tìm kiếm Google, Yahoo,.. là các ví dụ rất cụ thể
cho nhu cầu tìm kiếm thông tin, khi m i ngày có hàng tỷ thao tác tìm
kiếm được thực hiện trên các công cụ này.
Khi xem xét vấn đề ở phạm vi nhỏ hơn, với các ứng dụng cá
nhân hàng ngày, như phần mềm soạn thảo Microsoft Word chẳng
hạn, thì thao tác tìm kiếm cũng được sử dụng khá nhiều trong quá
trình người dùng sử dụng phần mềm. Do đó có thể nói, cho dù ở
phạm vi nhỏ trong một ứng dụng soạn thảo hay phạm vi lớn hơn là
trong các công cụ tìm kiếm trên mạng thì thao tác tìm kiếm luôn
được sử dụng nhiều nhất.
Bên cạnh đó, khi nói tới thao tác tìm kiếm thì ta cũng thường
đề cập đến thao tác sắp xếp, bởi cả hai thao tác tìm kiếm và sắp xếp
đều thực hiện với dữ liệu và khi dữ liệu trở nên bùng n thì việc t
chức, sắp xếp lại, hệ thống lại dữ liệu đóng vai trò rất quan trọng,
tác động đến hiệu n ng của thao tác tìm kiếm. Để đạt được điều
này dữ liệu phải được t chức theo một thứ tự nào đó để hiệu quả
hơn, vì vậy nhu cầu sắp xếp dữ liệu cũng rất quan trọng. Như thế,
nhu cầu tìm kiếm và nhu cầu sắp xếp dữ liệu luôn song hành với nhau
và hai yếu tố này luôn có mối quan hệ tương h lẫn nhau trong các
hệ thống quản lý và lưu trữ dữ liệu.
15
2.2. ĐỊNH NGHĨA BÀI O N SẮP XẾP
Sắp xếp là quá trình xử lý một danh sách các phần tử để đặt
chúng theo một thứ tự thỏa mãn một tiêu chu n nào đó dựa trên dữ
liệu được lưu trong các phần tử.
Bài toán sắp xếp được phát biểu như au:
ho trước một dãy số a1 , a2 ,… , aN gồm N phần tử được lưu trữ
trong cấu trúc dữ liệu mảng. Hãy sắp xếp lại dãy số a1 , a2 ,… , aN, sao
cho hình thành được dãy mới ak1 , ak2 ,… ,akN có thứ tự (ví dụ t ng
dần, nghĩa là aki > aki-1).
Như thế, sau khi sắp xếp xong thứ tự các phần tử trong dãy sẽ
thay đ i vị trí sao cho thỏa mãn yêu cầu trên.

2.3. CÁC GIẢI THUẬT SẮP XẾP NỘI


Khi xây dựng các thuật toán sắp xếp, ta cần chú ý tìm cách giảm
thiểu những thao tác o ánh và đ i ch các phần tử để t ng hiệu quả
của thuật toán. Thao tác sắp xếp có thể tiến hành ở bộ nhớ trong của
máy tính trong trường hợp số lượng các phần tử không quá lớn) được
gọi là sắp xếp nội hoặc sắp xếp tiến hành trên các thiết bị lưu trữ ngoài
(ví dụ như các phần tử dữ liệu lưu trên fil ) được gọi là sắp xếp ngoại.
Một số giải thuật sắp xếp nội thông dụng :
 Đ i ch trực tiếp
 Chọn trực tiếp (Selection sort)
 Chèn trực tiếp (Insertion sort)
 Đ i ch trực tiếp (Interchange sort)
 N i bọt (Bubble sort)
 Shaker sort
 Chèn nhị phân (Binary Insertion sort)
 Shell sort

16
 Heap sort
 Quick sort
 Merge sort

2.3.1. Các giải thuật sắp xếp ơ bản

2.3.1.1. Giải thuật sắp xếp chọn trực tiếp

Ý tưởng giải thuật


Giải thuật chọn trực tiếp chọn phần tử nhỏ nhất trong N phần tử
của dãy số đã cho ban đầu. Đưa phần tử này về vị trí đúng là đầu dãy
hiện hành. Sau đó, không quan t m đến nó nữa, xem dãy hiện hành
chỉ còn N-1 phần tử của dãy ban đầu (bắt đầu từ vị trí thứ 2), lặp lại
quá trình trên cho trên dãy N-1 phần tử hiện hành cho đến khi dãy chỉ
còn một phần tử.

Các bước giải thuật


- Bước 1: Khởi gán giá trị biến i =1
- Bước 2: Tìm phần tử a[min] nhỏ nhất trong dãy hiện hành từ
a[i] đến a[N]
- Bước 3: Hoán vị phần tử a[min] và a[i]
- Bước 4: Nếu i ≤ N thì
- Gán i = i +1
- Lặp lại bước 2
Ngược lại : dừng giải thuật

Cài đặt giải thuật


void SelectionSort(int a[],int N )
{
int i,j;

17
int min; // chỉ số hần ử nhỏ nhấ rong dãy hiện hành
for (i=0; i<N-1; i++)
{ min = i;
for (j = i+1; j <N; j++)
{ if (a[j ] < a[min])
min = j; // vị rí hần ử hiện ại nhỏ nhấ
}
if (min!=i)
Swap(a[min],a[i]); //hàm hoán vị a[min], a[i]
}
}
Ví dụ:
Giả ử cho dãy ban đầu là ={3, 9, 6, 1, 2}. Hãy ắp xếp dãy ố
đã cho có thứ tự t ng dần.
Các bước giải thuật được thực hiện như sau:
 Vòng lặp i=0; dãy : 3, 9, 6, 1, 2, ố phần tử của dãy 5
- Giả ử phần tử nhỏ nhất Min = [0] = 3
- Tìm được phần tử nhỏ nhất thực tế Min = [3]=1
- Hoán vị [0] và [3], được dãy ố mới 1, 9, 6, 3, 2
- Loại phần tử [0] ra khỏi dãy
 Vòng lặp i=1; dãy :1, 9, 6, 3, 2, ố phần tử của dãy 4
- Giả ử phần tử nhỏ nhất Min = [1] = 9
- Tìm được phần tử nhỏ nhất thực tế Min = A[4]=2
- Hoán vị [1] và [4], được dãy ố mới 1, 2, 6, 3, 9
- Loại phần tử A[1] ra khỏi dãy

18
 Vòng lặp i=2; dãy :1, 2, 6, 3, 9, ố phần tử của dãy 3
- Giả ử phần tử nhỏ nhất Min = [2] = 6
- Tìm được phần tử nhỏ nhất thực tế Min = [3]=3
- Hoán vị [2] và [3], được dãy ố mới 1, 2, 3, 6, 9
- Loại phần tử [2] ra khỏi dãy
 Vòng lặp i=3; dãy :1, 2, 3, 6, 9, ố phần tử của dãy 2
- Giả ử phần tử nhỏ nhất Min = [3] = 6
- Tìm được phần tử nhỏ nhất thực tế Min = [3]=6
- hông hoán vị trong trường hợp này do phần tử giả ử nhỏ
nhất và phần tử nhỏ nhất thực tế c ng nằm ở một ví trí trong
dãy.
- Loại phần tử [3] ra khỏi dãy
 Vòng lặp i=4; dãy :1, 2, 3, 6, 9, ố phần tử của dãy 1
Dừng giải thuật.
Vậy dãy au khi ắp xếp t ng 1, 2, 3, 6, 9.

Đánh giá giải thuật


Với m i giá trị của i, giải thuật thực hiện N – i – 1) ph p o ánh
và vì i chạy từ 0 cho tới (N – 2), giải thuật sẽ cần (N - 1) + (N - 2) +
… + 1 = N N - 1)/2 tức là O(N2) phép so sánh.
Trong mọi trường hợp ố lần o ánh của giải thuật là không đ i.
M i lần chạy của vòng lặp đối với biến i, có thể có nhiều nhất một lần
đ i ch hai phần tử nên ố lần đ i ch nhiều nhất của giải thuật là N.
Như vậy, trong trường hợp tốt nhất, giải thuật cần 0 lần đ i ch , trung
bình cần N 2 lần đ i ch và xấu nhất cần N lần đ i ch .

19
2.3.1.2. Giải thuật sắp xếp đ i ch trực tiếp

Ý tưởng giải thuật


Xuất phát từ đầu dãy, tìm tất cả các cặp phần tử gần nhau
không thỏa mãn yêu cầu sắp xếp thứ tự (các cặp phần tử nghịch
thế , tức thế các cặp phần tử có giá trị theo thứ tự ngược lại so với
nhu cầu sắp xếp mong muốn) và tiến hành đ i ch vị trí cho nhau,
ví dụ nếu yêu cầu sắp thứ tự t ng và cặp phần tử gần nhau không
thỏa mãn là t ng thì tiến hành đ i ch vị trí cho nhau để thỏa điều
kiện là sắp thứ tự t ng, lặp lại xử lý trên với các cặp phần tử tiếp
theo trong dãy.

Các bước giải thuật


- Bước 1: hởi gán i = 0; // bắ đầ d yệ ừ dãy
- Bước 2: hởi gán j = i + 1;
- Bước 3: Trong khi j < N thì
{ Nếu (a[j] < a[i]) //xé cặ hần ử a[i], a[j]
Swap (a[i], a[j]); // hàm hoán vị a[i], a[j]
j = j+1;
}
- Bước 4: i = i +1;
Nếu i < N – 1 thì
Lặp lại bước 2.
Ngược lại
Dừng giải thuật.

Cài đặt giải thuật


void InterchangeSort(int a[], int N )
{
20
int i, j;
for (i = 0 ; i<N-1 ; i++)
for (j =i+1; j < N ; j++)
if (a[j ]< a[i])
Swap(a[i], a[j]);
}
Ví dụ
Giả ử cho dãy ban đầu là ={3, 9, 6, 1, 2}. Hãy ắp xếp dãy ố
đã cho có thứ tự t ng dần.
ác bước giải thuật được thực hiện như au
 Vòng lặp i=0; dãy : 3, 9, 6, 1, 2
- Vòng lặp j = 1 do [0] < [1] không hoán vị
- Vòng lặp j = 2 do [0] < [2] không hoán vị
- Vòng lặp j = 3 do [0] > [3] hoán vị [0] và [3]. Dãy
mới {1, 9, 6, 3, 2}
- Vòng lặp j = 4 do [0] < [4] không hoán vị
 Vòng lặp i=1; dãy 1, 9, 6, 3, 2
- Vòng lặp j = 2 do [1] > A[2] :hoán vị [1] và [2]. Dãy
mới {1, 6, 9, 3, 2}
- Vòng lặp j = 3 do A[1] > A[3] : hoán vị [1] và A[3]. Dãy
mới {1, 3, 9, 6, 2}
- Vòng lặp j = 4 do [1] > [4] hoán vị [1] và [4]. Dãy
mới {1, 2, 9, 6, 3}
 Vòng lặp i=2; dãy : 1, 2, 9, 6, 3
- Vòng lặp j = 3 do [2] > [3] hoán vị [2] và [3]. Dãy
mới {1, 2, 6, 9, 3}

21
- Vòng lặp j = 4 do [2] > [4] hoán vị [2] và [4]. Dãy
mới {1, 2, 3, 9, 6}
 Vòng lặp i=3; dãy : 1, 2, 3, 9, 6
- Vòng lặp j = 4 do [3] > [4] hoán vị [3] và [4]. Dãy
mới {1, 2, 3, 6, 9}
Vậy dãy au khi ắp xếp t ng 1, 2, 3, 6, 9.
Đánh giá giải thuật
ó thể thấy rằng, so với giải thuật sắp xếp chọn trực tiếp, giải
thuật ắp xếp bằng cách đ i ch trực tiếp cần ố bước o ánh tương
đương tức là N*(N - 1) 2 lần o ánh. Nhưng ố bước đ i ch hai
phần tử cũng bằng với ố lần o ánh : N*(N - 1)/2.
Trong trường hợp xấu nhất ố bước đ i ch của giải thuật bằng
với ố lần o ánh, trong trường hợp trung bình ố bước đ i ch là N
*(N - 1)/4. Trong trường hợp tốt nhất, ố bước đ i ch bằng 0. Như
vậy, giải thuật ắp xếp đ i ch trực tiếp nói chung là chậm hơn nhiều
o với giải thuật ắp xếp chọn trực triếp, do ố lần đ i ch nhiều hơn.

2.3.1.3. Giải thuật sắp xếp chèn trực tiếp

Ý tưởng giải thuật


Giả sử cho dãy số a1, a2 ,… , aN , trong đó có i phần tử đầu tiên đã
có thứ tự. tưởng giải thuật là tìm cách chèn ph n tử ai+1 vào vị trí
thích hợp của đoạn từ phần tử a1 đến ai sao cho từ a1 đến ai+1 cũng có
thứ tự.

Các bước giải thuật


- Bước 1: Khởi gán i = 1; //giả sử có đoạn a[1] đã được sắp
- Bước 2: Gán x = a[i]; // tìm vị trí pos thích hợ rong đoạn
a[1] đế a[i-1] để chèn a[i] vào
- Bước 3: Dời ch các phần tử từ a[po ] đến a[i-1] sang phải 1 vị
trí để dành ch cho a[i]

22
- Bước 4: Gán a[pos] = x; //đoạn a[1]..a[i] đã được sắp
- Bước 5: Gán i = i+1
Nếu i < n thì
Lặp lại Bước 2
Ngược lại:
Dừng giải thuật

Cài đặt giải thuật


void InsertionSort(int a[], int N)
{
int pos, i;
int x; //lư giá rị a[i] ránh bị ghi đè khi dời chỗ các hần ử.
for (i=1 ; i<N ; i++) // giả sử đoạn a[0] đã sắ hứ ự
{ x = a[i];
pos = i-1;
while((pos >= 0) && (a[pos] > x)) // ìm vị rí chèn x
{ // dời chỗ các hần ử sẽ đứng sa x rong dãy mới
a[pos+1] = a[pos];
pos--;
}
a[pos+1] = x; // chèn x vào dãy
}
}
Ví dụ
Giả ử cho dãy ban đầu là ={3, 9, 6, 1, 2}. Hãy ắp xếp dãy ố
đã cho có thứ tự t ng dần.
23
ác bước giải thuật được thực hiện như au
 Vòng lặp i=1; dãy : 3, 9, 6, 1, 2
- Phần tử đã ắp xếp [0] = 3
- So ánh phần tử [1]= 9 và [0]= 3 để chèn [1] vào dãy
để được dãy có 2 phần tử [0], [1] t ng dần
- hông thực hiện đ i ch [0], [1], do [1] > [0]
- Ta được 2 phần tử ắp t ng trong dãy 3, 9, 6, 1, 2
 Vòng lặp i=2; dãy : 3, 9, 6, 1, 2
- Phần tử đã ắp xếp [0], [1]
- So ánh phần tử [2]= 6 và { [0]= 3, [1]=9} để chèn
[2] vào dãy để được dãy có 3 phần tử { [0], [1], [2]}
t ng dần
- Do [2]< [1], nên dời ch qua lại [1] và [2]
- Ta được 3 phần tử ắp t ng trong dãy 3, 6, 9, 1, 2
 Vòng lặp i=3; dãy 3, 6, 9, 1, 2
- Phần tử đã ắp xếp [0], [1], A[2]
- So ánh phần tử [3]= 1 và {A[0]=3, A[1]=6, A[2]=9} để
chèn A[3] vào dãy để được dãy có 4 phần tử { [0], [1],
A[2], A[3]} t ng dần
- Do [3] nhỏ hơn đồng thời [0], [1], [2], nên dời ch
A[0], A[1], [2] qua một vị trí, [3] thế ch [0] au dời
ch .
- Ta được 4 phần tử ắp t ng trong dãy 1, 3, 6, 9, 2
 Vòng lặp i=4; dãy : 1, 3, 6, 9, 2
- Phần tử đã ắp xếp [0], A[1], A[2], A[3]

24
- So ánh phần tử [4]= 2 và {A[0]=1, A[1]=3, A[2]=6,
A[3]=9} để chèn [4] vào dãy để được dãy có 5 phần tử
{A[0], A[1], A[2], A[3], A[4] } t ng dần
- Do A[4] nhỏ hơn đồng thời [1], [2], A[3] nên dời ch
A[1], A[2], [3] qua một vị trí, [4] thế ch [1] au dời
ch .
- Ta được 5 phần tử ắp t ng trong dãy 1, 3, 6, 2, 9
Vậy dãy au khi ắp xếp t ng 1, 2, 3, 6, 9.

Đánh giá giải thuật


Với m i bước i, ta cần thực hiện so sánh phần tử hiện tại (a[i])
với nhiều nhất là i phần tử và vì i chạy từ 1 tới N - 1 nên ta phải thực
hiện nhiều nhất 1 + 2 + … + N - 1 = N(N - 1)/2 tức là O(N2) ph p o
ánh tương tự như giải thuật sắp xếp chọn trực tiếp. Tuy nhiên, vòng
lặp while không phải lúc nào cũng được thực hiện và nếu thực hiện thì
cũng không nhất định là lặp i lần nên trên thực tế giải thuật ắp xếp
chèn nhanh hơn o với giải thuật ắp xếp chọn.
Trong trường hợp tốt nhất, giải thuật chỉ cần ử dụng đúng N lần
o ánh và 0 lần đ i ch . Trên thực tế một dãy bất kỳ gồm nhiều dãy
con đã được sắp nên giải thuật chèn hoạt động khá hiệu quả.
Giải thuật ắp xếp chèn trực tiếp là giải thuật nhanh nhất trong
các giải thuật ắp xếp cơ bản.

2.3.1.4. Giải thuật sắp xếp n i bọt

Ý tưởng giải thuật


Xuất phát từ cuối dãy, đ i ch các cặp phần tử kế cận nhau để
đưa phần tử nhỏ hơn hoặc lớn hơn trong cặp phần tử đó về vị trí đúng
ở đầu dãy hiện hành, au đó ẽ không x t đến nó ở bước tiếp theo, do
vậy ở lần xử lý thứ i sẽ có vị trí đầu dãy là i. Lặp lại xử lý trên cho đến
khi không còn cặp phần tử nào để xét.

25
Các bước giải thuật
- Bước 1:Khởi gán i= 0;
- Bước 2:Khởi gán j = N - 1; //Duyệt từ cuối dãy ngược về vị trí i
Trong khi (j > i) thực hiện :
{ Nếu (a[j] < a[j-1]) thì
Swap(a[j],a[j-1]); // hoán vị a[j], a[j-1]
j = j - 1;
}
- Bước 3: i = i + 1;
Nếu i > = N – 1 thì
Dừng giải thuật
Ngược lại :
Lặp lại bước 2.

Cài đặt giải thuật


void BubbleSort(int a[],int N)
{
int i, j;
for (i = 0; i < N - 1; i++)
for (j = N - 1; j >i; j --)
if (a[j]< a[j-1])
Swap(a[j], a[j-1]); // hoán vị a[j], a[j-1]
}
Ví dụ
Giả ử cho dãy ban đầu là A= {5, 6, 2, 2, 10, 12, 9, 10, 9, 3}. Hãy
ắp xếp dãy ố đã cho có thứ tự t ng dần.

26
Các bước của giải thuật được thực hiện như sau:

Đánh giá giải thuật


Giải thuật có đô phức tạp là O(N*(N - 1)/2) = O(N2), bằng ố lần
o ánh và ố lần đ i ch nhiều nhất của giải thuật trong trường hợp
xấu nhất). Giải thuật ắp xếp n i bọt là giải thuật chậm nhất trong số
các giải thuật sắp xếp cơ bản, nó còn chậm hơn giải thuật ắp xếp đ i
ch trực tiếp mặc d có ố lần o ánh bằng nhau, nhưng do đ i ch
hai phần tử kề nhau nên ố lần đ i ch nhiều hơn.

2.3.2. Giải thuật sắp xếp cây – Heap sort

Ý tưởng giải thuật


tưởng cơ bản của giải thuật là thực hiện sắp xếp thông qua
việc tạo các H ap, trong đó heap là một cây nhị phân hoàn chỉnh có
tính chất là giá trị khóa ở nút cha bao giờ cũng lớn hơn giá trị khóa
ở các nút con. Nói một cách khác, giả sử nếu ta có dãy phần tử a1,
a2,… , aN thì khi đó dãy ẽ trở thành heap khi mọi phần tử ai phải thỏa
mãn ai  a2i và ai  a2i+1 gọi là các phần tử liên đới.

27
Việc thực hiện giải thuật này được chia làm 2 giai đoạn:
- Giai đoạn thứ nhất : Tạo heap từ dãy ban đầu. Th o định nghĩa
của heap thì nút cha bao giờ cũng lớn hơn tất cả các nút con. Do đó,
nút gốc của heap bao giờ cũng là phần tử lớn nhất.
- Giai đoạn thứ hai : Sắp xếp dãy dựa trên heap tạo được ở giai
đoạn trước đó. Do nút gốc là nút lớn nhất nên nó sẽ được chuyển về
vị trí cuối cùng của dãy và phần tử cuối cùng sẽ được thay vào gốc
của h ap. hi đó, ta có một cây mới, c y này chưa phải heap (với
số nút bớt đi 1), ta tiếp tục chuyển cây thành heap và lặp lại quá
trình này cho tới khi heap chỉ còn một nút. Đó chính là phần tử bé
nhất của dãy và được đặt lên đầu dãy.

Các bước giải thuật


- Giai đoạn 1: Hiệu chỉnh dãy số ban đầu thành Heap
- Giai đoạn 2: Sắp xếp dãy số dựa trên Heap
Bước 1: Đưa phần tử lớn nhất về vị trí đúng ở cuối dãy:
r = n;
Hoán vị (a1, ar);
Bước 2: Loại bỏ phần tử lớn nhất ra khỏi Heap:
r = r – 1;
Hiệu chỉnh phần còn lại của dãy a1, a2... ar thành
Heap.
Bước 3: Nếu r > 1 thì // Heap còn phần tử
Lặp lại Bước 2.
Ngược lại:
Dừng giải thuật.

28
Cài đặt giải thuật
Giai đoạn 1 : Hiệu chỉnh dãy số ban đầu thành Heap
Hiệu chỉnh a1, a1+1, ..,ar thành Heap
void shift(int a[], int l, int r)
{
int x,i,j;
i=l;
j=2*i+1;
x=a[i];
while(j<=r)
{ if (j<r)
if (a[j]<a[j+1])
j++;
if (a[j]<=x)
return;
else
{ a[i]=a[j];
a[j]=x;
i=j;
j=2*i+1;
x=a[i];
}
}
}

29
Hiệu chỉnh a ,..a thành Heap
0 n-1

void CreateHeap(int a[],int N)


{
int l;
l=N/2-1;
while (l>=0)
{ shift(a,l,N-1);
l=l-1;
}
}
Giai đoạn 2 : Sắp xếp dãy số
Hàm HeapSort
void HeapSort(int a[],int N)
{
int r;
CreateHeap(a,N);
r=N-1;
while (r>0)
{ Swap(a[0],a[r]);// hóan vị a[0] (nút gốc ), a[r]
r--;
if (r>0)
CreateHeap(a,r);
}
}

30
Ví dụ
Giả ử cho dãy ố A= { 6, 5, 3, 1, 8, 7, 2, 4 }. Hãy ắp xếp dãy
ố đã cho t ng dần.
ác bước của giải thuật được thực hiện như au
Giai đoạn 1: Hiệu chỉnh dãy ban đầu thành Heap

Thêm
Hoán
Heap vào
vị
Heap

Null 6

6 5

6, 5 3

6, 5, 3 1

6, 5, 3, 1 8

6, 5, 3, 1, 8 5, 8

31
6, 8, 3, 1, 5 6, 8

8, 6, 3, 1, 5 7

8, 6, 3, 1,
3, 7
5, 7

8, 6, 7, 1, 5,
2
3

8, 6, 7, 1, 5,
4
3, 2

8, 6, 7, 1, 5,
1, 4
3, 2,4

8, 6, 7, 4, 5,
3, 2, 1

Giai đoạn 2: Sắp xếp dãy số dựa trên Heap

32
Loại
Hoán
Heap phần ãy đã sắp Ghi chú
vị
tử

8, 6, 7, 4,
8, 1 Hoán vị phần tử 8 và 1
5, 3, 2,1

Loại phần tử 8 khỏi


1, 6, 7, 4,
8 8 heap và thêm vào dãy
5, 3, 2,8
đã ắp

Hoán vị phần tử 7 và 1,
1, 6, 7, 4, do hai phần tử này
1, 7 8
5, 3, 2 không thỏa mãn yêu cầu
của h ap

Hoán vị phần tử 3 và 1,
7, 6, 1, 4, do hai phần tử này
1, 3 8
5, 3, 2 không thỏa mãn yêu cầu
của h ap

7, 6, 3, 4,
7, 2 8 Hoán vị phần tử 7 và 2
5, 1, 2

33
Loại phần tử 7 khỏi
2, 6, 3, 4,
7 7, 8 heap và thêm vào dãy
5, 1, 7
đã ắp

Hoán vị phần tử 2 và 6,
2, 6, 3, 4, do hai phần tử này
2, 6 7, 8
5, 1 không thỏa mãn yêu cầu
của h ap

Hoán vị phần tử 2 và 5,
6, 2, 3, do hai phần tử này
2, 5 7, 8
4, 5, 1 không thỏa mãn yêu cầu
của h ap

6, 5, 3, 4,
6, 1 7, 8 Hoán vị phần tử 6 và 1
2, 1

Loại phần tử 6 khỏi


1, 5, 3, 4,
6 6,7, 8 heap và thêm vào dãy
2, 6
đã ắp

1, 5, 3, 4, Hoán vị phần tử 1 và 5,
1, 5 6, 7, 8
2 do hai phần tử này
không thỏa mãn yêu cầu

34
của h ap

Hoán vị phần tử 1 và 4,
5, 1, 3, 4, do hai phần tử này
1, 4 6, 7, 8
2 không thỏa mãn yêu cầu
của h ap

5, 4, 3,
5, 2 6, 7, 8 Hoán vị phần tử 5 và 2
1, 2

Loại phần tử 5 khỏi


2, 4, 3,
5 5,6, 7, 8 heap và thêm vào dãy
1, 5
đã ắp

Hoán vị phần tử 2 và 4,
do hai phần tử này
2, 4, 3, 1 2, 4 5, 6, 7, 8
không thỏa mãn yêu cầu
của h ap

4, 2, 3, 1 4, 1 5, 6, 7, 8 Hoán vị phần tử 4 và 1

1, 2, 3, 4 4 4,5, 6, 7, 8 Loại phần tử 4 khỏi


heap và thêm vào dãy

35
đã ắp

Hoán vị phần tử 1 và 3,
do hai phần tử này
1, 2, 3 1, 3 4, 5, 6, 7, 8
không thỏa mãn yêu cầu
của h ap

3, 2, 1 3, 1 4, 5, 6, 7, 8

Loại phần tử 3 khỏi


1, 2, 3 3 3,4, 5, 6, 7, 8 heap và thêm vào dãy
đã ắp

Hoán vị phần tử 1 và 2,
3, 4, 5, 6, 7, do hai phần tử này
1, 2 1, 2
8 không thỏa mãn yêu cầu
của h ap

3, 4, 5, 6, 7,
2, 1 2, 1
8

2,3, 4, 5, 6,
1, 2 2 Loại phần tử 2 khỏi
7, 8
heap và thêm vào dãy

36
đã ắp

Loại phần tử 1 khỏi


2, 3, 4, 5, 6,
1 1 heap và thêm vào dãy
7, 8
đã ắp

1, 2, 3, 4, 5,
Dãy đã được ắp t ng
6, 7, 8

Vậy dãy sau khi sắp xếp t ng 1, 2, 3, 4, 5, 6, 7, 8.

Đánh giá giải thuật


Heap sort là một giải thuật đảm bảo kể cả trong trường hợp xấu
nhất thì thời gian thực hiện giải thuật cũng chỉ là O(NlogN).

2.3.3. Giải thuật sắp xếp độ phức tạp giảm dần – Shell sort

Ý tưởng giải thuật


Giải thuật này nhằm mục tiêu cải tiến phương pháp chèn trực tiếp
bằng cách phân hoạch dãy thành các dãy con, sắp xếp các dãy con
th o phương pháp chèn trực tiếp và tiến hành d ng phương pháp chèn
trực tiếp sắp xếp lại cả dãy.
Ph n chia dãy ban đầu thành những dãy con gồm các phần tử ở
cách nhau h vị trí.
Dãy ban đầu: a , a , ..., a được x m như ự xen kẽ của các dãy
1 2 n
con sau :
 Dãy con thứ nhất: a a a ...
1 h+1 2h+1

37
 Dãy con thứ hai: a a a ...
2 h+2 2h+2

 Dãy con thứ h: a a a ...


h 2h 3h

Tiến hành sắp xếp các phần tử trong cùng dãy con sẽ làm cho các
phần tử được đưa về vị trí đúng tương đối
Giảm khoảng cách h để tạo thành các dãy con mới. Dừng giải
thuật khi h = 1
Các bước giải thuật
- Bước 1:
Chọn k khoảng cách h[1], h[2], ..., h[k]
Khởi gán i = 1
- Bước 2:
Ph n chia dãy ban đầu thành các dãy con cách nhau h[i]
khoảng cách
Sắp xếp từng dãy con bằng phương pháp chèn trực tiếp
- Bước 3:
Gán i = i+1
Nếu (i > k) thì
Dừng giải thuật
Ngược lại :
Lặp lại bước 2.

Cài đặt giải thuật

void ShellSort(int a[],int N, int h[], int k)

{ int step,i,j, x,len;

38
for (step = 0 ; step <k; step++)

len = h[step];

for (i = len; i <d; i++)

{ x = a[i];

j = i-len; // a[j] đứng kề rước a[i] rong cùng dãy con

// sắ xế dãy con chứa x bằng hương pháp


chèn rực iế

while ((x<a[j]) && (j>=0))

{ a[j+len] = a[j];

j = j - len;

a[j+len] = x;

}
Ví dụ:
Giả ử cho dãy A= {6, 5, 3, 2, 8, 7, 1, 4}. Hãy ắp xếp dãy ố đã
cho t ng dần.
Các bước của giải thuật được thực hiện như sau, với h = 3, 2, 1
39
h=3
6 5 3 2 8 7 1 4

6 4 3 2 5 7 1 8

6 4 3 2 5 7 1 8

1 4 3 2 5 7 6 8

h=2
1 4 3 6 5 7 2 8

1 4 2 6 3 7 5 8
h=1
1 4 2 6 3 7 5 8
Kết quả sắp xếp

1 2 3 4 5 6 7 8

Đánh giá giải thuật


Giải thuật ở trên là phiên bản nguyên thủy của giải thuật do
D.L.Sh ll đưa ra n m 1959. Dễ thấy rằng, để giải thuật hoạt động
đúng thì chỉ cần dãy bước h giảm dần về 1 sau m i bước lặp là được,
đã có một số nghiên cứu về việc chọn dãy bước h cho giải thuật nhằm
t ng hiệu quả của giải thuật.
Giải thuật tương đối dễ cài đặt, tuy nhiên việc đánh giá độ phức
tạp tính toán chính xác của Sh ll ort là tương đối khó, vì phụ thuộc
vào giá trị bước h, cụ thể như au :
40
- Nếu các bước h được chọn theo thứ tự ngược dựa trên công
thức: 4i+1 + 3*2i+1, …, 77, 23, 8,1 thì độ phức tạp tính toán là O(N4/3)
- Nếu các bước h được chọn theo thứ tự ngược dựa trên công
thức: 2i - 1, …, 15, 7, 3, 1 thì độ phức tạp tính toán là O(N3/2)
- Nếu bước h được chọn theo thứ tự ngược dựa trên công thức:
2i3j,…, 16, 12, 9, 8, 6, 4, 3, 2, 1 thì độ phức tạp tính toán là
O(N(logN)2)

2.3.4. Giải thuật sắp xếp dựa trên phân hoạch – Quick sort

Ý tưởng giải thuật


Dựa trên việc chia dãy ban đầu thành 3 dãy con là S1, S2, S3,
trong đó dãy S1 bao gồm các phần tử a(k) < x, S2 là các phần tử a(k)
= x và S3 bao gồm các phần tử a(k) > x như hình dưới đ y. Trong đó,
dãy con S2 đã có thứ tự. Nếu dãy con S1 và S3 chỉ có một phần tử thì
chúng cũng đã có thứ tự. hi đó dãy con ban đầu đã được sắp. Ngược
lại, nếu dãy con S1 và S3 có nhiều hơn một phần tử ta lần lượt phân
hoạch từng dãy con th o tư tưởng dãy ban đầu

Các bước giải thuật


- Bước 1: Nếu (l ft ≥ right) thì //dãy có í hơn 2 hần tử
Kết thúc giải thuật //dãy đã được sắp xếp
- Bước 2: Phân hoạch dãy a …a thành các đoạn: a .. a ,
left right left j
a .. a , a .. a
j+1 i-1 i right

 Đoạn 1 : a .. a  x
left j

 Đoạn 2: a .. a =x
j+1 i-1

 Đoạn 3: a .. a x
i right

41
- Bước 3: Sắp xếp đoạn 1: a .. a
left j

- Bước 4: Sắp xếp đoạn 3: a .. a


i right

Chi tiết giải thuật


- Bước 1: Chọn tùy ý một phần tử a[k] trong dãy là giá trị mốc
( left ≤ k ≤ right):
i = left ;
j = right ;
x = a[k] ;
- Bước 2: Phát hiện và hiệu chỉnh cặp phần tử a[i], a[j] nằm
sai ch :
 Bước 2a: Trong khi (a[i] < x)
i++;
 Bước 2b: Trong khi (a[j] > x)
j--;
 Bước 2c: Nếu i< j
swap(a[i],a[j]); // hoán vị a[i] và a[j]
- Bước 3: Nếu i < j :
Quay lại Bước 2.
Ngược lại:
Dừng giải thuật.

Cài đặt giải thuật


void QuickSort(int a[], int left, int right)
{
int i, j, x;

42
x = a[(left+right)/2];
i = left;
j = right;
while (i < j)
{ while (a[i] < x)
i++;
while (a[j] > x)
j--;
if (i <= j)
{ swap(a[i],a[j]); // hàm hoán vị a[i] và a[j]
i++ ;
j--;
}
}
if (left<j)
QuickSort(a, left, j);
if (i<right)
QuickSort(a, i, right);
}
Ví dụ
Giả ử cho dãy A= {6, 5, 3, 2, 8, 7, 1, 4}. Hãy ắp xếp dãy ố đã
cho t ng dần.
ác bước của giải thuật được thực hiện như au :
 Phân hoạch đoạn left =1, right = 8
x = A[(left +right)/2]= A[4] = 2

43
6 5 3 2 8 7 1 4

1 2 5 3 8 7 6 4
Đoạn 1 gồm 1 phần tử nên đã được sắp, đoạn 2 cũng đã được
sắp, tiếp tục thực hiện phân hoạch đoạn 3 : {5, 3, 8, 7, 6, 4}

 Cập nhật lại left= 3, right=8 : x = A[5]=8

1 2 5 3 8 7 6 4

1 2 5 3 8 7 6 4

1 2 5 3 7 6 4 8
Sau phân hoạch, đoạn 3 không có phần tử nào lớn hơn 8, nên
số phần tử đoạn này không có. Đoạn 1 gồm các phần tử : {5, 3, 7,
6, 4}, đoạn 2 gồm {8} và đã được sắp.Tiếp tục phân hoạch đoạn 1.

 Cập nhật lại left= 3, right=7 : x = A[5]=7

1 2 5 3 7 6 4 8

1 2 5 3 7 6 4 8

1 2 5 3 6 4 7 8
Sau phân hoạch, đoạn 3 không có phần tử nào lớn hơn 7, nên
số phần tử đoạn này không có. Đoạn 1 gồm các phần tử : {5, 3, 6,
4}, đoạn 2 gồm {7} và đã được sắp. Tiếp tục phân hoạch đoạn 1.

44
 Cập nhật lại left= 3, right=6 : x = A[4]=3

1 2 5 3 6 4 7 8

1 2 3 5 6 4 7 8
Sau phân hoạch, đoạn 1 không có phần tử nào nhỏ hơn 3, nên
số phần tử đoạn này không có. Đoạn 3 gồm các phần tử : {5, 6, 4},
đoạn 2 gồm {3} và đã được sắp. Tiếp tục phân hoạch đoạn 3.

 Cập nhật lại left= 4, right=6 : x = A[5]=6

1 2 3 5 6 4 7 8

1 2 3 5 6 4 7 8

1 2 3 5 4 6 7 8
Sau phân hoạch, đoạn 3 không có phần tử nào lớn hơn 6, nên
số phần tử đoạn này không có. Đoạn 1 gồm các phần tử : {5, 4},
đoạn 2 gồm {6} và đã được sắp. Tiếp tục phân hoạch đoạn 1.

 Cập nhật lại left= 4, right=5 : x = A[4]=5

1 2 3 5 4 6 7 8

1 2 3 5 4 6 7 8

1 2 3 4 5 6 7 8

45
Sau phân hoạch, đoạn 3 không có phần tử nào lớn hơn 5, nên
số phần tử đoạn này không có. Đoạn 1 gồm 1 phần tử {4} và đã
được sắp, đoạn 2 gồm {5} và đã được sắp.

1 2 3 4 5 6 7 8
 Cập nhật lại left= 4, right=4 : Dừng giải thuật.

Dãy sau khi sắp xếp t ng 1, 2, 3, 4, 5, 6, 7, 8

Đánh giá giải thuật


Hiệu quả của giải thuật phụ thuộc vào việc chọn giá trị mốc. Nếu
m i lần phân hoạch đều chọn phần tử trung gian của dãy, khi đó dãy
phân chia làm hai phần bằng nhau thì cần log2(N) phân hoạch. Nếu
chia dãy trên mà chọn mốc là cực đại hoặc cực tiểu thì dãy đã cho chia
làm hai phần, phần đầu có một phần tử, phần còn lại có (N - 1) phần
tử do vậy cần N lần phân hoạch.

2.3.5. Giải thuật sắp xếp trộn trực tiếp – Merge sort

Ý tưởng giải thuật


Giả sử cho dãy ban đầu a1, a2, …, an. Ta luôn có thể coi dãy
đã cho là tập hợp liên tiếp của các dãy có thứ tự và gọi các dãy có
thứ tự này là các dãy con.
Trong giải thuật này, vấn đề là tìm cách phân hoạch dãy ban đầu
thành các dãy con. Sau khi phân hoạch, dãy ban đầu sẽ được tách
thành hai dãy phụ theo nguyên tắc phân phối luân phiên dãy con. Sau
đó, trộn từng cặp dãy con của hai dãy phụ thành một dãy con của dãy
ban đầu. Ta nhận thấy số dãy con của dãy ban đầu lúc này giảm đi ít
nhất là một nửa. Cứ thế sau một số bước, ta sẽ nhận được dãy ban đầu
với số dãy con bằng 1, có nghĩa là đã ắp xếp xong.
Trộn trực tiếp: đ y là phương pháp trộn đơn giản nhất. Việc
phân hoạch dãy ban đầu đơn giản như au với dãy ban đầu có n phân
tử, ta cứ phân hoạch thành n dãy con. Vì m i dãy con chỉ có 1 phần tử

46
nên nó là dãy có thứ tự. Cứ m i lần tách – trộn, chiều dài của dãy con
sẽ được nh n đôi.

Các bước giải thuật


- Bước 1: Khởi động k = 1 //Với k là chiều dài dãy con
- Bước 2: Phân phối dãy a = a1, a2, …, an vào hai dãy phụ b, c
theo nguyên tắc phân phối luân phiên từng dãy con.
b = a1, …, ak, a2k+1, …, a3k, …
c = ak+1, …, a2k, a3k+1, …, a4k
- Bước 3: Từ 2 dãy phụ b, c, ta trộn từng cặp dãy con và đưa vào
dãy a.
- Bước 4: k = k*2
Nếu (k<n) thì
Quay lại bước 2.
Ngược lại:
Dừng giải thuật.

Cài đặt giải thuật


void MergeSort (A, left, right)
{
if (left < right)
{ q = (left+right)/2;
MergeSort (A, left, q);
MergeSort (A, q+1, right);
Merge (A, left, q, right);
}
}

47
Ví dụ
Giả ử cho dãy A = {12, 13, 45, 32, 100, 34, 65, 10}. Hãy ắp
xếp dãy ố đã cho t ng dần.
ác bước của giải thuật được thực hiện như au :
Có 8 phần tử cần được sắp xếp: ý tưởng của giải thuật là thay vì
sắp xếp 8 phần tử, ta chia dãy đó ra làm đôi và sắp xếp các dãy con rồi
ghép 2 dãy con lại. Ta thực hiện như au
 Chia đôi dãy ban đầu thành hai dãy con là {12, 13, 45, 32} gọi là
dãy A và {100, 34, 65, 10}gọi là dãy B.
 Sắp 2 dãy con lại: {12, 13, 45, 32} gọi là dãy A, {100, 34, 65,
10} gọi là dãy B.
+ Muốn sắp ta cũng làm như trên: chia đôi , được 2 dãy
mới là dãy A11 = {12,13}; dãy A12 = {45,32}. hia đôi B
được 2 dãy mới là dãy B11 = {100,34}; dãy B12 = {65,10}
+ Sắp xếp A11, B11 , A12 , B12
+ Muốn sắp xếp 11 thì ta cũng chia đôi được 2 dãy con là
A21 = {12} A22 = {13}.
+ Sắp 2 dãy con trên được (đơn giản vì chỉ có một phần tử )
A21 = {12}; A22 = {13}. Sắp xong ta trộn lại thành dãy A11
= {12,13}.
+ Tương tự sắp xếp cho B11, A12, B12 ta cũng có dãy B11 =
{34, 100}; dãy B12 = {10, 65}; dãy A12 = {32, 45}.
+ Sắp xếp xong, ta sẽ trộn A11, A12 lại thành dãy A = {12 13
32 45}
+ Trộn B11, B12 thành dãy B = {10, 34, 65,100}. Sắp xong
A, B, ta sẽ trộn lại thành dãy ban đầu: {10,12,13, 32, 34, 45,
65,100}.
Vậy dãy được sắp xếp t ng dần: {10,12,13, 32, 34, 45, 65,100}.
48
Đánh giá giải thuật
Nhận thấy rằng, số lần lặp của bước 2 (phân phối) và bước 3
(trộn) bằng log2N. Chi phí thực hiện bước 2 và bước 3 tỉ lệ thuận với
N. Như vậy, ta có thể ước tính chi phí thực hiện của giải thuật này
thuộc O(Nlog2N).

2.4. ĐỊNH NGHĨA BÀI O N ÌM KIẾM


Cho dãy có N phần tử a , a , a …, a có giá trị và phần tử có giá
0 1 2 n-1
trị X cần tìm. Hãy xác định xem phần tử X có xuất hiện trong dãy N
phần tử hay không.
Với bài toán này, ta quan t m tới bài toán tìm kiếm trên một dãy
các phần tử hoặc một danh ách các phần tử c ng kiểu và tập các phần
tử này được gọi là không gian tìm kiếm của bài toán. Không gian tìm
kiếm được lưu hoàn toàn trong bộ nhớ trong của máy tính. ết quả
tìm kiếm là vị trí của phần tử thỏa mãn điều kiện tìm kiếm.
Có hai giải thuật tìm kiếm thông dụng :
 Giải thuật tìm kiếm tuyến tính (tìm kiếm tuần tự)
 Giải thuật tìm kiếm nhị phân

2.5. CÁC GIẢI THUẬT TÌM KIẾM NỘI

2.5.1. Giải thuật tìm kiếm tuyến tính

Ý tưởng giải thuật


Duyệt qua tất cả các phần tử của dãy, trong quá trình duyệt nếu
tìm thấy phần tử có giá trị khóa bằng với giá trị khóa cần tìm kiếm thì
trả về vi trí của phần tử tìm được. Ngược lại, nếu duyệt tới hết dãy mà
vẫn không có phần tử thỏa mãn yêu cầu trả về giá trị mặc định nào đó.

Các bước giải thuật


- Bước 1: Khởi gán i=0

49
- Bước 2: So sánh a[i] với giá trị x cần tìm, có 2 khả n ng :
+ Nếu (a[i] = x)
Tìm thấy x. Dừng giải thuật
+ Ngược lại :
Chuyển ang bước 3
- Bước 3: i = i+1 // Xét tiếp phần tử kế tiếp trong dãy
Nếu i = N thì // hết dãy
Dừng giải thuật
Ngược lại:
Quay lại bước 2.

Cài đặt giải thuật


int sequential_search(int a[], int N, int x)
{
int i;
for (i=0;i<N;i++)
if (a[i]==k)
return i;
else
return -1;
}

Đánh giá giải thuật


Độ phức tạp thuật toán trong trường hợp trung bình và xấu nhất là
O(N).Trong trường hợp tốt nhất thuật toán có độ phức tạp O(1).
Nhận xét :

50
Số phép so sánh của thuật toán trong trường hợp xấu nhất là 2*N.
Để giảm thiểu số phép so sánh trong vòng lặp cho thuật toán, ta có
thêm phần tử “lính canh” vào cuối dãy, khi đó giải thuật sẽ có được
hiệu quả cao hơn.
int LinearSearch(int a[],int N, int x)
{
int i=0;
a[N]=x; // a[N] là phần tử “lính canh”
while (a[i]!=x)
i++;
if (i==N)
return 0; // Tìm không thấy x
else
return 1; // Tìm thấy
}

2.5.2. Giải thuật tìm kiếm nhị phân

Ý tưởng giải thuật


Giải thuật tìm kiếm nhị phân là giải thuật d ng để tìm kiếm phần
tử trong một dãy đã được sắp xếp. Trong m i bước, ta tiến hành so
sánh phần tử cần tìm với phần tử nằm ở chính giữa dãy. Nếu hai phần
tử bằng nhau thì thao tác tìm kiếm thành công và giải thuật kết thúc.
Nếu chúng không bằng nhau thì tùy vào phần tử nào lớn hơn, giải
thuật lặp lại bước so sánh trên với nửa đầu hoặc nửa sau của dãy và cứ
tiếp tục như thế.

Các bước giải thuật


Giả sử dãy tìm kiếm hiện hành bao gồm các phần tử nằm trong
a , a , các bước của giải thuật như au
left right

51
- Bước 1: Khởi gán left=0;
Khởi gán right=N-1;
- Bước 2:
 mid= (left+right) / 2; //chỉ số phần tử giữa dãy hiện hành
 So sánh a[mid] với x. Có 3 khả n ng
 Nếu (a[mid]= x) thì
Trả lời tìm thấy x trong dãy. Dừng giải thuật
 Nếu (a[mid]>x) thì
Cập nhật Right= mid-1;
 Nếu (a[mid]<x) thì
Cập nhật Left= mid+1;
- Bước 3: Nếu (Left <=Right) thì // còn phần tử trong dãy
Quay lại bước 2
Ngược lại :
Dừng giải thuật.

Cài đặt giải thuật


int BinarySearch(int a[],int n,int x)
{
int left, right, mid;
left=0;
right=n-1;
do
{ mid=(left+right) / 2;
if (a[mid]==x)
return 1;
52
else if (a[mid]<x)
left=mid+1;
else
right=mid-1;
}while(left<=right);
return 0;
}
Ví dụ
Giả ử cho dãy A = {12, 8, 2, 14, 3,5}. Phần tử cần tìm là X = 3.
Các bước của giải thuật ẽ được thực hiện như sau:
- left = 0, right =5, mid = (left + right) /2 = 3

12 8 2 14 3 5
So sánh : A[mid] = 2 và X = 3 : không phải X

- Cập nhật lại left = 4

- left = 4, right =5, mid = (left + right) /2 = 4

12 8 2 14 3 5
So sánh [mid] = 3 và X = 3 đúng giá trị X cần tìm.

- Dừng giải thuật.

Kết quả tìm thấy X = 3.

Đánh giá giải thuật :


Với giải thuật này, trong quá trình tìm kiếm, số lượng phần tử
trong dãy cần xem xét giảm đi một nửa sau m i bước, nên thời gian
thực thi của thuật toán là O(log N).
2

53
BÀI TẬP CHƯƠNG 2
1. Trình bày ngắn gọn tư tưởng các giải thuật tìm kiếm, các giải
thuật này có thể được vận dụng trong các trường hợp nào, cho ví
dụ minh họa?
2. Hãy trình bày các ưu, nhược điểm của các giải thuật tìm kiếm?
3. Hãy cài đặt các giải thuật tìm kiếm bằng cách sử dụng các kiểu
vòng lặp khác nhau whil , do…whil , for) và có nhận xét về các
trường hợp này.
4. Giả sử cho một dãy số nguyên N phần tử, các phần tử có thứ tự
t ng dần và được yêu cầu áp dụng giải thuật tìm kiếm tuyến tính
để tìm kiếm phần tử trong dãy, hãy thực hiện các yêu cầu sau :
a. ài đặt theo yêu cầu trên và đánh giá về số lần so sánh
trong các trường hợp tìm kiếm phần tử khi cho phần tử cần
tìm kiếm nằm ở đầu dãy, cuối dãy và giữa dãy.
b. Cải tiến giải thuật tìm tuyến tính để phù hợp với giả thiết
đã cho.
c. Đánh giá và o ánh tính hiệu quả về bộ nhớ và số giao tác
khi thực hiện hai giải thuật trong câu a và câu b.
5. Giả sử cho một dãy số nguyên N phần tử, các phần tử có thứ tự
giảm dần và được yêu cầu áp dụng giải thuật tìm kiếm nhị phân
để tìm kiếm phần tử trong dãy, hãy thực hiện các yêu cầu sau :
a. ài đặt theo yêu cầu trên bằng cách dùng k thuật đệ qui và
không dùng k thuật đệ qui.
b. Đánh giá về số lần o ánh trong các trường hợp tìm kiếm
phần tử khi cho phần tử cần tìm kiếm nằm ở đầu dãy, cuối
dãy và giữa dãy trong câu a.
6. Giả sử cho một dãy số nguyên gồm M phần tử (100<M< 30.000),
au đó chọn ngẫu nhiên trong dãy M một số nguyên K, áp dụng
đồng thời 2 giải thuật tìm kiếm nhị phân và tuyến tính để tìm K
trong M. Hãy cho nhận xét về thời gian thực hiện nếu phát sinh
54
ngẫu nhiên 100 lần M và K khác nhau và thực hiện hai giải thuật
nêu trên để tìm K trong M.
7. Giả sử cho một dãy số nguyên N phần tử (100 < N < 10.000).
a. Hãy cho biết thời gian thực hiện giải thuật tìm kiếm tuyến
tính để tìm kiếm một phần tử K trong dãy N khi áp dụng k
thuật phần tử lính canh và khi không dùng k thuật phần
tử “lính canh”.
b. Thời gian thực hiện giải thuật khi áp dụng k thuật lính
canh và khi không dùng k thuật lính canh trong c u a
có tỷ lệ tuyến tính với nhau khi t ng hoặc giảm số phần tử
N không? Vì sao?
8. Giả sử cho một mảng 2 chiều các số nguyên có kích thước M x N
(100 < M < 10.000), (100 < N < 20.000). Hãy cho biết thời gian
thực hiện giải thuật tìm kiếm tuyến tính để tìm kiếm một phần tử
K trong mảng 2 chiều trên, khi áp dụng k thuật phần tử “lính
canh” và khi không dùng k thuật phần tử “lính canh”.
9. Giả sử sử dụng hàm Random trong ++ để phát sinh ngẫu
nhiên dãy 10.000 số nguyên và lưu trong fil X.
a. Hãy áp dụng giải thuật tìm tuyến tính để tìm kiếm số
nguyên trong fil X được phát sinh ngẫu nhiên)
b. Cho nhận xét về thời gian tìm kiếm trong câu a sẽ như thế
nào, nếu thực hiện 5 lần chạy với dãy số nguyên có 100,
1.000, 5.000, 10.000, 50.000 phần tử trong m i lần chạy
10. Giả sử sử dụng hàm Random trong ++ để phát sinh ngẫu
nhiên dãy 10.000 số nguyên và lưu trong fil X.
a. Hãy áp dụng giải thuật tìm nhị ph n để tìm kiếm số nguyên
trong fil X được phát sinh ngẫu nhiên).
b. Nhận xét về thời gian tìm kiếm trong câu a sẽ như thế nào,
nếu thực hiện 5 lần chạy với dãy số nguyên có 100, 1.000,
5.000, 10.000, 50.000 phần tử trong m i lần chạy.
55
11. Hãy viết chương trình minh họa trực quan hóa các giải thuật
tìm kiếm.
12. Hãy trình bày ngắn gọn tư tưởng của các giải thuật sắp xếp?
13. Hãy trình bày ưu điểm và hạn chế của các giải thuật sắp xếp,
đề xuất cách tốt nhất để khắc phục hạn chế của các giải
thuật này.
14. Giả sử sử dụng hàm Random trong ++ để phát sinh ngẫu
nhiên dãy số nguyên có kích thước 100, 1.000, 5.000, 10.000,
50.000 số. Hãy cài đặt các giải thuật sắp xếp để sắp dãy số
nguyên đã cho theo thứ tự t ng dần và nhận xét về thời gian thực
hiện của các giải thuật.
15. Viết chương trình o ánh các giải thuật Selection sort, Heap Sort,
Quick sort, Merge sort về các yếu tố sau: thời gian chạy, số phép
gán và số phép so sánh.
16. Hãy viết chương trình cài đặt giải thuật Quick sort, so sánh thời
gian chạy giải thuật khi lấy x là phần tử chính giữa dãy, đầu dãy,
cuối dãy và không dùng k thuật đệ qui.
17. Hãy viết chương trình cài đặt giải thuật Quick sort, so sánh thời
gian chạy giải thuật khi lấy x là phần tử chính giữa dãy, đầu dãy,
cuối dãy và dùng k thuật đệ qui
18. Giả ử cho dãy ban đầu là A : {12, 8, 2, 14, 3, 5, 10, 15, 36}. Hãy
sắp xếp dãy đã cho th o quy tắc:
a. Các số chẵn (nếu có) có thứ tự t ng dần.
b. Các số lẻ (nếu có) có thứ tự giảm dần.
c. Tính chất chẵn / lẻ tại m i vị trí trong dãy không thay đ i
sau khi sắp xếp (tức là trước khi sắp xếp, tại vị trí i của
dãy A là số chẵn/lẻ thì tại vị trí i của mảng sau khi sắp
xếp cũng là ố chẵn/lẻ).
19. Dựa trên ý tưởng của giải thuật chèn trực tiếp, hãy trình bày ý
tưởng cải tiến giải thuật này ao cho độ phức tạp trong trường
hợp tốt nhất là O(NlogN).
56
20. Trong ba giải thuật sắp xếp chọn trực tiếp, chèn trực tiếp và n i
bọt, giải thuật nào thực hiện sắp xếp nhanh nhất nếu cho một dãy
đã có thứ tự, giải thích và cho ví dụ minh họa.
21. Hãy đề xuất giải thuật tìm phần tử trung vị (phần tử giữa –
median) trong giải thuật sắp xếp Quick sort sao cho hiệu quả nhất
với một dãy số nguyên N phần tử.
22. Giả sử cho một đa thức P bậc N (0<N<100) gồm các số hạng,
biết rằng thông tin số hạng bao gồm:
- Dấu: âm hoặc dương quy định 0 là m, 1 là dương)
- Hệ số: là một số thực
- Bậc: là một số nguyên (giá trị từ 1 đến 99)
Hãy thực hiện các yêu cầu sau:
a. Định nghĩa cấu trúc dữ liệu để biểu diễn đa thức P.
b. Áp dụng một giải thuật sắp xếp để sắp xếp các số hạng
trong đa thức theo thứ tự t ng dần của bậc.
23. Giả sử cho thông tin một sinh viên, bao gồm:
- Mã sinh viên: là một số nguyên dương qui định là số duy
nhất, không trùng)
- Họ: là một chu i
- Tên: là một chu i
- Ngày, tháng, n m inh là một số nguyên dương th o đúng qui
tắc của ngày tháng n m inh trong thực tế
- Giới tính: là một số nguyên qui định: 0 là Nữ, 1: Nam)
- Điểm trung bình học tập: là một số thực qui định giá trị từ 0.0
đến 10.0)
Hãy thực hiện các yêu cầu sau:

57
a. Định nghĩa cấu trúc dữ liệu để biểu diễn thông tin sinh
viên.
b. Viết hàm nhập danh sách N sinh viên, trong đó N nhập từ
bàn phím và lưu vào trong fil Sinhvi n.dat, với yêu cầu
đảm bảo ràng buộc qui định về miền giá trị ở trên.
c. Áp dụng một giải thuật sắp xếp để sắp xếp danh sách sinh
viên theo mã số inh viên t ng dần với thông tin sinh viên
đọc từ file trong câu b.
d. Áp dụng một giải thuật sắp xếp để sắp xếp danh sách sinh
viên th o điểm trung bình giảm dần với thông tin sinh viên
đọc từ file trong câu b.
e. Hiển thị danh sách sinh viên theo kết quả sắp xếp trong
câu c, d.
24. Trong các giải thuật trình bày trong chương, có phải sẽ luôn có
một giải thuật tốt nhất về thời gian xử lý và không phụ thuộc vào
tình trạng dãy số đã cho ban đầu không?. Hãy giải thích và cho ví
dụ minh họa.
25. Hãy viết chương trình minh họa trực quan hóa các giải thuật
sắp xếp.

58
CHƯƠNG 3
DANH SÁCH LIÊN KẾT

3.1. DANH SÁCH TUYẾN TÍNH

3.1.1. Định nghĩa


Danh sách tuyến tính 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 danh sách, khi N = 0, ta có danh sách r ng.
Một tính chất quan trọng của danh sách là các phần tử có thể
được sắp xếp tuyến tính theo vị trí của chúng trong danh ách. Như
thế, phần tử ai đi trước phần tử ai+1, phần tử ai+1 đi au phần tử ai và
phần tử ai ở vị trí thứ i.
Danh sách là một cấu trúc thường gặp nhất, ví dụ như danh ách
sinh viên, danh sách giảng viên, danh sách nhân viên, danh sách các
ngành đào tạo trong một trường đại học,…
Mã ngành Tên ngành
N001 Khoa học máy tính
N002 K thuật máy tính
N003 Hệ thống thông tin
N004 Mạng máy tính
N005 Công nghệ phần mềm
N006 Công nghệ thông tin
N007 An toàn thông tin

Hình 3.1. Danh ách các ngành đào tạo

59
3.1.2. Các thao tác trên danh sách

3.1.2.1. Tìm kiếm một phần tử trong danh sách


Khi tìm kiếm cần phải biết được thông tin lưu trữ để tìm kiếm.
Kết quả của thao tác tìm kiếm là vị trí của phần tử trong danh sách,
nếu tìm thấy hoặc là một giá trị mặc định nào đó ví dụ giá trị 0), nếu
không tìm thấy.

3.1.2.2. Thêm một phần tử vào danh sách


Về nguyên tắc, ta có thể thêm phần tử mới vào các vị trí khác
nhau trong danh sách, cụ thể là vị trí đầu danh sách, khoảng giữa danh
sách hoặc vị trí cuối danh sách. Sau khi hoàn thành thao tác thêm vào
thì số phần tử trong danh ách t ng thêm một phần tử. Chú ý rằng các
phần tử liền kề với phần tử thêm vào sẽ bị thay đ i vị trí sau khi thực
hiện thao tác thêm.

3.1.2.3. Loại bỏ một phần tử trong danh sách


Ngược lại với thao tác thêm là thao tác loại bỏ phần tử trong danh
sách. Tùy theo nhu cầu mà thao tác loại bỏ phần tử có thể thực hiện
theo sau một hay nhiều điều kiện nào đó. Như thế, gắn với thao tác
loại bỏ là thao tác tìm kiếm phần tử thỏa mãn điều kiện nào đó và thao
tác tìm kiếm sẽ thực hiện trước, và tùy theo kết quả của thao tác tìm
kiếm mà thao tác loại bỏ có thể thực hiện hay không. Cụ thể, nếu
không tìm thấy phần tử cần loại bỏ (không thỏa mãn điều kiện tìm
kiếm đặt ra) thì không thể thực hiện thao tác loại bỏ. Trong trường
hợp tìm thấy phần tử cần loại bỏ thì ta thực hiện thao tác loại bỏ, khi
đó ố phần tử trong danh sách giảm đi một phần tử. Chú ý rằng các
phần tử đi au phần tử loại bỏ có thể bị thay đ i vị trí sau khi thực hiện
thao tác loại bỏ.

3.1.2.4. Thay thế một phần tử của danh sách bởi một phần
tử khác
Thao tác thực hiện thay thế một phần tử của danh sách bởi một
phần tử khác thường được thực hiện khi kết hợp với một hay nhiều
điều kiện nào đó liên quan đến giá trị của phần tử đã lưu trữ trong

60
danh ách. Như thế, cũng như thao tác loại bỏ, thao tác thay thế cũng
thường gắn với thao tác tìm kiếm trước khi tiến hành thay thế.

3.1.2.5. Ghép nhiều danh sách thành một danh sách


Thao tác này cũng thường được sử dụng nhằm mục đích tạo ra
một danh sách lớn hơn, phục vụ cho một yêu cầu nào đó. ết quả của
thao tác ghép này sẽ là một danh ách mà trong đó đi au các phần tử
của danh sách thứ nhất sẽ là phần tử của danh sách thứ hai và cứ tiếp
tục như thế cho các danh sách còn lại. Xét về mặt số phần tử thì số
phần tử của danh sách hợp nhất sẽ là t ng của các danh sách. Trong
thao tác này vị trí của phần tử trong danh sách sau khi được ghép
chung sẽ có sự thay đ i.

3.1.2.6. Tách một danh sách thành nhiều danh sách


Ngược lại với thao tác ghép danh sách là thao tác tách danh sách.
Với thao tác này một danh ách ban đầu được tách thành nhiều danh
sách nhỏ hơn t y th o yêu cầu. Khi thực hiện thao tác này cần xác
định thông tin về vị trí bắt đầu và vị trí kết thúc khi tách danh sách.
Xét về mặt số phần tử thì số phần tử của tất cả các danh sách con sẽ
bằng t ng phần tử trong danh ách ban đầu. Trong thao tác này phần
tử trong danh sách khi được tách vào các danh sách khác nhau sẽ có vị
trí khác nhau.

3.1.2.7. Sao chép danh sách


Thao tác này thực hiện sao chép một danh sách nguồn thành
một danh ách đích có c ng ố phần tử và vị trị các phần tử không
thay đ i.

3.1.2.8. Sắp thứ tự các phần tử trong danh sách


Với thao tác này, việc sắp thứ tự các phần tử trong danh sách
được thực hiện theo một tiêu chí cho trước phụ thuộc vào thông tin và
giá trị của các thông tin đã lưu trữ trong phần tử. Kết quả của thao tác
này là sự thay đ i vị trí của các phần tử trong danh ách ban đầu theo
một tiêu chí xác định. Thao tác này không làm thay đ i số phần tử
trong danh sách. Có nhiều phương pháp để cài đặt cho thao tác sắp thứ
61
tự và t y th o phương pháp ử dụng mà có độ phức tạp tính toán và
hiệu quả riêng. Việc chọn phương pháp nào phụ thuộc vào đặc điểm,
tính chất và yêu cầu của thông tin lưu trữ trong danh sách và bài toán
cần giải quyết.

3.1.3. Bi u diễn anh s h ới dạng mảng


Để biểu diễn danh ách dưới dạng mảng, ta lưu 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 sẽ gồm hai thành phần: thành phần 1 là mảng các phần
tử và thành phần 2 cho biết vị trí của phần tử cuối cùng trong
danh sách.
Về mặt ưu điểm, cách biểu diễn này thuận lợi trong việc truy
xuất nhanh đến các phần tử trong danh sách. Tuy nhiên, hạn chế
lớn nhất trong cách biểu diễn này đó là không linh hoạt về mặt kích
thước danh sách trong mọi trường hợp, do ta không biết trước số
phần tử cần thiết phải có trong danh ách, thông thường ta phải
khai báo kích thước tối đa cho mảng để dự phòng, khi đó ẽ dẫn
đến tình trạng lãng phí bộ nhớ. Danh sách liên kết là một cách để
khắc phục hạn chế, tạo sự linh hoạt.

3.2. DANH SÁCH LIÊN KẾT

3.2.1. Định nghĩa


Cho T là một kiểu được định nghĩa trước, kiểu danh sách Tx
gồm các phần tử thuộc kiểu T được định nghĩa là Tx = <Vx,
Ox>, trong đó
- Vx = {tập hợp có thứ tự các phần tử có kiểu dữ liệu T được liên
kết với nhau theo trình tự tuyến tính}
- Ox = {Tạo danh sách; Tìm một phần tử trong danh sách; Chèn
một phần tử vào danh sách; Loại một phần tử khỏi danh sách; Tìm
kiếm; Sắp xếp danh sách; ...}

62
Mối liên hệ giữa các phần tử trong danh ách được thể hiện tường
minh: m i phần tử ngoài các thông tin về bản thân còn chứa một địa
chỉ đến phần tử kế trong danh ách nên được gọi là danh sách liên kết.
Do có liên kết tường minh, với hình thức này các phần tử trong
danh sách không cần lưu trữ kế cận trong bộ nhớ nên khắc phục được
các khuyết điểm của hình thức t chức mảng, nhưng việc truy xuất
đến một phần tử đòi hỏi phải thực hiện truy xuất qua một số phần tử
khác.
Có nhiều kiểu t chức liên kết giữa các phần tử trong danh
sách:
- Danh sách liên kết đơn là danh ách mà m i phần tử trong danh
sách chỉ liên kết với phần tử đứng sau nó trong danh sách.

- Danh sách liên kết kép là danh sách mà m i phần tử trong danh
sách liên kết với phần tử đứng trước và sau nó bằng hai mối liên kết.

- Danh sách liên kết vòng là danh sách mà phần tử đứng cuối
danh sách lại liên kết với phần tử đầu danh sách theo dạng danh sách
liên kết đơn hoặc danh sác kép.

Hình thức liên kết này cho phép các thao tác thêm, hủy trên danh
ách được thực hiện dễ dàng, phản ánh được bản chất linh động của
danh sách.

63
3.2.2. Danh sách liên kết đơn
Danh sách liên kết đơn là danh ách mà m i phần tử trong
danh sách chỉ liên kết với phần tử đứng sau nó trong danh sách.

3.2.2.1. Định nghĩa cấu trúc dữ liệu của một nút


typedef struct tagNode
{ Data Info; // Lư hôn tin phần tử (Info) có kiểu dữ liệu là
Data
struct tagNode *Next; //Lư địa chỉ phần tử đứng sau
}Node;
Cấu trúc dữ liệu của danh sách liên kết đơn
typedef struct tagList
{ Node *pHead; //Lư địa chỉ phần tử đầu tiên
Node *pTail; //Lư địa chỉ phần tử cuối
}LIST;

3.2.2.2. ác thao tác cơ bản trên danh sách liên kết đơn
Các thao tác trên một danh sách liên kết đơn gồm : tạo danh sách
liên kết đơn r ng, tạo một phần tử có thành phần Info bằng x, tìm một
phần tử có Info bằng x, thêm một phần tử có khóa x vào danh sách,
hủy một phần tử trong danh sách, duyệt danh sách và sắp xếp danh
sách liên kết.

a. Khởi tạo danh sách liên kết rỗng


Thao tác thực hiện :

Thao tác này sẽ khởi tạo một danh sách đơn r ng ban đầu.

64
Địa chỉ của nút đầu tiên và địa chỉ của nút cuối c ng đều là
NULL
Hàm cài đặ :
void CreateList(List &l)
{
l.pHead=NULL;
l.pTail=NULL;
}

b. Tạo một phần tử có thành phần dữ liệu là x


Thao tác thực hiện :
Thao tác này sẽ tạo một phần tử mới và gán thành phần dữ liệu là
x, chu n bị cho thao tác thêm vào trong danh sách. Hàm trả về địa chỉ
phần tử mới tạo.
Hàm cài đặ :
Node* CreateNode(Data x)
{ Node *p;
p = new Node; //cấp phát vùng nhớ cho phần tử
if ( p==NULL) // không cấ há được vùng nhớ
exit(1); // thoát
p ->Info = x; //gán dữ liệu cho phần tử
p->Next = NULL;
return p;
}

c. Thêm một phần tử vào danh sách


Thao tác thực hiện :

65
Với thao tác này, các vị trí có thể thêm vào danh sách gồm:
 Thêm phần tử vào đầu danh sách
 Thêm phần tử vào cuối danh sách
 Thêm phần tử vào sau một phần tử q cho trước trong
danh sách
 Thêm phần tử vào trước một phần tử q cho trước trong
danh sách

Thao tác thêm một phần tử vào đầu danh sách liên kết

Nếu danh sách r ng thì


+ Head = p
+ Tail = Head
Ngược lại
+ p->Next = Head
+ Head = p
Hàm cài đặ :
void AddHead(LIST &l, Node* p)
{
if (l.pHead==NULL)
{ l.pHead = p;
l.pTail = l.pHead;

66
}
else
{ p->Next = l.pHead;
l.pHead = p;
}
}

Thao tác thêm một phần tử vào cuối danh sách liên kết

Nếu danh sách r ng thì


+ Head = p
+ Tail = Head
Ngược lại
+ Tail->Next=p
+ Tail=p
Hàm cài đặ :
void AddTail(LIST &l, Node *p)
{
if (l.pHead==NULL)
{ l.pHead = p;
l.pTail = l.pHead;
}
67
else
{ l.pTail->Next = p;
l.pTail = p;
}
}

Thao tác thêm phần tử p vào sau phần tử q cho rước trong danh sách

Nếu (q!=NULL) thì


- Bước 1: p -> Next = q -> Next
- Bước 2: q -> Next = p
Nếu (q = pail) thì
Tail = p
Hàm cài đặ :
void InsertAfterQ(List &l, Node *p, Node *q)
{
if (q!=NULL)
{ p->Next=q->Next;
q->Next=p;
if (l.pTail==q)
l.Tail=q;

68
}
else
AddHead(l,q);// thêm q vào đầ danh sách
}

d. Hủy phần tử trong danh sách liên kết


Thao tác thực hiện :
Nguyên tắc: Phải cô lập phần tử cần hủy trước khi hủy.
Các vị trí phần tử có thể hủy:
 Phần tử đứng đầu danh sách
 Phần tử đứng cuối danh sách
 Phần tử có khoá bằng x
 Phần tử đứng sau q trong danh sách
 Phần tử đứng trước q trong danh sách

Thao tác hủy phần tử đứng dầu danh sách liên kết

Nếu (Head!=NULL) thì


- Bước 1: p=Head
- Bước 2: Head = Head->Next
delete p;
- Bước 3: Nếu (Head==NULL) thì

69
Tail=NULL
Hàm cài đặ :
int RemoveHead(List &l, int &x)
{
Node *p;
if (l.pHead!=NULL)
{ p=l.pHead;
x=p->Info; //lư data của phần tử cần hủy
l.pHead=l.pHead->Next;
delete p;
if (l.pHead==NULL)
l.pTail=NULL;
return 1;
}
return 0;
}

Thao tác hủy hần ử sa hần ử rong danh sách liên kế

Nếu (q!=NULL) thì //q tồn tại trong danh sách


- Bước 1: p=q->Next; // p là phần tử cần hủy

70
- Bước 2: Nếu (p!=NULL) thì // q không phải là phần tử cuối
q->Next=p->Next; // tách p ra khỏi danh sách
Nếu (p== Tail) //phần tử cần hủy là phần tử cuối
Tail=q;
delete p; // hủy phần tử p
Hàm cài đặ :
int RemoveAfterQ(List &l,Node *q, int &x)
{ Node *p;
if (q!=NULL)
{ p=q->Next; // là hần ử cần xoá
if (p!=NULL) // không hài là hần ử c ối
{
if (p==l.pTail) // hần ử xoá là hần ử c ối
l.pTail=q; //cậ nhậ lại Tail
q->Next=p->Next;
x=p->Info;
delete p;
}
return 1;
}
else
return 0;
}

71
Thao tác hủy phần tử có khoá x trong danh sách liên kết
- Bước 1: Tìm phần tử p có khoá bằng x, gán con trỏ q luôn chạy
trước con trỏ p
- Bước 2 Nếu p!=NULL) thì // ìm hấy hần ử có khoá bằng x
Hủy p ra khỏi danh sách bằng cách hủy phần tử
đứng au q
Ngược lại
Thông báo không tìm thấy phần tử có khoá bằng x
Hàm cài đặ :
int RemoveX(List &l, int x)
{
Node *p,*q = NULL;
p=l.Head;
while ((p!=NULL) && (p->Info!=x)) //tìm x
{ q=p;
p=p->Next;
}
if (p==NULL) //không ìm hấy hần ử có khoá bằng x
return 0;
if (q!=NULL) // ìm hấy hần ử có khoá bằng x
DeleteAfterQ(l,q,x);
else // hần ử cần xoá nằm đầ danh sách
RemoveHead(l,x);
return 1;
}

72
e. ìm một phần tử trong danh sách liên kết
Thao tác thực hiện :
Trong thao tác này, thực hiện duyệt tuần tự các phần tử trong
danh sách, so sánh giá trị của phần tử đang x t với giá trị phần tử cần
tìm và trả lời kết quả có tìm thấy hoặc không tìm thấy phần tử cần tìm.
Các bước tìm phần tử có thành phần Info bằng x trong danh sách:
- Bước 1: p=Head; // địa chỉ của phần tử đầu trong danh sách
- Bước 2:
Trong khi (p!=NULL) và (p->Info!=x)
p=p->Next; // xét phần tử kế
- Bước 3:
+ Nếu (p!=NULL) thì
p lưu địa chỉ của nút có thành phần Info = x
+ Ngược lại :
Không có phần tử cần tìm.
Hàm cài đặt :
Hàm cài đặt trả về địa chỉ của phần tử có thành phần Info bằng x,
ngược lại hàm trả về giá trị NULL nếu không tìm thấy.
Node *Search(LIST l, Data x)
{ Node *p;
p = l.pHead;
while ((p!= NULL) && (p->Info != x))
p = p->pNext;
return p;
}

73
h. uyệt danh sách
Thao tác thực hiện :
Thao tác duyệt danh sách liên kết là thao tác thường được thực
hiện khi có nhu cầu cần xử lý với các dữ liệu lưu trong các phần tử
trong danh ách, như :
 Thao tác đếm số lượng các phần tử trong danh sách
 Thao tác tìm tất cả các phần tử trong danh sách thỏa mãn điều
kiện nào đó
 Thao tác duyệt danh ách để hủy toàn bộ hay một phần danh
sách
 …

- Bước 1:
p = Head; // p lư địa chỉ của phần tử đầu trong danh sách
- Bước 2:
Trong khi danh ách chưa hết) thực hiện
+ xử lý yêu cầu cụ thể với phần tử p

74
+ p=p->Next; // chuyển qua phần tử kế tiếp trong
danh sách
Hàm cài đặ :
void PrintList(List l)
{ Node *p;
p=l.pHead;
while (p!=NULL)
{ printf %d , p->Info);
p=p->Next;
}
}

i. Hủy danh sách liên kết


Thao ác hực hiện :
Thao tác thực hiện hủy toàn bộ danh ách liên kết bằng cách
duyệt từ phần tử đầu đến phần tử cuối, tiến hành cô lập phần tử cần
hủy, cập nhật lại con trỏ đầu danh ách và au đó hủy phần tử đã cô
lập trước đó.

75
- Bước 1: Trong khi danh ách chưa hết) thực hiện:
B1.1:
p = Head;
Head = Head->Next; // cập nhật lại con trỏ Head
B1.2: Hủy p
- Bước 2:
Tail = NULL;// bảo toàn tính nhất quán khi danh sách rỗng
Hàm cài đặ :
void RemoveList(List &l)
{ Node *p;
while (l.pHead!=NULL) //còn hần ử rong danh sách
{ p = l.pHead;
l.pHead = p->Next;
delete p;
}
}

j. Sắp xếp danh sách


Thao ác hực hiện :
Thao tác ắp xếp trên danh ách là một thao tác cũng thường
được thực hiện trên danh ách, nhằm mục đích ắp xếp lại danh sách
ao cho có thứ tự. Để thực hiện thao tác này, có 2 cách thực hiện

76
 Cách thứ nhất: Thực hiện thao tác thay đ i thành phần dữ liệu
trong các phần tử để danh sách có thứ tự

Sau khi thay đ i

 Cách thứ hai: Thực hiện thao tác thay đ i thành phần liên kết
các phần tử, sao cho việc thay đ i này tạo lập được thứ tự liên kết
mong muốn để danh sách có thứ tự.

Sau khi thay đ i

 Nhận xét :
Với cách thứ nhất Thay đ i thành phần dữ liệu
o Ưu điểm: ài đặt đơn giản, tương tự như ắp xếp
mảng
o Nhược điểm Đòi hỏi thêm vùng nhớ khi hoán vị nội
dung của hai phần tử, do đó chỉ phù hợp với những
danh sách có kích thước nhỏ. Khi danh sách có kích

77
thước lớn, chi phí cho việc hoán vị sẽ lớn, dẫn đến làm
cho thao tác sắp xếp chậm.
Ví dụ
Viết hàm ắp xếp danh ách d ng thuật toán ắp xếp chọn trực
tiếp l ction ort) đã học trong chương trước để ắp xếp danh ách
t ng dần th o cách thứ nhất.
void SelectionSort(LIST &l)
{
Node *p,*q,*min;
p=l.pHead;
while (p!=l.pTail)
{ min=p;
q=p->next;
while (q!=NULL)
{ if (q->Info<p->Info)
min=q;
q=q->Next;
}
Swap (min->Info,p->Info); // hàm hoán vị hai giá rị
p=p->Next;
}
}

 Nhận xét :
Với cách thứ hai Thay đ i thành phần liên kết
o Ưu điểm: Không phụ thuộc vào kích thước bản chất
dữ liệu lưu tại m i nút, thao tác sắp xếp nhanh hơn.
78
o Nhược điểm ài đặt phức tạp
ác thuật toán ắp xếp hiệu quả trên danh ách liên kết bằng các
thay đ i thành phần liên kết có hiệu quả cao, bao gồm
- Thuật toán sắp xếp Quick Sort
- Thuật toán sắp xếp Merge Sort
- Thuật toán sắp xếp Radix Sort
Ví dụ
Thực hiện ắp xếp danh ách d ng thuật toán ắp xếp Quick Sort:
- Bước 1:
+ Chọn X là phần tử đầu danh sách L làm phần tử cầm canh
+ Loại X ra khỏi danh sách L
- Bước 2:
Tách danh sách L ra làm 2 danh sách :
+ Danh sách L (gồm các phần tử nhỏ hơn hoặc bằng X)
1

+ Danh sách L (gồm các phần tử lớn hơn X)


2

- Bước 3:
Nếu (L !=NULL) thì
1

+ Gọi thực hiện đệ quy hàm Quick Sort(L )


1

- Bước 4:
Nếu (L !=NULL) thì
2

+ Gọi thực hiện đệ quy hàm Quick Sort(L )


2

- Bước 5:

79
Nối danh sách L , X, L lại theo thứ tự ta có danh sách L đã
1 2
được sắp xếp
Minh họa các bước thực hiện thuật toán trên :
Giả ử cho dãy ố ban đầu ={4, 6, 5, 1, 8, 2). Hãy ắp xếp
dãy ố đã cho có thứ tự t ng dần.

 Chọn phần tử X =4 để phân hoạch danh sách thành hai danh


sách con L1, L2

 Sắp xếp danh sách L : chọn phần tử X = 6 cầm canh và tách


2
danh sách L thành hai danh sách con L và L
2 21 22

 Nối danh sách L , X, L thành danh sách L


21 22 2

80
 Nối danh sách L1, X, L2 thành danh sách L

Hàm cài đặ h ậ oán sắ xế danh sách


void Quick Sort(List &l)
{
Node *p;
Node *X; //X lưu địa chỉ của phần tử cầm canh
List L1;
List L2;
if (l.pHead==l.pTail)
return; // danh sách L rỗng hoặc chỉ có mộ hần ử
else
{ CreateList(l1); // hàm ạo danh sách L1 rỗng
CreateList(l2); // hàm ạo danh sách L2 rỗng
X=l.pHead;
l.pHead=X->Next;
while (l.pHead!=NULL) //tách danh sách L= L1+ L2
{ p=l.pHead;
l.pHead=p->Next;
p->Next=NULL;
if (p->Info<=X->Info)

81
AddHead(L1,p); // hàm hêm hần ử vào L1
else
AddHead(L2,p); // hàm hêm hần ử vào L2
}
QuickSort(L1); //Gọi hàm đệ y sắ xế L1
QuickSort(L2); //Gọi hàm đệ y sắ xế L2
if (L1.pHead!=NULL) //nối L1, L2 và X vào danh sách
{ l.pHead=L1.pHead;
L1.pTail->Next=X;
}
else
l.pHead=X;
X->Next=L2.pHead;
if (L2.pHead!=NULL) // L2 có rên mộ hần ử
l.pTail=L2.pTail;
else // L2 không có hần ử nào
l.pTail=X;
}

3.2.3. Danh sách liên kết kép


Danh sách liên kết kép là danh sách mà m i phần tử trong danh
sách liên kết với các phần tử đứng trước và sau nó bằng hai mối
liên kết.

82
3.2.3.1. Định nghĩa cấu trúc dữ liệu của một nút
Cấu trúc dữ liệu một nút
typedef struct tagDnode
{ Data Info;
struct tagDnode *pPre;
struct tagDnode *pNext;
} DNode;
Cấu trúc danh sách kép
typedef struct tagDList
{ DNode *pHead;
DNode *pTail;
}DList;

3.2.3.2. Các thao tác trên danh sách liên kép


• Khởi tạo danh sách liên kết kép r ng
• Tạo một nút có thành phần dữ liệu là x
• Chèn một phần tử vào danh sách
 Chèn phần tử vào đầu danh sách
 Chèn phần tử vào cuối danh sách
 Chèn phần tử vào sau phần tử q cho trước trong danh sách
 Chèn phần tử vào trước phần tử q cho trước trong danh
sách
• Huỷ một phần tử trong danh sách
 Hủy phần tử đầu danh sách
 Hủy phần tử cuối danh sách
 Hủy một phần tử có khoá bằng x

83
 Hủy phần tử sau phần tử q cho trước trong danh sách
 Hủy phần tử trước phần tử q cho trước trong danh sách
• Duyệt danh sách
• Tìm một phần tử trong danh sách theo một điều kiện nào đó
• Sắp xếp danh sách theo một điều kiện nào đó

a. Tạo một danh sách rỗng


Thao tác thực hiện :
Thao tác này sẽ khởi tạo một danh sách kép r ng ban đầu.
Khởi gán địa chỉ của phần tử đầu tiên và địa chỉ của phần tử cuối
cùng trong danh sách đều là NULL
Hàm cài đặ :
void CreateDList(DList &l)
{
l.DHead=NULL;
l.DTail=NULL;
}

b. Tạo một phần tử có thành phần dữ liệu là X


Thao tác thực hiện
Thao tác này ẽ tạo một phần tử mới và gán thành phần dữ liệu là
X và chu n bị cho thao tác thêm vào trong danh ách. Hàm trả về địa
chỉ phần tử mới tạo.
Nếu p!=NULL) thì
+ p->Info=X
+ p->Next=NULL
+ p->Pre=NULL

84
+ return p
Hàm cài đặ
DNode *CreateDNode(int X)
{
DNode *p;
p=new DNode;
if (p==NULL)
{ printf("khong con du bo nho");
exit(1);
}
else
{ p->Info=X;
p->pNext=NULL;
p->pPre=NULL;
return p;
}
}

c. Thêm một phần tử vào đầu danh sách


Thao tác thực hiện
Thao tác này ẽ thực hiện thêm một phần tử vào đầu danh sách
đang có và cập nhật lại phần tử mới thêm vào là phần tử đầu của danh
ách mới.

85
Nếu danh ách r ng thì

+ Head = p

+ Tail = Head

Ngược lại

+ p->Next = Head

+ Head->Pre=p

+ Head = p
Hàm cài đặ
void AddFirst(Dlist &l, Dnode *p)
{
if (l.pHead==NULL) // danh sách rỗng
{ l.pHead=p;
l.pTail=l.pHead;
}
else
{ p->pNext=l.pHead;
l.pHead->pPre=p;
l.pHead=p;
}
}

d. Thêm vào cuối danh sách


Thao tác thực hiện

86
Thao tác này ẽ thực hiện thêm một phần tử vào cuối danh sách
đang có và cập nhật lại phần tử mới thêm vào là phần tử cuối của danh
ách mới.

Nếu danh sách r ng thì

+ Head = p

+ Tail = Head

Ngược lại

+ p->Pre=Tail

+ Tail->Next=p

+ Tail=p

Hàm cài đặt


void AddEnd(DList &l,DNode *p)
{
if (l.pHead==NULL)
{ l.pHead=p;
l.pTail=l.pHead;
}
else
{ p->pPre=l.pTail;
87
l.pTail->pNext=p;
l.pTail=p;
}
}
e. Thêm vào sau phần tử q cho trước trong danh sách
Thao tác thực hiện :
Thao tác thêm một phần tử vào sau phần tử q cho trước trong
danh sách thực hiện chèn một phần tử mới vào giữa 2 phần tử trong
danh sách đang có, khi đó các mối liên kết giữa 2 phần tử này ẽ thay
đ i. Trong trường hợp phần tử q là phần tử cuối trong danh sách thì
phần tử mới thêm vào ẽ trở thành phần tử cuối mới trong danh ách,
au khi tiến hành thao tác thêm.

temp=q->pNext

Nếu q!=NULL) thì


+ p->pNext=temp
+ p->pPre=q
+ q->pNext=tam
+ Nếu temp!=NULL)
temp->pPre=p
+ Nếu q==l.pTail) //thêm vào sau danh sách
l.pTail=p

88
Hàm cài đặ

void AddLastQ(DList &l,DNode *p, DNode *q)


{
DNode *p;
temp=q->pNext;
if (q!=NULL)
{ p->pNext=temp;
p->pPre=q;
q->pNext=tam;
if (temp!=NULL)
temp->pPre=p;
if (q==l.pTail) //thêm vào c ối danh sách
l.pTail=p;
}
else // nế là NULL, ức danh sách rỗng, iến hành gọi hàm
hêm vào đầ danh sách
AddFirst(l,p);
}
f. Thêm vào trước phần tử q cho trước trong danh sách
Thao tác thực hiện
Thao tác thêm một phần tử vào trước nút phần tử cho trước trong
danh sách thực hiện chèn một phần tử mới vào giữa 2 phần tử trong
danh ách đang có, khi đó các mối liên kết giữa 2 phần tử này ẽ thay
đ i. Trong trường hợp phần tử q là phần tử đầu trong danh sách thì
phần tử mới thêm vào ẽ trở thành phần tử đầu mới trong danh sách,
au khi tiến hành thao tác thêm.

89
temp=q->Pre;

Nếu q!=NULL) thì


+ p->Next=q
+ q->Pre=p
+ p->Pre=temp
+ Nếu temp!=NULL) thì
temp->Next=tam
+ Nếu (q==Head)
Head = p

Hàm cài đặ :
void AddBeforeQ(DList &l,DNode *p,DNode *q)
{
DNode *p;
temp=q->pPre;
if (q!=NULL)
{ p->pNext=q;
q->pPre=p;
p->pPre=temp;
if (temp!=NULL)

90
temp->pNext=p;
if (q==l.pHead)
l.pHead = p;
}
else // nế là NULL, ức danh sách rỗng, iến hành gọi hàm
hêm vào đầ danh sách
AddFirst(l,p);
}
g. Hủy phần tử đầu danh sách
Thao tác thực hiện
Thao tác này thực hiện xóa một phần tử đứng đầu danh ách. Sau
khi thực hiện thao tác hủy, ta ẽ cập nhật lại phần tử đứng đầu danh
sách là phần tử kế tiếp của phần tử đã xóa khỏi danh ách. Nguyên tắc
khi hủy là phải thực hiện cô lập phần tử cần hủy trước khi hủy.

91
Nếu (Head!= NULL) thì
+ p=Head
+ Head=Head->Next
+ Head->Pre=NULL
+ delete p
Hàm cài đặ
void DeleteFirst(DList &l)
{
DNode *p;
if(l.pHead!=NULL)
{ p=l.pHead;
l.pHead=l.pHead->pNext;
l.pHead->pPre=NULL;
delete p;
if (l.pHead==NULL)
l.pTail=NULL;
}
}

h. Hủy nút cuối danh sách


Thao tác thực hiện
Thao tác hủy phần tử cuối trong danh sách thực hiện xóa một
phần tử đứng cuối danh ách. Sau khi thực hiện thao tác hủy, ta ẽ cập
nhật lại phần tử đứng cuối danh ách mới là phần tử đứng trước của
phần tử đã xóa khỏi danh ách. Nguyên tắc khi hủy là phải thực hiện
cô lập phần tử cần hủy trước khi hủy.

92
Nếu H ad!=NULL) thì // danh sách có hơn mộ hần ử
+ p= Tail
+ Tail=Tail->Pre
+ Tail->Next=NULL
+ delete p
Hàm cài đặ
void DeleteEnd(Dlist &l )
{
Dnode *p;
if (l.pHead!=NULL)
{ p=l.pTail ;
l.pTail=l.pTail->Pre ;
l.pTail->pNext=NULL;

93
delete p;
if (l.pTail==NULL)
l.pHead=NULL;
}
}
i. Hủy một phần tử sau phần tử q cho trước
Thao tác thực hiện
Thao tác hủy thực hiện hủy một phần tử đứng sau phần tử q cho
trước trong danh ách đang có, khi đó các mối liên kết giữa phần tử q
và phần tử đứng sau q ẽ thay đ i.

Nếu q!=NULL) thì


+ p=q->Next

94
+ Nếu p!=NULL) thì
q->Next=p->Next
Nếu p là nút cuối thì
Tail=q
Ngược lại
p->Next->Pre=q;
delete p
Hàm cài đặ
void DeleteLastQ(DList &l,DNode *q)
{
DNode *p; //lư hần ử đứng sau hần ử q
if (q!=NULL)
{ p=q->pNext;
if(p!=NULL)
{ q->pNext=p->pNext;
if (p==l.pTail)
l.pTail=q;
else
p->pNext->pPre=q;
delete p;
}
}
else
DeleteFirst(l);
}

95
j. Huỷ một phần tử đứng trước phần tử q
Thao tác thực hiện
Thao tác hủy thực hiện hủy một phần tử đứng trước phần tử q cho
trước trong danh ách đang có, khi đó các mối liên kết giữa phần tử q
và phần tử đứng trước q ẽ thay đ i.

Nếu q!= NULL) thì // nế ồn ại nú


+ p=q->Pre
+ Nếu (p!=NULL) thì
q->Pre=p->Pre
Nếu là nút đầu danh ách thì
96
Head=q
Ngược lại
(p->pPre)->pNext=q
delete p
Hàm cài đặ
void DeleteBeforeQ(Dlist &l,Dnode *q)
{
Dnode *p;
if (q!=NULL) // nế ồn ại nút q
{
p=q->pPre;
if (p!=NULL)
{
q->pPre=p->pPre;
if (p==l.pHead)
l.pHead=q;
else
(p->pPre)->pNext=q;
delete p;
}
}
else
DeleteEnd(l);
}

97
k. Hủy một phần tử có khoá bằng x
Thao ác hực hiện
Thao tác hủy một phần tử có khoá bằng x cho trước trong danh
sách được thực hiện qua các bước như au
 Bước 1 Duyệt danh ách và tìm phần tử có thành phần nfo
bằng x
 Bước 2
Nếu có tìm thấy phần tử có khóa bằng x thì
 Nếu phần tử cần hủy nằm ở đầu danh ách thì
Gọi hàm hủy phần tử ở đầu danh ách
 Ngược lại nếu phần tử cần hủy nằm ở cuối danh ách thì
Gọi hàm hủy phần tử ở cuối danh ách
Ngược lại :
Gọi hàm hủy phần tử au q hoặc trước q
Ngược lại
Thông báo không có phần tử bằng x trong danh ách

98
Hàm cài đặ
int DeleteX(DList &l,int x)
{ DNode *p;
DNode *q;
q=NULL;
p=l.pHead;
while (p!=NULL)
{ if (p->Info==x) break;
q=p; //q là nút có hành hần Info = x
p=p->pNext;
}
if (q==NULL)
return 0; //không ìm hấy nút có hành hần Info = x
if (q!=NULL)
if (q==l.pTail)
DeleteLastQ(l,q);
else if (q==l.pHead)
DeleteFirst(l,q);
else DeleteAfterQ(l,q);
return 1;
}

99
BÀI TẬP CHƯƠNG 3
1. Trình bày định nghĩa các loại danh sách liên kết, ưu và nhược
điểm của từng loại danh sách liên kết và cho ví dụ ứng dụng trong
thực tế.
2. Vận dụng các thuật toán sắp xếp đã học trong môn học, hãy cài
đặt các hàm sắp xếp trên danh sách liên kết đơn và kép theo hai
cách:
a. Sử dụng chỉ một con trỏ đầu danh sách.
b. Sử dụng đồng thời hai con trỏ đầu và cuối danh sách.
3. Trong các thuật toán sắp xếp đã học trong môn học, thuật toán
nào dễ, khó vận dụng trên danh sách liên kết đơn, liên kết kép?
Giải thích vì sao?
4. Hãy cài đặt thuật toán Heap sort trên danh sách liên kết kép.
5. Hãy cài đặt thuật toán Merge sort trên danh sách liên kết kép.
6. Hãy cài đặt chương trình tạo một máy tính cho phép thực hiện
các phép tính +, -, *, / và div chia dư) trên các số có tối đa 30
chữ số và các chức n ng nhớ (M+, M-, MC, MR) giống như một
máy tính cầm tay trong thực tế.
7. Hãy cài đặt chương trình cho ph p nhập vào một biểu thức bao
gồm: các số, các toán tử +, -, *, /, div chia dư) và các hàm
toán học sin, cos, tan, ln, ex, trong biểu thức có các dấu mở,
đóng ngoặc "(", ")" và chương trình ẽ tính toán giá trị của biểu
thức này.
8. Một ma trận chỉ chứa rất ít phần tử với giá trị có nghĩa ví dụ:
phần tử ≠ 0) được gọi là ma trận thưa.
Ví dụ như ma trận sau:

100
a. Hãy dùng cấu trúc danh sách liên kết để biểu diễn một ma
trận thưa ao cho tiết kiệm bộ nhớ nhất.
b. Viết hàm cho phép nhập xuất ma trận theo cấu trúc dữ liệu
sử dụng ở câu a.
c. Viết hàm cho phép cộng và nhân hai ma trận đã nhập trong
câu b và hiển thị kết quả lên màn hình.
9. Cho cấu trúc dữ liệu trên như au để mô tả đa thức một biến:
struct PolyNode
{ int coeff; // chứa hệ số
int pow; // chứa số mũ
struct PolyNode *link;
}
typedef struct PolyNode Polynom;
Hãy viết hàm Polynom * PolyMult (Polynom *Poly1, Poly2)
nhận đầu vào là hai đa thức Poly1 và Poly2, tính và trả lại tích
của hai đa thức đã cho.
10. Cho danh sách liên kết được mô tả bởi cấu trúc dữ liệu trên như
sau:
struct Node
{ int info;
struct Node *next;
};
Hãy viết hàm void Shuffle (Node *list) nhận đầu vào là danh sách
liên kết với phân tử đầu tiên được trỏ bởi list, thực hiện các công
việc au đ y
a. Sắp xếp lại các phần tử của danh ách đã cho, sao cho các
nút chẵn đứng trước các nút lẻ và trong trường hợp ngược
lại, thứ tự tương đối ban đầu của các nút là không thay đ i.

101
Một nút được gọi là nút chẵn hay lẻ nếu nó đứng ở vị trí chẵn
hay lẻ trong danh sách (vị trí của các nút trong danh sách
được đánh ố từ phần tử đầu tiên đến phần tử cuối cùng bắt
đầu từ 0).
b. Đưa ra màn hình danh ách thu được.
Ví dụ, nếu danh ách đã cho là 11, 13, 7, 9, 3,10) thì kết quả
hiển thị lên màn hình là (13, 9, 10, 11, 7, 3).
11. Giả sử cho một danh sách liên kết đơn hoặc kép) có thành phần
dữ liệu là các số nguyên dương, người ta muốn tách danh ách đã
cho thành hai danh sách riêng biệt, trong đó một danh ách lưu ố
chẵn, một danh ách lưu ố lẻ. Hãy trình bày giải thuật và thực
hiện cài đặt để tách danh sách đã cho ao cho hiệu quả nhất về
thời gian xử lý và bộ nhớ sử dụng, đặc biệt xét cả trong trường
hợp danh ách đã cho bao gồm tất cả là số chẵn hoặc số lẻ.
12. Hãy trình bày giải thuật và thực hiện cài đặt trộn hai danh sách
liên kết đơn hoặc kép) đã có thứ tự t ng hoặc giảm dần) thành
một danh sách có thứ tự sao cho tối ưu bộ nhớ nhất có thể.
13. Hãy viết chương trình mô phỏng cho bài toán Tháp Hà Nội
bằng cách sử dụng danh sách liên kết.
14. Hãy viết chương trình cho phép thực hiện yêu cầu sau :
a. Nhập vào từ bàn phím một dãy số nguyên và lưu trong một
danh sách liên kết có thứ tự không giảm, bằng cách: với
m i phần tử được nhập vào thì phải tìm vị trí thích hợp để
chèn vào ao cho đảm bảo danh sách có thứ tự không giảm.
b. Nếu thay cấu trúc danh sách liên kết bằng mảng thì thời
gian thực hiện trên mảng sẽ như thế nào so với danh sách
liên kết ?
15. Hãy viết chương trình cho phép thực hiện yêu cầu sau :

102
a. Giả sử cho một danh sách liên kết đơn hoặc k p) lư các số
nguyên, hãy viết chương trình xóa các phần tử trùng nhau
trên danh sách (với các số nguyên trùng nhau, giữ lại một
số nguyên duy nhất).
b. Nếu thay cấu trúc danh sách liên kết bằng mảng thì thời
gian thực hiện trên mảng sẽ như thế nào so với danh sách
liên kết ?
16. Giả sử cho một danh sách liên kết đơn hoặc kép) lưu các ố
nguyên, hãy viết chương trình cho ph p nhập vào danh sách các
số nguyên, sao cho m i số nguyên chỉ xuất hiện một lần trên
danh ách và đảm bảo danh sách luôn trong trạng thái là danh
sách có thứ tự không giảm.
17. Giả sử cho cấu trúc dữ liệu lưu trữ thông tin nhân sự như au
struct NS
{ int maso; // lư hông in mã số nhân sự
char * hoten ; // lư hông in họ và tên nhân sự
int thamnien; // lư hông in số năm hâm niên
float hesoluong ; // lư hông in hệ số lương
float luongcoban ; // lư hông in lương cơ bản
struct Node *next;
};
Hãy thực hiện các yêu cầu sau:
a. Tạo ra danh sách gồm 50 nhân sự bằng cách m i lần thêm
vào một nhân sự sẽ thêm vào từ cuối danh sách.
b. Sắp xếp danh sách theo thâm niên công tác giảm dần.
c. Tính lương trung bình của các nhân sự trong câu a, biết
rằng lương = hệ số lương * lương cơ bản.

103
d. Hiển thị lên màn hình 5 nhân sự cho lương cao nhất, nhưng
có thâm niên công tác ngắn nhất và 5 nhân sự có lương
thấp nhất, nhưng có th m niên công tác l u nhất.
18. Giả sử cho một danh sách hàng hóa bao gồm nhiều mặt hàng,
trong đó m i mặt hàng có các thông tin:
- Tên mặt hàng
- Giá mặt hàng
- Số lượng còn trong kho
Hãy thực hiện các yêu cầu sau:
a. Khai báo danh dách liên kết đơn lưu danh ách các mặt
hàng th o thông tin như mô tả trên.
b. Viết hàm sắp xếp danh sách mặt hàng ở câu a theo giá mặt
hàng t ng dần, nếu cùng giá thì sắp xếp theo tên mặt hàng
và hiển thị lên màn hình.
c. Viết hàm nhập vào 2 số nguyên x, y (x<y), hiển thị lên màn
hình danh sách các mặt hàng có số lượng trong kho lớn
hơn x và nhỏ hơn y.
19. Giả sử cho một danh sách liên kết đơn hoặc kép) lưu các ố
nguyên. Hãy thực hiện các yêu cầu sau:
a. Viết hàm tạo ra một danh sách 50.000 số nguyên ngẫu
nhiên.
b. Viết hàm tính giá trị trung bình của các số nguyên trong
câu a.
c. Viết hàm kiểm tra xem danh sách các số nguyên trong câu
a có thứ tự không? t ng dần hoặc giảm dần)
20. Giả sử cho hai danh sách liên kết đơn hoặc kép) lưu các ố
nguyên, hãy hiển thị lên màn hình:
a. Phần giao của hai danh sách liên kết.
b. Phần hội của hai danh sách liên kết.
104
21. Giả sử cho một danh sách liên kết lưu dữ liệu là các số nguyên,
hãy viết chương trình để đảo ngược danh sách liên kết trong hai
trường hợp :
a. Đảo ngược thành phần dữ liệu.
b. Thay đ i liên kết giữa các phần tử trong danh sách
22. Giả sử người ta muốn quản lý việc bán vé tham dự bu i hòa nhạc
của một dàn nhạc n i tiếng với yêu cầu là không có ai được phép
mua vé nhiều hơn một lần nhằm tạo cơ hội tham dự cho nhiều
người và m i người chỉ được mua tối đa 2 v . Để làm việc này,
ban t chức thực hiện việc phát phiếu đ ng ký cho những người
muốn mua vé.
Thông tin trên phiếu đ ng ký bao gồm: họ tên người mua vé, số
chứng minh nh n d n, địa chỉ, số lượng v đ ng ký mua. Nếu
người nào đó thực hiện việc đ ng ký hai lần thì phiếu đ ng ký lần
hai sẽ bị loại bỏ.
Hãy viết chương trình cho ph p
a. Thực hiện duyệt bán vé theo yêu cầu trên khi ban t chức
phát ra 10.000 v đ ng ký, biết rằng số ghế ngồi là 15.000
ghế, chương trình ẽ chỉ còn giải quyết duyệt bán vé khi
còn ghế trống.
b. Hiển thị lên màn hình danh ách người mua vé và số vé
tương ứng được mua.
23. Giả sử tại một trường học X, người ta có một tập tin dữ liệu, lưu
thông tin tất cả các học inh trong trường. Tập tin bao gồm nhiều
bản ghi r cord), trong đó m i bản ghi lưu thông tin họ và tên
học sinh, mã lớp (ví dụ 7a, 7b, 8c, 9a), điểm trung bình.
Hãy thực hiện các yêu cầu sau :
a. Tạo ra cho m i lớp một danh sách liên kết lưu thông tin tất
cả học sinh của lớp ấy.

105
b.Sắp xếp danh sách các lớp trong câu a theo họ và tên
học sinh
c. Hiển thị lên màn hình danh sách các lớp trong câu b.
24. Giả sử người ta phải xây dựng một chương trình oạn thảo v n
bản, hãy chọn cấu trúc dữ liệu thích hợp để lưu trữ v n bản trong
quá trình soạn thảo. Biết rằng:
- Số dòng v n bản không hạn chế.
- M i dòng v n bản có chiều dài tối đa 80 ký tự.
- Các thao tác yêu cầu gồm:
 Di chuyển trong v n bản (lên, xuống, qua trái, qua phải)
 Thêm, xóa, sửa ký tự trong một dòng
 Thêm, xóa một dòng trong v n bản
 Đánh dấu, sao chép khối.
25. Giả sử theo quy tắc t chức quản lý nhân viên của một công ty,
thông tin về một nhân viên bao gồm lý lịch và bảng chấm công:
+ Lý lịch nhân viên:
- Mã nhân viên: chu i 8 ký tự
- Tên nhân viên: chu i 20 ký tự
- Tình trạng gia đình 1 ký tự (M = Married, S = Single)
- Số con: số nguyên ≤ 20
- Trình độ v n hóa chu i 2 ký tự (C1 = cấp 1; C2 = cấp 2;
C3 = cấp 3; ĐH = đại học; CH = cao học)
- Lương c n bản: số ≤ 1000000
+ Chấm công nhân viên:
- Số ngày nghỉ có phép trong tháng: số ≤ 28
- Số ngày nghỉ không phép trong tháng: số ≤ 28
106
- Số ngày làm thêm trong tháng: số ≤ 28
- Kết quả công việc: chu i 2 ký tự (T = Tốt; TB = Đạt; K
= Kém)
- Lương thực lĩnh trong tháng: số ≤ 2000000

+ Quy tắc tính lương


Lương thực lĩnh = Lương c n bản + Phụ trội
trong đó nếu:
- Số con > 2 : Phụ trội = +5% Lương c n bản
- Trình độ v n hóa = H: Phụ trội = +10% Lương c n bản
- Làm thêm : Phụ trội = +4% Lương c n bản/ngày
- Nghỉ không phép : Phụ trội = -5% Lương c n bản/ngày

+ Chức n ng yêu cầu:


- Cập nhật lý lịch, bảng chấm công cho nhân viên (thêm,
xóa, sửa)
- Xem bảng lương hàng tháng
- Tìm thông tin của một nhân viên
Hãy khai báo cấu trúc dữ liệu thích hợp để biểu diễn các thông tin
trên và cài đặt chương trình th o các chức n ng đã mô tả ở trên.

107
CHƯƠNG 4
NGĂN XẾP HÀNG ĐỢI

4.1. NGĂN XẾP

4.1.1. Định nghĩa


Ng n xếp (stack) là một cấu trúc dữ liệu trừu tượng hoạt động
theo nguyên lý "vào sa ra rước" (Last In First Out – LIFO, nghĩa là
dữ liệu được nạp vào ng n xếp trước sẽ được xử lý sau cùng và dữ
liệu được nạp vào sau cùng lại được xử lý đầu tiên).
Nói một cách khác, ng n xếp là một cấu trúc dữ liệu dạng thùng
chứa của các phần tử và trên đó có hai thao tác cơ bản: Push và
Pop. Thao tác Push b sung một phần tử vào đỉnh của ng n xếp, nghĩa
là sau các phần tử đã có trong ng n xếp. Trong khi đó, thao tác
Pop lấy ra và trả về phần tử đang đứng ở đỉnh của ng n xếp.
Trong ng n xếp, các đối tượng có thể được thêm vào bất kỳ lúc
nào nhưng chỉ có đối tượng thêm vào sau cùng mới được phép lấy ra
khỏi ng n xếp
Hình minh họa dưới đ y mô tả cách thức hoạt động của ng n xếp
với hai thao tác Pu h và Pop trên ng n xếp các phần tử là các số
nguyên.

Hình 4.1. Ng n xếp và các thao tác thêm vào và lấy ra trên ng n xếp
108
4.1.2 Các thao tác trên ngăn xếp
ác thao tác trên ng n xếp bao gồm :
 Thao tác Push(object): thêm một phần tử vào ng n xếp
 Thao tác Pop(): lấy một phần tử ra khỏi ng n xếp và trả lại giá
trị phần tử
 Thao tác Top(): trả lại vị trí phần tử nạp sau cùng vào ng n xếp
 Thao tác Size(): trả lại số lượng phần tử được lưu trong ng n
xếp
 Thao tác isEmpty(): kiểm tra xem có phải ng n xếp r ng không.
4.1.3 Ứng dụng với ngăn xếp
Dựa theo cách thức t chức, cơ chế hoạt động của ng n xếp, ta có
thể xây dựng nhiều ứng dụng đa dạng, như
 Xây dựng ứng dụng theo dõi lịch sử duyệt trang trong trình
duyệt web.
 Xây dựng chức n ng Undo trong phần mềm soạn thảo v n bản
hoặc các phần mềm khác cần chức n ng phục hồi lại một thao
tác đã thực hiện trước đó
 Xây dựng ứng dụng đ i cơ ố.
 Xây dựng ứng dụng trong cài đặt chương trình dịch.
 Xây dựng chương trình kiểm tra tính hợp lệ của các dấu ngoặc
trong biểu thức.
 Sử dụng trong khử đệ quy khi lập trình.
…

4.1.4 Cài đặt ngăn xếp


ó hai cách cài đặt ng n xếp:
 ài đặt ng n xếp dùng mảng

109
 ài đặt ng n xếp dùng danh sách liên kết

4.1.4.1. ài đặt ng n xếp dùng mảng


Cách đơn giản nhất để cài đặt ng n xếp là dùng mảng. Ta nạp các
phần tử theo thứ tự từ trái sang phải. Có biến lưu giữ chỉ số của phần
tử ở đầu ng n xếp.Thao tác cài đặt ng n xếp dùng mảng được thực
hiện qua các bước sau :
Bước 1: Khai báo cấu trúc dữ liệu của ngăn xếp
typedef struct tagStack
{ int a[max];
int t;
}Stack;
Bước 2 : Viết các hàm thực hiện các hao ác rên ngăn xếp, bao
gồm các thao tác chính :
 Thao tác khởi tạo ng n xếp
 Thao tác kiểm tra ng n xếp có r ng không
 Thao tác thêm phần tử vào ng n xếp
 Thao tác kiểm tra ng n xếp có đầy hay không
 Thao tác lấy phần tử ra khỏi ng n xếp
- Hàm khởi tạo ngăn xếp
void CreateStack(Stack &s)
{
s.t=-1;
110
}
- Hàm kiểm ra ngăn xếp có rỗng không
int IsEmpty(Stack s)
{
if(s.t==-1)
return 1;
else
return 0;
}
- Hàm kiểm ra ngăn xế có đầy hay không ?
int IsFull(Stack s)
{
if(s.t>=max)
return 1;
else
return 0;
}
- Hàm hêm hần ử vào ngăn xế
int Push(Stack &s, int x)
{
if (IsFull(s)==0)
{ s.t++;
s.a[s.t]=x;
return 1;
}
111
else
return 0;
}
- Hàm lấy hần ử ra khỏi ngăn xế
int Pop(Stack &s, int &x)
{
if (IsEmpty(s)==0)
{ x=s.a[s.t];
s.t--;
return 1;
}
else return 0;
}

4.1.4.2. ài đặt ng n xếp dùng danh sách liên kết


Thao tác cài đặt ng n xếp dùng danh sách liên kết được thực hiện
qua các bước sau :
Bước 1: Khai báo cấu trúc dữ liệu của ngăn xếp
struct StackNode
{ float item;
StackNode *next;
};
struct Stack
{ StackNode *top;
};

112
Bước 2 : Viết các hàm thực hiện các hao ác rên ngăn xếp,cũng
bao gồm các hao ác như hực hiện trên cấu trúc mảng
 Thao tác khởi tạo ng n xếp
 Thao tác kiểm tra ng n xếp có r ng không
 Thao tác thêm phần tử vào ng n xếp
 Thao tác kiểm tra ng n xếp có đầy hay không
 Thao tác lấy phần tử ra khỏi ng n xếp
- Hàm khởi tạo ngăn xếp
Stack *StackConstruct ()
{
Stack *s;
s = (Stack *)malloc (sizeof (Stack));
if (s == NULL)
return NULL;
s->top = NULL;
return s;
}

- Hàm hủy ngăn xếp


void StackDestroy (Stack *s)
{
while (!SatckEmpty(s))
{ StackPop(s);
free (s);
}

113
}

- Hàm kiểm tra ngăn xếp rỗng hay không


int StackEmpty (const Stack *s)
{
return (s->top == NULL);
}
- Hàm kiểm tra ngăn xếp có đầy hay không
int StackFull()
{
getch();
return 1;
}

- Hàm đưa hần tử vào ngăn xếp


int StackPush (Stack *s, float item)
{
StackNode *node;
node = (StackNode *)malloc(sizeof(StackNode));
if (node == NULL)
{ StackFull();
return 1; // tràn ngăn xếp
}
node->item = item;
node->next = s->top;

114
s->top = node;
return 0;
}

- Hàm lấy phần tử ra khỏi ngăn xếp


float StackPop (Stack *s)
{
float item;
StackNode *node;
if (StackEmpty (s))
return NULL;
node = s->top;
item = node->item;
s->top = node->next;
free (node);
return item;
}

4.2. HÀNG ĐỢI

4.2.1. Định nghĩa


Hàng đợi (queue) là một cấu trúc dữ liệu d ng để chứa các đối
tượng dữ liệu, hoạt động th o cơ chế FIFO (First In First Out - nghĩa
là dữ liệu được nạp vào hàng đợi trước sẽ được xử lý trước).
Các đối tượng có thể được thêm vào hàng đợi bất kỳ lúc nào và ở
vị trí cuối hàng đợi, chỉ có đối tượng thêm vào đầu tiên mới được
phép lấy ra khỏi hàng đợi đầu tiên. Thao tác thêm vào và lấy một đối
tượng ra khỏi hàng đợi được gọi là "enqueue" và "dequeue".
115
Hình 4.2. Hàng đợi và các thao tác thêm vào và lấy ra

Cấu trúc dữ liệu hàng đợi có nhiều ứng dụng giúp h trợ giải
quyết các bài toán trong tin học như: khử đệ quy, lưu vết các quá
trình tìm kiếm theo chiều rộng, quay lui, vét cạn, t chức quản lý và
phân phối tiến trình trong các hệ điều hành, t chức bộ đệm bàn
phím….

4.2.2. tha t t ên hàng đ i


Tương tự như ng n xếp, hàng đợi h trợ các thao tác:
 EnQu u obj) thêm đối tượng obj vào cuối hàng đợi.
 DeQueue(): lấy đối tượng ở đầu hàng đợi ra khỏi hàng đợi và
trả về giá trị của nó. Nếu hàng đợi r ng thì l i sẽ xảy ra.
 IsEmpty(): kiểm tra x m hàng đợi có r ng không.
 Front(): trả về giá trị của phần tử nằm ở đầu hàng đợi mà
không hủy nó. Nếu hàng đợi r ng thì l i sẽ xảy ra.

4.2.3. à đ t hàng đ i
ó 2 cách cài đặt hàng đợi: cài đặt hàng đợi dùng mảngvà dùng
danh sách liên kết.

4.2.3.1. ài đặt hàng đợi sử dụng mảng

a. Sử dụng mảng thông thường


Bước 1: Khai báo cấu trúc dữ liệu hàng đợi
- Khai báo mảng các phần tử
#define MAX 20 // giả sử dùng mảng gồm 20 phần tử

116
- Khai báo kiểu dữ liệu lưu trữ trong hàng đợi
typedef <Kiểu dữ liệu> El_type;
- Định nghĩa cấu trúc dữ liệu hàng đợi Queue
typedef struct Queue
{
El_type el[MAX];
int front;
int rear;
} Queue;
Bước 2 : Viết các hàm thực hiện các hao ác rên hàng đợi
- Hàm khởi tạo hàng đợi ban đầu
Eltype *initQ(Queue *q)
{
q = (Queue *)malloc(sizeof(Queue));
if (q != NULL)
{ q->front = -1;
q->rear = -1;
}
return q;
}

- Hàm kiểm ra xem hàng đợi có rỗng không?


int isEmpty(Queue *q)
{
return (q->front == -1);

117
}

- Hàm kiểm tra xem hàng đợi có đầy không?


int isFull(Queue q)
{
return (q.rear-q.front+1 == MAX);
}

- Hàm thêm một phần tử vào hàng đợi


void enQ(El_type new_el, Queue *q)
{
if (!isFull(q))
{ if (isEmpty(q))
q->front = q->front+1;
q->rear = q->rear+1;
q->el[q->rear] = new_el;
}
else
printf("Hàng đợi đầy.\n");
}

- Hàm xóa một phần tử khỏi hàng đợi


void deQ(Queue *q)
{
if (!isEmpty(q))

118
q->front = q->front+1;
else
printf("Hàng đợi trống.\n");
}
Nhận xét :
Qua m i lần xóa, phần sử dụng được của mảng sẽ giảm đi do giá
trị biến front t ng lên). Trong trường hợp đó, có thể khắc phục bằng
cách sử dụng mảng vòng.

b. Sử dụng mảng vòng


- Hàm khởi tạo hàng đợi
Eltype *initQ(Queue *q)
{
q = (Queue *)malloc(sizeof(Queue));
if (q != NULL)
{ q->front = -1;
q->rear = -1;
}
return q;
}
- Hàm kiểm ra xem hàng đợi có rỗng không?
int empty_queue(queue q)
{
return q.front==-1;
}
- Thao tác kiểm ra xem hàng đợi có đầy không?

119
int full_queue(queue q)
{
return ((q.rear-q.front+1) % maxlength==0);
}
- Hàm đưa hêm một phần tử vào hàng đợi
void enqueue(elementtype x,queue *q)
{
if (!full_queue(*q))
{ if (empty_queue(*q))
q->front=0;
q->rear=(q->rear+1) % maxlength;
q->data[q->rear]=x;
}
el printf "Hàng đợi đầy!");
}
- Hàm xóa một phần tử khỏi hàng đợi
void dequeue(queue *q)
{
if (!empty_queue(*q))
{ if (q->front==q->rear)
makenull_queue(q);
else
q->front=(q->front+1) % maxlength;
}
el printf "Hàng đợi r ng");
120
}
Nhận xét :
Mặc d phương pháp ử dụng mảng vòng có thể tận dùng toàn bộ
các mảng đã được cấp pháp ban đầu nhưng khi mảng đầy thì không
thể thêm phần tử vào hàng được nữa. Trong trường hợp này, có thể
khắc phục bằng cách sử dụng danh sách liên kết để mở rộng.

4.2.3.2. ài đặt hàng đợi dùng danh sách liên kết


Bước 1: Khai báo cấu trúc dữ liệu hàng đợi
- Khai báo kiểu dữ liệu lưu trữ trong hàng đợi
typedef <kiểu dữ liệu> elementtype;
- Định nghĩa cấu trúc dữ liệu hàng đợi Queue
typedef struct node
{ elementtype data;
node* next;
};
typedef node* position;
typedef struct queue
{ position front,rear;
};
Bước 2 : Viết các hàm thực hiện các hao ác rên hàng đợi
- Hàm khởi tạo hàng đợi
void makenull_queue(queue *q)
{ q->front=(node*)malloc(sizeof(node));
q->front->next=NULL;
q->rear=q->front;

121
}

- Hàm xem hàng đợi có rỗng không?


int empty_queue(queue q)
{
return (q.front==q.rear);
}

- Hàm đưa hêm một phần tử vào hàng đợi


void enqueue(elementtype x, queue *q)
{
q->rear->next=(node*)malloc(sizeof(node));
q->rear=q->rear->next;
q->rear->data=x;
q->rear->next=NULL;
}

- Hàm xóa một phần tử khỏi hàng đợi


void dequeue(queue *q)
{
position t;
t=q->front;
q->front=q->front->next;
free(t);
}

122
Nhận xét :
Khi sử dụng hàng đợi bằng cách cài đặt thông qua danh sách liên
kết giúp khắc phục được tình trạng đầy của việc sử dụng mảng để cài
đặt hàng đợi, tuy nhiên thao tác trên danh sách phức tạp hơn o với
dùng mảng.

BÀI TẬP CHƯƠNG 4


1. Giả sử cho hàm push(a) là hàm thực hiện nạp a vào ng n xếp và
hàm pop() là hàm lấy phần tử ra khỏi ng n xếp. Giả sử cho dãy
thao tác au đ y, biết rằng ng n xếp ban đầu được khởi tạo r ng :
push(5), push(3), pop(), push(2), push(8), pop(), pop(), pop(),
push(9), push(1), pop(), push(7), push(6), pop(), pop(), push(4),
pop(), pop().
Hãy viết ra dãy phần tử của danh của ng n xếp (chỉ rõ vị trí đầu
ng n xếp) sau khi thực hiện m i thao tác.
2. Giả sử cho hàm enq(a) là hàm thực hiện nạp a vào hàng đợi và
hàm deq() là hàm thực hiện lấy phần tử ra khỏi hàng đợi. Giả sử
cho dãy các thao tác au đ y, biết rằng hàng đợi ban đầu được
khởi tạo r ng:
enq(5), enq(3), deq(), enq(2), enq(8), deq(), enq(9), enq(1), deq(),
enq(7), enq(6), deq(), deq(). enq(4), deq(), deq().
Hãy viết ra dãy các phần tử của hàng đợi (chỉ rõ vị trí đầu và cuối
của hàng đợi) sau khi thực hiện m i thao tác.
3. Hãy trình bày cách sử dụng ng n xếp để chuyển biểu thức dạng
trung tố au đ y về dạng biểu thức hậu tố:
a. a – b * c ^ d + f
b. 1 + 2 + 3 * 4 + 5 – 6 * 7 + 8

123
4. Hãy trình bày cách tính giá trị của biểu thức hậu tố au đ y nhờ
sử dụng ng n xếp:
a. 1 2 + 3 1 + * 1 1 + 1 + /
b. 3 4 + 3 5 + * 7 + 8 *
5. Viết chương trình nhập vào một số nguyên không âm bất kỳ, sau
đó hiển thị lên màn hình số đảo ngược thứ tự của số nguyên vừa
nhập vào (ví dụ: nếu nhập số 12567, hiển thị lên màn hình số
76521) bằng cách:
a. Sử dụng ng n xếp
b. Sử dụng hàng đợi
6. Viết chương trình chuyển đ i một số nguyên N trong hệ thập
phân (hệ 10) sang biểu diễn ở các hệ sau, sử dụng ng n xếp:
a. Hệ nhị phân (hệ 2)
b. Hệ thập lục phân (hệ 16)
c. Hệ bát phân (hệ 8)
7. Hãy viết chương trình mô phỏng cho bài toán Tháp Hà Nội
bằng cách sử dụng ng n xếp.
8. Có thể vận dụng ng n xếp hoặc hàng đợi để khử đệ quy trong giải
thuật sắp xếp Quick sort không? Nếu có giải thích vì sao ?
9. Có thể vận dụng ng n xếp hoặc hàng đợi để khử đệ quy trong giải
thuật sắp xếp Merge sort không? Nếu có giải thích vì sao ?
10. Viết chương trình tìm tất cả các cặp dấu ngoặc tương ứng trong
một chương trình viết bằng ngôn ngữ C/C++.

124
CHƯƠNG 5
CÂY

5.1. ĐỊNH NGHĨA VÀ C C KH I NIỆM

5 Định nghĩa ây
y được định nghĩa là tập hợp hữu hạn các nút, được t chức
theo cấu trúc phân cấp, trong đó có một nút đặc biệt được gọi là nút
gốc (root) và tập hợp các nút còn lại nối với nút gốc thông qua các
nhánh trên cây gọi là các nút con và các nút lá. Nút lá là nút tận cùng
của cây.
Hình bên dưới là cấu trúc t chức của c y, trong đó R là nút gốc,
R1, R2,..Rk là các nút con của nút gốc R, các nút T1, T2, T3 là các nút
lá. y trong hình bên dưới là cây bao gồm k nhánh.

Hình 5.1. Cấu trúc cây


Một số ví dụ hình ảnh của cấu trúc cây trong thực tế:

125
Hình 5.2. Cây gia phả

Hình 5.3. Cây cấu trúc t chức trong một công ty

Hình 5.4. Cây cấu trúc t chức các chương mục trong một cuốn sách

126
5.1.2. Các khái niệm trên cây
- Nút gốc – root: Nút gốc là nút trên cùng của cây theo thứ tự
phân cấp trong c y. Điều đó có nghĩa là nếu một nút được gọi là nút
gốc thì nút này không có nút nào ở trên nó trong phân cấp của cây.
- Nút lá (leaf): Một nút không có nút con được gọi là nút lá
- Nút con (child): Một nút gọi là nút con khi mà trên nó có một
nút khác theo thứ tự phân cấp
- Nút cha (parent): Một nút được gọi là nút cha nếu tồn tại một
nút là nút con của nó theo thứ tự phân cấp
- T tiên (ancestors) và Hậu duệ (descendants): Nếu có đường đi
từ nút a đến nút b, thì nút a được gọi là nút t tiên (ancestor) của b,
còn b được gọi là nút hậu duệ (descendant) của nút a.
- Nút anh em (sibling): Các nút có cùng nút cha được gọi là nút
anh em.
- Chiều cao (height): Chiều cao của nút trên cây bằng độ dài
đường đi dài nhất từ nút đó đến lá cộng 1
- Đường đi (path): Nếu n1, n2, …, nk là dãy nút trên cây sao cho ni
là cha của ni + 1 với 1 ≤ i ≤ k, thì dãy này được gọi là đường đi từ nút n1
đến nk.
- Độ dài (length) của đường đi bằng số lượng nút trên đường đi
trừ bớt đi 1. Như vậy, đường đi độ dài 0 là đường đi từ một nút đến
chính nó.
- Độ sâu/ mức d pth l v l) độ sâu của nút là độ dài của đường
đi duy nhất từ gốc cộng thêm 1
- Cây con (subtree) của một cây là một nút cùng với tất cả các
hậu duệ của nó.

127
Hình 5.5. Ví dụ một cây con b và các nút con của cây b: e, f, i, j

Hình 5.6. Đường đi trên c y, đường đi 1: từ a qua b, qua f và đến i

Hình 5.7. Chiều cao và độ sâu trên cây

128
5.1.3. Cây có thứ tự
Trên c y, các nút được sắp xếp ở các vị trí khác nhau và từ đó
chúng ta có các cây khác nhau. Ví dụ dưới đ y minh họa hai cây
khác nhau.

a a

b c c b

Hình 5.8. Ví dụ hai cây bố trí các nút ở vị trí khác nhau

Cây với các nút được sắp xếp có thứ tự được gọi là cây có thứ tự,
ta sẽ xét chủ yếu là cây có thứ tự. Vì vậy, tiếp th o đ y thuật ngữ cây
là chỉ để chỉ cây có thứ tự. Khi muốn khẳng định không quan t m đến
thứ tự, ta sẽ phải nói rõ là cây không có thứ tự.

5.1.4. Duyệt cây theo thứ tự


ó ba cách để duyệt cây theo thứ tự là thứ tự trước, giữa và sau.

Duyệt theo thứ tự trước


Duyệt theo thứ tự trước (Preorder Traversal) của các nút thuộc cây
T:
- Gốc R của T
- Tiếp đến là các nút của T1 (là con của nút R) theo thứ tự trước
- Sau đó là các nút của T2 (là con của nút R) theo thứ tự trước;
-…
- Và cuối cùng là các nút của Tk (là con của nút R) theo thứ
tự trước.
Thuật toán duyệt theo thứ tự rước
void PREORDER (nodeT r)
129
{
Xuất ra giá trị của nút R;
Vòng lặp (m i nút con C của nút gốc R, nếu có, theo thứ tự
từ trái sang) thực hiện :
Gọi hàm PREORDER(C);
}

Duyệt theo thứ tự sau


Duyệt theo thứ tự sau (Postorder Traversal) của các nút của cây T :
- Các nút của T1 theo thứ tự sau;
- Tiếp đến là các nút của T2 theo thứ tự sau;
- …;
- Các nút của Tk theo thứ tự sau;
- Sau cùng là nút gốc R.
Thuật toán duyệt theo thứ tự sau
void POSORDER (nodeT r)
{
Vòng lặp (m i nút con C của nút gốc R, nếu có, theo thứ tự từ
trái sang) thực hiện :
Gọi hàm POSORDER(C);
Xuất ra giá trị của nút R;
}

Duyệt theo thứ tự giữa


Duyệt theo thứ tự giữa (Inorder Traversal) của các nút của cây T :
- Các nút của T1 theo thứ tự giữa;
- Tiếp đến là nút gốc R.
130
- Tiếp theo là các nút của T2, …, Tk, m i nhóm nút được xếp
theo thứ tự giữa.
void INORDER (nodeT r)
{
Nếu (R là lá)
Xuất ra giá trị của nút R;
Ngược lại
{ Gọi hàm INORDER (con trái nhất của R);
Xuất ra giá trị của nút R;
Vòng lặp (m i nút con của nút R, ngoại trừ con trái
nhất, theo thứ tự từ trái sang) thực hiện :
Gọi hàm INORDER (c);
}
}
Ví dụ:
Giả sử cho cây, bao gồm các nút a, b, c, d, e, k, g, h, i, l như hình
như au :

Thứ tự duyệt cây theo thứ tự trước, giữa và sau sẽ được kết quả
như au

131
- Thứ tự trước : a, b, c, e, h, i, k, l, d, g
- Thứ tự sau : b, h, i, e, l, k, c, g, d, a
- Thứ tự giữa : b, a, h, e, i, c, l, k, g, d

5.2. BIỂU DIỄN CÂY TRONG MÁY TÍNH


Có nhiều cách biểu diễn cây trên máy tính. Có hai cách biểu diễn
c y cơ bản: biểu diễn cây sử dụng mảng và biểu diễn cây sử dụng
danh sách liên kết.

5.2.1. Bi u diễn cây sử dụng mảng


Giả sử T là cây với các nút được đặt tên là nút 1, 2, …, N. ách
đơn giản để biểu diễn cây T là dùng 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 nút gốc của T
có thể phân biệt bởi con trỏ r ng. Khi dùng mảng, đặt A[i] = j nếu nút
j là cha của nút i và A[i] = 0, nếu nút i là gốc.
Cách biểu diễn này dựa trên cơ ở là m i nút của cây (ngoại trừ
gốc) đều có duy nhất một nút cha. Với cách biểu diễn này, đường đi từ
một nút đang x t đến t tiên của chúng có thể xác định dễ dàng qua
công thức truy hồi : n ← par nt n) ← par nt par nt n)) ← …
Ngoài ra, cũng có thể sử dụng thêm một mảng phụ, tạm gọi là N
để có thể h trợ việc ghi nhận nhãn (tên gọi cho các nút) tương ứng
với m i phần tử A[i]
Ví dụ: Giả sử cho cây bao gồm 10 nút như au:

132
Mảng d ng để lưu c y trong máy tính được t chức như au
0 1 1 2 2 3 6 6 6 3

5.2.2. Bi u diễn cây sử dụng danh sách liên kết


Trong cách biểu diễn này, với m i nút của cây, ta tạo các danh
sách liên kết thể hiện mối quan hệ giữa nút đang x t với các nút khác
(nếu chúng có quan hệ với nhau – có cạnh nối với nhau trực tiếp hoặc
gián tiếp). Tuy nhiên, chú ý rằng số lượng con của các nút rất khác
nhau, nên danh sách móc nối thường là lựa chọn thích hợp nhất.
Ví dụ : Giả sử cho cây bao gồm 10 nút như au

Danh sách liên kết lưu c y trong máy tính được t chức như au

133
5.3. CÂY NHỊ PHÂN

5.3.1. Định nghĩa và tính h t cây nhị phân


Định nghĩa 1: Cây nhị phân là cây mà tại m i nút trên cây có tối
đa hai nút con.
Như vậy m i nút của cây nhị phân hoặc không có nút con, hoặc
chỉ có nút con bên trái; hoặc chỉ có nút con bên phải; hoặc có đồng
thời cả nút con trái và nút con phải.

Hình 5.9. Cây nhị ph n có đủ hai con trái và phải tại m i nút

Hình 5.10. Cây nhị ph n không đủ hai con trái và phải tại m i nút

Định nghĩa 2: Cây nhị ph n đầy đủ là cây nhị phân thỏa mãn:
m i nút lá đều có c ng độ u và các nút trong có đúng hai con.

134
Hình 5.11. Cây nhị ph n đầy đủ

Định nghĩa 3: Cây nhị phân hoàn chỉnh là cây nhị ph n độ sâu n,
có thể coi 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ệch sang trái nhất có thể.

Hình 5.12. Cây nhị phân hoàn chỉnh

5.3.2. Tính ch t của cây nhị phân

Bổ đề 1:
(i) Số đỉnh lớn nhất ở mức trên 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 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à: k = log2(n+1)
Chứng minh:
(i) Bằng quy nạp theo i.

135
ơ 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 quy nạp: Giả sử với mọi j, 1 ≤ j < i, ố đỉ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 th o định
nghĩa, m i đỉnh trên cây nhị phân có không quá hai nút con, ta suy ra
số lượng nút lớn nhất trên mức i không vượt quá hai lần số lượng nút
trên mức i – 1, nghĩa là không vượt quá 2 * 2i – 2 = 2i – 1.
(ii) Số lượng nút lớn nhất của cây nhị phân chiều cao k không
vượt quá t ng số nút lớn nhất trên các mức i = 1, 2, …, k. Theo b đề
1, số này không vượt quá:

(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ó

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

Bổ đề 2: Cây nhị ph n đầy đủ với độ sâu n có 2n – 1 nút.


Chứng minh: Suy ra trực tiếp từ b đề 1.
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 ra trực tiếp từ định nghĩa và b đề 1.

5.3.3. Bi u diễn cây nhị phân


Tương tự như cách biểu diễn cấu trúc cây t ng quát trong máy
tính. Cây nhị ph n là trường hợp riêng của c y, do đó cũng có hai
cách biểu diễn cây nhị ph n cơ bản như au
- Biểu diễn cây sử dụng mảng
136
- Biểu diễn cây sử dụng danh sách liên kết
Ví dụ như đối với cách biểu diễn bằng danh sách liên kết, ta có
thể định nghĩa cấu trúc dữ liệu như au cho m i nút trên cây :

Cấu trúc dữ liệu của một nút


typedef struct tagTNode
{ Data Info; // dữ liệ lư rong nú có kiểu Info
struct tagTNode *pLeft;
struct tagTNode *pRight;
}TNode;
Cấu trúc dữ liệu của cây
typedef TNode *TREE;

5.3.4. Duyệt cây nhị phân


Tương tự cách duyệt trên cây t ng quát, ta có các cách duyệt cây
nhị phân như au:
Thứ tự trước – NLR
Thao tác thực hiện
- Bước 1 : Xuất giá trị của nút đang x t
- Bước 2 : Gọi hàm đệ quy PreOrder duyệt cây con trái theo thứ
tự trước
- Bước 3 :Gọi hàm đệ quy PreOrder duyệt cây con phải theo thứ
tự trước

137
Hàm cài đặt
void PreOrder (TNode *TREE)
{
if (TREE != NULL)
{ printf % \n , TREE->Info);
PreOrder (TREE->pLeft);
PreOrder (TREE->pRight);
}
}

Thứ tự giữa – LNR


Thao tác thực hiện
- Bước 1 : Gọi hàm đệ quy nOrd r để duyệt cây con trái theo thứ
tự giữa
- Bước 2 : Xuất giá trị của nút đang x t
- Bước 3 :Gọi hàm đệ quy nOrd r để duyệt cây con phải theo thứ
tự giữa
Hàm cài đặt
void InOrder (TreeNode *TREE)
{
if (TREE != NULL)
{ InOrder (TREE->pLeft);
printf % \n , TREE->Info);
InOrder (TREE->pRight);
}
}

138
Thứ tự sau – LRN
Thao tác thực hiện
- Bước 1 :Gọi hàm đệ quy Po Ord r để duyệt cây con trái theo
thứ tự sau
- Bước 2 : Gọi hàm đệ quy Po Ord r để duyệt cây con phải theo
thứ tự sau
- Bước 3 : Xuất giá trị của nút đang x t
Hàm cài đặt
void PosOrder (TreeNode *TREE)
{
if (TREE != NULL)
{ PosOrder (TREE->pLeft);
PosOrder (TREE->pRight);
printf % \n , TREE->Info);
}
}
Ví dụ:
Giả sử cho cây nhị ph n như hình dưới đ y :

139
Thứ tự duyệt cây theo các thứ tự trước và giữa như trình bày ở
trên có kết quả như au
- Duyệt theo thứ tự trước : 9, 2, 6, 1, 10, 8, 5, 3, 7, 12, 4.
- Duyệt theo thứ tự giữa : 6, 2, 10, 1, 9, 3, 5, 8, 12, 7, 4.

5.3.5. Một số ứng dụng cây nhị phân trong thực tế

5.3.5.1. Cây nhị phân tính toán


Cây nhị phân tính toán là cây nhị ph n được t chức cho việc tính
toán, như au
1. M i nút lá chứa một toán hạng.
2. M i nút con chứa một phép toán.
3. Các cây con trái và phải của nút con biểu diễn các biểu thức
con cần được thực hiện trước khi thực hiện phép toán ở gốc ở nút cha
phía trên.

Hình 5.13. Cây nhị phân tính toán

5.3.5.2. Cây quyết định


Cây quyết định là một kiểu mô hình dự báo, nghĩa là một ánh xạ
từ các quan sát về một sự vật hoặc hiện tượng tới các kết luận về giá
trị mục tiêu của sự vật hoặc hiện tượng ấy. M i một nút trên cây

140
tương ứng với một biến; đường nối giữa nó với nút con của nó thể
hiện một giá trị cụ thể cho biến đó. M i nút lá đại diện cho giá trị dự
đoán của biến mục tiêu, các giá trị cho trước của các biến được biểu
diễn bởi đường đi từ nút gốc tới nút lá đó.
Ví dụ
David là quản lý của một c u lạc bộ đánh golf n i tiếng. nh ta
đang có rắc rối về chuyện các thành viên đến hay không đến. ó ngày
ai cũng muốn chơi golf nhưng ố nh n viên c u lạc bộ lại không đủ để
phục vụ. ó hôm, không hiểu vì lý do gì mà chẳng ai đến chơi và c u
lạc bộ lại thừa nh n viên.
Mục tiêu của David là tối ưu hóa ố nh n viên phục vụ m i ngày
bằng cách dựa th o thông tin dự báo thời tiết để đoán x m khi nào
người ta ẽ đến chơi golf. Để thực hiện điều đó, anh cần hiểu được tại
ao khách hàng quyết định chơi và tìm hiểu x m có cách giải thích
nào cho việc đó hay không.Vậy là trong hai tuần, anh ta thu thập
thông tin về thời tiết để giải thích cho c u hỏi về ố người đến chơi
goft. Dựa trên thông tin thu thập được, David vẽ được c y ph n loại
chơi hoặc không chơi golf như au

Hình 5.14. Cây phân loại chơi hoặc không chơi golf

141
Từ cây phân loại này, David rút ra được các thông tin mang tính
quyết định như au Nếu trời nhiề mây, người a l ôn l ôn chơi
golf. Có một số người ham mê đến mức chơi golf cả khi trời mưa.
Tiếp th o, David lại chia nhóm trời nắng thành hai nhóm nhỏ hơn
và thấy rằng : Khách hàng không m ốn chơi golf nế độ ẩm lên á
70%.
uối c ng, David chia nhóm trời mưa thành hai nhómvà thấy
rằng: Khách hàng sẽ không chơi golf nế rời nhiề gió.
Và đ y là lời giải ngắn gọn cho bài toán mô tả bởi c y ph n loại.
David cho phần lớn nh n viên nghỉ vào những ngày trời nắng và m,
hoặc những ngày mưa gió. Vì hầu như ẽ chẳng có ai chơi golf trong
những ngày đó. Vào những hôm khác, khi nhiều người ẽ đến chơi
golf, anh ta có thể thuê thêm nh n viên thời vụ để phụ giúp công việc.
Như thế c y quyết định giúp ta biến một biểu diễn dữ liệu phức
tạp thành một cấu trúc đơn giản hơn rất nhiều.

5.4. CÂY NHỊ PHÂN TÌM KIẾM

5.4.1. Định nghĩa


Cây nhị phân tìm kiếm (CNPTK) là cây nhị phân trong đó tại m i
nút, giá trị khóa của nút đang x t lớn hơn giá trị khóa của tất cả các
nút thuộc cây con trái và nhỏ hơn khóa của tất cả các nút thuộc cây
con phải.

Hình 5.15. Cây nhị phân tìm kiếm lưu trữ giá trị các số nguyên

142
Nhận xét:
Nhờ ràng buộc về giá trị khóa trên cây nhị phân tìm kiếm, việc
tìm kiếm trở nên có định hướng. Hơn nữa, do cấu trúc cây việc tìm
kiếm trở nên nhanh hơn đáng kể. Chi phí tìm kiếm trung bình là
log2N.

5.4.2. Các thao tác trên cây nhị phân tìm kiếm

5.4.2.1. Tạo một cây nhị phân tìm kiếm


Thao tác thực hiện
Ta có thể tạo một cây nhị phân tìm kiếm bằng cách lặp lại quá
trình thêm một phần tử vào một cây r ng như trình bày dưới đ y

5.4.2.2. Duyệt cây


Thao tác thực hiện
Thao tác duyệt c y trên c y nhị ph n tìm kiếm hoàn toàn giống
như trên c y nhị ph n. hỉ có một lưu ý nhỏ là khi duyệt th o thứ tự
giữa, trình tự các nút duyệt qua ẽ cho ta một dãy các nút th o thứ tự
t ng dần của khóa.
Hàm cài đặt
LPNODE TravelNode(TREE T)
{
if( T != NULL )
{ printf %d , T->Key);
TravelNode(T->pLeft);
TravelNode(T->pRight);
}
return NULL;
}

143
5.4.2.3. Thêm một phần tử X vào cây
Thao tác thực hiện
Việc thêm một phần tử X vào cây phải bảo đảm điều kiện ràng
buộc của cây nhị phân tìm kiếm. Khi chấm dứt quá trình tìm kiếm
cũng chính là lúc tìm được ch cần thêm vào.
Hàm cài đặt
int insertNode(TREE &T, Data X)
{
if ( T != NULL )
{ if (T->Key == X)
return 0; //đã có hần tử X
else if (T->Key > X)
return insertNode(T->pLeft, X);
else
return insertNode(T->pRight, X);
}
T = new TNode;
if (T == NULL)
return -1; //không thể cấp phát bộ nhớ
T->Key = X;
T->pLeft = NULL;
T->pRight = NULL;
return 1; } //thêm vào thành công

144
Hình 5.16. Minh họa thao tác thêm phần tử X=48 vào cây

5.4.2.4. Tìm một phần tử X trên cây


Thao tác thực hiện
Thao tác tìm một phần tử trên cây sẽ duyệt qua các nút trên cây
và thực hiện so sánh giá trị X cần tìm với giá trị tại nút đang x m x t.
Hàm cài đặt
LPNODE searchNode(TREE T, Data X)
{ if( T != NULL )
{ if (T->Key == X)
return T;
else if (T->Key > X)
return searchNode(T->pLeft, X);
else return searchNode(T->pRight, X);
}
return NULL;
}
145
Ta có thể x y dựng hàm tìm kiếm tương đương không đệ quy :
LPTNODE searchNode(TREE Root, Data x)
{ LPNODE p = Root;
while (p != NULL)
{ if (x == p->Key) return p;
else if(x < p->Key)
p = p->pLeft;
else // if(x > p->Key)
p = p->pRight;
}
return NULL;
}
Nhận x t : Số lần o ánh tối đa phải thực hiện để tìm phần tử X
là h, với h là chiều cao của c y. Như vậy, thao tác tìm kiếm trên c y
nhị ph n tìm kiếm có n nút tốn chi phí trung bình khoảng O log2N).

Hình 5.17. Minh họa thao tác tìm kiếm trên cây, tìm phần tử X=82

146
5.4.2.5. Hủy một phần tử có khóa X trên cây
Thao tác thực hiện
Việc hủy một phần tử X ra khỏi cây phải bảo đảm điều kiện ràng
buộc của cây nhị phân tìm kiếm
ó 3 trường hợp khi hủy nút X có thể xảy ra:
- Trường hợp 1 : X là nút lá.
- Trường hợp 2 :X chỉ có một nút con (trái hoặc phải)
- Trường hợp 3 :X có đủ cả hai nút con
Trường hợp 1:
Chỉ đơn giản hủy X, vì X không móc nối đến phần tử nào khác.

Hình 5.18. Minh họa thao tác xóa phần tử 75 trên cây
Trường hợp 2:
Trước khi hủy X, ta móc nối cha của X với con duy nhất của nó.

147
Hình 5.19. Minh họa thao tác xóa phần tử 49 trên cây

Trường hợp 3:
Khi không thể hủy trực tiếp, do X có đủ hai nút con, ta sẽ hủy
gián tiếp. Thay vì hủy nút X, ta sẽ tìm một phần tử thế mạng gọi là nút
Y. Phần tử này có tối đa một con. Thông tin lưu tại nút Y sẽ được
chuyển lên lưu tại nút X. Sau đó, nút bị hủy thật sự sẽ là nút X giống
như 2 trường hợp đầu.
Vấn đề là phải chọn nút Y ao cho khi lưu nút Y vào vị trí của nút
X, cây vẫn là cây nhị phân tìm kiếm.
Có 2 phần tử thỏa mãn yêu cầu:
- Phần tử nhỏ nhất (trái nhất) trên cây con phải.
- Phần tử lớn nhất (phải nhất) trên cây con trái.
Việc chọn lựa phần tử nào là phần tử thế mạng hoàn toàn phụ
thuộc vào ý thích của người lập trình. Ở đ y, giả sử ta sẽ chọn phần tử
phải nhất trên cây con trái làm phân tử thế mạng.
Ví dụ:
Giả sử ta muốn xóa phần tử X = 82 ra khỏi cây

148
Hình 5.20. Minh họa thao tác xóa phần tử 82 trên cây

Sau khi hủy phần tử X=82 ra khỏi cây tình trạng của cây sẽ như
trong hình (phần tử 75 là phần tử thế mạng)
Hàm cài đặt
Hàm deleteNode trả về giá trị 1, 0 khi hủy thành công hoặc không
có X trong cây
int deleteNode(TREE &T, Data X)
{
if (T == NULL)
return 0;
else if (T->Key > X)
return deleteNode (T->pLeft, X);
else if(T->Key < X)
return deleteNode (T->pRight, X);
else //T->Key == X
{ LPNODE p = T;
if (T->pLeft == NULL)
149
T = T->pRight;
else if (T->pRight == NULL)
T = T->pLeft;
else //T có 2 nút con
{ LPNODE q = T->pRight;
searchStandFor(p, q); // Hàm tìm phần tử
thế mạng
}
delete p;
}
}
Trong đó, hàm archStandFor được viết như au
void searchStandFor(TREE &p, TREE &q)
{
if (q->pLeft)
searchStandFor(p, q->pLeft);
else
{ p->Key = q->Key;
p = q;
q = q->pRight;
}
}

5.4.2.6. Hủy toàn bộ cây nhị phân tìm kiếm


Thao tác thực hiện

150
Việc hủy toàn bộ cây có thể được thực hiện thông qua thao tác
duyệt cây theo thứ tự sau: hủy cây con trái, cây con phải, rồi mới hủy
nút gốc.
Hàm cài đặt
void removeTree(TREE &T)
{
if ( T != NULL )
{ removeTree(T->pLeft);
removeTree(T->pRight);
delete T;
}
}
Tất cả các thao tác thêm, xóa trên cây nhị phân tìm kiếm đều có
độ phức tạp trung bình O(h), với h là chiều cao của cây.
Trong trong trường hợp tốt nhất, cây nhị phân tìm kiếm có n nút
sẽ có độ cao h = log2(n). Chi phí tìm kiếm khi đó ẽ tương đương tìm
kiếm nhị phân trên mảng có thứ tự.
Tuy nhiên, trong trường hợp xấu nhất, cây có thể bị suy biến
thành một danh sách liên kết (khi mà m i nút đều chỉ có một nút con
trừ nút lá). Lúc đó, các thao tác trên sẽ có độ phức tạp O(n). Vì vậy
cần cải tiến cấu trúc của cây nhị phân tìm kiếm để đạt được chi phí
cho các thao tác là log2(n).

5.5. CÂY CÂN BẰNG

5.5.1. Định nghĩa


Chỉ số cân bằng của một nút là hiệu của chiều cao cây con phải
và cây con trái của nó. Đối với một cây cân bằng, chỉ số cân bằng
(CSCB) của m i nút chỉ có thể mang một trong ba giá trị au đ y
CSCB(p) = 0 <=> Độ cao c y trái p) = Độ cao cây phải (p)
151
S B p) = 1 <=> Độ cao c y trái p) < Độ cao cây phải (p)
CSCB(p) = -1 <=> Độ cao c y trái p) > Độ cao cây phải (p)
Độ cao cây trái (p) ký hiệu là hL
Độ cao cây phải (p) ký hiệu là hR
Để khảo sát cây cân bằng, ta cần lưu thêm thông tin về chỉ số cân
bằng tại m i nút.
Lúc đó, c y c n bằng có thể được khai báo như au:
typedef struct tagAVLNode
{ DataType key;
char balFactor; // chỉ số cân bằng
struct tagAVLNode* pLeft;
struct tagAVLNode* pRight;
}AVLNode, *AVLTree;
Ta định nghĩa một số hằng số sau:
#define LH -1 // c y con trái cao hơn
#define EH 0 // hai cây con bằng nhau
#define RH 1 // cây con phải cao hơn

5.5.2. t ờng h p m t cân bằng


Ta sẽ không khảo sát tính cân bằng của một cây nhị phân bất kỳ
mà chỉ quan t m đến các khả n ng mất cân bằng xảy ra khi thêm hoặc
hủy một nút trên cây nhị phân tìm kiếm cân bằng. Như vậy, khi mất
cân bằng, độ lệch chiều cao giữa 2 cây con sẽ là 2, khi đó ta có 6 khả
n ng au
+ rường hợp 1: cây T lệch về bên trái, có các khả n ng au

152
+ rường hợp 2: cây T lệch về bên phải, có các khả n ng au

Ta có thể thấy rằng các trường hợp lệch về bên phải hoàn toàn
đối xứng với các trường hợp lệch về bên trái. Vì vậy ta chỉ cần khảo
át trường hợp lệch về bên trái và làm tương tự cho trường hợp lệch
bên phải.
+ rường hợp 1.1: cây T1 lệch về bên trái. Ta thực hiện phép
quay đơn L ft-Left

+ rường hợp 1.2: cây T1 không lệch. Ta thực hiện phép quay
đơn L ft-Left

153
+ rường hợp 1.3: cây T1 lệch về bên phải. Ta thực hiện phép
quay kép Left-Right
Do T1 lệch về bên phải ta không thể áp dụng ph p quay đơn đã
áp dụng trong 2 trường hợp trên vì khi đó c y T ẽ chuyển từ trạng
thái mất cân bằng do lệch trái thành mất cân bằng do lệch phải, cần áp
dụng cách khác. Hình dưới đ y minh họa phép quay kép áp dụng cho
trường hợp này:

Nhận xét :
Trước khi cân bằng cây T có chiều cao h+2 trong cả 3 trường hợp
1.1, 1.2 và 1.3. Sau khi cân bằng, trong 2 trường hợp 1.1 và 1.3 cây có
chiều cao h+1, ở trường hợp 1.2 cây vẫn có chiều cao h + 2. Đ y cũng
là trường hợp duy nhất sau khi cân bằng nút T cũ có chỉ số cân bằng 0.
Với những x m x t trên, x t tương tự cho trường hợp cây T lệch
về bên phải, ta có thể xây dựng 2 hàm quay đơn và 2 hàm quay k p
như sau:

154
Hàm q ay đơn Lef -Left
void rotateLL ( AVLTree &T)
{
AVLNode * T1 = T-> pLeft ;
T->pLeft = T1->pRight ;
T1-> pRight = T;
switch( T1-> balFactor )
{ case LH: { T-> balFactor = EH;
T1-> balFactor = EH;
break ;
}
case EH: { T-> balFactor = LH;
T1-> balFactor = RH;
break ;
}
}
T = T1;
155
}
Hàm q ay đơn Righ -Right
void rotateRR(AVLTree &T)
{
AVLNode * T1 = T-> pRight ;
T->pRight = T1-> pLeft ;
T1-> pLeft = T;
switch ( T1-> balFactor )
{ case RH: { T-> balFactor = EH;
T1-> balFactor = EH;
break ;
}
case EH: { T-> balFactor = RH;
T1-> balFactor = LH;
break ;
}
}
T = T1;
}

156
Hàm quay kép Left-Right

void rotateLR(AVLTree &T)

AVLNode * T1 = T-> pLeft ;

AVLNode * T2 = T1-> pRight ;

T-> pLeft = T2-> pRight ;

T2-> pRight = T;

T1-> pRight = T2-> pLeft ;

T2-> pLeft = T1;

switch (T2-> balFactor )

{ case LH: { T-> balFactor = RH;

T1-> balFactor = EH;


157
break ;

case EH: { T-> balFactor = EH;

T1-> balFactor = EH;

break ;

case RH: { T-> balFactor = EH;

T1-> balFactor = LH;

break ;

T2-> balFactor = EH;

T = T2;

Hàm quay kép Right-Left

void rotateRL ( AVLTree &T)

AVLNode * T1 = T-> pRight ;

158
AVLNode * T2 = T1-> pLeft ;

T->pRight = T2-> pLeft ;

T2->pLeft = T;

T1->pLeft = T2-> pRight ;

T2-> pRight = T1;

switch (T2-> balFactor )

{ case RH: { T-> balFactor = LH;

T1-> balFactor = EH;

break ;

case EH: { T-> balFactor = EH;

T1-> balFactor = EH;

break ;

case LH: { T-> balFactor = EH;

T1-> balFactor = RH;

break ;

159
T2-> balFactor = EH;

T = T2;

Ta xây dựng 2 hàm cân bằng lại khi cây bị lệch trái hoặc lệch phải,
như au

Hàm cân bằng khi cây bị lệch về bên trái

int balanceLeft(AVLTree &T)

{ switch (T-> pLeft -> balFactor )

{ case LH: { rotateLL(T);

return 2;

case EH: { rotateLL(T);

return 1;

case RH: { rotateLR(T);

return 2;

return 0;

}
160
Hàm cân bằng khi cây bị lệch về bên phải

int balanceRight(AVLTree &T)

{ switch (T-> pRight -> balFactor )

{ case LH: { rotateRL(T);

return 2;

case EH: { rotateRR(T);

return 1;

case RH: { rotateRR(T);

return 2;

return 0;

5.5.3. Thêm một phần tử trên cây


Thao tác thực hiện
Thêm một phần tử vào cây nhị phân tìm kiếm cân bằng diễn ra
tương tự như trên c y nhị phân tìm kiếm. Tuy nhiên, sau khi thêm
xong, nếu chiều cao của c y thay đ i, từ vị trí thêm vào, ta phải lần
ngược lên gốc để kiểm tra xem có nút nào bị mất cân bằng không.

161
Nếu có, ta phải cân bằng lại ở nút này. Việc cân bằng lại chỉ cần thực
hiện một lần tại nơi mất cân bằng.
Hàm cài đặt
Hàm insertNode trả về giá trị -1, 0, 1 khi không đủ bộ nhớ, gặp
nốt cũ hay thành công. Nếu sau khi thêm, chiều cao cây bị t ng lên,
giá trị 2 sẽ được trả về:

int insertNode(AVLTree &T, DataType X)

{ int res;

if (T != NULL )

{ if(T->key == X)

return 0; //đã có phần tử X

else if (T->key > X)

{ res = insertNode(T->pLeft, X);

if (res < 2)

return res;

switch (T->balFactor)

{ case RH: { T->balFactor= EH;

return 1;

case EH: { T->balFactor = LH;

return 2;

162
}

case LH: { balanceLeft(T);

return 1;

else // if (T->key < X)

{ res = insertNode(T-> pRight, X);

if (res < 2)

return res;

switch (T->balFactor)

{ case LH: { T->balFactor = EH;

return 1;

case EH: { T->balFactor = RH;

return 2;

case RH: { balanceRight(T);

return 1;

163
}

T = new TNode;

if (T == NULL)

return -1; //không thể cấp phát bộ nhớ

T->key = X;

T->balFactor = EH;

T->pLeft = T->pRight = NULL;

return 2; // thành công, chiều cao t ng

5.5.4. Hủy một phần tử X trên cây

Thao tác thực hiện


ũng giống như thao tác thêm một nút, việc hủy một phần tử X ra
khỏi cây cân bằng thực hiện giống như trên cây nhị phân tìm kiếm.
Sau khi hủy, nếu tính cân bằng của cây bị vi phạm ta sẽ thực hiện việc
cân bằng lại.
Nhận xét rằng, thao tác cân bằng lại trong thao tác hủy sẽ phức
tạp hơn nhiều do có thể xảy ra phản ứng dây chuyền.

164
Hàm cài đặt
Hàm deleteNode trả về giá trị 1, 0 khi hủy thành công hoặc không
có X trong cây. Nếu sau khi huỷ, chiều cao cây bị giảm đi, giá trị 2 sẽ
được trả về :
int deleteNode(AVLTree &T, DataType X)
{ int res ;
if (T==NULL)
return 0;
if (T->key > X)
{ res = deleteNode (T-> pLeft , X);
if ( res < 2)
return res ;
switch ( T-> balFactor )
{ case LH: { T-> balFactor = EH;
return 2;
}
case EH: { T-> balFactor = RH;
return 1;
}
case balanceRight (T);
}
}
else if (T->key < X)
{ res = deleteNode (T-> pRight , X);
if ( res < 2) return res ;

165
switch (T-> balFactor )
{ case RH: { T-> balFactor = EH;
return 2;
}
case EH: { T-> balFactor = LH;
return 1;
}
case LH: return balanceLeft (T);
}
}
else // if (T->key == X)
{
AVLNode * p = T;
if (T-> pLeft == NULL)
{
T = T->pRight ; res = 2;
}
else if (T-> pRight == NULL)
{
T = T-> pLeft ; res = 2;
}
else // if (T-> pLeft != NULL && T-> pRight != NULL)
{
res = searchStandFor ( p, T -> pRight );
if( res < 2)

166
return res;
switch( T-> balFactor )
{ case RH: { T-> balFactor = EH;
return 2;
}
case EH: { T-> balFactor = LH;
return 1;
}
case LH: return balanceLeft (T);
}
}
delete p;
return res ;
}
}

Hàm tìm phần tử thế mạng


int searchStandFor(AVLTree &p, AVLTree &q)
{ int res;
if (q->pLeft != NULLL)
{
res = searchStandFor(p, q->pLeft);
if (res < 2)
return res;
switch (q->balFactor)

167
{ case LH: { q->balFactor= EH;
return 2;
}
case EH:{ q->balFactor= RH;
return 1;
}
case RH: return balanceRight(T);
}
}
else
{ p->key = q->key;
p = q;
q = q->pRight;
return 2;
}
}

168
BÀI TẬP CHƯƠNG 5
1. Giả sử cho c y như trong hình au
A

B C

D E F

G H I J
a. Trình bày thứ tự duyệt các nút của cây theo thứ tự trước.
b. Trình bày thứ tự duyệt các nút của cây theo thứ tự giữa.
c. Trình bày thứ tự duyệt các nút của cây theo thứ tự sau.
2. Hãy vẽ cây nhị ph n có độ cao là 3, khi kết quả duyệt theo thứ tự
sau cho ta dãy khóa là: (10, 30, 50, 20, 40, 70, 60)
3. Cho các cây nhị ph n như trong hình au, hãy trình bày cách biểu
diễn bằng cấu trúc mảng, cấu trúc danh sách liên kết cho các cây.
a) b) A
D

I J B C

E F G H D G H I

A B C E F J K

4. Giả sử cho cây nhị ph n trong hình cho dưới đ y. Hãy trình
bày thứ tự các đỉnh xuất hiện khi duyệt cây theo thứ tự trước,
giữa và sau.

169
a) D b) D

B F B F

C E A C E A

H I H I

G G
5. Cho dãy số sau: 20, 5, 1, 17, 30, 24, 7, 8, 25, 32, 50, 18, 99, 68,
29, 50.
Hãy thực hiện các yêu cầu sau:
a. Xây dựng cây nhị phân tìm kiếm từ dãy số đã cho th o thứ
tự thêm các số vào cây theo thứ tự từ trái sang của dãy số.
b. Xóa khỏi cây các nút chứa các khóa 24, 20, 17, 5, 29, 68.
M i lần xóa một nút vẽ lại cây và cân bằng lại cây khi cây bị
mất cân bằng.
6. Giả sử cho thông tin một sinh viên bao gồm các thông tin:
- Mã sinh viên : dạng số nguyên dương, mã inh viên là giá trị
duy nhất để phân biệt
- Họ sinh viên : dạng chu i
- Tên sinh viên : dạng chu i
- Điểm trung bình học tập : dạng số thực, có miền giá trị từ 0.0
đến 10.0
Hãy thực hiện các yêu cầu sau:
a. Khai báo cấu trúc cây nhị phân tìm kiếm để lưu thông tin
sinh viên theo mô tả ở trên.
b. Viết hàm thêm các sinh viên vào cây.

170
c. Viết hàm xóa các inh viên có điểm trung bình < 5.0 ra
khỏi cây.
d. Viết hàm hiển thị danh sách sinh viên khi duyệt cây theo thứ
tự trước.
7. Giả sử cho danh sách các từ khóa trong ngôn ngữ như au
struct, typedef, int, void, return, for, while, goto, else, continue,
break, main.
Hãy thực hiện các yêu cầu sau:
a. Khai báo cấu trúc cây nhị phân tìm kiếm để lưu thông tin các
từ khóa theo mô tả ở trên.
b. Viết hàm thêm các từ khóa trên vào cây
c. Viết hàm hiển thị danh sách các từ khóa lưu trong c y khi
duyệt cây theo thứ tự sau.
8. Giả sử cho các biểu thức số học như au
(A – B) – C
A – (B – C)
A / (B – (C – (D – (E – F))))
((A * (B + C)) / (D – (E + F))) * (G / (H / (I *J)))
Hãy thực hiện các yêu cầu sau:
a. Khai báo cấu trúc dữ liệu c y để lưu các biểu thức trên và
trong trường hợp t ng quát.
b. Giả sử các ký tự , B, , D, E, F, G, H, , J được thay thế là
các số, hãy viết hàm tính giá trị của các biểu thức đã cho và
trong trường hợp t ng quát.
9. Hãy vẽ cây nhị phân bao gồm các ký tự khi kết quả duyệt cây
theo thứ tự giữa có kết quả như au G, F, H, , D, L, , W, R,
Q, P, Z.

171
10. Hãy vẽ cây nhị phân bao gồm các ký tự khi kết quả duyệt cây
theo thứ tự trước có kết quả như au , D, F, G, H, , L, P, Q, R,
W, Z.
11. Hãy vẽ cây nhị phân bao gồm các ký tự khi kết quả duyệt cây
theo thứ tự sau có kết quả như au F, G, H, D, , L, P, Q, R Z,
W, K.
12. Cho một cây nhị phân T, hãy thực hiện các yêu cầu sau:
a. Viết hàm hiển thị danh sách các nút lá trên cây, nếu cây r ng
thì in ra giá trị NULL
b. Viết hàm tính chiều cao của cây, nếu cây r ng thì in ra giá trị
NULL
c. Viết hàm in ra các nút ở mức K trên cây, biết rằng K nằm
trong phạm vi chiều cao của cây, nếu cây r ng thì in ra giá trị
NULL
13. Xét cấu trúc dữ liệu au đ y trong ngôn ngữ C định nghĩa cây nhị
phân:
struct Tnode
{ int key; // dữ liệu của nút
struct Tnode *left;
struct Tnode *right;
};
typedef struct Tnode treeNode;
Hãy thực hiện các yêu cầu sau:
a. Viết hàm int countLeaft (treeNode *Root, int k) trả lại số
lượng lá của cây trỏ bởi con trỏ Root.
b. Viết hàm int OddSum (treeNode *Root) trả lại t ng các số lẻ
được lưu tại các nút của cây nhị phân với gốc được trỏ bởi
con trỏ Root.

172
c. Viết hàm int Sum (treeNode *Root) trả lại giá trị là t ng của
các giá trị số lưu tại các nút của cây nhị phân với gốc được
trỏ bởi con trỏ Root.
d. Viết hàm int EvenLeaf (treeNode *Root) trả lại số lượng nút
lá với trường dữ liệu key của nút là số chẵn của cây với gốc
được cho bởi con trỏ Root.
e. Viết hàm int countNodes (treeNode *Root, int k) trả lại số
nút có trường key lớn hơn k trong c y được trỏ bởi con trỏ
Root.
14. Xét cấu trúc dữ liệu biểu diễn cây nhị phân tìm kiếm:
struct TreeNode
{ int info; // dữ liệu của nút
struct TreeNode *left;
struct TreeNode *right;
};
Hãy thực hiện các yêu cầu sau:
a. Viết hàm int EvenMax (TreeNode *Root) nhận đầu vào Root
là con trỏ đến gốc của cây nhị phân tìm kiếm, trả lại giá trị
info chẵn lớn nhất trong các nút của cây nhị ph n đã cho.
b. Gọi T(n) là thời gian tính của hàm trong c u 1a), hãy đưa ra
đánh giá cho T(n), trong đó n là số lượng nút của cây nhị
ph n đầu vào.
15. Xét cấu trúc dữ liệu mô tả cây nhị phân sau:
struct TreeNode
{ int info; // dữ liệu của nút
TreeNode *left;
TreeNode *right;
};

173
Hãy viết hàm bool IsBST (Tree *Root) nhận đầu vào là con trỏ
Root đến gốc của cây, trả lại giá trị true khi và chỉ khi cây là cây
nhị phân cân bằng.
16. Xét cấu trúc dữ liệu mô tả cây nhị phân sau :
struct TreeNode
{ int info; // dữ liệu của nút
struct TreeNode *left;
struct TreeNode *right;
};
Hãy viết hàm bool IsLeft (TreeNode *Root, int key) nhận đầu
vào là con trỏ Root đến gốc của cây, trả lại giá trị true khi và chỉ
khi mọi nút của c y đều có trường info nhỏ hơn k y.

174
CHƯƠNG 6
BẢNG BĂM

6.1. BẢNG BĂM

6.1.1. Định nghĩa


Bảng b m (hash table) là phương pháp tham chiếu trực tiếp một
phần tử trong trong bảng lưu dữ liệu thông qua việc biến đ i số học
trên khóa của phần tử để có được địa chỉ tương ứng của phần tử đang
xét ở trong bảng.
Như thế, giả sử các khóa là những số nguyên từ 1 đến N thì có thể
lưu phần tử thứ i ở vị trí thứ i của bảng và có thể truy xuất trực tiếp
các phần tử này bằng giá trị khóa của phần tử. Điều đó cho thấy bảng
b m là một trong những phương pháp tìm kiếm hữu hiệu.
Về mặt toán học, bảng b m là một ánh xạ H từ tập các khóa K
vào tập các địa chỉ A: H: K → A
Trong bảng b m, thông thường tập các khóa lớn hơn rất nhiều so
với tập các địa chỉ bộ nhớ (chỉ số của bảng). Do đó, hàm H là một
hàm ánh xạ nhiều – một.
Bảng b m có thể x m như ự mở rộng của mảng thông thường.
Việc địa chỉ hóa trực tiếp trong mảng cho phép truy nhập đến phần tử
bất kỳ trong thời gian O(1). Mặc dù, trong tình huống xấu nhất thao
tác tìm kiếm đòi hỏi thời gian O(N) giống như trên danh sách liên kết,
nhưng trên thực tế bảng b m làm việc hiệu quả hơn nhiều.

6.1.2. Quy trình thực hiện u t ữ trong bảng băm


Quy trình thực hiện lưu trữ trong bảng b m được thực hiện qua
hai bước :
- Bước đầu tiên là xác định hàm H để biến đ i khóa cần tìm thành
địa chỉ trong bảng. Trường hợp lý tưởng nhất là những khóa khác
nhau thông qua hàm H sẽ cho ra những địa chỉ khác nhau. Tuy nhiên,

175
trong thực tế thì hai hoặc nhiều khóa khác nhau thông qua hàm H sẽ
có thể cho ra cùng một địa chỉ trong bảng.
- Bước tiếp theo là quá trình giải quyết xung đột (collision) cho
trường hợp những khóa khác nhau có cùng một địa chỉ trong bảng.
Một trong những phương pháp giải quyết đụng độ ph biến là dùng
danh sách liên kết, do sẽ tạo ra sự linh hoạt trong việc kết hợp các
khóa có cùng địa chỉ khi kết hợp danh sách liên kết.

6.1.3. Các thao tác trên bảng băm


Các thao tác chính trên bảng b m được xây dựng nhằm h trợ
cho thực hiện các thao tác trên dữ liệu được lưu trữ trong bảng b m.
 Thao tác khởi tạo: thao tác này khởi tạo bảng b m, cấp phát
vùng nhớ hay quy định số phần tử kích thước) của bảng b m.
 Thao tác kiểm tra r ng: mục đích kiểm tra bảng b m có r ng
hay không ?
 Thao tác lấy kích thước của bảng b m cho biết số phần tử hiện
có trong bảng b m.
 Thao tác tìm kiếm : tìm kiếm một phần tử trong bảng b m th o
khoá k được chỉ định trước đó.
 Thao tác thêm mới phần tử : thêm một phần tử vào bảng b m.
Sau khi thêm số phần tử hiện có của bảng b m t ng thêm một
đơn vị.
 Thao tác loại bỏ phần tử : loại bỏ một phần tử ra khỏi bảng b m
và số phần tử sẽ giảm đi một đơn vị.
 Thao tác sao chép: mục đích tạo một bảng b m mới từ một bảng
b m cũ đã có trước đó.
 Thao tác duyệt bảng b m: duyệt bảng b m th o thứ tự địa chỉ từ
nhỏ đến lớn.
Xét về tính hiệu quả, bảng b m là một phương pháp hiệu quả về
thời gian và vùng nhớ. Việc tìm kiếm một khóa bất kỳ với một lần
truy xuất vùng nhớ bằng cách cho khóa đó địa chỉ của vùng nhớ khi
176
không bị giới hạn về vùng nhớ. Cũng tương tự như thế, nếu không bị
giới hạn về thời gian tìm kiếm thì có thể sử dụng một vùng nhớ có
kích thước tối thiểu với phương pháp tìm kiếm tuyến tính.

6.2. HÀM BĂM


Bước đầu tiên trong sử dụng cấu trúc bảng b m là chọn hàm biến
đ i khóa để biến đ i các khóa thành các địa chỉ trong bảng. Hàm ánh
xạ này gọi là hàm b m (hash function).
Về mặt t ng quát, một hàm b m tốt là hàm có khả n ng ph n bố
đều trên miền giá trị của địa chỉ.
Giả sử gọi N là các phần tử được chứa trong bộ nhớ thường N là
số nguyên tố), hàm b m ẽ biến đ i các khóa thường là các số
nguyên hoặc là các chu i ký tự ngắn) thành số nguyên trong đoạn
[0..N – 1].
Giả sử các khóa là những số nguyên, hàm b m H(k) là:
H(k) = k mod N
với k là khóa và H(k) là hàm lấy số dư của k chia cho N

6.3. C C PHƯƠNG PHÁP GIẢI QUYẾ ĐỤNG ĐỘ

6.3.1. Ph ơng ph p nối kết

6.3.1.1. Phương pháp nối kết trực tiếp


Ý ưởng hương há
Có một câu hỏi đặt ra, đó là trong trường hợp các khóa khác nhau
có cùng một địa chỉ trong bảng thì phải lưu trữ như thế nào trong
bảng?. Một cách đơn giản là ứng với m i địa chỉ sẽ có một danh sách
liên kết chứa các phần tử có khóa khác nhau có cùng một địa chỉ. Như
vậy, phải sử dụng một danh sách gồm N phần tử chứa địa chỉ đầu của
danh sách liên kết. Đó là cách thực hiện của phương pháp nối kết trực
tiếp (direct chaining).

177
Hình 6.1. Mô tả quá trình thực hiện phương pháp nối kết trực tiếp
khi thêm các phần tử vào bảng

Cài đặ bảng băm dùng hương há nối kế rực iế


a. Khai báo cấu trúc bảng băm
#define M 100 // giả sử dùng bảng băm gồm 100 hần ử
typedef struct tagNODE
{
int key;
tagNODE *next
}NODE, *NODEPTR;
b. Khai báo mảng bucket chứa M phần tử
NODEPTR bucket[M];
c. Xây dựng hàm băm
Giả ử chọn hàm b m f k y)=k y % M.
int hashfunc (int key)
{
return (key % M);
}

178
d. Xây dựng hàm các thao tác trên bảng băm
- Hàm khởi ạo bảng băm
void initbuckets( )
{
int b;
for (b=0; b<M; b++)
bucket[b] = NULL;
}

- Hàm kiểm ra b cke b có bị rỗng không?


int emptybucket (int b)
{
return (bucket[b] ==NULL ?TRUE :FALSE);
}

- Hàm kiểm ra bảng băm có rỗng không?


int empty( )
{
int b;
for (b = 0;b<M;b++)
if (bucket[b] !=NULL)
return FALSE;
return TRUE;
}

179
- Hàm thêm hần ử có khóa k vào bảng băm
Giả ử các phần tử trên các buck t là có thứ tự, để thêm một phần
tử khóa k vào bảng b m trước tiên chúng ta xác định buck t ph hợp,
sau đó d ng hàm plac của danh ách liên kết để đặt phần tử vào vi trí
ph hợp trên buck t.
void insert(int k)
{
int b;
b= hashfunc(k)
place(b,k);
}

- Hàm xóa hần ử có khóa k rong bảng băm


Giả ử các phần tử trên các buck t là có thứ tự, để xóa một phần
tử khóa k trong bảng b m cần thực hiện
 Xác định buck t ph hợp
 Tìm phần tử để xóa trong buck t đã được xác định, nếu tìm
thấy phần tử cần xóa thì loại bỏ phần tử th o các ph p toán
tương tự loại bỏ một phần tử trong danh ách liên kết.
void remove ( int k)
{
int b;
NODEPTR q, p;
b = hashfunc(k);
p = hashbucket(k);
q=p;
while (p !=NULL && p->key !=k)
180
{ q=p;
p=p->next;
}
if (p == NULL)
printf("\n không có nút có khóa %d" ,k);
else if (p == bucket [b])
pop(b); // ác vụ o
else
delafter(q); // ác vụ xóa hần ử sa trong danh sách
liên kế
}

- Hàm ìm kiếm
Tìm kiếm một phần tử trong bảng b m, nếu không tìm thấy hàm
trả về giá trị NULL,nếu tìm thấy hàm trả về địa chỉ phần tử tìm thấy.
NODEPTR search(int k)
{
NODEPTR p;
int b;
b = hashfunc (k);
p = bucket[b];
while (k > p->key && p !=NULL)
p=p->next;
if (p == NULL | | k !=p->key) // không ìm hấy hần ử
return NULL;
181
else // ìm hấy hần ử
return p;
}
Nhận x t:
Bảng b m d ng phương pháp nối kết trực tiếp ẽ "b m N phần
tử vào bảng gồm M phần tử.
Để tốc độ thực hiện các ph p toán trên bảng hiệu quả, cần chọn
hàm b m ao cho b m đều N phần tử của bảng b m cho M phần tử,
lúc này trung bình m i buck t ẽ có N / M phần tử.
Nếu chọn M càng lớn thì tốc độ thực hiện các ph p toán trên bảng
b m càng nhanh. Tuy nhiên ẽ dùng nhiều bộ nhớ. Do vậy, cần điều
chỉnh M để dung hòa giữa tốc độ truy xuất và dung lượng bộ nhớ cần
ử dụng.
Nếu chọn M = N thì n ng xuất tương đương với truy xuất trên
mảng có bậc O 1)), tuy nhiên tốn nhiều bộ nhớ.
Nếu chọn M = N / k (với k =2,3,4,…) thì ít tốn bộ nhớ hơn k lần,
nhưng tốc độ lại chậm đi k lần.

6.3.1.2. Phương pháp nối kết hợp nhất


Ý ưởng hương há
T chức bảng b m của phương pháp nối kết hợp nhất (coalesced
chaining) là phương pháp nối kết mà m i phần tử có một vùng chứa
chỉ số của phần tử kế tiếp, khi có sự đụng độ xảy ra.
Giả sử các khóa có giá trị hàm b m và thứ tự thêm vào như au
Khóa: A B C D
Hash: 1 2 1 1
hi đó, khi thêm khóa vào bảng b m, vị trí H(A) = 1 là vị trí
trống nên khóa được thêm vào ngay vị trí 1.

182
Khi thêm khóa C vào bảng b m, vị trí H ) = 1 đang chứa
khóa A nên xảy ra đụng độ, do đó phải tìm kiếm vị trí trống đầu
tiên từ phía dưới lên. Vị trí trống đầu tiên là N, ta thêm khóa C vào
vị trí N.
Khi thêm vào khóa D, vì vị trí H D) = 1 đã chứa khóa , ta đi
theo con trỏ Next của vị trí này để đến vị trí M, nhưng vị trí M đã
chứa khóa C và con trỏ Next của vị trí này bằng 0 nên ta thêm khóa
D vào vị trí M – 1 (vị trí trống đầu tiên từ phía dưới bảng trở lên).

Hình 6.2. Mô tả quá trình thực hiện phương pháp nối kết hợp nhất
khi thêm các phần tử vào bảng

Cài đặ bảng băm dùng hương há nối kế hợ nhấ


a. Khai báo cấu trúc bảng băm
#define NULLKEY –1
#define M 100 // giả sử dùng bảng băm gồm 100 hần ử
struct node
{
int key; // khóa của nú rên bảng băm

183
int Next; // con rỏ chỉ nú kế iế khi có x ng độ
};
b. Khai báo bảng băm
struct node hashtable[M];
int avail; // biến chỉ nú rống ở c ối bảng, cậ nhậ khi có
x ng độ
c. Xây dựng hàm băm
Giả ử chúng ta chọn hàm b m dạng modulo f k y)=k y % 10.
int hashfunc(int key)
{
return (key % 10);
}
d. Xây dựng hàm các thao tác trên bảng băm
- Hàm khởi ạo bảng băm
Hàm này khởi tạo bảng b m bằng cách gán tất cả các phần tử trên
bảng có trường k y là Null, con trỏ N xt có giá trị là –1 cho tất cả các
phần tử trên bảng b m.
Gán biến toàn cục avail=M-1 là phần tử cuối danh ách chu n bị
cấp phát nếu xảy ra xung đột.
void initialize()
{
for (int i = 0; i<M; i++)
{ hashtable[i].key = NULLKEY;
hashtable[i].key = -1;
}
avail = M-1;

184
}
-Hàm kiểm ra bảng băm rỗng ?
int empty ()
{
int i;
for (i = 0; i< M; i++)
if (hashtable[i].key !=NULLKEY)
return FALSE;
return TRUE;
}

- Hàm ìm kiếm rong bảng băm


Hàm tìm kiếm th o phương pháp tuyến tính, nếu không tìm thấy
hàm tìm kiếm trả về trị M, nếu tìm thấy hàm này trả về địa chỉ tìm
thấy.
int search(int k)
{
int i;
i=hashfunc(k);
while (k !=hashtable[i].key && i !=-1)
i=hashtable[i].next;
if (k== hashtable[i]key)
return i; // ìm hấy hần ử
return M; //không ìm hấy hần ử
}

185
- Hàm lấy hần ử rống rong bảng băm
Hàm này chọn phần tử còn trống phía cuối bảng b m để cấp phát
khi xảy ra xung đột.
int getempty()
{
while (hashtable[avail].key !=NULLKEY)
avail - -;
return avail;
}

- Hàm hêm hần ử mới vào bảng băm


Hàm thực hiện thêm phần tử có khóa k vào bảng b m.
int insert(int k)
{
int i;
int j;
i = search(k);
if (i !=M)
{ printf " hóa %d bị tr ng, không thêm nút này được",k);
return i;
}
i=hashfunc(k);
while(hashtable[i]next >=0)
i=hashtable[i].next;
186
if (hashtable[i].key == NULLKEY)
j = i;
else
{ j = getempty();
if (j < 0)
{ printf "Bảng b m đầy,không thêm nút có khóa
%d được",k);
return j;
}
else
hashtable[i].next = j;
}
hashtable[j].key = k;
return j;
}
Nhận x t
Thực chất cấu trúc bảng b m này chỉ tối ưu khi b m đều, nghĩa là
m i danh ách liên kết chứa một vài phần tử bị xung đột, tốc độ truy
xuất lúc này lả O 1). Trường hợp xấu nhất là b m không đều, vì hình
thành một danh ách có N phần tử nên tốc độ truy xuất lúc này là
O(N).

6.3.2. Ph ơng ph p địa chỉ mở


Ý ưởng hương há
Phương pháp nối kết trực tiếp có một nhược điểm là phải duy trì
các danh sách liên kết và m i phần tử phải có thêm vùng liên kết địa
chỉ đến phần tử kế tiếp trong danh sách. Có một cách khác để giải

187
quyết đụng độ là khi có đụng độ xảy ra thì ta sẽ tìm đến vị trí kế tiếp
nào đó ở trong bảng cho đến khi nào tìm thấy phần tử mong muốn
hoặc vị trí kế tiếp là vị trí trống.
Trong phương pháp địa chỉ mở (open addressing), tất cả các
phần tử đều được cất giữ vào bảng. Do đó m i ô của bảng hoặc là
chứa khóa hoặc là NULL. tưởng của phương pháp này như au
 Để thực hiện thêm phần tử, nếu ô địa chỉ sau khi thực hiện
hàm b m bị chiếm giữ, ta sẽ tiến hành dò thử các ô còn lại của
bảng cho đến khi tìm được ô r ng để nạp phần tử vào. Thay vì
tìm tuần tự theo thứ tự 0, 1, …, N – 1 (thời gian Θ N)), dãy
các vị trí sẽ phụ thuộc vào giá trị khóa của phần tử được b
ung. Để xác định các ô dò thử, ta sẽ mở rộng định nghĩa hàm
b m.
 Khi tìm kiếm trên bảng b m, ta sẽ thực hiện giống như cách dò
thử như khi thực hiện chèn phần tử vào bảng.
+ Nếu con trỏ trả về là NULL thì phần tử cần tìm không có
trong bảng.
+ Ngược lại là đã có phần tử trong bảng.
 Mở rộng định nghĩa hàm b m như au
h: U × {0, 1, …, N – 1} → {0, 1, …, N – 1}
 Trong phương pháp địa chỉ mở ta đòi hỏi, với m i khóa k, dãy
dò thử <h k, 0), h k, 1), …, h k, N – 1)> phải là một hoán vị
của < 0, 1, …, N – 1>, do đó m i vị trí trong bảng sẽ được xét
như là một ô để chứa khóa mới khi ta tiến hành b sung vào
bảng.
Cài đặ bảng băm dùng hương há địa chỉ mở
- Hàm b sung khóa k vào bảng băm được thực hiện qua các bước :
- Bước 1 : Khởi tạo i = 0
- Bước 2 : Thực hiện lặp cho đến khi i = N

188
Gán j = h(k, i)
Nếu (T[j] = NULL)
{
Gán T[j] = k
Trả về j
}
Ngược lại :
Gán i = i + 1

- Hàm tìm kiếm khóa k trong bảng băm được thực hiện qua các bước:
- Bước 1 : Khởi gán i = 0
- Bước 2 : Thực hiện lặp cho đến khi T[j] = NULL hoặc i = N
Gán j = h(k, i)
Nếu (T[j] = j)
Trả về j
Gán i = i + 1
Trả về NULL

- Hàm dò thử trong bảng băm :


Có ba k thuật dò thử thường được sử dụng:
 Dò tuyến tính (Linear Probing)
 Dò toàn phương Quadratic Probing)
 B m k p Doubl Hashing).
Các k thuật này đều đảm bảo <h k, 1), h k, 2), …, h k, m)> là
hoán vị của <0, 1, …, N – 1> đối với m i khóa k.

189
6.3.2.1 Dò tuyến tính
Ý ưởng hương há
Phương pháp địa chỉ mở đơn giản nhất có thể thực hiện là dò
thử tuyến tính. hi đó, một khi có đụng độ xảy ra ta tìm đến vị trí
kế tiếp (chỉ số t ng lên 1).
Giả sử cho hàm b m h’ U → {0, 1, …, N – 1}, phương pháp dò
tuyến tính sử dụng hàm b m mở rộng như sau:
h(k,i) = h’ k) + i) mod N , với i = 0, 1, …, N – 1.
Như thế giả sử cho khóa k, ô đầu tiên trong bảng b m được dò
thử tuyến tính là T[h’ k)]. Sau đó sẽ dò thử ô T[h’ k) + 1], T[h’ k) +
2],… cho đến ô T[N – 1].
Cài đặ bảng băm dùng hương há dò yến tính
a. Khai báo cấu trúc bảng băm
#define NULLKEY –1
#define M 100 // giả sử dùng bảng băm gồm 100 hần ử
struct node
{
int key;
};
b. Khai báo bảng băm
struct node hashtable[M]; // khai báo bảng băm có M hần ử
int NODEPTR; // biến oàn cục chỉ số nú hiện có rên bảng băm
c. Xây dựng hàm băm
Giả ử chúng ta chọn hàm b m: f(key)=key %10.
int hashfunc(int key)
{
return(key% 10);
190
}
d. Xây dựng hàm các thao tác trên bảng băm
- Hàm khởi ạo bảng băm
Gán tất cả các phần tử trên bảng có trường k y là NULL. Gán
biến toàn cục N=0.
void initialize( )
{
int i;
for (i=0;i<M;i++)
hashtable[i].key=NULLKEY;
N=0;
}
- Hàm kiểm ra bảng băm rỗng ?
int empty( )
{
return(N==0 ? TRUE : FALSE);
}

- Hàm kiểm ra bảng băm có đầy không ?


int full( )
{
return (N==M-1 ? TRUE : FALSE);
}

- Hàm ìm kiếm
int search(int k)
191
{
int i;
i=hashfunc(k);
while (hashtable[i].key!=k
&& hashtable[i].key !=NULKEY)
{ i=i+1;
if (i>=M)
i=i-M;
}
if (hashtable[i].key==k)
return i;
else
return M;
}

- Hàm hêm hần ử có khoá k vào bảng băm


int insert(int k)
{
int i, j;
if (full( ))
{ printf "Bảng b m đầy, không thể thêm nút có khóa
%d ",k);
return;
}
i=hashfunc(k);
while (hashtable[i].key !=NULLKEY)
192
{ i ++;
if (i >M)
i= i-M;
}
hashtable[i].key=k;
N=N+1;
return i;
}

6.3.2.2. Dò toàn phương


Ý ưởng hương há
Trong phương pháp dò thử tuyến tính ở trên, ta thấy rằng để tìm
kiếm một phần tử có khóa k1 thì bắt đầu tìm kiếm từ vị trí h(k1) của
bảng b m. Nếu phần tử ở vị trí này không phải là phần tử cần tìm thì
ta tìm đến vị trí kế tiếp và cứ như thế cho đến khi nào tìm thấy phần tử
này hoặc cho đến khi gặp vị trí trống p nào đó.
Trong trường hợp không tìm thấy và cần thêm phần tử mới vào
bảng b m thì ta lưu phần tử mới tại vị trí p. Nhưng au đó, ta không
thể thêm một phần tử có khóa là k2 tại vị trí p nữa, mặc d hàm b m
h(k2) có giá trị là p và ta phải tìm đến vị trí trống kế tiếp để thêm phần
tử khóa k2 vào trong bảng b m. Như vậy, thời gian tìm kiếm một vị trí
trống kế tiếp sẽ rất lâu khi bảng gần đầy.
Sử dụng hàm b m mở rộng như au:
H k, i) = H’ k) + c1i + c2i2) mod N
trong đó H’ là hàm b m ban đầu,
c1 và c2 ≠ 0 là các hằng số, i = 0, 1, …, N – 1.
Cài đặ bảng băm dùng hương há dò oàn hương
a. Khai báo cấu trúc bảng băm
#define NULLKEY –1
193
#define M 100 // giả sử dùng bảng băm gồm 100 hần ử
struct node
{
int key;
};
b. Khai báo bảng băm
struct node hashtable[M]; //khai báo bảng băm có M hần ử
int NODEPTR; // biến oàn cục chỉ số nú hiện có rên bảng băm
c. Xây dựng hàm băm
Giả ử chúng ta chọn hàm b m: f(key)=key %10.
int hashfunc(int key)
{
return(key% 10);
}
d. Xây dựng hàm các thao tác trên bảng băm
- Hàm khởi ạo bảng băm
Gán tất cả các phần tử trên bảng có trường k y là NULL.
Gán biến toàn cục N=0.
void initialize( )
{
int i;
for (i=0;i<M;i++)
hashtable[i].key=NULLKEY;
N=0;
}

194
- Hàm kiểm ra bảng băm có rống không ?
int empty( )
{
return(N==0 ? TRUE : FALSE);
}

- Hàm kiểm ra bảng băm có đầy không ?


int full( )
{
return (N==M-1 ? TRUE : FALSE);
}

- Hàm ìm kiếm
int search(int k)
{
int i, d;
i = hashfuns(k);
d = 1;
while (hashtable[i].key!=k
&&hashtable[i].key !=NULLKEY)
{ i = (i+d) % M; //băm lại
d = d+2;
}
hashtable[i].key =k;
N = N+1;

195
return i;
}

6.3.2.3. Hàm b m k p
Ý ưởng hương há
B m k p (double hashing) là phương pháp tốt nhất có thể sử dụng
trong phương pháp địa chỉ mở ,vì dãy dò thử tạo được bởi phương
pháp này có tính chất giống như một hoán vị ngẫu nhiên.
Trong phương pháp hàm b m k p, ta ử dụng hàm b m mở rộng:
h(k, i) = (h1(k) + ih2(k)) mod N,
trong đó h1, h2 là các hàm b m b trợ.
Cài đặ bảng băm dùng hương há băm ké
a.Khai báo cấu trúc bảng băm
#define NULLKEY –1
#define M 100 // giả sử dùng bảng băm gồm 100 hần ử
struct node
{
int key;
};
b.Khai báo bảng băm
struct node hashtable[M]; //khai báo bảng băm có M hần ử
int NODEPTR; // biến oàn cục chỉ số nú hiện có rên bảng băm
c.Xây dựng hàm băm
Giả ử ta chọn hai 2 hàm b m :
f1(key)=key % M
f2(key) =M - 2 - key % (M-2).

196
int hashfunc(int key)
{
return(key%M);
}
int hashfunc2(int key)
{
return(M-2 -key%(M-2));
}
d. Xây dựng hàm các thao tác trên bảng băm
- Hàm khởi ạo bảng băm
Gán tất cả các phần tử trên bảng có trường k y là NULL.
Gán biến toàn cục N = 0.
void initialize( )
{
int i;
for (i=0;i<M;i++)
hashtable[i].key=NULLKEY;
N=0;
}

- Hàm kiểm ra bảng băm có rống không ?


int empty( )
{
return(N==0 ? TRUE : FALSE);
}

197
- Hàm kiểm ra bảng băm có đầy không ?
int full( )
{
return (N==M-1 ? TRUE : FALSE);
}

- Hàm ìm kiếm
int search(int k)
{
int i, j ;
i = hashfunc (k);
j = hashfunc2 (k);
while (hashtable[i].key!=k
&& hashtable[i].key ! = NULLKEY)
i = (i+j) % M ; //băm lại
if (hashtable [i]).key == k) // ìm hấy k
return i ;
else // không ìm hấy k
return M ;
}

- Hàm hêm hần ử có khoá k vào bảng băm


int insert(int k)
{ int i, j;
if (full())

198
{ printf "Bảng b m đầy") ;
return M ;
}
if (search (k) < M)
{ printf ("Đã có khóa này trong bảng b m") ;
return M;
}
i = hashfunc(k); // hàm băm 1
j = hashfunc2 (k); // hàm băm 2
while (hashtable [i].key ! = NULLEY)
i = (i + j) % M; // băm lại
hashtable [i].key = k ;
N = N+1;
return i ;
}

Nhận x t :
Nên chọn ố địa chỉ M là ố nguyên tố. Bảng b m đầy khi N =
M-1, ta nên dành ít nhất một phần tử trống trên bảng b m.
Bảng b m được cài đặt th o cấu trúc này linh hoạt hơn bảng b m
d ng phương pháp dò tuyến tính và phương pháp dò bậc hai, do d ng
hai hàm b m khác nhau nên việc rải phần tử mang tính ngẫu nhiên
hơn, nếu bảng b m chưa đầy tốc độ truy xuất có bậc O 1).
Trường hợp xấu nhất là bảng b m gần đầy, tốc độ truy xuất chậm
do thực hiện nhiều lần o ánh.

199
6.3.3. Ph ơng ph p nhân
Ý ưởng hương há
Phương pháp nh n (multiplication method) để xây dựng hàm
b m được tiến hành th o hai bước.
 Đầu tiên ta nhân k với một hằng số A, 0 < A < 1 và lấy phần
thập phân của kA.
 Sau đó nh n giá trị này với m rồi lấy phần nguyên của kết
quả.
Cài đặt
 Chọn hằng số A, 0 < A < 1
 Hàm h(k) = M(kA - |kA|)
 Nên chọn giá trị M = 2p.
 Nên chọn giá trị A không quá gần với 0 hoặc 1.

6.4. HẠN CHẾ CỦA BẢNG BĂM


Bên cạnh các ưu điểm của bảng b m đã được trình bày trong
các phần trên, bảng b m có 2 hạn chế cần quan tâm trong quá trình sử
dụng bảng b m.
Việc quan t m đến các hạn chế này giúp cho việc sử dụng
bảng b m trở nên hiệu quả hơn.
Vấn đề kích hước bảng băm
Nhận thấy rằng, kích thước bảng b m thường là cố định và
không thể điều chỉnh theo yêu cầu thực tế trong khi đang ử dụng. Do
đó, bằng cách nào đó, trước khi sử dụng ta cần phải ước lượng trước
số phần tử cần được sử dụng để tránh sử dụng lãng phí vùng nhớ. Tuy
nhiên, ngay cả trong trường hợp số phần tử cần lưu trữ là được biết
trước thì để có được hiệu quả cao thì kích thước của bảng b m đôi khi
cũng thường phải lớn.

200
Vấn đề thực hiện thao tác trong bảng băm
Việc thực hiện các thao tác trên bảng b m, ví như thao tác loại
bỏ hoặc thao tác thêm một phần tử trên bảng b m đôi khi lại trở nên
phức tạp với một số phương pháp đã trình bày ở trên, do bản chất của
vấn đề nằm ở ch là thao tác có dễ dàng hoặc thuận lợi hay không là
nằm ở cách thức ta t chức cấu trúc lưu trữ bảng b m khi tiến hành sử
dụng.
Do đó, việc xem xét một cách thức t chức dữ liệu nào đó nhằm
mang đến sự dễ dàng, thuận lợi hơn là một yếu tố cũng cần xem xét.
Cấu trúc cây có thể mang lại hiệu quả hơn và có thể phù hợp hơn
trong bối cảnh khối lượng dữ liệu cần lưu trữ trong bảng b m là lớn và
ta chưa biết trước được t ng khối lượng dữ liệu cần lưu trữ này.

BÀI TẬP CHƯƠNG 6


1. Hãy trình bày ưu điểm và hạn chế của cấu trúc bảng b m và cho
ví dụ minh họa cụ thể ?
2. Cho bảng kích thước 11 ô và tập khóa K = {7, 20, 16, 24, 12,
40, 15}, ta cần nạp các giá trị khóa K vào bảng A sử dụng hàm
b m H k) = k % 11. Hãy vẽ bảng A sau khi tất cả các giá trị khóa
trong tập được lưu trữ trong bảng A, sử dụng k thuật danh
sách liên kết để xử lý xung đột.
3. Cho bảng kích thước 11 ô và tập khóa K = {30, 10, 56, 14, 22,
60, 15}, ta cần nạp các giá trị khóa K vào bảng A sử dụng hàm
b m H k) = k % 7. Hãy vẽ bảng A sau khi tất cả các giá trị khóa
trong tập được lưu trữ vào bảng A, sử dụng k thuật dò tuyến
tính để xử lý xung đột.
4. Cho bảng kích thước 11 ô và tập khóa K = {23, 12, 65, 27, 8,
50, 58}, ta cần nạp các giá trị khóa K vào bảng A sử dụng hàm
b m H k) = k % 10. Hãy vẽ bảng A sau khi tất cả các giá trị

201
khóa trong tập được lưu trữ vào bảng A, sử dụng k thuật dò
toàn phương để xử lý xung đột.
5. Cho bảng kích thước 11 ô và tập khóa K = {7, 20, 16, 24, 12,
40, 15}, ta cần nạp các giá trị khóa K vào bảng A sử dụng hàm
b m H k) = k % 11. Hãy vẽ bảng A sau khi tất cả các giá trị khóa
trong tập được lưu trữ vào bảng A, sử dụng k thuật b m k p
để xử lý xung đột, với hàm b m k p thứ 2 tự định nghĩa.
6. Viết chương trình minh hoạ bảng b m d ng phương pháp nối kết
trong các trường hợp sau :
a. Dữ liệu lưu trữ là số nguyên (khoá tìm kiếm là số
nguyên).
b. Dữ liệu lưu trữ là thông tin học sinh, bao gồm: họ tên học
sinh, lớp, tên trường (khoá tìm kiếm là tên lớp).
7. Viết chương trình minh hoạ bảng b m d ng phương pháp dò
tuyến tính trong các trường hợp sau:
a. Dữ liệu lưu trữ là số nguyên (khoá tìm kiếm là số
nguyên).
b. Dữ liệu lưu trữ là thông tin học sinh, bao gồm: họ tên học
sinh, lớp, tên trường (khoá tìm kiếm là tên lớp).
8. Viết chương trình minh hoạ bảng b m d ng phương pháp dò bậc
hai trong các trường hợp sau:
a. Dữ liệu lưu trữ là số nguyên (khoá tìm kiếm là số nguyên).
b. Dữ liệu lưu trữ là thông tin học sinh, bao gồm: họ tên học
sinh, lớp, tên trường (khoá tìm kiếm là tên lớp).
9. Viết chương trình tạo ra một Từ điển với các thao tác sử dụng
bảng b m với các chức n ng như au, biết rằng dữ liệu từ điển
lưu trên tập tin :
a. Thêm một từ mới: thêm vào từ và nghĩa của từ
b. Tra từ: nhập vào từ và cho biết nghĩa của từ
202
c. Cập nhật từ : thay đ i từ hoặc nghĩa của từ
d. Xoá từ : xóa từ và và nghĩa của từ
10. Giả sử cho một tập tin lưu thông tin tên người dùng (username)
và mật kh u (password) của các người d ng, danh ách người
dùng gồm 1000 người.
Hãy thực hiện các yêu cầu sau:
a. Tạo file chứa 1000 người dùng với thông tin tên người
dùng và mật kh u cho từng người.
b. Viết hàm cho phép nhập vào tên người dùng và mật kh u,
dùng bảng b m để kiểm tra tính hợp lệ của tên người
dùng và mật kh u.

203
BÀI TẬP TỔNG HỢP
1. Hãy trình bày và o ánh ưu điểm và hạn chế của các cấu trúc dữ
liệu đã học trong môn học.
2. Việc lưu trữ số nguyên có giá trị tuyệt đối rất lớn, là một yêu cầu
thường gặp khi giải quyết các bài toán trong máy tính.
Hãy thực hiện các yêu cầu sau:
a. Đề xuất cấu trúc dữ liệu thích hợp để lưu trữ các số
nguyên có giá trị tuyệt đối rất lớn trong bộ nhớ trong của
máy tính.
b. Với cấu trúc dữ liệu bạn đề xuất, hãy trình bày thuật toán
và cài đặt các hàm để thực hiện các thao tác cộng, trừ,
nhân, chia nguyên, chia lấy số dư và o ánh về giá trị các
số nguyên này.
3. Giả sử cho các tập tin lưu các v n bản, m i tập tin bao gồm nhiều
dòng, m i dòng trong file có chiều dài không quá 127 ký tự.
Hãy thực hiện các yêu cầu sau:
a. Đề xuất cấu trúc dữ liệu thích hợp để lưu trữ nội dung
v n bản trong bộ nhớ trong của máy tính.
b. Trình bày thuật toán và viết hàm cài đặt tính tần suất xuất
hiện của các từ trong các tập tin v n bản.
c. Trình bày thuật toán và viết hàm cho biết trong m i v n
bản có bao nhiều từ, từ nào xuất hiện nhiều nhất trong
v n bản, trong tập v n bản.
d. Viết hàm thực hiện hiển thị nội dung tập tin lên màn hình,
kết hợp phím Pag Up Pag Down để cuộn lên / cuộn
xuống màn hình trong trường hợp nội dung trong tập tin
v n bản nhiều hơn phạm vi một màn hình
4. Ma trận thưa là ma trận có kích thước lớn (số hàng và số cột lớn),
nhưng trong ma trận lại có rất ít các ô có lưu giá trị, đa phần các ô
là trống hoặc lưu giá trị là 0 (số không).

204
Hãy thực hiện các yêu cầu sau:
a. Hãy đề xuất cấu trúc dữ liệu thích hợp để lưu trữ ma trận
thưa trong bộ nhớ trong của máy tính sao cho hiệu quả
nhất về không gian lưu trữ cần thiết.
b. Hãy trình bày thuật toán và viết hàm thực hiện các thao
tác cộng, trừ, nhân hai ma trận thưa ao cho hiệu quả nhất
về thời gian tính toán.
5. Ngày nay, người ta có thể sử dụng máy tính để xây dựng chương
trình lưu thông tin gia phả của một dòng họ nhằm tiện lợi trong
quản lý, tra cứu thông tin, đồng thời giúp việc thực hiện các thao
tác nhanh chóng.
Trên cơ ở đó, hãy x y dựng một chương trình quản lý gia phả,
biết rằng gia phả lưu thông tin về từng dòng họ mà m i cặp vợ
chồng trong đó có không quá 4 người con, với các yêu cầu sau :
a. Đề xuất cấu trúc dữ liệu thích hợp để lưu trữ gia phả của
một dòng họ nào đó trong bộ nhớ trong của máy tính.
b. Trình bày thuật toán và viết hàm cài đặt thao tác thêm
một người vào gia phả.
c. Trình bày thuật toán và viết hàm cài đặt thao tác tìm kiếm
một người X nào đó có trong gia phả hay không.
d. Trình bày thuật toán và viết hàm cài đặt thao tác xóa một
người Y nào đó trong gia phả.
e. Trình bày thuật toán và viết hàm cài đặt kiểm tra hai
người A và B có quan hệ như thế nào, ví dụ như quan hệ
: bố - con, anh – em, mẹ - con, ông – cháu, anh em
họ,…và A và B ai là người ở thứ bậc cao hơn trong gia
phả.
f. Viết hàm thực hiện hiển thị toàn bộ gia phả lên màn hình,
kết hợp phím Pag Up Pag Down để cuộn lên / cuộn
xuống màn hình trong trường hợp nội dung cần hiển thị
nhiều hơn phạm vi một màn hình.

205
6. Hãy đề xuất cấu trúc dữ liệu và xây dựng chương trình quản lý
một Từ điển Anh – Việt với đầy đủ các chức n ng của một
từ điển.
7. Hãy đề xuất cấu trúc dữ liệu và xây dựng chương trình oạn thảo
v n bản đơn giản với các chức n ng cho ph p oạn thảo, lưu trữ,
x m và in v n bản.
8. Giả sử cho một chu i ký tự có chiều dài 1 triệu ký tự. Hãy thực
hiện các yêu cầu sau:
a. Đề xuất cấu trúc dữ liệu để lưu trữ chu i ký tự này
trong bộ nhớ trong của máy tính sao cho sử dụng ít bộ
nhớ nhất có thể.
b. Trình bày thuật toán và viết hàm cài đặt kiểm tra xem ký
tự nào trong chu i ký tự nào xuất hiện nhiều nhất, ít nhất
với thời gian thực hiện nhanh nhất có thể.
9. Giả sử cho một từ điển bao gồm 10.000 từ và 100 tập tin v n bản,
biết rằng m i tập tin v n bản có tối đa 1.000 từ. Người ta có nhu
cầu kiểm tra xem các từ trong v n bản có được viết đúng chính tả
không bằng cách so sánh lần lượt từng từ trong v n bản với từ
điển. Nếu từ trong v n bản có trong từ điển thì từ đó đã được viết
đúng chính tả, ngược lại là không đúng chính tả.
Hãy thực hiện các yêu cầu sau:
a. Đề xuất giải pháp để thực hiện yêu cầu trên với thời gian
thực hiện nhanh nhất có thể.
b. Nếu số lượng tập tin v n bản t ng lên thành 50.000 thì
thời gian thực hiện trong giải pháp của bạn ở câu a có
t ng tuyến tính theo không? Giải pháp trong câu a có còn
khả thi không?
10. M i loại lịch hiện nay có ngày và tháng nhuận khác nhau.
- Với dương lịch, chu kỳ Trái đất quay quanh Mặt trời là 365 +
1 4 ngày. Th o quy ước hiện nay thì m i n m chỉ có 365 ngày,

206
nên n m dương lịch ẽ chênh với thời gian thực là 1 4 ngày.
Điều này có nghĩa au 4 n m thì dương lịch ẽ dư một ngày và
ẽ có một n m nhuận một ngày. N m nhuận này th o quy ước
rơi vào tháng hai tức là tháng có 29 ngày).
- Với m lịch có 354 ngày và nếu o ánh với dương lịch thì m
lịch ngắn hơn 11 ngày. Như vậy, cứ ba n m, m lịch lại ngắn
hơn dương lịch 33 ngày, tức là ba n m m lịch ẽ nhuận một
tháng chứ không nhuận một ngày như dương lịch.
- Muốn tính n m m lịch nào đó có tháng nhuận hay không chỉ
cần làm ph p toán đơn giản là lấy n m dương lịch chia cho 19
nếu chia hết hoặc có các ố dư 3, 6, 9, 11, 14, 17 thì chắc chắn
n m đó là n m nhuận.
- Giả ử cho a và b là 2 n m dương lịch, a ≤ b, c u hỏi đặt ra là
làm ao biết trong các n m từ a đến b kể cả a và b) có bao
nhiêu n m nhuận th o m lịch?
Hãy trình bày thuật toán và cài đặt các hàm liên quan để giải
quyết vấn đề nêu trên.

207
TÀI LIỆU THAM KHẢO

[1] Peter Brass trong Advanced Data Structures, Cambridge University


Press, 2008
[2] Donald Knuth trong The Art of Computer Programming, 1997
[3] Aho, A. V. , J. E. Hopcroft, J. D. Ullman. "Data Structure and
Algorihtms", Addison–Wesley, 1983
[4] Michel T. Goodrich, Rob rto Tama ia, David Mount, Data
Structur and lgorithm in ++ , Weley International Edition,
2004.
[5] Đ Xuân Lôi, Cấu trúc dữ liệu và giải thuật , Nhà xuất bản Khoa học
và K thuật, Hà Nội, 1995.
[6] Nguyễn Trung Trực, "Cấu trúc dữ liệu", BK TP HCM, 1990.
[7] Dương nh Đức, Trần Hạnh Nhi, Giáo trình Cấu trúc dữ liệu và Giải
thuật , NXB ĐHQG.
[8] Nguyễn Đình Tê, Hoàng Đức Hải, Giáo trình lý thuyết và bài tập
ngôn ngữ , NXB Giáo dục, 1998.
[9] Lê Xuân Trường, Giáo trình cấu trúc dữ liệu bằng ngôn ngữ ++ ,
NXB thống kê, 1999.
[10] Nguyễn Thanh Thủy, Nguyễn Quang Huy, Bài tập lập trình ngôn
ngữ , NXB hoa học k thuật, 1999.
[11] Ngô Trung Việt, Ngôn ngữ lập trình C và C++ Bài giảng- Bài tập –
Lời giải mẫu , NXB Giao thông vận tải, 2000.

208

You might also like