Tải bản đầy đủ (.pdf) (116 trang)

Tài liệu hỗ trợ môn cấu trúc dữ liệu 2

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


Trương Hải Bằng-Cấu trúc dữ liệu 2

Trương Hải Bằng-Cấu trúc dữ liệu 2









Tµi liÖu tham kh¶o hç trî m«n häc
CÊu tróc d÷ liÖu 2





















mục lục

Chơng 1. Sắp xếp ngoại 4
1.1. Thao tác trên tệp bằng ngôn ngữ lập trình C++ 4
1.1.1. Định nghĩa tệp 4
1.1.2. Các kiểu truy cập tệp: nhị phân và văn bản 5
1.1.3. Các hàm thao tác trên tệp 6
1.2. Phơng pháp trộn run có độ dài cố định 7
1.2.1. Mô tả thuật toán 7
1.2.2. Cài đặt chơng trình 9
1.3. Phơng pháp trộn run tự nhiên 14
1.3.1. Mô tả thuật toán 14
1.3.2. Cài đặt chơng trình 15
1.4. Phơng pháp trộn đa lối cân bằng (Balanced multiway merging) 20
1.4.1. Mô tả thuật toán 20
1.4.2. Cài đặt chơng trình 21
1.5. Phơng pháp trộn đa pha (Polyphase merge) 28
Chơng 2. Bảng băm (Hash table) 29
2.1. Mở đầu 29
2.2. Các phơng pháp tránh đụng độ 30
2.2.1. Dùng danh sách liên kết 30
2.2.2. Dùng danh sách kề ngoài 31
2.2.3. Phơng pháp dò tuyến tính (linear probing method) 31
2.2.4. Phơng pháp dò bậc hai (quadratic probing method) 32
2.3. Cài đặt bảng băm 32
2.3.1. Cài đặt bảng băm dùng danh sách liên kết ngoài 32

2.3.2. Cài đặt bảng băm dùng danh sách kề ngoài 37
2.3.3. Cài đặt bảng băm dùng liên kết trong 43
2.3.4. Vài nhận xét về bảng băm 45
Chơng 3. Cây đỏ đen 46
3.1. Mở đầu 46
3.2. Cây nhị phân tìm kiếm 47
3.3. Cây 2-3-4 48
3.3.1. Định nghĩa cây 2-3-4 48
3.3.2. Tìm kiếm trên cây 2-3-4 49
3.3.3. Thêm khóa vào cây 2-3-4 49
3.3.4. Loại bỏ khóa trên cây 2-3-4 50
3.3.5. Phân tích các thuật toán trên cây 2-3-4 52
3.4. Cây đỏ đen (Red-black tree) 52

Trng Hi Bng-Cu trỳc d liu 2


3.4.1. Định nghĩa 52
3.4.2. Sự tơng đơng giữa cây đỏ đen và cây 2-3-4 53
3.5. Cây cân bằng chiều cao (Height balanced tree) 53
3.5.1. Thao tác xoay cây nhị phân 53
3.5.2. Chỉ số cân bằng (balance factor) của một nút trên cây AVL 54
3.5.3. Cân bằng lại cây khi thêm nút 54
3.5.4. Xoá nút trên cây AVL 59
3.5.5. Vài nhận xét về cây AVL 62
3.5.6. Cài đặt cây AVL 63
3.5.7. Cài đặt cây AVL trên bộ nhớ ngoài 73
Chơng 4. B - cây và bộ nhớ ngoài 74
4.1. Mở đầu 74
4.2. B - cây 74

4.2.1. Cây tìm kiếm nhiều nhánh (Multiway Search Tree) 74
4.2.2. Định nghĩa B - cây 75
4.2.3. Tìm kiếm trên B - cây 75
4.2.4. Thêm khóa vào B - cây 75
4.2.5. Loại bỏ khóa trên B - cây 76
4.2.6. Phân tích các thuật toán trên B - cây 78
4.3. Cài đặt B - cây 78
4.4. B - cây và bộ nhớ ngoài 87
Câu hỏi và bài tập 89
Chơng 1. Sắp xếp ngoại 89
Chơng 2. Bảng băm 89
Chơng 3. Cây đỏ đen 90
Chơng 4. B-cây 90
Các bài tập lớn dành cho sinh viên khá giỏi 90
Tài liệu tham khảo 92
Các câu hỏi lý thuyết và các bài thực hành chuẩn bị cho thi hết môn 93
A. Phần lý thuyết 93
B. Phần thực hành 95


Trng Hi Bng-Cu trỳc d liu 2

Chơng 1
Sắp xếp ngoại

Trong nhiều ứng dụng của tin học, ta phải sắp xếp các tập tin rất lớn. Ví dụ tệp tin lu thông
tin nhân sự của một tập đoàn sản xuất lớn có thể chứa hàng chục ngàn bản ghi, mỗi bản ghi lại
chứa rất nhiều thông tin: họ tên, quê quán, ngày sinh, quá trình công tác, Nếu cần sắp xếp theo
một trờng nào đó nh ngày sinh chẳng hạn, nếu chỉ sử dụng phơng pháp sắp xếp trong bộ nhớ
(internal sorting) thì ta phải đổ tất cả dữ liệu vào bộ nhớ RAM và có thể bộ nhớ này không đủ để

chứa dữ liệu. Trong những trờng hợp này ngời ta phải tìm cách sắp xếp tập tin mà chỉ đổ một
phần rất nhỏ dữ liệu vào bộ nhớ còn phần lớn các thao tác đợc thực hiện trực tiếp trên tập tin.
Cách sắp xếp nh thế này đợc gọi là sắp xếp ngoại (external sorting). Khi sắp xếp một dãy các
phần tử trong bộ nhớ ta có thể truy xuất dễ dàng các thành phần của dãy. Ví dụ ta có thể đổi chỗ
các phần tử, dịch chuyển chúng bằng các phép gán gần nh đợc thực hiện tức thời. Việc truy
xuất các phần tử trên tệp, nhất là tệp truy xuất tuần tự nh băng từ chẳng hạn, sẽ khó khăn và tốn
kém hơn nhiều so với truy xuất trong bộ nhớ. Vì vậy việc sắp xếp ngoại phải xem việc hạn chế
dịch chuyển và truy xuất trên tệp là một điều kiện quan trọng. Trong các phơng pháp truyền
thống, ngời ta thấy rằng phơng pháp trộn là thích hợp hơn cả cho công việc này. Trong chơng
này chúng ta sẽ tìm hiểu một số phơng pháp sắp xếp ngoại thờng dùng là: trộn các run có độ
dài cố định, trộn tự nhiên, trộn đa lối cân bằng và trộn đa pha. Mặc dầu trong thực tế các phần tử
của tập tin thờng là có cấu trúc phức tạp, nhng để đơn giản và lột tả đợc bản chất thuật toán,
chúng ta chỉ xét tập tin nhị phân chứa các phần tử là các số thực (mỗi số thực đợc chứa trong 4
byte). Chúng tôi không chọn trờng hợp đơn giản hơn nữa là các ký tự hoặc số nguyên, vì loại
dữ liệu này dễ nhầm với chỉ số. Trớc khi tìm hiểu cụ thể các thuật toán, chúng tôi tóm tắt lại
một số lệnh C++ liên quan đến tệp.

1.1. Thao tác trên tệp bằng ngôn ngữ lập trình C++
Trong chơng này chúng ta sẽ nghiên cứu và cài đặt các thuật toán sắp xếp trên tệp bằng ngôn
ngữ C++. Để các bạn nghe giảng và đọc tài liệu đợc thuận lợi hơn, sau đây chúng tôi xin giới
thiệu sơ lợc về các thao tác trên tệp băng ngôn ngữ C++. Chúng tôi sẽ không giới thiệu chi tiết
cú pháp các lệnh, vì chúng ta cần dành thời gian cho nội dung chính là "Cấu trúc dữ liệu".

1.1.1. Định nghĩa tệp
Tệp thực chất là một tập hợp thông tin đợc đặt tên trên thiết bị nhớ ngoại vi nh đĩa cứng,
đĩa mềm, đĩa CD, USB, băng từ (từ nay về sau ta gọi đơn giản là đĩa). Về mặt vật lý, thông tin
trên tệp có thể không đợc lu trữ một cách liên tục, nghĩa là các phần của tệp có thể nằm rải rác
ở nhiều vị trí trên đĩa, nhng về mặt logic thì có thể xem tệp là một dy liên tiếp các byte trải dài
liên tục từ đầu tệp đến cuối tệp. Thờng thì ở cuối tệp chứa một số byte có giá trị tối đa, tức là
tất cả giá trị trong các bit đều bằng 1 (nh vậy mã ASCII mở rộng của byte là 11111111 = 255).

Tuy nhiên các byte này không đợc tính vào độ lớn của tệp. Nếu bạn mở một tệp và cha nhập gì
cả thì tệp có độ lớn = 0 byte, còn nếu bạn nhập vào một ký tự thì độ lớn tệp là 1. Hệ điều hành sử
dụng bảng FAT (File Allocation Table) và bảng th mục (Directory Table) đợc lu trữ trên đĩa
(thờng là ở vùng đầu của đĩa) để lu giữ các thông tin liên quan đến tệp. Bảng th mục chứa
thông tin về tên tập tin, kích cỡ tính theo byte, ngày cập nhật cuối cùng, thuộc tính, vị trí bắt đầu
trên đĩa, còn tệp FAT ghi lại thông tin về sự kết nối các vùng lu trữ tệp trên đĩa. Các byte
trong tệp có thể có giá trị bất kỳ từ 0 - 255. Tuy nhiên nếu ta dùng các chơng trình soạn thảo văn
bản nh NCEDIT hoặc C hay Pascal thì chỉ nhập đợc các ký tự có trên bàn phím do đó nội
dung tệp trông nh một văn bản có thể đọc đợc. Nếu ta dùng chơng trình soạn thảo văn bản để
xem một tệp bất kỳ, ví dụ tệp có đuôi .EXE thì sẽ thấy có nhiều ký tự lạ, vì khi đó chơng trình
soạn thảo văn bản đã cho hiện lên cả những ký tự không có trên bàn phím.


