Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3Giáo trình Cấu trúc dữ liệu và giải thuật 3
TỔNG QUAN VỀ CẤU TRÚC DỮ LIỆU & GT
Đánh giá Cấu trúc dữ liệu & Giải thuật
Chương 1: TỔNG QUAN VỀ CẤU TRÚC DỮ LIỆU VÀ GIẢI THUẬT
1.1 Tầm quan trọng của cấu trúc dữ liệu và giải thuật trong một đề án tin học
1.1.1 Xây dựng cấu trúc dữ liệu
Trong mọi chương trình máy tính đều có dữ liệu để xử lý, gồm dữ liệu đầu vào, dữ liệu trung gian và dữ liệu đầu ra Việc tổ chức và lưu trữ dữ liệu một cách hợp lý là yếu tố then chốt cho toàn bộ hệ thống phần mềm, giúp tối ưu hóa hiệu suất và dễ bảo trì Xây dựng cấu trúc dữ liệu phù hợp quyết định chất lượng của phần mềm cũng như mức độ khó khăn và công sức mà lập trình viên bỏ ra trong quá trình thiết kế và triển khai chương trình.
Giải thuật, hay thuật toán, là tập hợp các bước có trật tự nhằm giải quyết một vấn đề một cách có thể tái sử dụng Nó có thể được minh họa bằng ngôn ngữ tự nhiên, bằng sơ đồ luồng (flow chart) hoặc bằng mã giả (pseudo code) để mô tả trình tự xử lý Trong thực tế, giải thuật thường được thể hiện dưới dạng mã giả hoặc dưới một ngôn ngữ lập trình mà người lập trình chọn để triển khai, ví dụ như C, Pascal hoặc các ngôn ngữ khác Nhờ việc chuyển đổi giải thuật sang mã nguồn, nó có thể được triển khai thành một chương trình máy tính chạy được.
Ngay khi xác định được cấu trúc dữ liệu phù hợp, người lập trình bắt đầu xây dựng thuật giải đáp ứng yêu cầu của bài toán dựa trên cấu trúc dữ liệu đã chọn Trong quá trình giải quyết, có thể có nhiều phương pháp khác nhau, vì vậy việc cân nhắc và tính toán để lựa chọn phương pháp phù hợp là cần thiết, bởi lựa chọn tối ưu có thể ảnh hưởng tới hiệu quả và tính đúng đắn của lời giải Lựa chọn phương pháp cũng có thể góp phần đáng kể vào việc giảm khối lượng công việc của người lập trình khi triển khai thuật toán trên một ngôn ngữ cụ thể, tiết kiệm thời gian và công sức cho quá trình cài đặt.
1.1.3 Mối quan hệ giữa cấu trúc dữ liệu và giải thuật
Mối quan hệ giữa cấu trúc dữ liệu và Giải thuật có thể minh họa bằng đẳng thức:
Cấu trúc dữ liệu + Giải thuật = Chương trình
Với cấu trúc dữ liệu tốt và nắm vững giải thuật, việc triển khai chương trình bằng một ngôn ngữ lập trình cụ thể chỉ còn là vấn đề thời gian Khi có cấu trúc dữ liệu mà chưa tìm ra giải thuật sẽ không thể có chương trình, và ngược lại, một giải thuật đúng vẫn chưa thể áp dụng nếu chưa có cấu trúc dữ liệu phù hợp Một chương trình máy tính thực sự hoàn thiện khi có đầy đủ cả cấu trúc dữ liệu để lưu trữ dữ liệu và giải thuật để xử lý dữ liệu theo yêu cầu của bài toán.
1.2 Đánh giá cấu trúc dữ liệu và giải thuật
1.2.1 Các tiêu chuẩn đánh giá cấu trúc dữ liệu Để đánh giá một cấu trúc dữ liệu chúng ta thường dựa vào một số tiêu chí sau:
- Cấu trúc dữ liệu phải tiết kiệm tài nguyên (bộ nhớ trong),
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
- Cấu trúc dữ liệu phải phản ảnh đúng thực tế của bài toán,
- Cấu trúc dữ liệu phải dễ dàng trong việc thao tác dữ liệu
1.2.2 Đánh giá độ phức tạp của thuật toán
Việc đánh giá độ phức tạp của một thuật toán không phải dễ dàng Mục tiêu là ước lượng thời gian thực thi T(n) để so sánh tương đối các thuật toán với nhau Thời gian thực thi thực tế phụ thuộc vào nhiều yếu tố như cấu hình máy tính và dữ liệu đầu vào, nên ở đây chúng ta chỉ xem xét thời lượng dựa trên kích thước dữ liệu ban đầu Để ước lượng thời gian thực thi, ta phân tích hai trường hợp phổ biến khi kích thước dữ liệu n biến đổi, từ đó xác định được quan hệ giữa n và T(n) và hỗ trợ quyết định chọn thuật toán phù hợp.
- Trong trường hợp tốt nhất: Tmin
- Trong trường hợp xấu nhất: Tmax
Kiểu dữ liệu
1.3.1 Khái niệm về kiểu dữ liệu
Kiểu dữ liệu T có thể xem như là sự kết hợp của 2 thành phần:
- Miền giá trị mà kiểu dữ liệu T có thể lưu trữ: V,
- Tập hợp các phép toán để thao tác dữ liệu: O
Mỗi kiểu dữ liệu được đại diện bởi một tên định danh Mỗi phần tử dữ liệu có kiểu T sẽ có giá trị thuộc miền V và có thể thực hiện các phép toán thuộc tập hợp O của các phép toán Để lưu trữ các phần tử dữ liệu này trong bộ nhớ, cần cấp phát một số byte, và số byte đó gọi là kích thước của kiểu dữ liệu.
1.3.2 Các kiểu dữ liệu cơ sở
Hầu hết các ngôn ngữ lập trình đều cung cấp các kiểu dữ liệu cơ sở để biểu diễn các giá trị quen thuộc như số nguyên, số thực, ký tự và chuỗi ký tự, cùng với kiểu boolean cho các điều kiện, và ở nhiều ngôn ngữ còn có các biến thể như byte hay phạm vi ký tự Tùy vào ngôn ngữ, tên gọi của các kiểu dữ liệu cơ sở có thể khác nhau, nhưng chúng nhìn chung được phân thành các nhóm cơ bản như số nguyên (int, long), số thực (float, double), ký tự (char) và chuỗi (string), cũng như kiểu logic (bool) Hiểu rõ sự khác biệt và phạm vi của từng kiểu dữ liệu cơ sở giúp viết mã hiệu quả hơn, tối ưu hóa bộ nhớ và hiệu suất, đồng thời tăng khả năng tương thích và dễ bảo trì cho phần mềm.
- Kiểu số nguyên: Có thể có dấu hoặc không có dấu và thường có các kích thước sau: + Kieồu soỏ nguyeõn 1 byte
Kiểu số nguyên thường được thực hiện với các phép toán: O = {+, -, *, /, DIV, MOD, M[Mid]: Rút ngắn phạm vi tìm kiếm về nửa sau của dãy M (First = Mid+1)
Để tìm một phần tử có giá trị X trong một mảng, ta lặp lại quá trình tìm kiếm cho đến khi X được tìm thấy hoặc phạm vi tìm kiếm (từ First đến Last) không còn nữa, nghĩa là First vượt quá Last Trong cùng một mục tiêu, thuật toán đệ quy (Recursion Algorithm) thực hiện bằng cách chia nhỏ phạm vi tìm kiếm và gọi đệ quy cho phần còn lại cho đến khi tìm thấy X hoặc phạm vi tìm kiếm trở nên rỗng.
B3: IF (First > Last) //Hết phạm vi tìm kiếm
B5.1: Tìm thấy tại vị trí Mid
Tìm đệ quy từ First đến Last = Mid – 1
Tìm đệ quy từ First = Mid + 1 đến Last
Bkt: Keát thuùc c Cài đặt thuật toán đệ quy:
Hàm BinarySearch có prototype: int BinarySearch (T M[], int N, T X);
BinarySearch là hàm thực hiện tìm kiếm phần tử có giá trị X trong mảng M có N phần tử đã được sắp xếp tăng dần Nếu tìm thấy, hàm trả về một chỉ số nguyên từ 0 đến N-1 cho biết vị trí của phần tử X; nếu không tìm thấy, hàm trả về -1 Hàm BinarySearch sử dụng hàm đệ quy RecBinarySearch có prototype: int RecBinarySearch(T M[], int First, int Last, T X); trong đó RecBinarySearch duy trì phạm vi tìm kiếm từ First tới Last và so sánh X với phần tử ở vị trí giữa để thu hẹp khoảng tìm kiếm cho kết quả nhanh hơn.
Hàm RecBinarySearch thực hiện việc tìm kiếm phần tử có giá trị X trong mảng M, trong phạm vi từ phần tử thứ First đến phần tử thứ Last Nếu tìm thấy, hàm trả về một số nguyên nằm trong khoảng từ First đến Last chính là vị trí tương ứng của phần tử tìm thấy Ngược lại, khi không tìm thấy phần tử X, hàm trả về giá trị –1 để chỉ ra sự vắng mặt của phần tử trong phạm vi được xét Nội dung của các hàm như sau:
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật int RecBinarySearch (T M[], int First, int Last, T X)
{ if (First > Last) return (-1); int Mid = (First + Last)/2; if (X == M[Mid]) return (Mid); if (X < M[Mid]) return(RecBinarySearch(M, First, Mid – 1, X)); else return(RecBinarySearch(M, Mid + 1, Last, X));
} d Phân tích thuật toán đệ quy:
- Trường hợp tốt nhất khi phần tử ở giữa của mảng có giá trị bằng X:
Số phép so sánh: Smin = 2
- Trường hợp xấu nhất khi không tìm thấy phần tử nào có giá trị bằng X:
Số phép gán: Gmax = log2N + 1
Số phép so sánh: Smax = 3log2N + 1
Số phộp gỏn: Gavg = ẵ log2N + 1
Số phộp so sỏnh: Savg = ẵ(3log2N + 3) e Thuật toán không đệ quy (Non-Recursion Algorithm):
B5.1: Tìm thấy tại vị trí Mid
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật f Cài đặt thuật toán không đệ quy:
Hàm NRecBinarySearch có prototype: int NRecBinarySearch (T M[], int N, T X);
Hàm NRecBinarySearch thực hiện việc tìm kiếm phần tử có giá trị X trong mảng M gồm N phần tử đã được sắp xếp tăng dần Quá trình tìm kiếm sử dụng phương pháp nhị phân và được triển khai ở dạng đệ quy, trả về chỉ số của phần tử tìm thấy với giá trị từ 0 đến N-1, tương ứng với vị trí của phần tử trong mảng Nếu X không tồn tại trong M, hàm trả về -1 để cho biết không tìm thấy Khai báo hàm: int NRecBinarySearch (T M[], int N, T X).
{ int First = 0; int Last = N – 1; while (First Last Mid M[Mid] X M[Mid]
Ban đầu 0 9 False 4 8 False True False
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Kết quả sau 3 lần lặp (đệ quy) thuật toán kết thúc
- Bây giờ ta thực hiện tìm kiếm phần tử có giá trị X = 7 (không tìm thấy):
Lần lặp First Last First > Last Mid M[Mid] X M[Mid]
Ban đầu 0 9 False 4 8 False True False
Kết quả sau 4 lần lặp (đệ quy) thuật toán kết thúc
Thuật toán tìm nhị phân chỉ có thể vận dụng khi dãy hoặc mảng đã được sắp xếp Trong trường hợp tổng quát, dữ liệu không có thứ tự sẽ không cho phép áp dụng tìm kiếm nhị phân và chúng ta buộc phải dùng thuật toán tìm kiếm tuần tự (tìm kiếm tuyến tính) Do đó, với dữ liệu bất kỳ, tìm kiếm tuần tự là lựa chọn mặc định để đảm bảo kết quả đúng mà không cần sắp xếp trước.
Thuật toán đệ quy ngắn gọn và dễ hiểu, nhưng lại tốn bộ nhớ vì mỗi lần gọi đệ quy phải ghi nhận trạng thái trên ngăn xếp chương trình, có thể khiến chương trình chậm khi xử lý dữ liệu lớn Trong thực tế, khi viết mã, nếu có thể nên ưu tiên thuật toán phi đệ quy (thuật toán lặp) để giảm tiêu thụ bộ nhớ và nâng cao hiệu suất Tuy đệ quy vẫn hữu ích cho các bài toán có cấu trúc tự nhiên và đòi hỏi ít độ sâu đệ quy, sự cân nhắc giữa tính dễ đọc và hiệu suất là điều cần xem xét khi thiết kế thuật toán.
Các giải thuật tìm kiếm ngoại
Giả sử chúng ta có một tập tin F lưu trữ N phần tử Vấn đề đặt ra là có hay không phần tử có giá trị X được lưu trữ trong F; nếu có thì phần tử có giá trị X nằm ở vị trí nào trên tập tin F? Ta có thể thực hiện tìm kiếm bằng cách quét lần lượt từ đầu đến cuối và kiểm tra từng phần tử để xem nó có bằng X hay không Nếu phát hiện X, ta trả về chỉ số tương ứng của phần tử đó trong F (tức là i sao cho F[i] = X); nếu không tìm thấy, kết quả cho biết X không tồn tại trong F Câu trả lời này có thể được mở rộng bằng các kỹ thuật tìm kiếm khác nhau tùy thuộc vào cấu trúc lưu trữ và yêu cầu về hiệu năng.
2.3.2 Tìm tuyeán tính a Tư tưởng:
Để tìm giá trị X trong tập tin F, ta lần lượt đọc các phần tử từ đầu tập tin và so sánh với X cho đến khi gặp phần tử có giá trị X hoặc cho đến khi đọc hết tập tin F Đây là một thuật toán tìm kiếm tuyến tính trên tập tin F: bắt đầu từ phần tử đầu tiên, so sánh từng phần tử với X và dừng lại khi có sự khớp hoặc khi kết thúc dữ liệu Kết quả trả về vị trí của X nếu được tìm thấy hoặc thông báo không tìm thấy nếu X không có trong F.
B2: rewind(F) //Về đầu tập tin F
B3: read(F, a) //Đọc một phần tử từ tập tin F
B4: k = k + sizeof(T) //Vị trí phần tử hiện hành (sau phần tử mới đọc) B5: IF a ≠ X AND !(eof(F))
Tìm thấy tại vị trí k byte(s) tính từ đầu tập tin
Không tìm thấy phần tử có giá trị X
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B8: Keát thuùc c Cài đặt thuật toán:
Hàm FLinearSearch có prototype: long FLinearSearch (char * FileName, T X);
FLinearSearch là hàm thực hiện tìm kiếm phần tử có giá trị X trong tập tin có tên FileName Nếu tìm thấy, hàm trả về một số nguyên kiểu long cho biết vị trí của phần tử so với đầu tập tin, được tính bằng số byte và nằm trong phạm vi từ 0 đến filelength(FileName) Trong trường hợp không tìm thấy hoặc gặp lỗi thao tác trên tập tin, hàm trả về giá trị -1 Cú pháp của hàm là long FLinearSearch(char * FileName, T X).
Fp = fopen(FileName, “rb”); if (Fp == NULL) return (-1); long k = 0;
T a; int SOT = sizeof(T); while (!feof(Fp))
{ if (fread(&a, SOT, 1, Fp) == 0) break; k = k + SOT; if (a == X) break;
} fclose(Fp); if (a == X) return (k - SOT); return (-1);
- Trường hợp tốt nhất khi phần tử đầu tiên của tập tin có giá trị bằng X:
Số phép so sánh: Smin = 2 + 1 = 3
Số lần đọc tập tin: Dmin = 1
- Trường hợp xấu nhất khi không tìm thấy phần tử nào có giá trị bằng X:
Số phép so sánh: Smax = 2N + 1
Số lần đọc tập tin: Dmax = N
Số phép so sánh: Savg = (3 + 2N + 1) : 2 = N + 2
Số lần đọc tập tin: Davg = ẵ(N + 1)
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật 2.3.3 Tỡm kieỏm theo chổ muùc (Index Search)
Như đã biết, mỗi phần tử dữ liệu lưu trữ trong tập tin dữ liệu F có kích thước lớn, khiến tập tin F cũng trở nên lớn và việc thao tác trực tiếp lên dữ liệu trở nên chậm và tiềm ẩn mất mát dữ liệu Để giải quyết vấn đề này, tập tin dữ liệu thường đi kèm với các tập tin chỉ mục (Index File) để điều khiển thứ tự truy xuất dữ liệu trên tập tin theo một khóa chỉ mục (Index key) Mỗi phần tử trong tập tin chỉ mục IDX gồm hai thành phần: khóa chỉ mục và vị trí vật lý của phần tử dữ liệu có khóa tương ứng trên tập tin dữ liệu Cấu trúc dữ liệu của các phần tử trong tập tin chỉ mục được định nghĩa như typedef struct IdxElement.
Trong hệ thống lưu trữ dữ liệu, tập tin chỉ mục luôn được sắp xếp theo thứ tự tăng của khóa chỉ mục để tối ưu hóa truy vấn và quản lý dữ liệu Việc tạo tập tin chỉ mục IDX sẽ được nghiên cứu trong Chương 3; ở phần này chúng ta xem như đã có tập tin IDX để thao tác a Tư tưởng:
Thuật toán lần lượt đọc các phần tử từ đầu tập tin IDX và so sánh khóa chỉ mục của từng phần tử với giá trị X cho đến khi gặp phần tử có khóa chỉ mục lớn hơn hoặc bằng X hoặc cho đến khi kết thúc IDX Nếu tìm thấy phần tử thỏa mãn điều kiện, ta có vị trí vật lý của phần tử dữ liệu trên tập tin dữ liệu F và có thể truy cập trực tiếp đến vị trí này để đọc dữ liệu của phần tử đó Trường hợp không tìm thấy phần tử phù hợp trước khi kết thúc IDX, quá trình tìm kiếm kết thúc mà không có vị trí vật lý được xác định Thuật toán này mô tả tra cứu tuần tự trên IDX và cho phép truy cập dữ liệu trên tập tin F một cách trực tiếp khi điều kiện tìm kiếm được đáp ứng.
B3: IF ai.IdxKey < X AND !(eof(IDX))
Tìm thấy tại vị trí ai.Pos byte(s) tính từ đầu tập tin
Không tìm thấy phần tử có giá trị X
B6: Keát thuùc c Cài đặt thuật toán:
Hàm IndexSearch có prototype: long IndexSearch (char * IdxFileName, T X);
Hàm thực hiện tìm kiếm phần tử có giá trị X dựa trên tập tin chỉ mục có tên IdxFileName Nếu tìm thấy phần tử, hàm trả về một số nguyên từ 0 đến filelength(FileName)-1, là vị trí tương ứng của phần tử tìm thấy so với đầu tập tin dữ liệu (tính bằng byte) Trong trường hợp không tìm thấy hoặc gặp lỗi khi thao tác trên tập tin chỉ mục, hàm trả về -1.
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật long IndexSearch (char * IdxFileName, T X)
IDXFp = fopen(IdxFileName, “rb”); if (IDXFp == NULL) return (-1);
IdxType ai; int SOIE = sizeof(IdxType); while (!feof(IDXFp))
{ if (fread(&ai, SOIE, 1, IDXFp) == 0) break; if (ai.IdxKey >= X) break;
} fclose(IDXFp); if (ai.IdxKey == X) return (ai.Pos); return (-1);
- Trường hợp tốt nhất khi phần tử đầu tiên của tập tin chỉ mục có giá trị khóa chỉ mục lớn hơn hoặc bằng X:
Số phép so sánh: Smin = 2 + 1 = 3
Số lần đọc tập tin: Dmin = 1
- Trường hợp xấu nhất khi mọi phần tử trong tập tin chỉ mục đều có khóa chỉ mục nhỏ hơn giá trị X:
Số phép so sánh: Smax = 2N + 1
Số lần đọc tập tin: Dmax = N
Số phép so sánh: Savg = (3 + 2N + 1) : 2 = N + 2
Số lần đọc tập tin: Davg = ẵ(N + 1)
Câu hỏi và Bài tập
1 Trình bày tư tưởng của các thuật toán tìm kiếm: Tuyến tính, Nhị phân, Chỉ mục? Các thuật toán này có thể được vận dụng trong các trường hợp nào? Cho ví dụ?
2 Cài đặt lại thuật toán tìm tuyến tính bằng các cách:
- Sử dụng vòng lặp for,
- Sử dụng vòng lặp do … while?
Có nhận xét gì cho mỗi trường hợp?
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
3 Trong trường hợp các phần tử của dãy đã có thứ tự tăng, hãy cải tiến lại thuật toán tìm tuyến tính? Cài đặt các thuật toán cải tiến? Đánh giá và so sánh giữa thuật toán nguyên thủy với các thuật toán cải tiến
4 Trong trường hợp các phần tử của dãy đã có thứ tự giảm, hãy trình bày và cài đặt lại thuật toán tìm nhị phân trong hai trường hợp: Đệ quy và Không đệ quy?
5 Vận dụng thuật toán tìm nhị phân, hãy cải tiến và cài đặt lại thuật toán tìm kiếm dựa theo tập tin chỉ mục? Đánh giá và so sánh giữa thuật toán nguyên thủy với các thuật toán cải tiến?
6 Sử dụng hàm random trong C để tạo ra một dãy (mảng) M có tối thiểu 1.000 số nguyên, sau đó chọn ngẫu nhiên (cũng bằng hàm random) một giá trị nguyên K Vận dụng các thuật toán tìm tuyến tính, tìm nhị phân để tìm kiếm phần tử có giá trị K trong mảng M
Với cùng một dữ liệu như nhau, cho biết thời gian thực hiện các thuật toán
7 Trình bày và cài đặt thuật toán tìm tuyến tính đối với các phần tử trên mảng hai chiều trong hai trường hợp:
- Không sử dụng phần tử “Cầm canh”
- Có sử dụng phần tử “Cầm canh”
Cho biết thời gian thực hiện của hai thuật toán trong hai trường hợp trên
8 Sử dụng hàm random trong C để tạo ra tối thiểu 1.000 số nguyên và lưu trữ vào một tập tin có tên SONGUYEN.DAT, sau đó chọn ngẫu nhiên (cũng bằng hàm random) một giá trị nguyên K Vận dụng thuật toán tìm tuyến tính để tìm kiếm phần tử có giá trị K trong tập tin SONGUYEN.DAT
9 Thông tin về mỗi nhân viên bao gồm: Mã số – là một số nguyên dương, Họ và Đệm – là một chỗi có tối đa 20 ký tự, Tên nhân viên – là một chuỗi có tối đa 10 ký tự, Ngày, Tháng, Năm sinh – là các số nguyên dương, Phái – Là “Nam” hoặc “Nữ”, Hệ số lương, Lương căn bản, Phụ cấp – là các số thực Viết chương trình nhập vào danh sách nhân viên (ít nhất là 10 người, không nhập trùng mã giữa các nhân viên với nhau) và lưu trữ danh sách nhân viên này vào một tập tin có tên NHANSU.DAT, sau đó vận dụng thuật toán tìm tuyến tính để tìm kiếm trên tập tin NHANSU.DAT xem có hay không nhân viên có mã là K (giá trị của K có thể nhập vào từ bàn phím hoặc phát sinh bằng hàm random) Nếu tìm thấy nhân viên có mã là K thì in ra màn hình toàn bộ thông tin về nhân viên này
10 Với tập tin dữ liệu có tên NHANSU.DAT trong bài tập 9, thực hiện các yêu cầu sau:
- Tạo một bảng chỉ mục theo Tên nhân viên
Trong quy trình quản lý dữ liệu nhân sự, tiến hành tìm kiếm trên bảng chỉ mục của tập tin NHANSU.DAT để xác định xem có nhân viên có tên X hay không; nếu có, hệ thống sẽ in ra toàn bộ thông tin về nhân viên này, bao gồm mã nhân viên, họ tên đầy đủ, ngày sinh, giới tính, vị trí công việc, phòng ban và các thuộc tính liên quan khác.
- Lưu trữ bảng chỉ mục này vào trong tập tin có tên NSTEN.IDX
KỸ THUẬT SẮP XẾP (SORTING)
Khái quát về sắp xếp
Để tiện lợi và giảm thời gian thao tác, đặc biệt là để tìm kiếm dữ liệu nhanh chóng, dữ liệu trên mảng hoặc tập tin thường đã được sắp xếp trước khi thực hiện bất kỳ thao tác nào Do đó, thao tác sắp xếp dữ liệu là một bước thiết yếu và thường gặp trong quá trình lưu trữ và quản lý dữ liệu, giúp tăng hiệu quả tìm kiếm và tối ưu hoá hiệu suất xử lý dữ liệu.
Thứ tự xuất hiện dữ liệu có thể là tăng dần (không giảm) hoặc giảm dần (không tăng) Trong phạm vi chương này, chúng ta sẽ thực hiện sắp xếp dữ liệu theo thứ tự tăng dần; việc sắp xếp theo thứ tự giảm dần hoàn toàn tương tự.
Có rất nhiều thuật toán sắp xếp và để hiểu rõ sự khác biệt giữa chúng, ta có thể phân loại chúng thành hai nhóm chính dựa trên vị trí lưu trữ dữ liệu trong máy tính: sắp xếp trên dữ liệu được lưu trong bộ nhớ RAM (thuật toán sắp xếp nội bộ) và sắp xếp ngoại bộ nhớ (external sort) cho các tập dữ liệu lớn phải xử lý bằng đĩa hoặc thiết bị lưu trữ ngoài Việc phân loại này giúp tối ưu hiệu suất và chọn thuật toán phù hợp với kích thước dữ liệu, giới hạn bộ nhớ và yêu cầu thời gian xử lý.
- Các giải thuật sắp xếp thứ tự nội (sắp xếp thứ tự trên dãy/mảng),
- Các giải thuật sắp xếp thứ tự ngoại (sắp xếp thứ tự trên tập tin/file)
Trong mỗi phần tử dữ liệu được xem xét, ta giả sử có một thành phần khóa (Key) để nhận diện phần tử đó, có kiểu dữ liệu là T, còn các thành phần còn lại chứa thông tin (Info) liên quan đến phần tử dữ liệu đó Do đó, mỗi phần tử dữ liệu có cấu trúc dữ liệu gồm thành phần khóa và phần thông tin, được thể hiện qua khai báo typedef struct DataElement { KeyType Key; InfoType Info; } DataElement; Việc phân tách rõ ràng thành phần khóa giúp việc tra cứu, sắp xếp và quản lý dữ liệu hiệu quả hơn, đồng thời cho phép định danh nhanh và an toàn của từng phần tử dữ liệu Mô hình này là nền tảng cho các hệ thống lưu trữ và xử lý thông tin, nơi mỗi phần tử dữ liệu được thiết kế để mở rộng và tương thích với các thao tác xử lý dữ liệu Khi triển khai, ta có thể thay đổi loại dữ liệu của khóa và thông tin mà không làm ảnh hưởng đến cách truy cập hay quản lý phần tử dữ liệu, miễn là cấu trúc dữ liệu vẫn giữ nguyên nguyên tắc nhận diện bằng khóa.
Trong chương này và toàn bộ tài liệu này, các thuật toán sắp xếp được định nghĩa là sắp xếp các phần tử dữ liệu sao cho chúng có thứ tự tăng dần theo thành phần khóa nhận diện (Key) Để đơn giản hóa, mỗi phần tử dữ liệu được xem như chỉ là một thành phần khóa nhận diện duy nhất.
Các giải thuật sắp xếp nội
Toàn bộ dữ liệu cần sắp xếp được đưa vào bộ nhớ trong (RAM), vì vậy số phần tử dữ liệu không lớn do giới hạn bộ nhớ nhưng tốc độ sắp xếp tương đối nhanh Các giải thuật sắp xếp nội bộ được phân thành các nhóm khác nhau, phù hợp với đặc trưng và nguyên lý hoạt động của từng nhóm.
- Sắp xếp bằng phương pháp đếm (counting sort),
- Sắp xếp bằng phương pháp đổi chỗ (exchange sort),
- Sắp xếp bằng phương pháp chọn lựa (selection sort),
- Sắp xếp bằng phương pháp chèn (insertion sort),
- Sắp xếp bằng phương pháp trộn (merge sort)
Trong phạm vi giáo trình này, chúng ta trình bày một số thuật toán sắp xếp tiêu biểu thuộc các nhóm thuật toán sắp xếp đã nêu ở trên và giả sử mảng M chứa N phần tử có kiểu dữ liệu T được sắp xếp theo thứ tự tăng.
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật 3.2.1 Sắp xếp bằng phương pháp đổi chỗ (Exchange Sort)
Các thuật toán ở phần này sẽ tìm và hoán đổi các phần tử đang ở vị trí sai so với mảng đã sắp xếp trong mảng M, nhằm ghép chúng vào đúng vị trí của chúng trên mảng mục tiêu Quá trình đổi chỗ được thiết kế để các phần tử tương ứng với mảng đã sắp xếp lần lượt vào đúng chỉ số, cho tới khi tất cả các phần tử đều nằm ở đúng vị trí và mảng M trở thành mảng sắp xếp đúng thứ tự Các kỹ thuật này tối ưu hóa số lần đổi chỗ và thời gian thực thi, giúp sắp xếp mảng M nhanh hơn và hiệu quả hơn.
M đều về đúng vị trí như mảng đã sắp xếp
Các thuật toán sắp xếp bằng phương pháp đổi chỗ bao gồm:
- Thuật toán sắp xếp nổi bọt (bubble sort),
- Thuật toán sắp xếp lắc (shaker sort),
- Thuật toán sắp xếp giảm độ tăng hay độ dài bước giảm dần (shell sort),
Thuật toán sắp xếp dựa trên sự phân hoạch (quick sort) là một trong những phương pháp sắp xếp hiệu quả dựa trên nguyên tắc phân chia và làm chủ dữ liệu Ở đây chúng ta trình bày hai thuật toán phổ biến: thuật toán sắp xếp nổi bọt và thuật toán sắp xếp dựa trên sự phân hoạch Thuật toán sắp xếp nổi bọt (Bubble Sort) là phương pháp đơn giản, thực hiện qua các lượt lặp và so sánh các phần tử kề nhau rồi đổi chỗ để đưa phần tử lớn hơn về cuối danh sách; độ phức tạp thời gian của nó là O(n^2) ở cả trường hợp trung bình và xấu, và nó thường được dùng để minh họa các khái niệm sắp xếp Ngược lại, thuật toán sắp xếp dựa trên sự phân hoạch (Quick Sort) áp dụng chiến lược chia để trị: chọn một phần tử làm chốt (pivot), dời các phần tử nhỏ hơn và lớn hơn pivot về hai phía, rồi đệ quy sắp xếp từng phần danh sách đến khi toàn bộ danh sách được sắp xếp, với độ phức tạp trung bình O(n log n) và hiệu suất tốt trên dữ liệu lớn.
Quá trình sắp xếp nổi bọt bắt đầu từ cuối mảng và di chuyển về đầu mảng Trong quá trình quét, nếu phần tử ở sau nhỏ hơn phần tử đứng trước nó, hai phần tử này sẽ hoán đổi cho nhau Theo nguyên lý của bọt khí, phần tử nhẹ hơn sẽ bị đẩy lên trên phần tử nặng hơn, giúp các cặp phần tử được sắp xếp và đổi chỗ nhanh chóng Kết quả là phần tử nhỏ nhất được đưa lên đầu mảng rất nhanh.
Quá trình sắp xếp diễn ra qua các lượt đi; sau mỗi lượt, ta đưa một phần tử về đúng vị trí của nó trong mảng M Nhờ đó, sau N−1 lượt đi, toàn bộ phần tử trong mảng M sẽ được sắp xếp theo thứ tự tăng dần.
B3.3.1: if (M[Under] < M[Under - 1]) Swap(M[Under], M[Under – 1]) //Đổi chỗ 2 phần tử cho nhau B3.3.2: Under
Hàm BubbleSort có prototype như sau: void BubbleSort(T M[], int N);
Đầu vào là N phần tử có kiểu dữ liệu T trên mảng M và kết quả là mảng M được sắp xếp tăng dần bằng thuật toán sắp xếp nổi bọt Hàm thực hiện điều này được khai báo như void BubbleSort(T M[], int N) và hoạt động bằng cách lặp qua mảng nhiều lần, so sánh hai phần tử kề nhau và hoán đổi chúng khi phần tử ở vị trí i lớn hơn phần tử ở vị trí i+1, cho đến khi không còn cặp nào bị đổi chỗ Nhờ đó, sau khi thực hiện BubbleSort, mảng M có N phần tử sẽ cho ra thứ tự tăng dần, phục vụ cho các bài học về cấu trúc dữ liệu và giải thuật.
{ for (int I = 0; I < N-1; I++) for (int J = N-1; J > I; J ) if (M[J] < M[J-1]) Swap(M[J], M[J-1]); return;
Hàm Swap có prototype như sau: void Swap(T &X, T &Y);
Hàm thực hiện việc hoán vị giá trị của hai phần tử X và Y cho nhau Nội dung của hàm như sau: void Swap(T &X, T &Y)
- Ví dụ minh họa thuật toán:
Giả sử ta cần sắp xếp mảng M có 10 phần tử sau (N = 10):
Ta sẽ thực hiện 9 lần đi (N - 1 = 10 - 1 = 9) để sắp xếp mảng M:
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
Sau 9 lần đi mảng M trở thành:
Số phộp so sỏnh: S = (N-1) + (N-2) + … + 1 = ẵN(N-1)
+ Trong trường hợp tốt nhất: khi mảng ban đầu đã có thứ tự tăng
Số phép hoán vị: Hmin = 0
+ Trong trường hợp xấu nhất: khi mảng ban đầu đã có thứ tự giảm
Số phộp hoỏn vị: Hmin = (N-1) + (N-2) + … + 1 = ẵN(N-1)
+ Số phộp hoỏn vị trung bỡnh: Havg = ẳN(N-1)
- Nhận xét về thuật toán nổi bọt:
+ Thuật toán sắp xếp nổi bọt khá đơn giản, dễ hiểu và dễ cài đặt
Thuật toán sắp xếp nổi bọt hoạt động bằng cách quét từ cuối mảng về đầu mảng, nơi phần tử nhẹ được trồi lên rất nhanh trong khi phần tử nặng lại chìm xuống chậm do không tận dụng được chiều đi xuống Chính đặc điểm này khiến quá trình sắp xếp trải qua nhiều vòng lặp, làm tăng thời gian xử lý và giảm hiệu suất so với các thuật toán tối ưu khác Hiểu rõ đặc tính của sắp xếp nổi bọt giúp nhận diện nhược điểm và cân nhắc tối ưu hóa hoặc lựa chọn thuật toán phù hợp với dữ liệu và yêu cầu tốc độ 처리.
Thuật toán nổi bọt không nhận ra khi các phần tử ở hai đầu mảng đã nằm đúng vị trí và vì thế không tận dụng được việc rút ngắn quãng đường di chuyển trong mỗi lượt lặp Trong khi đó, thuật toán sắp xếp dựa trên sự phân hoạch (Partitioning Sort) phân chia mảng thành các vùng và thực hiện phân hoạch quanh một giá trị pivot để đưa các phần tử về đúng vị trí, từ đó tăng hiệu quả sắp xếp và giảm số phép so sánh và hoán đổi so với phương pháp nổi bọt.
Thuật toán sắp xếp dựa trên sự phân hoạch còn được gọi là thuật toán sắp xếp nhanh (Quick Sort)
Phân hoạch dãy M thành 3 dãy con có thứ tự tương đối nhằm bảo toàn trình tự xuất hiện của phần tử Dãy con thứ nhất, nằm đầu dãy M, gồm các phần tử có giá trị nhỏ hơn giá trị trung bình của dãy M Các dãy con thứ hai và thứ ba lần lượt chứa các phần tử còn lại và được phân chia sao cho thứ tự ban đầu của từng phần tử được duy trì trong mỗi dãy Phương pháp này dựa trên ngưỡng giá trị trung bình của dãy M để tách các giá trị và hỗ trợ các bài toán phân tích dữ liệu, tối ưu hóa truy vấn và trình bày kết quả một cách trực quan.
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Dãy con thứ hai (giữa dãy M) gồm các phần tử có giá trị bằng giá trị trung bình của dãy M,
Dãy con thứ ba (cuối dãy M) gồm các phần tử có giá trị lớn hơn giá trị trung bình của dãy M,
+ Nếu dãy con thứ nhất và dãy con thứ ba có nhiều hơn 01 phần tử thì chúng ta lại tiếp tục phân hoạch đệ quy các dãy con này
Việc tìm giá trị trung bình của dãy M hoặc tìm phần tử có giá trị bằng giá trị trung bình của dãy M là rất khó khăn và tốn thời gian Trong thực tế, ta chọn một phần tử bất kỳ trong dãy các phần tử cần phân hoạch làm giá trị cho phần tử của dãy con thứ hai (dãy giữa) sau khi phân hoạch, thường là phần tử đứng ở vị trí giữa Phần tử này được gọi là phần tử biên (boundary element) Các phần tử trong dãy con thứ nhất sẽ có giá trị nhỏ hơn phần tử biên, còn các phần tử trong dãy con thứ ba sẽ có giá trị lớn hơn phần tử biên.
+ Việc phân hoạch một dãy được thực hiện bằng cách tìm các cặp phần tử đứng ở hai dãy con hai bên phần tử giữa (dãy 1 và dãy 3) nhưng bị sai thứ tự (phần tử đứng ở dãy 1 có giá trị lớn hơn giá trị phần tử giữa và phần tử đứng ở dãy 3 có giá trị nhỏ hơn giá trị phần tử giữa) để đổi chỗ (hoán vị) cho nhau
B3: IF (First ≥ Last) //Dãy con chỉ còn không quá 01 phần tử
B4: X = M[(First+Last)/2] //Lấy giá trị phần tử giữa
B5: I = First //Xuất phát từ đầu dãy 1 để tìm phần tử có giá trị > X
B8: J = Last //Xuất phát từ cuối dãy 3 để tìm phần tử có giá trị < X
B12.1: Phân hoạch đệ quy dãy con từ phần tử thứ First đến phần tử thứ J B12.2: Phân hoạch đệ quy dãy con từ phần tử thứ I đến phần tử thứ Last
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Hàm QuickSort có prototype như sau: void QuickSort(T M[], int N);
Hàm thực hiện việc sắp xếp N phần tử có kiểu dữ liệu T trên mảng M theo thứ tự tăng dựa trên thuật toán sắp xếp nhanh QuickSort sử dụng hàm phân hoạch đệ quy PartitionSort để thực hiện việc sắp xếp theo thứ tự tăng các phần tử của một dãy con giới hạn từ First đến Last trên mảng M Hàm PartitionSort có prototype như sau: void PartitionSort(T M[], int First, int Last);
Nội dung của các hàm như sau: void PartitionSort(T M[], int First, int Last)
T X = M[(First+Last)/2]; int I = First; int J = Last; do { while (M[I] < X)
- Ví dụ minh họa thuật toán:
Giả sử ta cần sắp xếp mảng M có 10 phần tử sau (N = 10):
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
J Phân hoạch các phần tử trong dãy con từ First -> J:
I Phân hoạch các phần tử trong dãy con từ First -> J:
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Phân hoạch:
Phân hoạch các phần tử trong dãy con từ I -> Last:
Phân hoạch các phần tử trong dãy con từ I -> Last:
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
J Phân hoạch các phần tử trong dãy con từ First -> J:
Phân hoạch các phần tử trong dãy con từ I -> Last:
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật
X = 30 Phân hoạch các phần tử trong dãy con từ I -> Last:
Toàn bộ quá trình phân hoạch kết thúc, dãy M trở thành:
+ Trường hợp tốt nhất, khi mảng M ban đầu đã có thứ tự tăng:
Số phép gán: Gmin = 1 + 2 + 4 + … + 2^[Log2(N) – 1] = N-1
Số phép so sánh: Smin = N×Log2(N)/2
Số phép hoán vị: Hmin = 0
Trong trường hợp xấu nhất của QuickSort, khi phần tử X được chọn ở giữa dãy con lại là giá trị lớn nhất của dãy con, quá trình phân hoạch sẽ tạo ra một bên chứa toàn bộ các phần tử còn lại và bên kia rỗng, khiến thuật toán phải lặp lại nhiều lần với kích thước dãy con giảm chậm Điều này đẩy thời gian thực thi lên tới O(n^2) và làm giảm hiệu suất đối với bộ dữ liệu có đặc điểm này Vì vậy, việc lựa chọn pivot thông minh hoặc áp dụng các biến thể như randomized QuickSort, hybrid hoặc introsort được xem xét để tránh rơi vào tình trạng này.
Số phép so sánh: Smax = (N-1)×(N-1)
Số phép hoán vị: Hmax = (N-1) + (N-2) + … + 1 = N×(N-1)/2
Số phép so sánh: Savg = [N×Log 2 (N)/2 + N×(N-1)]/2 = N×[Log 2 (N)+2N–2]/4
Số phép hoán vị: Havg = N×(N-1)/4
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật 3.2.2 Sắp xếp bằng phương pháp chọn (Selection Sort)
Các giải thuật sắp xếp ngoại
Với dữ liệu có quy mô lớn, một phần dữ liệu được nạp vào RAM để sắp xếp trong khi phần còn lại được lưu trên bộ nhớ ngoài (DISK) Do sự trao đổi dữ liệu giữa RAM và DISK chậm nên tốc độ sắp xếp trên tập tin thường thấp hơn so với sắp xếp toàn bộ trong RAM Các giải thuật sắp xếp ngoại (external sorting) được thiết kế để xử lý bài toán này và được phân loại thành nhiều nhóm nhằm tối ưu hóa hoạt động I/O giữa RAM và DISK.
- Sắp xếp bằng phương pháp trộn (merge sort),
- Saộp xeỏp theo chổ muùc (index sort)
Trong phần này, chúng ta tìm cách sắp xếp tập tin F chứa N phần tử dữ liệu có kiểu T, trong đó khóa nhận diện cho các phần tử cũng mang kiểu T Mục tiêu là sắp xếp các phần tử theo thứ tự tăng dần dựa trên khóa nhận diện, nhằm tối ưu hóa truy vấn và xử lý dữ liệu Quá trình sắp xếp này giúp đảm bảo tính nhất quán và cải thiện hiệu suất cho các thao tác tìm kiếm, chèn và sắp xếp lại vị trí của các phần tử trong tập tin.
3.3.1 Sắp xếp bằng phương pháp trộn (Merge Sort)
Giống như với sắp xếp theo phương pháp trộn trên mảng, trong các thuật giải ở đây chúng ta sẽ tìm cách phân phối các đường chạy từ dữ liệu vào các tập tin trung gian và sau đó ghép nối các đường chạy tương ứng trên các tập tin trung gian để tạo ra một đường chạy mới có chiều dài lớn hơn Quá trình này có thể được lặp lại cho đến khi toàn bộ dữ liệu được ghép thành một đường chạy duy nhất, nhờ đó hiệu quả sắp xếp ngày càng tăng nhờ việc tăng độ dài của đường chạy mỗi vòng ghép.
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Các thuật toán sắp xếp bằng phương pháp trộn trên tập tin bao gồm:
- Thuật toán sắp xếp trộn thẳng hay trộn trực tiếp (straight merge sort),
- Thuật toán sắp xếp trộn tự nhiên (natural merge sort),
- Thuật toán trộn đa lối cân bằng (multiways merge sort),
Thuật toán trộn đa pha (multiphases merge sort) là một họ thuật toán sắp xếp dựa trên việc ghép nối các phần tử đã được sắp xếp thành một dãy liên tục theo thứ tự tăng dần Ở đây chúng ta chỉ nghiên cứu hai thuật toán trộn đầu tiên nhằm nắm bắt cơ chế và hiệu quả triển khai trong thực tế Thuật toán sắp xếp trộn trực tiếp (Straight Merge Sort) là phương pháp ghép hai mảng đã sắp xếp với nhau một cách trực tiếp để tạo thành một mảng lớn hơn đồng bộ và đúng thứ tự Phương pháp này tối ưu hóa thời gian thực thi và số lần sao chép dữ liệu bằng cách tận dụng sự sắp xếp sẵn có của hai phần, đồng thời cho phép đánh giá độ phức tạp thời gian và tính ổn định của quá trình trộn Bài viết sẽ làm rõ các đặc điểm của Straight Merge Sort, so sánh với các thuật toán trộn khác trong cùng nhóm và chỉ ra các trường hợp nên áp dụng để đạt hiệu quả tối ưu.
Giống như thuật toán trộn trực tiếp trên mảng, ban đầu tệp Fd chứa N run với độ dài mỗi run bằng 1 Ta phân phối luân phiên N run của Fd vào K tệp phụ Ft1, Ft2, , FtK, mỗi tệp phụ có đúng N/K run Sau đó ta trộn đồng thời từng bộ K run từ các tệp phụ Ft1, Ft2, , FtK để tạo ra một run mới có độ dài L = K, đưa run này trở lại tệp Fd Kết quả là tệp Fd sẽ trở thành tệp có N/K run và mỗi run có độ dài L = K.
Như vậy, sau mỗi lần phân phối và trộn các run trên tập tin Fd thì số run trên tập tin
Fd sẽ giảm đi K lần và đồng thời chiều dài mỗi run trên Fd sẽ tăng lên K lần Do đó, sau log_K(N) lượt phân phối và trộn, tập tin Fd chỉ còn lại một run có chiều dài bằng N, và khi đó Fd trở thành một tập tin có thứ tự Đây cho thấy cơ chế sắp xếp dữ liệu thông qua phân phối và trộn làm giảm số lượng run và tăng kích thước mỗi run cho đến khi toàn bộ nội dung được sắp xếp theo thứ tự.
Trong thuật toán này, để dễ theo dõi, chúng ta sử dụng hai tập tin phụ (K = 2) và tách riêng quá trình phân phối khỏi giai đoạn trộn Các run được trình bày độc lập và sau đó ghép lại thành hai thuật toán riêng biệt, giúp phân tích và so sánh các bước xử lý một cách rõ ràng.
+ Thuật giải phân phối luân phiên (tách) các đường chạy với chiều dài L trên tập tin Fd về hai tập tin phụ Ft1, Ft2;
+ Thuật giải trộn (nhập) các cặp đường chạy trên hai tập tin Ft1, Ft2 có chiều dài
L về tập tin Fd thành các đường chạy với chiều dài 2*L;
Giả sử rằng các lỗi thao tác trên tập tin sẽ bị bỏ qua trong quá trình thực hiện
B1: Fd = fopen(DataFile, “r”) //Mở tập tin dữ liệu cần sắp xếp để đọc dữ liệu
Trong quy trình xử lý dữ liệu, chương trình mở hai tập tin trung gian để ghi dữ liệu: Ft1 mở DataTemp1 ở chế độ ghi nhằm lưu trữ phần dữ liệu đầu ra đầu tiên và Ft2 mở DataTemp2 ở chế độ ghi nhằm lưu trữ phần dữ liệu đầu ra thứ hai Việc dùng tập tin trung gian giúp quản lý dữ liệu tạm thời và tăng hiệu suất ghi dữ liệu Khi phân phối dữ liệu xong, chương trình kiểm tra bằng điều kiện feof(Fd) để xác nhận đã phân phối hết dữ liệu từ tập tin nguồn.
//Chép 1 run từ Fd sang Ft1
B5: K = 1 //Chỉ số đếm để duyệt các run
B7: fread(&a, sizeof(T), 1, Fd) //Đọc 1 phần tử của run trên Fd ra biến tạm a
B8: fwrite(&a, sizeof(T), 1, Ft1) //Ghi giá trị biến tạm a vào tập tin Ft1
B10: IF (feof(Fd)) //Đã phân phối hết
//Chép 1 run từ Fd sang Ft2
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B13: IF (K > L)
B14: fread(&a, sizeof(T), 1, Fd) //Đọc 1 phần tử của run trên Fd ra biến tạm a B15: fwrite(&a, sizeof(T), 1, Ft2) //Ghi giá trị biến tạm a vào tập tin Ft2
B17: IF (feof(Fd)) //Đã phân phối hết
During the data processing workflow, the program opens two intermediate files in read mode—DataTemp1 and DataTemp2—to access and gather the stored data It then opens the final data file, DataFile, in write mode to store the combined results This sequence enables safe data retrieval from temporary storage before producing and saving the processed output to the designated data file.
Đoạn mã thực hiện việc đọc dữ liệu từ hai file Ft1 và Ft2: fread(&a1, sizeof(T), 1, Ft1) đọc 1 phần tử của run trên Ft1 và lưu vào biến tạm a1, fread(&a2, sizeof(T), 1, Ft2) đọc 1 phần tử của run trên Ft2 và lưu vào biến tạm a2 Chỉ số duyệt các run trên Ft1 được khởi tạo bằng 1 với K1 = 1.
B7: K2 = 1 //Chỉ số để duyệt các run trên Ft2
B8: IF (a1 ≤ a2) // a1 đứng trước a2 trên Fd
B8.3: If (feof(Ft1)) //Đã chép hết các phần tử trong Ft1
Thực hiện B23 B8.4: fread(&a1, sizeof(T), 1, Ft1)
B8.5: If (K1 > L) //Đã duyệt hết 1 run trong Ft1
B9: ELSE // a2 đứng trước a1 trên Fd
B9.3: If (feof(Ft2)) //Đã chép hết các phần tử trong Ft2
Thực hiện B27 B9.4: fread(&a2, sizeof(T), 1, Ft2)
B9.5: If (K2 > L) //Đã duyệt hết 1 run trong Ft2
//Chép phần run còn lại trong Ft2 về Fd
B11: IF (K2 > L) //Đã chép hết phần run còn lại trong Ft2 về Fd
B14: IF (feof(Ft2)) //Đã chép hết các phần tử trong Ft2
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật //Chép phần run còn lại trong Ft1 về Fd
B17: IF (K1 > L) //Đã chép hết phần run còn lại trong Ft1 về Fd
B20: IF (feof(Ft1)) //Đã chép hết các phần tử trong Ft1
//Chép các phần tử còn lại trong Ft2 về Fd
//Chép các phần tử còn lại trong Ft1 về Fd
- Thuật toán sắp xếp trộn thẳng:
B1: L = 1 //Chiều dài ban đầu của các run
B2: IF (L ≥ N) //Tập tin Fd chỉ còn 01 run
B3: Phaân_Phoái(DataFile, DataTemp1, DataTemp2, L)
Hàm FileStraightMergeSort có prototype như sau: int FileStraightMergeSort(char * DataFile);
Chức năng này thực hiện việc sắp xếp các phần tử có kiểu dữ liệu T trên tập tin DataFile theo thứ tự tăng dần bằng thuật toán sắp trộn trực tiếp; nếu sắp xếp thành công, hàm trả về 1, ngược lại (do lỗi thao tác trên tập tin) trả về -1 Hàm sử dụng hai hàm phụ FileDistribute và FileMerge, với prototype int FileDistribute(char * DataFile, char * DataTemp1, char * DataTemp2, int L); và có ý nghĩa như sau: FileDistribute thực hiện phân phối luân phiên các đường chạy có chiều dài L trên DataFile sang các tập tin tạm DataTemp1 và DataTemp2 Quá trình phân phối và ghép nối này được lặp đi lặp lại cho đến khi DataFile chứa các phần tử đã được sắp xếp tăng dần.
Giáo trình Cấu Trúc Dữ Liệu và Giải Thuật giới thiệu một hàm FileMerge để ghép dữ liệu từ DataTemp1 và DataTemp2 vào DataFile với giới hạn độ dài L Hàm này trả về 1 khi việc phân phối dữ liệu hoàn tất, và ngược lại trả về -1 nếu chưa hoàn tất hoặc gặp lỗi Khai báo của hàm được viết dưới dạng int FileMerge(char * DataTemp1, char * DataTemp2, char * DataFile, int L); -**Support Pollinations.AI:** -🌸 **Ad** 🌸Powered by Pollinations.AI free text APIs [Support our mission](https://pollinations.ai/redirect/kofi) to keep AI accessible for everyone.
Hàm thực hiện việc trộn từng cặp đường chạy tương ứng có độ dài L từ hai tập tin tạm thời DataTemp1 và DataTemp2 về tập tin dữ liệu ban đầu DataFile, tạo ra các đường chạy có chiều dài 2*L Hàm trả về giá trị 1 khi quá trình trộn hoàn tất, ngược lại trả về -1.
Both functions rely on the Finished routine to perform cleanup tasks—closing opened files, freeing allocated memory, and other housekeeping—before they terminate by returning an integer value The Finished function is available in several variants, including int Finished(FILE *F1, int ReturnValue);, int Finished(FILE *F1, FILE *F2, int ReturnValue);, and int Finished(FILE *F1, FILE *F2, FILE *F3, int ReturnValue);
Nội dung của các hàm như sau: int Finished (FILE * F1, int ReturnValue)
//======================================================= int Finished (FILE * F1, FILE * F2, int ReturnValue)
//======================================================= int Finished (FILE * F1, FILE * F2, FILE * F3, int ReturnValue);
{ fclose (F1); fclose (F2); fclose (F3); return (ReturnValue);
//======================================================= int FileDistribute(char * DataFile, char * DataTemp1, char * DataTemp2, int L) { FILE * Fd = fopen(DataFile, “rb”); if (Fd == NULL) return (-1);
FILE * Ft1 = fopen(DataTemp1, “wb”); if (Ft1 == NULL) return(Finished(Fd, -1));
FILE * Ft2 = fopen(DataTemp2, “wb”); if (Ft2 == NULL) return(Finished(Fd, Ft1, -1));
Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật int SOT = sizeof(T); while (!feof(Fd))
{ int t = fread(&a, SOT, 1, Fd); if (t < 1) { if (feof(Fd)) break; return (Finished(Fd, Ft1, Ft2, -1));
} t = fwrite(&a, SOT, 1, Ft1); if (t < 1) return(Finished(Fd, Ft1, Ft2, -1));
} for(K = 0; K