Trng Hi Bng-Cu trỳc d liu 2

5
1.1.2. Các kiểu truy cập tệp: nhị phân và văn bản
Trong ngôn ngữ C++ ta có thể truy cập tệp bằng 2 cách: nhị phân hoặc văn bản. Với một tệp
bất kỳ ta đều có thể mở và thao tác theo kiểu nhị phân hoặc văn bản. Hai cách thao tác này có
một số khác biệt nh sau:
Trong tệp văn bản nếu ta ghi vào ký tự có mã là 26 (tức Ctrl+Z), hoặc -1 (chính là 255
nếu là ký tự không dấu unsigned char) thì khi ta dùng hàm fgetc() để đọc khi gặp giá
trị này ta sẽ nhận đợc c = -1 (tức là giá trị của hằng EOF có sẵn trong C). Lúc này giá trị
của hàm feof() sẽ nhận giá trị đúng (tức là giá trị # 0) và ta không thể đọc tiếp đợc các
ký tự sau đó.
Đối với tệp văn bản khi ta ghi vào tệp ký tự có mã 10 (LF = Line Feed tức là xuống
dòng) thì C tự động thêm ký tự 13 (CR = Carriage Return, tức là đa con chạy về đầu
dòng) vào tệp phía trớc ký tự 10 (Bạn có thể thử điều này: nếu tệp mở theo kiểu nhị phân
thì khi đa ký tự 10 vào tệp có độ lớn là 1, còn nếu mở tệp theo kiểu văn bản thì tệp lại có
độ lớn là 2 vì đã đợc ghi thêm ký tự 13).

Với tệp nhị phân thì cho dù bên trong tệp có chứa một dãy liên tiếp các ký tự -1 thì C
vẫn không coi đó là cuối tệp. Nh vậy với tệp nhị phân thì nhận biết cuối tệp không chỉ từ
các ký tự ở cuối tệp.
Để hiểu rõ hơn những điều trên đây bạn hãy chạy thử chơng trình sau:
void ThuTep()
{FILE * f = fopen("a.dat","w+t");
fputc('A',f); fputc(26,f); fputc('B',f); fputc(255,f); fputc(255,f);
fputc(10,f); fclose(f);
char c;

f = fopen("a.dat","rb");
while(!feof(f))
{c = fgetc(f);printf("%d ",c);}
fclose(f);

f = fopen("a.dat","rt");
printf("\n");
while(!feof(f))
{c = fgetc(f);printf("%d ",c);
}
fclose(f);
}
Kết quả trên màn hình là:
65 26 66 -1 -1 13 10 -1
65 -1
Vì ta tạo tệp theo kiểu văn bản nên khi ghi lên tệp ký tự 10 thì ký tự 13 cũng đợc chèn vào phía
trớc (vậy là trớc khi xuống dòng phải về đầu dòng). Nếu ta mở lại tệp theo kiểu nhị phân thì có
thể đọc đợc tất cả các ký tự đã ghi vào. Còn nếu ta mở tệp theo kiểu văn bản thì khi gặp ký tự
26 chơng trình cho là đã hết tệp và gán c = -1.
Cũng ví dụ trên nhng chúng ta thay lệnh f = fopen("a.dat","w+t"); bằng lệnh

f = fopen("a.dat","w+b"); thì kết quả là:
65 26 66 -1 -1 10 -1
65 -1
Tức là ký tự 13 không bị chèn vào trớc ký tự 10 nh trờng hợp tệp văn bản.


Trng Hi Bng-Cu trỳc d liu 2

6
1.1.3. Các hàm thao tác trên tệp
Mặc dầu tệp là tập hợp thông tin đợc đặt tên và đợc lu trữ trên đĩa, nhng khi thao tác trên
tệp thì tên tệp không đợc sử dụng trực tiếp. C dùng một con trỏ tệp đợc lu trong bộ nhớ để
thao tác trên tệp. Con trỏ này có kiểu FILE, là một kiểu cấu trúc đợc định nghĩa sẵn trong
STDIO.H.

a. Mở và đóng tệp
Mở tệp:
Cú pháp: <Con trỏ tệp> = fopen(<Tên tệp>,<Kiểu mở>);
<Kiểu mở> là chuỗi gồm 2 ký tự đợc quy định nh sau: ký tự đầu cho biết tệp đợc mở mới chỉ
đọc, mở mới có thể đọc ghi, mở tệp có sẵn chỉ đọc hay mở tệp có sẵn có thể đọc ghi; ký tự thứ 2
cho biết cách thức truy cập: b (binary) là nhị phân còn t (text) là văn bản. Ta có thể tóm tắt các
<Kiểu mở> trong bảng sau:
Kiểu
mở
ý nghĩa Kiểu
mở
ý nghĩa
wb Mở tệp nhị phân mới chỉ ghi (nh
vậy nếu đã có tệp cũ cùng tên thì
tệp cũ sẽ bị xóa)

wt Mở tệp văn bản mới chỉ ghi (nh vậy
nếu đã có tệp cũ cùng tên thì tệp cũ sẽ bị
xóa)
w+b Mở tệp nhị phân mới đọc/ ghi w+t Mở tệp văn bản mới đọc/ ghi
rb Mở tệp nhị phân đã có chỉ đọc rt Mở tệp văn bản đã có chỉ đọc
r+b Mở tệp nhị phân đã có đọc/ ghi r+t Mở tệp văn bản đã có đọc/ ghi

Đóng tệp:
Cú pháp: fclose(<Con trỏ tệp>);

Ví dụ đoạn chơng trình sau sẽ tạo một tệp có tên là A.DAT và ghi vào 3 ký tự A,B,C rồi đóng
tệp:
FILE *f; f = fopen("A.DAT","wb"); fput('A',f); fput(66,f); fput('C',f); fclose(f).

b. Ghi thông tin lên tệp
Có thể sử dụng các hàm sau để ghi thông tin lên tệp:
- Ghi một ký tự lên vị trí hiện thời của tệp: fput(<ký tự>, <con trỏ tệp>); Ví dụ: fput('O',f);
- Ghi một chuỗi ký tự lên tệp: fputs(<chuỗi ký tự>, <Con trỏ tệp>); Ví dụ: fputs("Chao",f);
- Ghi các biểu thức lên tệp: fprintf(<Con trỏ tệp>,<Chuỗi khuôn dạng>,<Các biểu thức>);
Lệnh này ghi lên tệp mã ASCII của các ký tự Ví dụ lệnh: fprintf(f,"A%6.1fB", 12.5); Ghi lên
tệp 8 ký tự là
65 32 32 49 50 46 53 66
Ký tự 'A' Dấu cách Dấu cách Số 1 Số 2 Dấu . Số 5 Ký tự B
- Ghi các khối thông tin từ một địa chỉ trong bộ nhớ lên vị trí hiện thời của tệp:
fwrite(<Địa chỉ>, <Số byte một khối>, <Số khối>, <con trỏ tệp>);
Ví dụ: fwrite(&x, 12, 5, f); Có nghĩa là lấy thông tin trong 5 khối dữ liệu, mỗi khối 12 byte,
tổng cộng là 12x5 = 60 byte bắt đầu từ địa chỉ của biến x và ghi lên vị trí hiện thời của tệp.
Ví dụ này chỉ có ý nghĩa minh họa, vì nếu x là biến thực thì chỉ có 4 byte tại địa chỉ của nó là
lu giá trị của nó mà thôi.


c. Đọc thông tin từ tệp và đa vào biến bộ nhớ

Trng Hi Bng-Cu trỳc d liu 2

7
Cách giải thích các lệnh sau tơng tự nh trờng hợp ghi lên tệp nhng theo chiều ngợc lại. ở
đây chúng tôi chỉ nêu các ví dụ:
- char c = fgetc(f); Đọc một ký tự từ vị trí hiện thời của tệp và gán cho biến ký tự c.
- fgets(<biến chuỗi ký tự>,<Số byte tối đa>, <con trỏ tệp>);//Lệnh này chỉ dùng cho tệp văn bản.
Ví dụ fgets(s, 100, f); //Đọc tối đa là 100 ký tự từ dòng hiện thời (kết thúc bằng dấu xuống
dòng). Nh vậy nếu dòng có 70 ký tự thì chỉ đọc 70 ký tự này. Còn nếu dòng có 120 ký tự
chẳng hạn thì chỉ đọc 100 ký tự. Vì một dòng trong tệp văn bản tối đa là 255 ký tự, nên lệnh
fgets(s,255,f); sẽ bảo đảm đọc cả dòng văn bản.
- fscanf(f,"%f", &x); Sẽ đọc số đợc ghi dới dạng từng ký tự riêng biệt ở tại hoặc sau vị trí hiện
thời của con trỏ tệp và gán cho biến x.
- fread(<Địa chỉ>, <Số byte một khối>, <Số khối>, <con trỏ tệp>);
Ví dụ: fread(&x, 12, 5, f); Có nghĩa là lấy thông tin trong 5 khối dữ liệu, mỗi khối 12 byte,
tổng cộng là 12x5 = 60 byte trên tệp bắt đầu từ vị trị hiện thời và gán cho biến x. Ví dụ này
chỉ có ý nghĩa minh họa, vì nếu x là biến thực thì chỉ có 4 byte tại địa chỉ của nó là lu giá trị
của nó mà thôi.

d. Dịch chuyển trên tệp
Khi môt tệp mở thì có một vị trí trên tệp đợc xem là con trỏ logic của tệp. Thông tin đọc/ ghi
trên tệp đợc thực hiện từ vị trí này.
- Lệnh rewind(<Con trỏ tệp>); sẽ đa con trỏ tệp về đầu tệp. Ví dụ rewind(f);
- fseek(<Con trỏ tệp>, <Số byte cần dịch chuyển>, <Vị trí xuất phát>);
<Vị trí xuất phát> = 0 nếu xuất phát từ đầu tệp, 1 nếu xuất phát từ vị trí hiện thời, 2 nếu xuất
phát từ cuối tệp.
<Số byte cần dịch chuyển> > 0 sẽ dịch chuyển về cuối tệp, < 0 dịch chuyển về phía đầu tệp.
Ví dụ: fseek(f,0,0); là chuyển con trỏ về đầu tệp; fseek(f,0,2); là chuyển con trỏ về cuối tệp.


e. Hàm ftell()
Hàm ftell(<con trỏ tệp>) cho ta độ lớn của tệp tính bằng byte tính từ đầu tệp đến vị trí hiện thời
của con trỏ.
Ví dụ giả sử tệp với con trỏ f chứa các số thực đợc lu theo kiểu nhị phân (tức là một số thực
chiếm 4 byte) thì các lệnh sau sẽ cho ta n là số phần tử trên tệp.
fseek(f,0,2); n = ftell(f)/sizeof(float);

1.2. Phơng pháp trộn run có độ dài cố định
1.2.1. Mô tả thuật toán
Khi trình bày các thuật toán sắp xếp, đặc biệt là phơng pháp trộn, ta rất hay phải dùng thuật
ngữ "dãy đã sắp xếp". Vì vậy ngời ta đã thay thuật ngữ này bằng một từ đơn giản là "Run".
Chúng ta sẽ không dịch thuật ngữ này sang tiếng Việt vì rất khó tìm đợc từ thích hợp. Vì các
phần tử của tập tin về mặt logic có thể xem là một dãy tuyến tính liên tục từ đầu đến cuối tệp, do
đó ta có thể dùng một dãy các phần tử A = {a
0
, a
1
, , a
n-1
} để mô tả các phần tử trên tệp.
Thuật toán này có thể mô tả nh sau:
Bớc 1: Ta xem tệp A gồm n run 1 phần tử (mỗi run có độ dài cố định p = 1). Ta sẽ lần lợt
phân bổ n run cho 2 tệp B và C theo cách a
0
cho B, a
1
cho C, a
2
cho B cứ nh

thế cho đến khi A hết phần tử. Có thể thấy rằng nếu n chẵn thì số run trên B và C
nh nhau, còn nếu n lẻ thì trên B có [
2
n
]+1 run, còn trên C có [
2
n
] run.

Trng Hi Bng-Cu trỳc d liu 2

8
Tiếp theo ta lần lợt trộn từng cặp run trên B và C với nhau và ghi vào A. Nếu số run
trên B nhiều hơn thì run cuối cùng trên B sẽ đợc đa vào A mà không phải trộn gì cả.
Trừ run cuối cùng có thể chỉ có 1 phần tử, các run trên A bây giờ đã có 2 phần tử. Và
số run là
2
n
(hàm trần của
2
n
.
Nếu tập A chỉ gồm 2 phần tử thì trên A bây giờ chỉ có 1 run duy nhất và việc sắp xếp
hoàn tất. Nếu n>2 ta đặt p = p*2 = 2 và chuyển sang bớc 2.
Giả sử dãy trong tệp A là các phần tử 1 2 9 8 7 6 5 ta có thể mô tả bớc này
trong bảng sau:

A
1 2 9 8 7 6 5
B

1 9 7 5
C
2 86
A'
1 2 8 9 6 7 5

. . .

Bớc k: Lúc này trừ run cuối cùng, các run trên A độ dài cố định p = 2
(k-1)
(có tất cả [
p
n
]+1
run). Ta sẽ lần lợt phân bổ các run cho 2 tệp B và C.
Tiếp theo ta lần lợt trộn từng cặp run trên B và C với nhau và ghi vào A.
Với k = 2 trong ví dụ trên ta có p = 2 và kết quả đợc thể hiện trong bảng sau:
A
1 2 8 9 6 7 5
B
1 2 6 7
C
8 9 5
A'
1 2 8 9 5 6 7

Đặt p = p*2. Nếu p n thì trên A bây giờ chỉ có 1 run duy nhất và việc sắp xếp hoàn
tất. Nếu p < n chuyển sang bớc k+1.
. . .
Trong ví dụ trên tại bớc k = 3 ta có p = 4 và kết quả phân bổ và trộn đợc thể hiện

trong bảng sau:
A
1 2 8 9 5 6 7
B
1 2 8 9
C
5 6 7
A'
1 2 5 6 7 8 9
Khi đặt p = p* 2 = 8 ta thấy p n do đó A chỉ chứa một run và tệp A đã đợc sắp
xếp.

Thuật toán trên cũng có thể mô tả gọn hơn nh sau:
Input: Tệp A có n phần tử thờng là cha sắp xếp.
Output: Tệp A đợc sắp xếp.
B1. Đặt p = 1. Chuyển sang bớc B2.
B2. Tệp A gồm các run độ dài p. Ta tạo mới 2 tệp B và C rồi lần lợt phân bổ các run trên A
sang B và C cho đến khi hết run trên A. Chuyển sang bớc B3.

Trng Hi Bng-Cu trỳc d liu 2

9
B3. Tạo mới tệp A sao cho trong A không chứa phần tử nào. Lần lợt trộn từng cặp run độ dài p
(trừ run cuối cùng trên B hoặc C có thể có độ dài ngắn hơn) trên B và C rồi đa vào A
cho đến khi hết run trên cả B và C. Lu ý là cặp run cuối cùng có thể có một run rỗng, lúc
này phép trộn đợc hiểu là đa toàn bộ run khác rỗng vào A.
B4. Sau khi thực hiện bớc B3 ta đã có trên A các run có độ dài 2*p (trừ run cuối cùng có thể
ngắn hơn). Do đó ta đặt p = p*2. Nếu p n thì kết thúc. Nếu không thì quay lại B2.

Trong các thuật toán sắp xếp ngoại còn lại chúng tôi để lại cách mô tả thuật toán nh trên đây

cho các bạn sinh viên thực hiện. Chúng tôi sẽ dùng cách mô tả dùng giả ngôn ngữ C (tức là ngôn
ngữ gần giống với C, chủ yếu để hiểu đợc ý tởng thuật toán) nh sau:
Đặt p = 1
while (p<n)
{ - Tạo mới 2 tệp B và C rồi lần lợt phân bổ các run có độ dài p từ tệp A sang 2 tệp B
và C theo cách: một run sang B thì run tiếp theo sang C, run sau đó sang B, cho đến
khi hết run trên A. Nh vậy tệp B có thể chứa nhiều hơn tệp C một run và run cuối
cùng trên B có thể có độ dài < p. (Nếu n chẵn thì run cuối cùng trên C có thể có độ
dài < p).
- Tiếp theo tạo mới tệp A rồi trộn từng cặp run có độ dài p trên B và C thành một run
có độ dài p*2 và lu vào tệp A. Sau khi hoàn tất, tệp A chứa các run có độ dài p*2 (trừ
run cuối cùng có thể có độ dài ngắn hơn).
- Đặt p = p*2 (và chuyển sang vòng lặp tiếp theo).
}

1.2.2. Cài đặt chơng trình
Chơng trình 11SXF.CPP sau đây cài đặt thuật toán sắp xếp một tệp bằng phơng pháp trộn độ
dài run cố định (hay còn gọi là trộn trực tiếp) trên tệp nhị phân.
Phần khai báo chung, khai báo nguyên mẫu hàm và hàm main:

//11SXF.CPP Sap xep file bang phuong phap tron truc tiep tren tep nhi phan
#include <conio.h>
#include <stdio.h>
#define true 1
#define false 0
#define sz (sizeof(float))
int FileNodes(char *TenTep);
int EoF(FILE *f);
void CreateFile(char *TenTep);
void ViewFile(char *TenTep);

void SplitFile(char *tepA,char *tepB, char *tepC, int p);
void MergeFile(char *tepB,char *tepC, char *tepA, int p);
void SortFile(char *TenTep);
//=====================
void main()
{clrscr();
char *TenTep="A.DAT";
CreateFile(TenTep);
ViewFile(TenTep);
printf("\n\nTep sau khi sap xep la:");
SortFile(TenTep);
ViewFile(TenTep);

Trng Hi Bng-Cu trỳc d liu 2

10
getch();
};

Hàm int FileNodes(char *TenTep): Trả về số phần tử (tức là các số thực) có trong tệp.
Hàm này thực hiện công việc sau: chuyển về cuối tệp. Dùng hàm ftell() để biết đợc tổng số byte
của tệp. Lấy tổng số byte chia cho số byte của một số thực thì đợc số phần tử trong tệp.
int FileNodes(char *TenTep)
{int k;float x;
FILE *f = fopen(TenTep,"rb");
fseek(f,0,2);//Ve cuoi tep
k = ftell(f)/sz;
fclose(f);
return(k);
};


Hàm int EoF(FILE *f) : Trả về giá trị true nếu con trỏ tệp đã đi qua số thực cuối cùng trong tệp.
Sở dĩ ta phải dùng hàm này thay cho hàm feof() của C vì lý do sau:
Giả sử ta chạy đoạn chơng trình sau:
float x; while(!feof(f)) {fread(&x,sz,1,f); cout<<x;}
giả sử phần tử cuối cùng trên tệp là 10. Khi đó có trờng hợp sau khi đọc xong phần tử cuối cùng
này thì hàm feof(f) vẫn cha có giá trị true, nghĩa là !feof() vẫn nhận giá trị true và lệnh fread
vẫn đợc thực hiện. Tuy nhiên vì đã vợt qua số thực cuối cùng nên lệnh này không đọc đợc gì
và giá trị x vẫn là 10. Nh vậy ta sẽ thấy trên màn hình giá trị 10 xuất hiện 2 lần. Điều này là
không đúng. Hàm EoF() hoạt động nh sau: nếu lệnh fread đọc đợc thông tin thì có nghĩa là vị
trí hiện thời cha vợt qua số thực cuối cùng trong tệp. Điều này có nghĩa là vị trí trớc khi gọi
hàm cha phải ở cuối tệp và hàm trả về giá trị false. Tuy nhiên trớc khi thoát khỏi hàm cần lùi
lại một số thực để trở về đúng vị trí trớc khi gọi hàm. Còn nếu không đọc đợc thông tin thì có
nghĩa là đã vợt qua số thực cuối cùng và nh vậy đã ở cuối tệp. Trong trờng hợp này hàm trả
về giá trị true và cũng không cần lùi lại.
int EoF(FILE *f)
{float x;
if(fread(&x,sz,1,f)>0)
{fseek(f,-1.0*sz,1);
return(false);
};
return(true);
}

Hàm void CreateFile(char *TenTep) : Tạo tệp và nhập vào n số thực
void CreateFile(char *TenTep)
{int i,m;float x;
FILE* f;
f = fopen(TenTep,"wb");
rewind(f);

char ch;
do
{clrscr();
printf("\nNhap du lieu vao tep:");
printf("\n1. Nhap truc tiep");
printf("\n2. Tao ngau nhien");

Trng Hi Bng-Cu trỳc d liu 2

11
printf("\n\n Hay chon 1 hoac 2: ");
ch=getche();
}
while(ch!='1'&& ch!='2');
printf("\nCho biet so phan tu can dua vao tep: ");
scanf("%d",&m);
if(ch=='1')
{printf("\nHay nhap %d so: ",m);
for(i=0;i<m;i++)
{scanf("%f",&x);
fwrite(&x,sz,1,f);
}
}
else
{randomize();
for(i=0;i<m;i++)
{x=float(random(5*m));
fwrite(&x,sz,1,f);
}
}

fclose(f);
};


Hàm void ViewFile(char *TenTep) : Mở một tệp nhị phân đã có và đọc các số thực đợc lu
trong tệp và cho hiện lên màn hình.
void ViewFile(char *TenTep)
{float x;
FILE* f;
f = fopen(TenTep,"rb");
rewind(f);
printf("\nCac phan tu tren tep %s: ",TenTep);
while(fread(&x,sz,1,f)>0) printf("%5.0f",x);
fclose(f);
};

Hàm void SplitFile(char *tepA,char *tepB, char *tepC, int p)
Giả sử rằng trên tệp A gồm các run độ dài p (trừ run cuối cùng). Hàm này lần lợt đọc các run
từ tệp A và phân bổ vào 2 tệp B và C. Vì A là tệp nguồn, còn B và C là các tệp đích, nên
tệp A đợc mở chỉ đọc, còn 2 tệp B và C thì đợc mở mới chỉ ghi.
Lệnh while(!EoF(a)) là điều kiện để kết thúc quá trình phân bổ. Rõ ràng nếu trên tệp A còn
phần tử (!EoF(a)) thì quá trình phân bổ cha kết thúc. Quá trính phân bổ run bao giờ cũng bắt
đầu bằng việc đọc một run trong tệp A và ghi vào tệp B. Biến dem ghi lại số phần tử của run
hiện thời đã đợc đọc và ghi vào B. Nếu số phần tử còn trên A mà nhỏ hơn p thì quá trình kết
thúc trớc khi đọc đủ p phần tử. Còn nếu không thì đọc đủ p phần tử ghi vào B sau đó chuyển
sang đọc p phần tử tiếp theo ghi vào C. Quá trình ghi vào tệp C cũng tơng tự nh quá trình ghi
vào B.
void SplitFile(char *tepA,char *tepB, char *tepC, int p)
{FILE *a,*b,*c;float x;int dem;
a = fopen(tepA,"rb");

b = fopen(tepB,"wb");

Trng Hi Bng-Cu trỳc d liu 2

12
c = fopen(tepC,"wb");
rewind(a);rewind(b);rewind(c);
while(!EoF(a))
{//Chia p phan tu cho b
dem = 0;
while(dem<p && !EoF(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,b);
dem++;
}
dem = 0;
while(dem<p && !EoF(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,c);
dem++;
}
}
fclose(a);fclose(b);fclose(c);
};

Hàm void MergeFile(char *tepB,char *tepC, char *tepA, int p)
sẽ trộn từng cặp run độ dài p từ tệp B và tệp C thành một run độ dài 2*p và đa vào tệp A
(trừ run cuối cùng).
Vì B và C là các tệp nguồn, còn A là tệp đích, nên 2 tệp B và C đợc mở chỉ đọc, còn tệp
A đợc mở mới chỉ ghi. Vì quá trình trộn chỉ thực hiện khi cả hai tệp B và C đều còn phần tử.

Nếu một trong hai tệp hết phần tử thì có nghĩa là các phần tử còn lại ở tệp kia không còn run để
trộn và đợc chuyển sang tệp A mà không cần so sánh gì cả. Vậy lệnh
while(!EoF(b) && !EoF(c))
có nghĩa là quá trình trộn đợc tiếp tục nếu cả hai tệp A và B đều còn phần tử.
Vì số phần tử trong mỗi run không vợt quá p nên ta dùng biến ix để ghi lại số phần tử trong
run hiện thời của tệp B đã đợc ghi sang A. Tơng tự, biến iy là số phần tử trong run hiện thời
của C đã đợc ghi sang A. Khi mới bắt đầu thì cả hai run đều có phần tử nên ta đọc lần lợt
phần tử đầu tiên của run trong B vào x, và phần tử đầu tiên trong run ở C vào y bằng các lệnh:
fread(&x,sz,1,b);
fread(&y,sz,1,c);
Sau đó quá trình trộn run đợc tiến hành. Quá trình này đợc thực hiện nếu điều kiện
ix<p&&iy<p còn thỏa mãn. Trong chơng trình ta thấy lệnh:
while(ix<p && iy<p)
Ta bắt đầu so sánh hai phần tử đầu run bằng lệnh if(x<y). Nếu điều kiện này đúng thì ta ghi x
vào A, tăng ix lên 1. Đồng thời kiểm tra xem nếu đã hết run trên B (tức là ix==p hoặc EoF(b)
thì ghi y lên tệp A và thoát ra khỏi quá trình trộn 2 run hiện thời. Nếu sau khi ghi x lên A
mà vẫn còn phần tử trong run ở B thì đọc phần tử tiếp theo gán vào x. Trờng hợp biểu thức
x<y sai thì làm tơng tự với tệp C.
Khi thoát khỏi vòng lặp while(ix<p && iy<p) thì có thể run trên B hoặc trên C vẫn cha hết.
Do đó ta cần ghi nốt phần còn lại của run hiện thời vào tệp A bằng các lệnh:
//Chep phan con lai cua p phan tu tren b len a
while(ix<p && !EoF(b))
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);ix++;
}
//Chep phan con lai cua p phan tu tren c len a
while(iy<p && !EoF(c))

Trng Hi Bng-Cu trỳc d liu 2


13
{fread(&y,sz,1,c);
fwrite(&y,sz,1,a);iy++;
}
Sau khi trộn hết cặp run thì có thể trên B hoặc C vẫn còn phần tử và ta cần đọc nốt các phần tử
này và ghi lên A. Sau đây là toàn văn hàm MergeFile:
void MergeFile(char *tepB,char *tepC, char *tepA, int p)
{FILE *a,*b,*c;
float x,y;
int ix,iy;
b = fopen(tepB,"rb");
c = fopen(tepC,"rb");
a = fopen(tepA,"wb");
rewind(a);rewind(b);rewind(c);
while(!EoF(b) && !EoF(c))
{ix = 0;//So phan tu cua b da ghi len a trong so 2*p phan tu can gi len a
iy = 0;//So phan tu cua c da ghi len a trong so 2*p phan tu can gi len a
fread(&x,sz,1,b);
fread(&y,sz,1,c);
while(ix<p && iy<p)
{if(x<y)
{fwrite(&x,sz,1,a);ix++;
if(ix==p || EoF(b)) {fwrite(&y,sz,1,a);iy++;break;}
//Chua du p phan tu va chua het file b
fread(&x,sz,1,b);
}
else //x>=y)
{fwrite(&y,sz,1,a);iy++;
if(iy==p || EoF(c)) {fwrite(&x,sz,1,a);ix++;break;}
//Chua du p phan tu va chua het file b

fread(&y,sz,1,c);
}
}//Het vong while(ix<p && iy<p)
//Chep phan con lai cua p phan tu tren b len a
while(ix<p && !EoF(b))
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);ix++;
}
//Chep phan con lai cua p phan tu tren c len a
while(iy<p && !EoF(c))
{fread(&y,sz,1,c);
fwrite(&y,sz,1,a);iy++;
}
}
//Chep phan con lai tren b len a
while(!EoF(b))
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);
}
//Vi co the so run tren b nhieu hon tren c 1, nen tren c da het run
fclose(a);fclose(b);fclose(c);
};


Trng Hi Bng-Cu trỳc d liu 2

14
Hàm void SortFile(char *TenTep) sẽ sắp xếp tệp có tên TenTep theo các thuật toán đã trình
bày ở trên. Các hàm quan trọng nhất đã đợc trình bày, ở đây ta chỉ cần gọi các hàm đó một cách
hợp lý. Chúng ta xuất phát từ p = 1, sau đó thực hiện quá trình phân bổ tệp A cho 2 tệp B và

C, rồi trộn B và C vào A. Sau mỗi bớc đặt p = p*2 và thực hiện quá trình lặp chừng nào
p<n. Nếu p<n nhng 2*p n thì quá trình sẽ hoàn tất sau khi phân bổ A, rồi trộn B và C trở
lại A.
void SortFile(char *TenTep)
{int p,n;char *tepB="vutb.dat",*tepC="vutc.dat";
n=FileNodes(TenTep);
p=1;
while(p<n)
{SplitFile(TenTep,tepB,tepC,p);
MergeFile(tepB,tepC,TenTep,p);
p = 2*p;
}
}

1.3. Phơng pháp trộn run tự nhiên
1.3.1. Mô tả thuật toán
Trong phơng pháp trộn trực tiếp ta coi dãy ban đầu có n run mà không hề quan tâm đến
thực tế là có thể đã có những run đã có độ dài > 1 ngay từ bớc xuất phát. Trong phần này
chúng ta sẽ phân bổ và trộn các run trên cơ sở thực tế: chúng ta sẽ xem xét các run tự nhiên trên
A và phân bổ chúng sang B và C. Khi trộn các run của B và C vào A ta cũng xem xét các
run thực tế trên B, C. Quá trình này sẽ kết thúc khi chúng ta kiểm tra và thấy rằng chỉ có một run
duy nhất trên A. Cách trộn nh thế này đợc gọi là phơng pháp trộn tự nhiên.
Thuật toán này có thể mô tả nh sau:
Phép lặp: Ta xem tệp A gồm các run có thể có độ dài khác nhau. Ta sẽ lần lợt phân bổ các
run cho 2 tệp B và C cho đến khi A hết phần tử.
Tiếp theo ta lần lợt trộn từng cặp run trên B và C với nhau và ghi vào A. Lu ý rằng
số run có trên B nói chung nhỏ hơn số run mà A đã phân bổ cho B. Vì nhiều khi nối
hai run bất kỳ một cách tình cờ ta cũng có thể đợc một run. Giả sử dãy trong tệp
A là các phần tử 1 2 9 8 7 6 5 ta có thể mô tả bớc này trong bảng sau:


A
1 2 9 8 7 6 5
B
1 2 9 7 5
C
8 6
A'
1 2 8 9 6 7 5

Sau khi trộn B và C vào A ta cần kiểm tra nếu A chỉ có một run thì việc sắp xếp kết
thúc, nếu không ta quay lại thực hiện phép lặp.

Có thể mô tả quá trình trên thông qua việc sắp xếp tệp A với các giá trị ban đầu ở trên nh sau:
Bớc 1:
A
1 2 9 8 7 6 5
B
1 2 9 7 5
C
8 6
A'
1 2 8 9 6 7 5

Trng Hi Bng-Cu trỳc d liu 2

15

Bớc 2:
A
1 2 8 9 6 7 5

B
1 2 8 9 5
C
6 7
A'
1 2 6 7 8 9 5

Bớc 3:
A
1 2 6 7 8 9 5
B
1 2 6 7 8 9
C
5
A'
1 2 5 6 7 8 9

Tại bớc 3 ta thấy rằng tệp A đã đợc sắp xếp và kết thúc.

Nếu mô tả thuật toán trên bằng giả ngôn ngữ C (tức là ngôn ngữ gần giống với C chủ yếu để
hiểu đợc ý tởng thuật toán) ta có thể viết gọn nh sau:

while (Số run trong tệp A >1)
{ - Lần lợt phân bổ các run tự nhiên từ tệp A sang 2 tệp B và C theo cách: một run sang
B thì run tiếp theo sang C, run sau đó sang B, cho đến khi hết run trên A. Nh vậy tệp
B có thể chứa nhiều hơn tệp C một run.
- Tiếp theo trộn từng cặp run tự nhiên trên B và C thành một run và ghi vào tệp A.
- Nếu A đã sắp xếp hay chỉ có một run thì kết thúc, nếu không quay lại vòng lặp.
}


1.3.2. Cài đặt chơng trình
Chơng trình 12SXF.CPP sau đây cài đặt thuật toán sắp xếp một tệp bằng phơng pháp trộn run
tự nhiên.
Phần khai báo chung, khai báo nguyên mẫu hàm và hàm main:

//12SXF.CPP Sap xep file bang phuong phap tron tu nhien tren tep nhi phan
#include <conio.h>
#include <stdio.h>
#define true 1
#define false 0
#define sz (sizeof(float))
int FileNodes(char *TenTep);//So phan tu tren tep
int EoF(FILE *f);//Cuoi tep
int EoR(FILE *f);//Cuoi Run
void CreateFile(char *TenTep);
void ViewFile(char *TenTep);
void SplitFile(char *tepA,char *tepB, char *tepC);
void MergeFile(char *tepB,char *tepC, char *tepA);
int SortedFile(char *TenTep);
void SortFile(char *TenTep);
//=====================

Trng Hi Bng-Cu trỳc d liu 2

16
void main()
{clrscr();
char *TenTep="A.DAT";
CreateFile(TenTep);
ViewFile(TenTep);

printf("\n\nTep sau khi sap xep la:");
SortFile(TenTep);
ViewFile(TenTep);
getch();
};

Hàm int EoR(FILE *f) có nghĩa là End of Run trả về giá trị true nếu con trỏ tệp đang ở ngay
phía sau phần tử cuối cùng của một run. Nếu con trỏ ở đầu tệp thì ta cho rằng không phải là
EoR() và trả về giá trị false. Điều này thể hiện qua lệnh:
if(ftell(f)==0) return(false);
Tiếp theo nếu con trỏ đã ở sau phần tử cuối cùng (tức là EoF() = true) thì cũng đợc xem là
EoR. Trong trờng hợp còn lại ta cần so sánh giá trị y vừa đọc với giá trị trớc nó. Do đó sau khi
đọc xong y ta phải quay lại 2 phần tử để đọc x. Nếu x<=y thì vị trí hiện tại cha phải là EoR.
Trong trờng hợp này hàm trả về giá trị false. Đó là lệnh:
if(x<=y) return(false);
Nếu (x<=y) sai, tức là x>y thì đúng là ta đã ở sau phần tử cuối cùng của run và phải trả về giá
trị true. Đó là lệnh:
return(true);
ở cuối hàm.
Vì ta đọc y, rồi lùi lại 2 vị trí để đọc x nên con trỏ đã ở vị trí trớc khi gọi hàm, nên không phải
dịch chuyển nữa.
int EoR(FILE *f)
{float x,y;
if(ftell(f)==0) return(false);
if(fread(&y,sz,1,f)<=0) return(true);
fseek(f,-2.0*sz,1);
fread(&x,sz,1,f);
if(x<=y) return(false);
else
return(true);

}

Hàm void SplitFile(char *tepA,char *tepB, char *tepC) sẽ phân bổ các run tự nhiên trên A
sang 2 tệp B và C. Vì A là tệp nguồn, còn B và C là các tệp đích, do đó tệp A đợc mở chỉ
đọc, còn các tệp B và C đợc mở mới. Việc phân bổ sẽ đợc thực hiện cho đến khi không còn
phần tử trên A. Điều này thể hiện ở dòng lệnh:
while(!EoF(a))
Một lợt phân bổ run sẽ đợc bắt đầu bằng việc đọc một phần tử ở đầu run trên A và ghi sang B.
Việc này chỉ kết thúc khi hết run hiện thời. Điều này đợc thể hiện ở các lệnh sau:
fread(&x,sz,1,a);
fwrite(&x,sz,1,b);
while(!EoR(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,b);
}
Sau khi ghi một run lên B mà trên A vẫn còn phân tử thì run tiếp theo đợc phân bổ cho C
bằng cách tơng tự. Cứ nh vậy cho đến hết A.

Trng Hi Bng-Cu trỳc d liu 2

17
void SplitFile(char *tepA,char *tepB, char *tepC)
{FILE *a,*b,*c;float x;
a = fopen(tepA,"rb");
b = fopen(tepB,"wb");
c = fopen(tepC,"wb");
rewind(a);rewind(b);rewind(c);
while(!EoF(a))
{//Chia 1 run tren a cho b
fread(&x,sz,1,a);

fwrite(&x,sz,1,b);
while(!EoR(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,b);
}
if(EoF(a)) break;
//Chia 1 run tren a cho c
fread(&x,sz,1,a);
fwrite(&x,sz,1,c);
while(!EoR(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,c);
}
}
fclose(a);fclose(b);fclose(c);
};

Hàm void MergeFile(char *tepB,char *tepC, char *tepA) thực hiện việc trộn các run tự nhiên
trên B và C vào A. Các tệp B và C là các tệp nguồn, do đó đợc mở chỉ đọc. Còn A là tệp
đích nên đợc mở mới. Việc trộn sẽ đợc thực hiện chừng nào còn phần tử trên cả B và C. Điều
này thể hiện ở lệnh:
while(!EoF(b) && !EoF(c))
Vì ta trộn 2 run, một ở trên B và một ở trên C. Việc trộn hai run hiện thời chỉ đợc thực hiện
chừng nào cả hai run vẫn còn phần tử. Vì nếu một run hết phần tử thì chỉ cần ghi phần còn lại của
run kia lên tệp A. Chúng ta sẽ dùng 2 biến logic con_x và con_y để ghi lại trạng thái này.
con_x = true có nghĩa là vẫn còn phần tử trong run hiện thời trên B; con_y = true có nghĩa là
vẫn còn phần tử trong run hiện thời trên C. Vòng lặp thể hiện quá trình trộn hai run hiện thời thể
hiên ở lệnh:
while(con_x && con_y)
Khi trộn ta so sánh x và y (x là của run trên B và y là phần tử của run trên C). Nếu x<y thì ta

ghi x vào A. Sau khi ghi x lên A thì ta kiểm tra xem còn phần tử trong run hiện thời trên B
không. Nếu không còn thì ghi nhận trạng thái này bằng lệnh con_x = false; nếu còn thì đọc một
phần tử trong run vào x để thay thế x đã bị lấy đi. Điều này thể hiện ở dòng lệnh:
if(EoR(b)) con_x=false; else fread(&x,sz,1,b);
Nếu x<y là sai, tức là x>=y thì làm tơng tự với C.
Thoát khỏi vòng lặp này có thể x hoặc y vẫn cha đợc ghi lên a. Ta ghi nốt phần tử cha đợc
ghi bằng các lệnh:
if(con_x) fwrite(&x,sz,1,a);
if(con_y) fwrite(&y,sz,1,a);
Sau khi trộn 2 run thì chỉ có thể một run vẫn còn phần tử cha đợc ghi lên tệp A. Trong trờng
hợp này ta ghi nốt phần tử sót lại lên A trớc khi thực hiện việc trộn run tiếp theo. Điều này thể
hiện trong đoạn lệnh sau:
//Chep phan con lai cua run hien thoi tu b len a
while(!EoR(b))

Trng Hi Bng-Cu trỳc d liu 2

18
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);
}
//Chep phan con lai cua run hien thoi tu c len a
while(!EoR(c))
{fread(&x,sz,1,c);
fwrite(&x,sz,1,a);
}

Khi đã hết run ở một trong hai tệp thì ta ghi phần còn lại trên tệp kia vào A. Điều này thể hiện ở
đoạn lệnh sau:
//Chep phan con lai tren b len a

while(!EoF(b))
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);
}
//Chep phan con lai tren c len a
while(!EoF(c))
{fread(&y,sz,1,c);
fwrite(&y,sz,1,a);
}
Chú ý. Khác với phơng pháp trộn run trực tiếp, sau khi phân bổ, do các run liền nhau có thể tạo
run mới nên rất có thể run tự nhiên trên b ít hơn trên c, nên không chắc là c hết trớc.

Sau đây là toàn văn hàm MergeFile:
void MergeFile(char *tepB,char *tepC, char *tepA)
{FILE *a,*b,*c;
float x,y;int con_x,con_y;
//con_x = true co nghia la con phan tu trong run tren b
//con_y = true co nghia la con phan tu trong run tren c
b = fopen(tepB,"rb");
c = fopen(tepC,"rb");
a = fopen(tepA,"wb");
rewind(a);rewind(b);rewind(c);
while(!EoF(b) && !EoF(c))
{fread(&x,sz,1,b);con_x=true;
fread(&y,sz,1,c);con_y=true;
while(con_x && con_y)
{if(x<y)
{fwrite(&x,sz,1,a);
if(EoR(b)) con_x=false; else fread(&x,sz,1,b);
//Neu sau khi ghi x vao a ma het run tren file b thi con_x=false

}
else //x>=y)
{fwrite(&y,sz,1,a);
if(EoR(c)) con_y=false; else fread(&y,sz,1,c);
//Neu sau khi ghi y vao a ma het run tren file c thi con_y=false
}
}
//Het Run tren b hoac c
//Neu chua ghi x hoac y len a thi ghi not
if(con_x) fwrite(&x,sz,1,a);
if(con_y) fwrite(&y,sz,1,a);

Trng Hi Bng-Cu trỳc d liu 2

19
//Chep phan con lai cua run hien thoi tu b len a
while(!EoR(b))
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);
}
//Chep phan con lai cua run hien thoi tu c len a
while(!EoR(c))
{fread(&x,sz,1,c);
fwrite(&x,sz,1,a);
}
}
//Chep phan con lai tren b len a
while(!EoF(b))
{fread(&x,sz,1,b);
fwrite(&x,sz,1,a);

}
//Chep phan con lai tren c len a
while(!EoF(c))
{fread(&y,sz,1,c);
fwrite(&y,sz,1,a);
}
fclose(a);fclose(b);fclose(c);
};

Hàm SortedFile(char *TenTep) dùng phơng pháp nổi bọt để kiểm tra xem một tệp đã sắp xếp
cha. Ta lần lợt đọc các phần tử trên tệp, mỗi lần đọc phần tử mới thì ghi nhận lại phần tử cũ
bằng lệnh x = y; Ta thực hiện việc so sánh 2 phần tử gần kề và chỉ cần phát hiện ra một trờng
hợp 2 phần tử gần kề cha sắp theo thứ tự tăng dần là ta kết thúc thuật toán và trả về giá trị false
(cha sắp). Nếu đi qua tệp mà không phát hiện ra trờng hợp so le nào thì có nghĩa là tệp đã sắp
xếp và ta trả về giá trị true.
int SortedFile(char *TenTep)
{FILE *f;float x,y;
f = fopen(TenTep,"rb");
rewind(f);
fread(&x,sz,1,f);
while(fread(&y,sz,1,f)>0)
{if(x>y) {fclose(f);return(false);}
x = y;
}
fclose(f);
return(true);
}

Cuối cùng là hàm void SortFile(char *TenTep) thực hiện việc sắp xếp một tệp đã cho theo đúng
thuật toán đã trình bày. Nghĩa là thực hiện vòng lặp : phân bổ và trộn cho đến khi tệp A đợc

sắp.

void SortFile(char *TenTep)
{int i;char *tepB="vutb.dat",*tepC="vutc.dat";
while(!SortedFile(TenTep))
{SplitFile(TenTep,tepB,tepC);
MergeFile(tepB,tepC,TenTep);
}

Trng Hi Bng-Cu trỳc d liu 2

20
}

1.4. Phơng pháp trộn đa lối cân bằng (Balanced multiway merging)
1.4.1. Mô tả thuật toán
Trong phơng pháp trộn trực tiếp hoặc trộn run tự nhiên, hai thao tác đợc sử dụng đan xen
nhau là phân phối run và trộn run. Thao tác phân phối run thờng chiếm một lợng thời gian
đáng kể. Ngời ta cải tiến các phơng pháp này bằng cách chỉ phân phối run một lần ban đầu,
sau đó chỉ thực hiện các phép trộn liên tiếp cho đến khi nhận đợc tệp sắp xếp.
Để làm điều này ngời ta sử dụng m tệp nguồn F = {tepN[0], tepN[1], , tepN[m-1]} và m
tệp đích G = {tepD[0], tepD[1], , tepD[m-1]}. ở bớc đầu tiên các run trong tệp A đợc lần
lợt phân phối cho m tệp nguồn theo cách: run đầu tiên đợc phân phối cho tepN[0], run thứ
hai đợc phân phối cho tepN[1], cứ nh vậy. Nếu có nhiều hơn m run thì sau khi phân phối
run cho tệp tepN[m-1] quá trình phân phối lại quay lại tepN[0], và cứ nh vậy cho đến khi toàn
bộ run đợc phân phối. Thay vì trộn các run trở lại tệp A, ngời ta tạo ra các tệp G = {tepD[0],
tepD[1], , tepD[m-1]} sau đó trộn từng bộ m run nằm ở đầu các tệp nguồn thành một run và
lần lợt ghi vào các tệp đích. Sau khi hoàn tất việc trộn, ngời ta hoán đổi vai trò của các tệp
nguồn và tệp đích. Các tệp đích trở thành các tệp nguồn và các tệp nguồn trở thành các tệp đích.
Thông thờng sau khi trộn thì số tệp đích giảm xuống. Cho đến khi số tệp đích là 1 thì quá trình

sắp xếp hoàn tất.
Giả sử dãy ban đầu các phần tử trong tệp A đợc cho nh sau:

A
2 1211 10 9 876543

Giả sử ta chọn 3 đờng cần bằng và phân bổ tệp A vào 3 tệp nguồn nh sau:

F1
2 12 9 6 3

F2
11 8 5

F3
10 7 4

Lần lợt trộn các bộ 3 run ở các tệp và lần lợt ghi vào các tệp đích G1, G2, G3 ta đợc

G1
2 10 11 12 3

G2
7 8 9

G3
4 5 6

Bây giờ ta lại xem các tệp G1, G2, G3 là các tệp nguồn và trộn các run vào các tệp đích F1, F2,
F3 và đợc kết quả là:


F1
2 4 5 6 7 8 9 10 11 12

F2
3

Trng Hi Bng-Cu trỳc d liu 2

21

Ta thấy rằng số tệp đích đã giảm đi 1. Bây giờ trộn F1 và F2 ta đợc một tệp đích duy nhất
G1:
G1
2 3 4 5 6 7 8 9 10 11 12
Vì chỉ có duy nhất một tệp đích, điều này cũng có nghĩa là chỉ có một run đợc tạo ra (vì nếu có
hai run trở lên thì G2 sẽ đợc phân phối), và nh vậy tệp G1 đã đợc sắp xếp.


Nếu mô tả thuật toán trên bằng giả ngôn ngữ C (tức là ngôn ngữ gần giống với C chủ yếu để
hiểu đợc ý tởng thuật toán) ta có thể viết gọn nh sau:
Phân bổ tệp A cho các tệp nguồn tepN[0], tepN[1], , tepN[m-1]
while(true)
{ - Lần lợt trộn các bộ gồm m run trên các tệp nguồn và lần lợt phân bổ vào các tệp đích
tepD[0], tepD[1], , tepD[m-1], mỗi lần chỉ phân bổ một run cho một tệp đích (lần trộn
run cuối cùng thì số run tham gia có thể nhỏ hơn m).
- Đặt m = số tệp đích đã đợc phân bổ run. Nếu m = 1 thì kết thúc. Nếu không thì đổi vai
trò các tệp đích và nguồn: các tệp tepD[0], tepD[1], , tepD[m-1] trở thành các tệp
nguồn, còn các tệp tepN[0], tepN[1], , tepN[m-1] trở thành các tệp đích rồi thực hiện
lại vòng lặp.

}

1.4.2. Cài đặt chơng trình
Chơng trình 13SXF.CPP sau đây cài đặt thuật toán sắp xếp một tệp bằng phơng pháp trộn đa
lối cân bằng vừa trình bày ở trên.
Phần khai báo chung, khai báo nguyên mẫu hàm và hàm main:

//13SXF.CPP Sap xep file bang phuong phap tron da loi can bang
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
#define true 1
#define false 0
#define sz (sizeof(float))
//=============================================
int FileNodes(char *TenTep);//So phan tu tren tep
int EoF(FILE *f);//Cuoi tep
int EoR(FILE *f);//Cuoi Run
void CreateFile(char *TenTep);
void ViewFile(char *TenTep);
void SplitFile(char *TenTep,char *TepNguon[], int &nWay);
void MergeFile(char *TepNguon[],char *TepDich[], int &nWay);
int SortedFile(char *TenTep);
void CopyFile(char *Tep1, char *Tep2);
void SortFile(char *TenTep);
/*TenTep la ten tep can sap xep, TepNguon[i] la ten cac tep nguon
TepDich[i] la ten cac tep dich
*/
//=====================

void main()

Trng Hi Bng-Cu trỳc d liu 2

22
{clrscr();
char *TenTep;
TenTep = new char[12];
strcpy(TenTep,"A.DAT");
CreateFile(TenTep);
ViewFile(TenTep);
SortFile(TenTep);
printf("\n\nTep sau khi sap xep la:");
ViewFile(TenTep);
getch();
};

Hàm void SplitFile(char *TenTep,char *TepNguon[], int &nWay) phân bổ các run trên tệp
cần sắp xếp có tên là TenTep (ta tạm gọi đơn giản là tệp A) vào các tệp nguồn ban đầu có các
tên là TepNguon[0], TepNguon[1], , TepNguon[nWay-1]. nWay chính là số đờng cân bằng
ban đầu. Ta khai báo biến nWay theo kiểu tham chiếu vì ta muốn rằng sự thay đổi giá trị của
nWay bên trong hàm sẽ đợc giữ nguyên khi ra ngoài. Ta có thể thấy rằng nếu số run trên tệp A
ít hơn số đờng cân bằng thì sau khi chạy hàm này số đờng cân bằng sẽ giảm xuống bằng số
tệp nguồn đợc phân phối run. Các lệnh
for(i=0;i<nWay && FileNodes(TepNguon[i])>0;i++);
nWay=i;
ở cuối hàm đếm các tệp ngồn có chứa phần tử và gán cho nWay.
Vì các tệp nguồn chỉ chứa các run đợc phân phối nên chúng đợc mở mới. Tệp A dĩ nhiên là
đợc mở chỉ đọc, vì nó chứa các phần tử cần phân bổ. Việc dùng lệnh rewind() ở đầu hàm có vẻ
không cần thiết vì khi mới mở thì con trỏ tệp luôn ở đầu tệp, nhng đôi khi thao tác này lại cần

thiết nh một sự kích hoạt vậy.
Quá trình phân bổ chỉ kết thúc khi tệp A hết phần tử. Lệnh
while(!EoF(a))
chỉ rõ điều này.
Chúng ta hạn chế số đờng trộn là nWay 10. Chúng ta dùng một mảng con trỏ tệp để tạo ra và
thao tác với các tệp nguồn. Lệnh khai báo mảng các con trỏ tệp thao tác các tệp nguồn và con trỏ
tệp để mở tệp A là:
FILE *f[10], *a;
Bởi vì vị trí bắt đầu của một run mới trên A cũng là vị trí EoR của run trớc đó, do đó khi bắt
đầu ghi một run lên tệp nguồn ta cha kiểm tra ngay điều kiện EoR. Vậy thao tác ghi một run lên
tệp f[i] đợc bắt đầu bằng việc kiểm tra xem còn phần tử trên A không. Nếu hết thì thoát khỏi
vòng lặp và sau đó kết thúc việc phân phối. (Điều kiện EoF(a) đúng tức là ! EoF(a) sai và vòng
lặp kết thúc). Lệnh
for(i=0;i<nWay;i++)
chỉ ra quá trình phân phối run cho các tệp f[0], f[1], , f[nWay-1].
Khi bắt đầu ghi một run, ta kiểm tra và nếu còn phần tử trên A thì đọc phần tử đầu tiên của run
trên A và ghi vào f[i]. Điều này thể hiện trong các lệnh:
if(EoF(a)) break;
fread(&x,sz,1,a);
fwrite(&x,sz,1,f[i]);
sau khi ghi xong phần tử đầu tiên của run, ta tiếp tục đọc các phần tử trong run hiện thời trên A
và ghi vào tệp f[i]:
while(!EoR(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,f[i]);
}

Trng Hi Bng-Cu trỳc d liu 2

23

Khi gặp EoR trên tệp A thì quá trình ghi run vào f[i] kết thúc và chuyển sang ghi run tiếp theo
lên f[i+1] nếu i+1<nWay. Nếu i =nWay -1 tức là ta đã ghi run vào tệp nguồn cuối thì quay lại
vòng lặp while(!EoF(a)) và bắt đầu chu kỳ mới với i = 0 là điểm xuất phát.
Cuối cùng ta đóng tất cả các tệp và tính lại nWay. Thông thờng nWay có xu hớng giảm dần
đến 1.
void SplitFile(char *TenTep,char *TepNguon[], int &nWay)
{
FILE *f[10], *a;float x;int i,j,k;
for(i=0;i<nWay;i++) f[i] = fopen(TepNguon[i],"wb");
a = fopen(TenTep,"rb");
rewind(a);
for(i=0;i<nWay;i++) rewind(f[i]);

while(!EoF(a))
{//Chia xoay vong lan luot cac run tren a cho f[0],f[1], ,f[nWay-1]
//hoac chi den f[k] nao do neu het phan tu tren a
for(i=0;i<nWay;i++)
{if(EoF(a)) break;
fread(&x,sz,1,a);
fwrite(&x,sz,1,f[i]);
while(!EoR(a))
{fread(&x,sz,1,a);
fwrite(&x,sz,1,f[i]);
}
}
}
fclose(a);
for(i=0;i<nWay;i++) fclose(f[i]);
for(i=0;i<nWay && FileNodes(TepNguon[i])>0;i++);
nWay=i;

};

Hàm void MergeFile(char *TepNguon[10],char *TepDich[10], int &nWay) thực hiện lần lợt
trộn các bộ gồm nWay run lấy từ TepNguon[0], TepNguon[1], , TepNguon[nWay-1] và ghi
vào các tệp đích TepDich[0], TepDich[1], , TepDich[nWay-1]. Nh ta đã thấy, các tệp nguồn
ở cuối có thể có số run ít hơn 1 so với số các run ở các tệp ở đầu, do đó lần trộn cuối có thể
không có đủ nWay run.
Thao tác trộn đợc thực hiện nh sau: ta so sánh phần tử ở đầu các run hiện thời và chọn ra
phần tử nhỏ nhất để ghi lên tệp đích hiện thời. Để có thể so sánh, ta phải đổ các giá trị ở đầu các
run vào bộ nhớ và lu trong mảng x[0], x[1], , x[nWay-1]. Tuy ở bớc trộn cuối cùng có thể
không có đủ nWay phần tử. Ngay trong các bớc trộn có đầy đủ run thì cũng có trờng hợp
một số run sẽ hết phần tử trớc. Ví dụ giả sử nWay = 3 và 3 run hiện thời là {1,2,3},
{4,5,6,7} và {8,9} thì ban đầu ta có x[0] = 1, x[1] = 4 và x[2] = 8. Ta thấy rằng x[0] là nhỏ
nhất và ghi giá trị này lên tệp đích, sau đó đọc giá trị tiếp theo của run vào x[0] và nhận đợc
x[0] = 2. ở bớc thứ 2 ta cần chọn giá trị nhỏ nhất trong 3 số x[0] = 2, x[1] = 4 và x[2] = 8.
Ta thấy rằng x[0] đợc chọn ghi lên tệp đích và giá trị tiếp theo của run đợc gán cho x[0] và
ta lại có x[0] = 3, x[1] = 4 và x[2] = 8. Tại bớc này ta ghi x[0]=3 lên tệp đích, nhng run hiện
thời đã hết phần tử nên ta phải vô hiệu hóa giá trị x[0] mà chỉ so sánh x[1] với x[2]. Để có thể
đánh dấu một run hiện thời nào đó đã hết phần tử, ta dùng một mảng con_x[i] (có nghĩa là còn
x[i]). Nếu còn phần tử ở run hiện thời trên tệp nguồn i thì ta đặt con_x[i] = true, nếu đã hết thì
ta đặt con_x[i] = false.
Bắt đầu quá trình trộn, chúng ta phải mở các tệp nguồn theo kiểu chỉ đọc, còn các tệp đích đợc
tạo mới. Biến CurrDich là chỉ số của tệp đích hiện thời. Ban đầu CurrDich=0, tức là g[0] sẽ là

Trng Hi Bng-Cu trỳc d liu 2

24
tệp đích đầu tiên nh thuật toán đã chỉ ra. Sau mỗi lần trộn và run đợc ghi vào tệp đích hiện thời
thì CurrDich tăng lên 1, nghĩa là tệp đích hiện thời sẽ là tệp tiếp theo của tệp đích hiện thời trớc
đó. Tuy nhiên nếu tệp đích trớc đó là tệp cuối cùng thì chu kỳ sẽ quay lại từ đầu, tức là

CurDich=0.
Để bắt đầu trộn một bộ run chúng ta sẽ đọc các phần tử đầu mỗi run và gán vào mảng x[i]. Tuy
nhiên nh ta đã thấy, có thể chỉ có một số run còn phần tử,. do đó ta phải kiểm tra xem việc đọc
có thành công không. Nếu thành công thì gán con_x[i]=true, còn nếu không thì gán
con_x[i]=false.
Chúng ta dùng biến nNguon để đếm số lần đọc thành công. Nếu nNguon=0 có nghĩa là tất cả
các lần đọc đều không thành công, và nh vậy thì có nghĩa là đã hết dữ liệu trên các file nguồn và
quá trình trộn kết thúc.
Đoạn lệnh sau thể hiện những điều vừa nói:
j=0;
for(i=0;i<nWay;i++)
if(fread(&t,sz,1,f[i])>0)
{x[i]=t;con_x[i]=true;j++;}
else
con_x[i]=false;
nNguon=j;
if(nNguon==0) break;//Da het cac phan tu tren cac file nguon
Sau bớc khởi đầu này ta bắt đầu thực hiện vòng lặp thực hiện việc trộn các run hiện thời và ghi
lên tệp đích hiện thời. Điều kiện dừng của vòng lặp khá phức tạp nên ta tạm đặt là while(true).
Bên trong thân vòng lặp sẽ có điều kiện để thoát khỏi vòng lặp.
Khi bắt đầu trộn run thì có một số phần tử trong mảng x[i]. Tuy nhiên trong quá trình trộn các
phần tử đợc ghi vào tệp đích phần tử mới đợc đọc từ run tơng ứng để thay thế nếu run vẫn
còn. Do vậy rất có thể đến lúc nào đó trong mảng x[i] sẽ không còn phần tử nào cả, khi đó việc
trộn run hiện thời sẽ kết thúc.
Để chọn phần tử ghi vào tệp đích hiên thời, ta cần chọn k sao cho x[k] là phần tử nhỏ nhất
trong các phần tử của mảng x[i] đã đợc đọc (con_x[i] = true). Theo thông thờng thì để xác
định min của một mảng đầu tiên ta chọn phần tử đầu tiên của mảng và coi là phần tử nhỏ nhất.
sau đó ta bắt đầu duyệt từ phần tử thứ 2 cho đến phần tử cuối cùng, mỗi lần gặp phần tử nhỏ hơn
min hiện thời thì ta lại thay thế min hiện thời bằng phần tử đó. Tuy nhiên trong trờng hợp
chúng ta đang xét thì có thể phần tử x[0], x[1] đã đợc ghi và không còn tham gia vào quá trình

chọn (con_x[i]= false). Do đó chúng ta phải quét từ đầu mảng và tìm ra vị trí đầu tiên mà x[i]
cha sử dụng. Nếu không tìm ra vị trí này thì có nghĩa là run đã hết. CurrDich tăng lên 1, nghĩa
là tệp đích hiện thời sẽ là tệp tiếp theo của tệp đích hiện thời trớc đó. Lúc này ta kết thúc vòng
lặp trộn run hiện thời và chuyển đến tệp đích tiếp theo. Tuy nhiên nếu đó là tệp cuối cùng thì chu
kỳ sẽ quay lại từ đầu, tức là CurDich=0.
Sau đây là đoạn lệnh thực hiện những điều vừa nói:
i=0;//Tim vi tri dau tien con phan tu trong run
while(!con_x[i] && i<nWay)i++;//Tim run dang con phan tu
if(i==nWay) //Het phan tu trong bo run, chuyen sang bo run khac
{CurrDich++;CurrDich = CurrDich%nWay;break;}
ở đây ta thấy rằng nếu i = nWay thì có nghĩa là tất cả con_x[j], j =0,1, ,nWay-1 đều là false.
Ngợc lại nếu i<nWay thì vị trí này chính là vị trí đầu tiên mà con_x[i] = true. Ta cần chọn vị trí
k mà con_x[k] true và x[k] là bé nhất. Ban đầu tạm đặt min tạm thời là t=x[i] và nh vậy
k=i. Đoạn lệnh sau sẽ duyệt qua phần còn lại của mảng x[j] và tìm ra vị trí chứa phần tử bé nhất.
t = x[i];k=i;
for(j=i+1;j<nWay;j++)
if(con_x[j] && x[j]<t) {t=x[j];k=j;}
Sau khi xác định đợc phần tử bé nhất, ta ghi nó vào tệp đích hiện thời bằng lệnh:
fwrite(&t,sz,1,g[CurrDich]);

Trng Hi Bng-Cu trỳc d liu 2

25
Sau khi ghi x[k] ta kiểm tra xem run hiện thời trên tệp nguồn f[k] có còn phần tử không. Nếu có
thì đọc phần tử của run vào x[k] và vẫn giữ nguyên giá trị con_x[k] = true. Nếu đã hết run thì ta
đánh dấu vị trí này bằng lệnh con_x[k]=false;
if(EoR(f[k]))
con_x[k]=false;
else
{fread(&t1,sz,1,f[k]);

x[k]=t1;
}
Quá trình trên đây đợc thực hiện cho đến khi hết tất cả các phần tử trên các tệp nguồn.
Khi việc trộn đã hoàn tất, có thể số tệp đích sẽ giảm xuống và ta cần tính lại. Ta dùng lệnh
for(i=0;i<nWay && FileNodes(TepDich[i])>0;i++); nWay=i;
để thực hiện điều này. ở đây ta vẫn cần điều kiện i<nWay để loại trừ khả năng có những tệp
cùng tên nhng không phải là các tệp chúng ta quan tâm.
void MergeFile(char *TepNguon[10],char *TepDich[10], int &nWay)
{FILE *f[10], *g[10];float x[10],t,t1;
int i,j,k,con_x[10],nNguon,CurrDich;
/*Neu run tren f[i] chua het thi con_x[i] =true
Neu het roi thi con_x[i] = false
nNguon la so file nguon co phan tu
nDich la so File dich da duoc cap phan tu
CurrDich la file dich hien thoi duoc ghi run
*/
for(i=0;i<nWay;i++)
{f[i] = fopen(TepNguon[i],"rb");
rewind(f[i]);
g[i] = fopen(TepDich[i],"wb");
}

CurrDich=0;
while(true) //Se ket thuc khi het cac phan tu tren file nguon
{//Vi cac run duoc phan phoi duoc phan bo tu i = 0 do do co the co
//mot so tep o phan cuoi co it hon 1 run
j=0;
for(i=0;i<nWay;i++)
if(fread(&t,sz,1,f[i])>0)
{x[i]=t;con_x[i]=true;j++;}

else
con_x[i]=false;

nNguon=j;
if(nNguon==0) break;//Da het cac phan tu tren cac file nguon
while(true) //Bat dau tron cac run va dua vao g[CurrDich]
{//Tim phan tu be nhat o dau run trong bo run
i=0;//Tim vi tri dau tien con phan tu trong run
while(!con_x[i] && i<nWay)i++;//Tim run dang con phan tu
if(i==nWay) //Het phan tu trong bo run, chuyen sang bo run khac
{CurrDich++;CurrDich = CurrDich%nWay;break;}
t = x[i];k=i;
for(j=i+1;j<nWay;j++)
if(con_x[j] && x[j]<t) {t=x[j];k=j;}
//x[k] chinh la gia tri nho nhat trong bo run hien thoi
fwrite(&t,sz,1,g[CurrDich]);

×