Giáo trình môn học Cấu trúc dữ liệu và giải thuật
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình môn học Cấu trúc dữ liệu và giải thuật", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên
Tài liệu đính kèm:
- giao_trinh_mon_hoc_cau_truc_du_lieu_va_giai_thuat.pdf
Nội dung text: Giáo trình môn học 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
- MỤC LỤC Mục Trang CHƯƠNG 1: TỔNG QUAN VỀ CẤU TRÚC DỮ LIỆU & GT 3 1.1. Tầm quan trọng của CTDL & GT trong một đề án tin học 3 1.1.1. Xây dựng cấu trúc dữ liệu 3 1.1.2. Xây dựng giải thuật 3 1.1.3. Mối quan hệ giữa cấu trúc dữ liệu và giải thuật 3 1.2. Đánh giá Cấu trúc dữ liệu & Giải thuật 3 1.2.1. Các tiêu chuẩn đánh giá cấu trúc dữ liệu 3 1.2.2. Đánh giá độ phức tạp của thuật toán 4 1.3. Kiểu dữ liệu 4 1.3.1. Khái niệm về kiểu dữ liệu 4 1.3.2. Các kiểu dữ liệu cơ sở 4 1.3.3. Các kiểu dữ liệu có cấu trúc 5 1.3.4. Kiểu dữ liệu con trỏ 5 1.3.5. Kiểu dữ liệu tập tin 5 Câu hỏi và bài tập 6 CHƯƠNG 2: KỸ THUẬT TÌM KIẾM (Searching) 8 2.1. Khái quát về tìm kiếm 8 2.2. Các giải thuật tìm kiếm nội 8 2.2.1. Đặt vấn đề 8 2.2.2. Tìm tuyến tính 8 2.2.3. Tìm nhị phân 10 2.3. Các giải thuật tìm kiếm ngoại 14 2.3.1. Đặt vấn đề 14 2.3.2. Tìm tuyến tính 14 2.3.3. Tìm kiếm theo chỉ mục 16 Câu hỏi và bài tập 17 CHƯƠNG 3: KỸ THUẬT SẮP XẾP (SORTING) 19 3.1. Khái quát về sắp xếp 19 3.2. Các giải thuật sắp xếp nội 19 3.2.1 Sắp xếp bằng phương pháp đổi chỗ 20 3.2.2. Sắp xếp bằng phương pháp chọn 28 3.2.3. Sắp xếp bằng phương pháp chèn 33 3.2.4. Sắp xếp bằng phương pháp trộn 40 3.3. Các giải thuật sắp xếp ngoại 60 3.3.1. Sắp xếp bằng phương pháp trộn 60 3.3.2. Sắp xếp theo chỉ mục 79 Câu hỏi và bài tập 82
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật CHƯƠNG 4: DANH SÁCH (LIST) 84 4.1. Khái niệm về danh sách 84 4.2. Các phép toán trên danh sách 84 4.3. Danh sách đặc 85 4.3.1. Định nghĩa 85 4.3.2. Biểu diễn danh sách đặc 85 4.3.3. Các thao tác trên danh sách đặc 85 4.3.4. Ưu nhược điểm và Ứng dụng 91 4.4. Danh sách liên kết 92 4.4.1. Định nghĩa 92 4.4.2. Danh sách liên kết đơn 92 4.4.3. Danh sách liên kết kép 111 4.4.4. Ưu nhược điểm của danh sách liên kết 135 4.5. Danh sách hạn chế 135 4.5.1. Hàng đợi 135 4.5.2. Ngăn xếp 142 4.5.3. Ứng dụng của danh sách hạn chế 147 Câu hỏi và bài tập 147 CHƯƠNG 5: CÂY (TREE) 149 5.1. Khái niệm – Biểu diễn cây 149 5.1.1. Định nghĩa cây 149 5.1.2. Một số khái niệm liên quan 149 5.1.3. Biểu diễn cây 151 5.2. Cây nhị phân 152 5.2.1. Định nghĩa 152 5.2.2. Biểu diễn và Các thao tác 152 5.2.3. Cây nhị phân tìm kiếm 163 5.3. Cây cân bằng 188 5.3.1. Định nghĩa – Cấu trúc dữ liệu 188 5.3.2. Các thao tác 189 Câu hỏi và bài tập 227 ÔN TẬP (REVIEW) 224 Hệ thống lại các Cấu trúc dữ liệu và các Giải thuật đã học 224 Câu hỏi và Bài tập ôn tập tổng hợp 227 TÀI LIỆU THAM KHẢO 229 Trang: 2
- Giáo trình: Cấu Trúc Dữ Liệu và 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 Có thể nói rằng không có một chương trình máy tính nào mà không có dữ liệu để xử lý. Dữ liệu có thể là dữ liệu đưa vào (input data), dữ liệu trung gian hoặc dữ liệu đưa ra (output data). Do vậy, việc tổ chức để lưu trữ dữ liệu phục vụ cho chương trình có ý nghĩa rất quan trọng trong toàn bộ hệ thống chương trình. Việc xây dựng cấu trúc dữ liệu quyết định rất lớn đến chất lượng cũng như công sức của người lập trình trong việc thiết kế, cài đặt chương trình. 1.1.2. Xây dựng giải thuật Khái niệm giải thuật hay thuật giải mà nhiều khi còn được gọi là thuật toán dùng để chỉ phương pháp hay cách thức (method) để giải quyết vần đề. Giải thuật có thể được minh họa bằng ngôn ngữ tự nhiên (natural language), bằng sơ đồ (flow chart) hoặc bằng mã giả (pseudo code). Trong thực tế, giải thuật thường được minh họa hay thể hiện bằng mã giả tựa trên một hay một số ngôn ngữ lập trình nào đó (thường là ngôn ngữ mà người lập trình chọn để cài đặt thuật toán), chẳng hạn như C, Pascal, Khi đã xác định được cấu trúc dữ liệu thích hợp, người lập trình sẽ bắt đầu tiến hành xây dựng thuật giải tương ứng theo yêu cầu của bài toán đặt ra trên cơ sở của cấu trúc dữ liệu đã được chọn. Để giải quyết một vấn đề có thể có nhiều phương pháp, do vậy sự lựa chọn phương pháp phù hợp là một việc mà người lập trình phải cân nhắc và tính toán. Sự lựa chọn này cũng có thể góp phần đáng kể trong việc giảm bớt công việc của người lập trình trong phần cài đặt thuật toán trên một ngôn ngữ cụ thể. 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 Như vậy, khi đã có cấu trúc dữ liệu tốt, nắm vững giải thuật thực hiện thì việc thể hiện chương trình bằng một ngôn ngữ cụ thể chỉ là vấn đề thời gian. Khi có cấu trúc dữ liệu mà chưa tìm ra thuật giải thì không thể có chương trình và ngược lại không thể có Thuật giải khi chưa có cấu trúc dữ liệu. Một chương trình máy tính chỉ có thể được 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 đặt ra. 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), Trang: 3
- 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 quả không dễ dàng chút nào. Ở dây, chúng ta chỉ muốn ước lượng thời gian thực hiện thuận toán T(n) để có thể có sự so sánh tương đối giữa các thuật toán với nhau. Trong thực tế, thời gian thực hiện một thuật toán còn phụ thuộc rất nhiều vào các điều kiện khác như cấu tạo của máy tính, dữ liệu đưa vào, , ở đây chúng ta chỉ xem xét trên mức độ của lượng dữ liệu đưa vào ban đầu cho thuật toán thực hiện. Để ước lượng thời gian thực hiện thuật toán chúng ta có thể xem xét thời gian thực hiện thuật toán trong hai trường hợp: - Trong trường hợp tốt nhất: Tmin - Trong trường hợp xấu nhất: Tmax Từ đó chúng ta có thể ước lượng thời gian thực hiện trung bình của thuật toán: Tavg 1.3. 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. T = Mỗi kiểu dữ liệu thường đượ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ị trong miền V và có thể được thực hiện các phép toán thuộc tập hợp các phép toán trong O. Để lưu trữ các phần tử dữ liệu này thường phải tốn một số byte(s) trong bộ nhớ, số byte(s) này 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 có cung cấp các kiểu dữ liệu cơ sở. Tùy vào mỗi ngôn ngữ mà các kiểu dữ liệu cơ sở có thể có các tên gọi khác nhau song chung quy lại có những loại kiểu dữ liệu cơ sở như sau: - 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: + Kiểu số nguyên 1 byte + Kiểu số nguyên 2 bytes + Kiểu số nguyên 4 bytes Kiểu số nguyên thường được thực hiện với các phép toán: O = {+, -, *, /, DIV, MOD, , =, =, } Trang: 4
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật - Kiểu số thực: Thường có các kích thước sau: + Kiểu số thực 4 bytes + Kiểu số thực 6 bytes + Kiểu số thực 8 bytes + Kiểu số thực 10 bytes Kiểu số thực thường được thực hiện với các phép toán: O = {+, -, *, /, , =, =, } - Kiểu ký tự: Có thể có các kích thước sau: + Kiểu ký tự byte + Kiểu ký tự 2 bytes Kiểu ký tự thường được thực hiện với các phép toán: O = {+, -, , =, =, ORD, CHR, } - Kiểu chuỗi ký tự: Có kích thước tùy thuộc vào từng ngôn ngữ lập trình Kiểu chuỗi ký tự thường được thực hiện với các phép toán: O = {+, &, , =, =, Length, Trunc, } - Kiểu luận lý: Thường có kích thước 1 byte Kiểu luận lý thường được thực hiện với các phép toán: O = {NOT, AND, OR, XOR, , =, =, } 1.3.3. Các kiểu dữ liệu có cấu trúc Kiểu dữ liệu có cấu trúc là các kiểu dữ liệu được xây dựng trên cơ sở các kiểu dữ liệu đã có (có thể lại là một kiểu dữ liệu có cấu trúc khác). Tùy vào từng ngôn ngữ lập trình song thường có các loại sau: - Kiểu mảng hay còn gọi là dãy: kích thước bằng tổng kích thước của các phần tử - Kiểu bản ghi hay cấu trúc: kích thước bằng tổng kích thước các thành phần (Field) 1.3.4. Kiểu dữ liệu con trỏ Các ngôn ngữ lập trình thường cung cấp cho chúng ta một kiểu dữ liệu đặc biệt để lưu trữ các địa chỉ của bộ nhớ, đó là con trỏ (Pointer). Tùy vào loại con trỏ gần (near pointer) hay con trỏ xa (far pointer) mà kiểu dữ liệu con trỏ có các kích thước khác nhau: + Con trỏ gần: 2 bytes + Con trỏ xa: 4 bytes 1.3.5. Kiểu dữ liệu tập tin Tập tin (File) có thể xem là một kiểu dữ liệu đặc biệt, kích thước tối đa của tập tin tùy thuộc vào không gian đĩa nơi lưu trữ tập tin. Việc đọc, ghi dữ liệu trực tiếp trên tập tin rất mất thời gian và không bảo đảm an toàn cho dữ liệu trên tập tin đó. Do vậy, trong thực tế, chúng ta không thao tác trực tiếp dữ liệu trên tập tin mà chúng ta cần chuyển từng phần hoặc toàn bộ nội dung của tập tin vào trong bộ nhớ trong để xử lý. Trang: 5
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Câu hỏi và Bài tập 1. Trình bày tầm quan trọng của Cấu trúc dữ liệu và Giải thuật đối với người lập trình? 2. Các tiêu chuẩn để đánh giá cấu trúc dữ liệu và giải thuật? 3. Khi xây dựng giải thuật có cần thiết phải quan tâm tới cấu trúc dữ liệu hay không? Tại sao? 4. Liệt kê các kiểu dữ liệu cơ sở, các kiểu dữ liệu có cấu trúc trong C, Pascal? 5. Sử dụng các kiểu dữ liệu cơ bản trong C, hãy xây dựng cấu trúc dữ liệu để lưu trữ trong bộ nhớ trong (RAM) của máy tính đa thức có bậc tự nhiên n (0 ≤ n ≤ 100) trên trường số thực (ai , x ∈ R): n i fn ( x ) = ∑ a i x i = 0 Với cấu trúc dữ liệu được xây dựng, hãy trình bày thuật toán và cài đặt chương trình để thực hiện các công việc sau: - Nhập, xuất các đa thức. - Tính giá trị của đa thức tại giá trị x0 nào đó. - Tính tổng, tích của hai đa thức. 6. Tương tự như bài tập 5. nhưng đa thức trong trường số hữu tỷ Q (các hệ số ai và x là các phân số có tử số và mẫu số là các số nguyên). 7. Cho bảng giờ tàu đi từ ga Saigon đến các ga như sau (ga cuối là ga Hà nội): TÀU ĐI S2 S4 S6 S8 S10 S12 S14 S16 S18 LH2 SN2 HÀNH TRÌNH 32 giờ 41 giờ 41 giờ 41 giờ 41 giờ 41 giờ 41 giờ 41 giờ 41 giờ 27giờ 10g30 SAIGON ĐI 21g00 21g50 11g10 15g40 10g00 12g30 17g00 20g00 22g20 13g20 18g40 MƯƠNG MÁN 2g10 15g21 19g53 14g07 16g41 21g04 1g15 3g16 17g35 22g58 THÁP CHÀM 5g01 18g06 22g47 16g43 19g19 0g08 4g05 6g03 20g19 2g15 NHA TRANG 4g10 6g47 20g00 0g47 18g50 21g10 1g57 5g42 8g06 22g46 5g15 TUY HÒA 9g43 23g09 3g39 21g53 0g19 5g11 8g36 10g50 2g10 DIÊU TRÌ 8g12 11g49 1g20 5g46 0g00 2g30 7g09 10g42 13g00 4g15 QUẢNG NGÃI 15g41 4g55 9g24 3g24 5g55 11g21 14g35 17g04 7g34 TAM KỲ 6g11 10g39 4g38 7g10 12g40 16g08 18g21 9g03 ĐÀ NẴNG 13g27 19g04 8g29 12g20 6g19 9g26 14g41 17g43 20g17 10g53 HUẾ 16g21 22g42 12g29 15g47 11g12 14g32 18g13 21g14 23g50 15g10 ĐÔNG HÀ 0g14 13g52 17g12 12g42 16g05 19g38 22g39 1g25 ĐỒNG HỚI 19g15 2g27 15g52 19g46 14g41 17g59 21g38 0g52 3g28 VINH 23g21 7g45 21g00 1g08 20g12 23g50 2g59 7g07 9g20 THANH HÓA 10g44 0g01 4g33 23g09 3g33 6g39 9g59 12g20 NINH BÌNH 12g04 1g28 5g54 0g31 4g50 7g57 11g12 13g51 NAM ĐỊNH 12g37 2g01 6g26 1g24 5g22 8g29 11g44 14g25 PHỦ LÝ 13g23 2g42 7g08 2g02 6g00 9g09 12g23 15g06 ĐẾN HÀ NỘI 5g00 14g40 4g00 8g30 3g15 7g10 10g25 13g45 16g20 Sử dụng các kiểu dữ liệu cơ bản, hãy xây dựng cấu trúc dữ liệu thích hợp để lưu trữ bảng giờ tàu trên vào bộ nhớ trong và bộ nhớ ngoài (disk) của máy tính. Với cấu trúc dữ liệu đã được xây dựng ở trên, hãy trình bày thuật toán và cài đặt chương trình để thực hiện các công việc sau: - Xuất ra giờ đến của một tàu T0 nào đó tại một ga G0 nào đó. Trang: 6
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật - Xuất ra giờ đến các ga của một tàu T0 nào đó. - Xuất ra giờ các tàu đến một ga G0 nào đó. - Xuất ra bảng giờ tàu theo mẫu ở trên. Lưu ý: - Các ô trống ghi nhận tại các ga đó, tàu này không đi đến hoặc chỉ đi qua mà không dừng lại. - Dòng “HÀNH TRÌNH” ghi nhận tổng số giờ tàu chạy từ ga Saigon đến ga Hà nội. 8. Tương tự như bài tập 7. nhưng chúng ta cần ghi nhận thêm thông tin về đoàn tàu khi dừng tại các ga chỉ để tránh tàu hay để cho khách lên/xuống (các dòng in nghiêng tương ứng với các ga có khách lên/xuống, các dòng khác chỉ dừng để tránh tàu). 9. Sử dụng kiểu dữ liệu cấu trúc trong C, hãy xây dựng cấu trúc dữ liệu để lưu trữ trong bộ nhớ trong (RAM) của máy tính trạng thái của các cột đèn giao thông (có 3 đèn: Xanh, Đỏ, Vàng). Với cấu trúc dữ liệu đã được xây dựng, hãy trình bày thuật toán và cài đặt chương trình để mô phỏng (minh họa) cho hoạt động của 2 cột đèn trên hai tuyến đường giao nhau tại một ngã tư. 10. Sử dụng các kiểu dữ liệu cơ bản trong C, hãy xây dựng cấu trúc dữ liệu để lưu trữ trong bộ nhớ trong (RAM) của máy tính trạng thái của một bàn cờ CARO có kích thước M×N (0 ≤ M, N ≤ 20). Với cấu trúc dữ liệu được xây dựng, hãy trình bày thuật toán và cài đặt chương trình để thực hiện các công việc sau: - In ra màn hình bàn cờ CARO trong trạng thái hiện hành. - Kiểm tra xem có ai thắng hay không? Nếu có thì thông báo “Kết thúc”, nếu không có thì thông báo “Tiếp tục”. Trang: 7
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Chương 2: KỸ THUẬT TÌM KIẾM (SEARCHING) 2.1. Khái quát về tìm kiếm Trong thực tế, khi thao tác, khai thác dữ liệu chúng ta hầu như lúc nào cũng phải thực hiện thao tác tìm kiếm. Việc tìm kiếm nhanh hay chậm tùy thuộc vào trạng thái và trật tự của dữ liệu trên đó. Kết quả của việc tìm kiếm có thể là không có (không tìm thấy) hoặc có (tìm thấy). Nếu kết quả tìm kiếm là có tìm thấy thì nhiều khi chúng ta còn phải xác định xem vị trí của phần tử dữ liệu tìm thấy là ở đâu? Trong phạm vi của chương này chúng ta tìm cách giải quyết các câu hỏi này. Trước khi đi vào nghiên cứu chi tiết, chúng ta giả sử rằng mỗi phần tử dữ liệu được xem xét có một thành phần khóa (Key) để nhận diện, có kiểu dữ liệu là T nào đó, các thành phần còn lại là thông tin (Info) liên quan đến phần tử dữ liệu đó. Như vậy mỗi phần tử dữ liệu có cấu trúc dữ liệu như sau: typedef struct DataElement { T Key; InfoType Info; } DataType; Trong tài liệu này, khi nói tới giá trị của một phần tử dữ liệu chúng ta muốn nói tới giá trị khóa (Key) của phần tử dữ liệu đó. Để đơn giản, chúng ta giả sử rằng mỗi phần tử dữ liệu chỉ là thành phần khóa nhận diện. Việc tìm kiếm một phần tử có thể diễn ra trên một dãy/mảng (tìm kiếm nội) hoặc diễn ra trên một tập tin/ file (tìm kiếm ngoại). Phần tử cần tìm là phần tử cần thỏa mãn điều kiện tìm kiếm (thường có giá trị bằng giá trị tìm kiếm). Tùy thuộc vào từng bài toán cụ thể mà điều kiện tìm kiếm có thể khác nhau song chung quy việc tìm kiếm dữ liệu thường được vận dụng theo các thuật toán trình bày sau đây. 2.2. Các giải thuật tìm kiếm nội (Tìm kiếm trên dãy/mảng) 2.2.1. Đặt vấn đề Giả sử chúng ta có một mảng M gồm N phần tử. Vấn đề đặt ra là có hay không phần tử có giá trị bằng X trong mảng M? Nếu có thì phần tử có giá trị bằng X là phần tử thứ mấy trong mảng M? 2.2.2. Tìm tuyến tính (Linear Search) Thuật toán tìm tuyến tính còn được gọi là Thuật toán tìm kiếm tuần tự (Sequential Search). a. Tư tưởng: Lần lượt so sánh các phần tử của mảng M với giá trị X bắt đầu từ phần tử đầu tiên cho đến khi tìm đến được phần tử có giá trị X hoặc đã duyệt qua hết tất cả các phần tử của mảng M thì kết thúc. Trang: 8
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật b. Thuật toán: B1: k = 1 //Duyệt từ đầu mảng B2: IF M[k] ≠ X AND k ≤ N //Nếu chưa tìm thấy và cũng chưa duyệt hết mảng B2.1: k++ B2.2: Lặp lại B2 B3: IF k ≤ N Tìm thấy tại vị trí k B4: ELSE Không tìm thấy phần tử có giá trị X B5: Kết thúc c. Cài đặt thuật toán: Hàm LinearSearch có prototype: int LinearSearch (T M[], int N, T X); Hàm thực hiện việc tìm kiếm phần tử có giá trị X trên mảng M có N phần tử. Nếu tìm thấy, hàm trả về một số nguyên có giá trị từ 0 đến N-1 là vị trí tương ứng của phần tử tìm thấy. Trong trường hợp ngược lại, hàm trả về giá trị –1 (không tìm thấy). Nội dung của hàm như sau: int LinearSearch (T M[], int N, T X) { int k = 0; while (M[k] != X && k < N) k++; if (k < N) return (k); return (-1); } d. Phân tích thuật toán: - Trường hợp tốt nhất khi phần tử đầu tiên của mảng có giá trị bằng X: Số phép gán: Gmin = 1 Số phép so sánh: Smin = 2 + 1 = 3 - 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 = 1 Số phép so sánh: Smax = 2N+1 - Trung bình: Số phép gán: Gavg = 1 Số phép so sánh: Savg = (3 + 2N + 1) : 2 = N + 2 e. Cải tiến thuật toán: Trong thuật toán trên, ở mỗi bước lặp chúng ta cần phải thực hiện 2 phép so sánh để kiểm tra sự tìm thấy và kiểm soát sự hết mảng trong quá trình duyệt mảng. Chúng ta có thể giảm bớt 1 phép so sánh nếu chúng ta thêm vào cuối mảng một phần tử cầm canh (sentinel/stand by) có giá trị bằng X để nhận diện ra sự hết mảng khi duyệt mảng, khi đó thuật toán này được cải tiến lại như sau: Trang: 9
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B1: k = 1 B2: M[N+1] = X //Phần tử cầm canh B3: IF M[k] ≠ X B3.1: k++ B3.2: Lặp lại B3 B4: IF k < N Tìm thấy tại vị trí k B5: ELSE //k = N song đó chỉ là phần tử cầm canh Không tìm thấy phần tử có giá trị X B6: Kết thúc Hàm LinearSearch được viết lại thành hàm LinearSearch1 như sau: int LinearSearch1 (T M[], int N, T X) { int k = 0; M[N] = X; while (M[k] != X) k++; if (k < N) return (k); return (-1); } f. Phân tích thuật toán cải tiến: - Trường hợp tốt nhất khi phần tử đầu tiên của mảng có giá trị bằng X: Số phép gán: Gmin = 2 Số phép so sánh: Smin = 1 + 1 = 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 = 2 Số phép so sánh: Smax = (N+1) + 1 = N + 2 - Trung bình: Số phép gán: Gavg = 2 Số phép so sánh: Savg = (2 + N + 2) : 2 = N/2 + 2 - Như vậy, nếu thời gian thực hiện phép gán không đáng kể thì thuật toán cải tiến sẽ chạy nhanh hơn thuật toán nguyên thủy. 2.2.3. Tìm nhị phân (Binary Search) Thuật toán tìm tuyến tính tỏ ra đơn giản và thuận tiện trong trường hợp số phần tử của dãy không lớn lắm. Tuy nhiên, khi số phần tử của dãy khá lớn, chẳng hạn chúng ta tìm kiếm tên một khách hàng trong một danh bạ điện thoại của một thành phố lớn theo thuật toán tìm tuần tự thì quả thực mất rất nhiều thời gian. Trong thực tế, thông thường các phần tử của dãy đã có một thứ tự, do vậy thuật toán tìm nhị phân sau đây sẽ rút ngắn đáng kể thời gian tìm kiếm trên dãy đã có thứ tự. Trong thuật toán này chúng ta giả sử các phần tử trong dãy đã có thứ tự tăng (không giảm dần), tức là các phần tử đứng trước luôn có giá trị nhỏ hơn hoặc bằng (không lớn hơn) phần tử đứng sau nó. Khi đó, nếu X nhỏ hơn giá trị phần tử đứng ở giữa dãy (M[Mid]) thì X chỉ có thể tìm Trang: 10
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật thấy ở nửa đầu của dãy và ngược lại, nếu X lớn hơn phần tử M[Mid] thì X chỉ có thể tìm thấy ở nửa sau của dãy. a. Tư tưởng: Phạm vi tìm kiếm ban đầu của chúng ta là từ phần tử đầu tiên của dãy (First = 1) cho đến phần tử cuối cùng của dãy (Last = N). So sánh giá trị X với giá trị phần tử đứng ở giữa của dãy M là M[Mid]. Nếu X = M[Mid]: Tìm thấy Nếu X 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) Lặp lại quá trình này cho đến khi tìm thấy phần tử có giá trị X hoặc phạm vi tìm kiếm của chúng ta không còn nữa (First > Last). b. Thuật toán đệ quy (Recursion Algorithm): B1: First = 1 B2: Last = N B3: IF (First > Last) //Hết phạm vi tìm kiếm B3.1: Không tìm thấy B3.2: Thực hiện Bkt B4: Mid = (First + Last)/ 2 B5: IF (X = M[Mid]) B5.1: Tìm thấy tại vị trí Mid B5.2: Thực hiện Bkt B6: IF (X M[Mid]) Tìm đệ quy từ First = Mid + 1 đến Last Bkt: Kết thúc c. Cài đặt thuật toán đệ quy: Hàm BinarySearch có prototype: int BinarySearch (T M[], int N, T X); Hàm thực hiện việc tìm kiếm phần tử có giá trị X trong mảng M có N phần tử đã có thứ tự tăng. Nếu tìm thấy, hàm trả về một số nguyên có giá trị từ 0 đến N-1 là vị trí tương ứng của phần tử tìm thấy. Trong trường hợp ngược lại, hàm trả về giá trị –1 (không tìm thấy). Hàm BinarySearch sử dụng hàm đệ quy RecBinarySearch có prototype: int RecBinarySearch(T M[], int First, int Last, T X); Hàm RecBinarySearch thực hiện việc tìm kiếm phần tử có giá trị X trên 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 có giá trị từ First đến Last là vị trí tương ứng của phần tử tìm thấy. Trong trường hợp ngược lại, hàm trả về giá trị –1 (không tìm thấy). Nội dung của các hàm như sau: Trang: 11
- 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 Last) B3.1: Không tìm thấy B3.2: Thực hiện Bkt B4: Mid = (First + Last)/ 2 B5: IF (X = M[Mid]) B5.1: Tìm thấy tại vị trí Mid B5.2: Thực hiện Bkt B6: IF (X M[Mid]) B7.1: First = Mid + 1 B7.2: Lặp lại B3 Bkt: Kết thúc Trang: 12
- 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 thực hiện việc tìm kiếm phần tử có giá trị X trong mảng M có N phần tử đã có thứ tự tăng. Nếu tìm thấy, hàm trả về một số nguyên có giá trị từ 0 đến N-1 là vị trí tương ứng của phần tử tìm thấy. Trong trường hợp ngược lại, hàm trả về giá trị –1 (không tìm thấy). Nội dung của hàm NRecBinarySearch như sau: int NRecBinarySearch (T M[], int N, T X) { int First = 0; int Last = N – 1; while (First Last Mid M[Mid] X = X M[Mid] M[Mid] M[Mid] Ban đầu 0 9 False 4 8 False True False 1 0 3 False 1 3 False False True 2 2 3 False 2 4 False False True 3 3 3 False 3 5 True Trang: 13
- 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 = X M[Mid] M[Mid] M[Mid] Ban đầu 0 9 False 4 8 False True False 1 0 3 False 1 3 False False True 2 2 3 False 2 4 False False True 3 3 3 False 3 5 False False True 4 4 3 True Kết quả sau 4 lần lặp (đệ quy) thuật toán kết thúc. Lưu ý: Thuật toán tìm nhị phân chỉ có thể vận dụng trong trường hợp dãy/mảng đã có thứ tự. Trong trường hợp tổng quát chúng ta chỉ có thể áp dụng thuật toán tìm kiếm tuần tự. Các thuật toán đệ quy có thể ngắn gọn song tốn kém bộ nhớ để ghi nhận mã lệnh chương trình (mỗi lần gọi đệ quy) khi chạy chương trình, do vậy có thể làm cho chương trình chạy chậm lại. Trong thực tế, khi viết chương trình nếu có thể chúng ta nên sử dụng thuật toán không đệ quy. 2.3. Các giải thuật tìm kiếm ngoại (Tìm kiếm trên tập tin) 2.3.1. Đặt vấn đề 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ị bằng X được lưu trữ trong tập tin F? Nếu có thì phần tử có giá trị bằng X là phần tử nằm ở vị trí nào trên tập tin F? 2.3.2. Tìm tuyến tính a. Tư tưởng: Lần lượt đọc các phần tử từ đầu tập tin F và so sánh với giá trị X cho đến khi đọc được phần tử có giá trị X hoặc đã đọc hết tập tin F thì kết thúc. b. Thuật toán: B1: k = 0 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)) Lặp lại B3 B6: IF (a = X) Tìm thấy tại vị trí k byte(s) tính từ đầu tập tin B7: ELSE Không tìm thấy phần tử có giá trị X Trang: 14
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B8: Kết thúc c. Cài đặt thuật toán: Hàm FLinearSearch có prototype: long FLinearSearch (char * FileName, T X); 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 có giá trị từ 0 đến filelength(FileName) là vị trí tương ứng của phần tử tìm thấy so với đầu tập tin (tính bằng byte). Trong trường hợp ngược lại, hoặc có lỗi khi thao tác trên tập tin hàm trả về giá trị –1 (không tìm thấy hoặc lỗi thao tác trên tập tin). Nội dung của hàm như sau: long FLinearSearch (char * FileName, T X) { FILE * Fp; 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); } d. Phân tích thuật toán: - 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 gán: Gmin = 1 + 2 = 3 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 gán: Gmax = N + 2 Số phép so sánh: Smax = 2N + 1 Số lần đọc tập tin: Dmax = N - Trung bình: Số phép gán: Gavg = ½(N + 5) Số phép so sánh: Savg = (3 + 2N + 1) : 2 = N + 2 Số lần đọc tập tin: Davg = ½(N + 1) Trang: 15
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật 2.3.3. Tìm kiếm theo chỉ mục (Index Search) Như chúng ta đã biết, mỗi phần tử dữ liệu được lưu trữ trong tập tin dữ liệu F thường có kích thước lớn, điều này cũng làm cho kích thước của tập tin F cũng khá lớn. Vì vậy việc thao tác dữ liệu trực tiếp lên tập tin F sẽ trở nên lâu, chưa kể sự mất an toàn cho dữ liệu trên tập tin. Để giải quyết vấn đề này, đi kèm theo một tập tin dữ liệu thường có thêm các tập tin chỉ mục (Index File) để làm nhiệm vụ đ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) nào đó. Mỗi phần tử dữ liệu trong tập tin chỉ mục IDX gồm có 2 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 chỉ mục 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 như sau: typedef struct IdxElement { T IdxKey; long Pos; } IdxType; Tập tin chỉ mục luôn luôn được sắp xếp theo thứ tự tăng của khóa chỉ mục. Việc tạo tập tin chỉ mục IDX sẽ được nghiên cứu trong Chương 3, trong phần này chúng ta xem như đã có tập tin chỉ mục IDX để thao tác. a. Tư tưởng: Lần lượt đọc các phần tử từ đầu tập tin IDX và so sánh thành phần khóa chỉ mục với giá trị X cho đến khi đọc được phần tử có giá trị khóa chỉ mục lớn hơn hoặc bằng X hoặc đã đọc hết tập tin IDX thì kết thúc. Nếu tìm thấy thì ta đã có vị trí vật lý của phần tử dữ liệu trên tập tin dữ liệu F, khi đó chúng ta có thể truy cập trực tiếp đến vị trí này để đọc dữ liệu của phần tử tìm thấy. b. Thuật toán: B1: rewind(IDX) B2: read(IDX, ai) B3: IF ai.IdxKey < X AND !(eof(IDX)) Lặp lại B2 B4: IF ai.IdxKey = X Tìm thấy tại vị trí ai.Pos byte(s) tính từ đầu tập tin B5: ELSE Không tìm thấy phần tử có giá trị X B6: Kết thú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, hàm trả về một số nguyên có giá trị 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 ngược lại, hoặc có lỗi khi thao tác trên tập tin chỉ mục hàm trả về giá trị –1 (không tìm thấy). Nội dung của hàm như sau: Trang: 16
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật long IndexSearch (char * IdxFileName, T X) { FILE * IDXFp; 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); } d. Phân tích thuật toán: - 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 gán: Gmin = 1 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 gán: Gmax = 1 Số phép so sánh: Smax = 2N + 1 Số lần đọc tập tin: Dmax = N - Trung bình: Số phép gán: Gavg = 1 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? Trang: 17
- 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. - Tìm kiếm trên bảng chỉ mục xem trong tập tin NHANSU.DAT có hay không nhân viên có tên là X, nếu có thì in ra toàn bộ thông tin về nhân viên này. - Lưu trữ bảng chỉ mục này vào trong tập tin có tên NSTEN.IDX. - Vận dụng thuật toán tìm kiếm dựa trên tập tin chỉ mục NSTEN.IDX để tìm xem có hay không nhân viên có tên là X trong tập tin NHANSU.DAT, nếu có thì in ra toàn bộ thông tin về nhân viên này. - Có nhận xét gì khi thực hiện tìm kiếm dữ liệu trên tập tin bằng các phương pháp: Tìm tuyến tính và Tìm kiếm dựa trên tập tin chỉ mục. Trang: 18
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Chương 3: KỸ THUẬT SẮP XẾP (SORTING) 3.1. Khái quát về sắp xếp Để thuận tiện và giảm thiểu thời gian thao tác mà đặc biệt là để tìm kiếm dữ liệu dễ dàng và nhanh chóng, thông thường trước khi thao tác thì dữ liệu trên mảng, trên tập tin đã có thứ tự. Do vậy, thao tác sắp xếp dữ liệu là một trong những thao tác cần thiết và thường gặp trong quá trình lưu trữ, quản lý dữ liệu. Thứ tự xuất hiện dữ liệu có thể là thứ tự tăng (không giảm dần) hoặc thứ tự giảm (không tăng dần). Trong phạm vi chương này chúng ta sẽ thực hiện việc sắp xếp dữ liệu theo thứ tự tăng. Việc sắp xếp dữ liệu theo thứ tự giảm hoàn toàn tương tự. Có rất nhiều thuật toán sắp xếp song chúng ta có thể phân chia các thuật toán sắp xếp thành hai nhóm chính căn cứ vào vị trí lưu trữ của dữ liệu trong máy tính, đó 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). Cũng như trong chương trước, chúng ta giả sử rằng mỗi phần tử dữ liệu được xem xét có một thành phần khóa (Key) để nhận diện, có kiểu dữ liệu là T nào đó, các thành phần còn lại là thông tin (Info) liên quan đến phần tử dữ liệu đó. Như vậy mỗi phần tử dữ liệu có cấu trúc dữ liệu như sau: typedef struct DataElement { T Key; InfoType Info; } DataType; Trong chương này nói riêng và tài liệu này nói chung, các thuật toán sắp xếp của chúng ta là sắp xếp sao cho các phần tử dữ liệu có thứ tự tăng theo thành phần khóa (Key) nhận diện. Để đơn giản, chúng ta giả sử rằng mỗi phần tử dữ liệu chỉ là thành phần khóa nhận diện. 3.2. Các giải thuật sắp xếp nội (Sắp xếp trên dãy/mảng) Ở đây, toàn bộ dữ liệu cần sắp xếp được đưa vào trong bộ nhớ trong (RAM). Do vậy, số phần tử dữ liệu không lớn lắm do giới hạn của bộ nhớ trong, tuy nhiên tốc độ sắp xếp tương đối nhanh. Các giải thuật sắp xếp nội bao gồm các nhóm sau: - 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 của giáo trình này chúng ta chỉ trình bày một số thuật toán sắp xếp tiêu biểu trong các thuật toán sắp xếp ở các nhóm trên và giả sử thứ tự sắp xếp N phần tử có kiểu dữ liệu T trong mảng M là thứ tự tăng. Trang: 19
- 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 trong phần này sẽ tìm cách đổi chỗ các phần tử đứng sai vị trí (so với mảng đã sắp xếp) trong mảng M cho nhau để cuối cùng tất cả các phần tử trong mảng 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). Ở đây chúng ta trình bày hai thuật toán phổ biến là thuật toán sắp xếp nổi bọt và sắp xếp dựa trên sự phân hoạch. a. Thuật toán sắp xếp nổi bọt (Bubble Sort): - Tư tưởng: + Đi từ cuối mảng về đầu mảng, trong quá trình đi nếu phần tử ở dưới (đứng phía sau) nhỏ hơn phần tử đứng ngay trên (trước) nó thì theo nguyên tắc của bọt khí phần tử nhẹ sẽ bị “trồi” lên phía trên phần tử nặng (hai phần tử này sẽ được đổi chỗ cho nhau). Kết quả là phần tử nhỏ nhất (nhẹ nhất) sẽ được đưa lên (trồi lên) trên bề mặt (đầu mảng) rất nhanh. + Sau mỗi lần đi chúng ta đưa được một phần tử trồi lên đúng chỗ. Do vậy, sau N–1 lần đi thì tất cả các phần tử trong mảng M sẽ có thứ tự tăng. - Thuật toán: B1: First = 1 B2: IF (First = N) Thực hiện Bkt B3: ELSE B3.1: Under = N B3.2: If (Under = First) Thực hiện B4 B3.3: Else 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 B3.3.3: Lặp lại B3.2 B4: First++ B5: Lặp lại B2 Bkt: Kết thúc - Cài đặt thuật toán: Hàm BubbleSort có prototype như sau: void BubbleSort(T M[], int N); Trang: 20
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật 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 nổi bọt. Nội dung của hàm như sau: void BubbleSort(T M[], int N) { for (int I = 0; I 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) { T Temp = X; X = Y; Y = Temp; return; } - 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): M: 15 10 2 20 10 5 25 35 22 30 Ta sẽ thực hiện 9 lần đi (N - 1 = 10 - 1 = 9) để sắp xếp mảng M: Lần 1: First = 1 J: 2 3 4 5 6 7 8 9 10 M: 15 10 2 20 10 5 25 35 22 30 M: 15 10 2 20 10 5 25 22 35 30 M: 15 10 2 20 10 5 22 25 35 30 M: 15 10 2 20 5 10 22 25 35 30 M: 15 10 2 5 20 10 22 25 35 30 M: 15 2 10 5 20 10 22 25 35 30 Trang: 21
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật M: 2 15 10 5 20 10 22 25 35 30 Lần 2: First = 2 J: 3 4 5 6 7 8 9 10 M: 2 15 10 5 20 10 22 25 35 30 M: 2 15 10 5 20 10 22 25 30 35 M: 2 15 10 5 10 20 22 25 30 35 M: 2 15 5 10 10 20 22 25 30 35 M: 2 5 15 10 10 20 22 25 30 35 Lần 3: First = 3 J: 4 5 6 7 8 9 10 M: 2 5 15 10 10 20 22 25 30 35 M: 2 5 10 15 10 20 22 25 30 35 Lần 4: First = 4 J: 5 6 7 8 9 10 M: 2 5 10 15 10 20 22 25 30 35 M: 2 5 10 10 15 20 22 25 30 35 Lần 5: First = 5 J: 6 7 8 9 10 M: 2 5 10 10 15 20 22 25 30 35 Lần 6: First = 6 J: 7 8 9 10 M: 2 5 10 10 15 20 22 25 30 35 Trang: 22
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Lần 7: First = 7 J: 8 9 10 M: 2 5 10 10 15 20 22 25 30 35 Lần 8: First = 8 J: 9 10 M: 2 5 10 10 15 20 22 25 30 35 Lần 9: First = 9 J: 10 M: 2 5 10 10 15 20 22 25 30 35 Sau 9 lần đi mảng M trở thành: M: 2 5 10 10 15 20 22 25 30 35 - Phân tích thuật toán: + Trong mọi trường hợp: Số phép gán: G = 0 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. + Trong thuật toán sắp xếp nổi bọt, mỗi lần đi từ cuối mảng về đầu mảng thì 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 khá chậm chạp do không tận dụng được chiều đi xuống (chiều từ đầu mảng về cuối mảng). + Thuật toán nổi bọt không phát hiện ra được các đoạn phần tử nằm hai đầu của mảng đã nằm đúng vị trí để có thể giảm bớt quãng đường đi trong mỗi lần đi. b. Thuật toán sắp xếp dựa trên sự phân hoạch (Partitioning Sort): 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). - Tư tưởng: + Phân hoạch dãy M thành 03 dãy con có thứ tự tương đối thỏa mãn điều kiện: Dãy con thứ nhất (đầ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, Trang: 23
- 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 kiếm phần tử trong M có giá trị bằng giá trị trung bình của dãy M rất khó khăn và mất thời gian. Trong thực tế, chúng ta chọn một phần tử bất kỳ (thường là phần tử đứng ở vị trí giữa) trong dãy các phần tử cần phân hoạch để làm giá trị cho các phần tử của dãy con thứ hai (dãy giữa) sau khi phân hoạch. Phần tử này còn đượ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 giá trị phần tử biên và các phần tử trong dãy con thứ ba sẽ có giá trị lớn hơn giá trị 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. - Thuật toán: B1: First = 1 B2: Last = N B3: IF (First ≥ Last) //Dãy con chỉ còn không quá 01 phần tử Thực hiện Bkt 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 B6: IF (M[I] > X) Thực hiện B8 B7: ELSE B7.1: I++ B7.2: Lặp lại B6 B8: J = Last //Xuất phát từ cuối dãy 3 để tìm phần tử có giá trị < X B9: IF (M[J] < X) Thực hiện B11 B10: ELSE B10.1: J B10.2: Lặp lại B9 B11: IF (I ≤ J) B11.1: Hoán_Vị(M[I], M[J]) B11.2: I++ B11.3: J B11.4: Lặp lại B6 B12: ELSE 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 Bkt: Kết thúc - Cài đặt thuật toán: Trang: 24
- 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. Hàm 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ừ phần tử thứ First đến phần tử thứ 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) { if (First >= Last) return; T X = M[(First+Last)/2]; int I = First; int J = Last; do { while (M[I] X) J ; if (I <= J) { Swap(M[I], M[J]); I++; J ; } } while (I <= J); PartitionSort(M, First, J); PartitionSort(M, I, Last); return; } //=== void QuickSort(T M[], int N) { PartitionSort(M, 0, N-1); return; } - 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): M: 45 55 25 20 15 5 25 30 10 3 Ban đầu: First = 1 Last = 10 X = M[(1+10)/2] =M[5] = 15 First X = 15 Last M: 45 55 25 20 15 5 25 30 10 3 Phân hoạch: Trang: 25
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật I X = 15 J M: 45 55 25 20 15 5 25 30 10 3 I X = 15 J M: 3 55 25 20 15 5 25 30 10 45 I X = 15 J M: 3 10 25 20 15 5 25 30 55 45 I X = 15 M: 3 10 5 20 15 25 25 30 55 45 J First X = 15 I Last M: 3 10 5 15 20 25 25 30 55 45 J Phân hoạch các phần tử trong dãy con từ First -> J: First = 1 Last = J = 4 X = M[(1+4)/2] = M[2] = 10 First X = 10 Last M: 3 10 5 15 20 25 25 30 55 45 Phân hoạch: I X = 10 J M: 3 10 5 15 20 25 25 30 55 45 X = 10 J M: 3 10 5 15 20 25 25 30 55 45 I J X = 10 M: 3 5 10 15 20 25 25 30 55 45 I Phân hoạch các phần tử trong dãy con từ First -> J: First = 1 Last = J = 2 X = M[(1+2)/2] = M[1] = 3 First Last M: 3 5 10 15 20 25 25 30 55 45 X = 3 Trang: 26
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Phân hoạch: I J M: 3 5 10 15 20 25 25 30 55 45 X = 3 I≡J M: 3 5 10 15 20 25 25 30 55 45 X = 3 J I M: 3 5 10 15 20 25 25 30 55 45 X = 3 First J I Last M: 3 5 10 15 20 25 25 30 55 45 Phân hoạch các phần tử trong dãy con từ I -> Last: First = I = 3 Last = 4 X = M[(3+4)/2] = M[3] = 10 First Last M: 3 5 10 15 20 25 25 30 55 45 X = 10 Phân hoạch: I J M: 3 5 10 15 20 25 25 30 55 45 X = 10 I≡J M: 3 5 10 15 20 25 25 30 55 45 X = 10 J I M: 3 5 10 15 20 25 25 30 55 45 X = 10 First J I Last M: 3 5 10 15 20 25 25 30 55 45 Phân hoạch các phần tử trong dãy con từ I -> Last: First = I = 5 Last = 10 X = M[(5+10)/2] = M[7] = 25 First X = 25 Last M: 3 5 10 15 20 25 25 30 55 45 Phân hoạch: Trang: 27
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật I X = 25 J M: 3 5 10 15 20 25 25 30 55 45 I X = 25 M: 3 5 10 15 20 25 25 30 55 45 J First X = 25 I Last M: 3 5 10 15 20 25 25 30 55 45 J Phân hoạch các phần tử trong dãy con từ First -> J: First = 5 Last = J = 6 X = M[(5+6)/2] = M[5] = 20 First Last M: 3 5 10 15 20 25 25 30 55 45 X = 20 Phân hoạch: I J M: 3 5 10 15 20 25 25 30 55 45 X = 20 I≡J M: 3 5 10 15 20 25 25 30 55 45 X = 20 J I M: 3 5 10 15 20 25 25 30 55 45 X = 20 First J I Last M: 3 5 10 15 20 25 25 30 55 45 Phân hoạch các phần tử trong dãy con từ I -> Last: First = I = 7 Last = 10 X = M[(7+10)/2] = M[8] = 30 First X = 30 Last M: 3 5 10 15 20 25 25 30 55 45 Phân hoạch: I X = 30 J M: 3 5 10 15 20 25 25 30 55 45 I≡J M: 3 5 10 15 20 25 25 30 55 45 Trang: 28
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật X = 30 J I M: 3 5 10 15 20 25 25 30 55 45 X = 30 First≡J I Last M: 3 5 10 15 20 25 25 30 55 45 X = 30 Phân hoạch các phần tử trong dãy con từ I -> Last: First = I = 9 Last = 10 X = M[(9+10)/2] = M[9] = 55 First Last M: 3 5 10 15 20 25 25 30 55 45 X = 55 Phân hoạch: I J M: 3 5 10 15 20 25 25 30 55 45 X = 55 J I M: 3 5 10 15 20 25 25 30 45 55 X = 55 M: 3 5 10 15 20 25 25 30 45 55 Toàn bộ quá trình phân hoạch kết thúc, dãy M trở thành: M: 3 5 10 15 20 25 25 30 45 55 - Phân tích thuật toán: + 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 + Trường hợp xấu nhất, khi phần tử X được chọn ở giữa dãy con là giá trị lớn nhất của dãy con. Trường hợp này thuật toán QuickSort trở nên chậm chạp nhất: Số phép gán: Gmax = 1 + 2 + + (N-1) = N×(N-1)/2 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 + Trung bình: Số phép gán: Gavg = [(N-1)+N(N-1)/2]/2 = (N-1)×(N+2)/4 Số phép so sánh: Savg = [N×Log2(N)/2 + N×(N-1)]/2 = N×[Log2(N)+2N–2]/4 Số phép hoán vị: Havg = N×(N-1)/4 Trang: 29
- 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 thuật toán trong phần này sẽ tìm cách lựa chọn các phần tử thỏa mãn điều kiện chọn lựa để đưa về đúng vị trí của phần tử đó, cuối cùng tất cả các phần tử trong mảng M đều về đúng vị trí. Các thuật toán sắp xếp bằng phương pháp chọn bao gồm: - Thuật toán sắp xếp chọn trực tiếp (straight selection sort), - Thuật toán sắp xếp dựa trên khối/heap hay sắp xếp trên cây (heap sort). Ở đây chúng ta chỉ trình bày thuật toán sắp xếp chọn trực tiếp Thuật toán sắp xếp chọn trực tiếp (Straight Selection Sort): - Tư tưởng: + Ban đầu dãy có N phần tử chưa có thứ tự. Ta chọn phần tử có giá trị nhỏ nhất trong N phần tử chưa có thứ tự này để đưa lên đầu nhóm N phần tử. + Sau lần thứ nhất chọn lựa phần tử nhỏ nhất và đưa lên đầu nhóm chúng ta còn lại N-1 phần tử đứng ở phía sau dãy M chưa có thứ tự. Chúng ta tiếp tục chọn phần tử có giá trị nhỏ nhất trong N-1 phần tử chưa có thứ tự này để đưa lên đầu nhóm N-1 phần tử. . Do vậy, sau N–1 lần chọn lựa phần tử nhỏ nhất để đưa lên đầu nhóm thì tất cả các phần tử trong dãy M sẽ có thứ tự tăng. + Như vậy, thuật toán này chủ yếu chúng ta đi tìm giá trị nhỏ nhất trong nhóm N-K phần tử chưa có thứ tự đứng ở phía sau dãy M. Việc này đơn giản chúng ta vận dụng thuật toán tìm kiếm tuần tự. - Thuật toán: B1: K = 0 B2: IF (K = N-1) Thực hiện Bkt B3: Min = M[K+1] B4: PosMin = K+1 B5: Pos = K+2 B6: IF (Pos > N) Thực hiện B8 B7: ELSE B7.1: If (Min > M[Pos]) B7.1.1: Min = M[Pos] B7.1.2: PosMin = Pos B7.2: Pos++ B7.3: Lặp lại B6 B8: HoánVị(M[K+1], M[PosMin]) B9: K++ B10: Lặp lại B2 Bkt: Kết thúc - Cài đặt thuật toán: Hàm SelectionSort có prototype như sau: Trang: 30
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật void SelectionSort(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 chọn trực tiếp. Nội dung của hàm như sau: void SelectionSort(T M[], int N) { int K = 0, PosMin; while (K M[Pos]) { Min = M[Pos]; PosMin = Pos } Swap(M[K], M[PosMin]); K++; } return; } - 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): M: 1 60 2 25 15 45 5 30 33 20 Ta sẽ thực hiện 9 lần chọn lựa (N - 1 = 10 - 1 = 9) phần tử nhỏ nhất để sắp xếp mảng M: Lần 1: Min = 1 PosMin = 1 K = 0 K+1 M: 1 60 2 25 15 45 5 30 33 20 Lần 2: Min = 2 PosMin = 3 K = 1 K+1 M: 1 60 2 25 15 45 5 30 33 20 K+1 M: 1 2 60 25 15 45 5 30 33 20 Lần 3: Min = 5 PosMin = 7 K = 2 K+1 M: 1 2 60 25 15 45 5 30 33 20 K+1 M: 1 2 5 25 15 45 60 30 33 20 Trang: 31
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Lần 4: Min = 15 PosMin = 5 K = 3 K+1 M: 1 2 5 25 15 45 60 30 33 20 K+1 M: 1 2 5 15 25 45 60 30 33 20 Lần 5: Min = 20 PosMin = 10 K = 4 K+1 M: 1 2 5 15 25 45 60 30 33 20 K+1 M: 1 2 5 15 20 45 60 30 33 25 Lần 6: Min = 25 PosMin = 10 K = 5 K+1 M: 1 2 5 15 20 45 60 30 33 25 K+1 M: 1 2 5 15 20 25 60 30 33 45 Lần 7: Min = 30 PosMin = 8 K = 6 K+1 M: 1 2 5 15 20 25 60 30 33 45 K+1 M: 1 2 5 15 20 25 30 60 33 45 Lần 8: Min = 33 PosMin = 9 K = 7 K+1 M: 1 2 5 15 20 25 30 60 33 45 K+1 M: 1 2 5 15 20 25 30 33 60 45 Lần 9: Min = 45 PosMin = 10 K = 8 K+1 M: 1 2 5 15 20 25 30 33 60 45 Trang: 32
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật K+1 M: 1 2 5 15 20 25 30 33 45 60 Sau lần 9: K = 9 và mảng M trở thành: M: 1 2 5 15 20 25 30 33 45 60 - Phân tích thuật toán: + Trong mọi trường hợp: Số phép so sánh: S = (N-1)+(N-2)+ +1 = N×(N-1)/2 Số phép hoán vị: H = N-1 + 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 = 2×(N-1) + Trường hợp xấu nhất, khi mảng M ban đầu đã có thứ tự giảm dần: Số phép gán: Gmax = 2×[N+(N-1)+ +1] = N×(N+1) + Trung bình: Số phép gán: Gavg = [2×(N-1)+N×(N+1)]/2 = (N-1) + N×(N+1)/2 3.2.3. Sắp xếp bằng phương pháp chèn (Insertion Sort) Các thuật toán trong phần này sẽ tìm cách tận dụng K phần tử đầu dãy M đã có thứ tự tăng, chúng ta đem phần tử thứ K+1 chèn vào K phần tử đầu dãy sao cho sau khi chèn chúng ta có K+1 phần tử đầu dãy M đã có thứ tự tăng. Ban đầu dãy M có ít nhất 1 phần tử đầu dãy đã có thứ tự tăng (K=1). Như vậy sau tối đa N-1 bước chèn là chúng ta sẽ sắp xếp xong dãy M có N phần tử theo thứ tự tăng. Các thuật toán sắp xếp bằng phương pháp chèn bao gồm: - Thuật toán sắp xếp chèn trực tiếp (straight insertion sort), - Thuật toán sắp xếp chèn nhị phân (binary insertion sort). Trong tài liệu này chúng ta chỉ trình bày thuật toán sắp xếp chèn trực tiếp. Thuật toán sắp xếp chèn trực tiếp (Straight Insertion Sort): - Tư tưởng: Để chèn phần tử thứ K+1 vào K phần tử đầu dãy đã có thứ tự chúng ta sẽ tiến hành tìm vị trí đúng của phần tử K+1 trong K phần tử đầu bằng cách vận dụng thuật giải tìm kiếm tuần tự (Sequential Search). Sau khi tìm được vị trí chèn (chắc chắn có vị trí chèn) thì chúng ta sẽ tiến hành chèn phần tử K+1 vào đúng vị trí chèn bằng cách dời các phần tử từ vị trí chèn đến phần tử thứ K sang phải (ra phía sau) 01 vị trí và chèn phần tử K+1 vào vị trí của nó. - Thuật toán: B1: K = 1 B2: IF (K = N) Trang: 33
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Thực hiện Bkt B3: X = M[K+1] B4: Pos = 1 B5: IF (Pos > K) Thực hiện B7 B6: ELSE //Tìm vị trí chèn B6.1: If (X Pos) //Nếu còn phải dời các phần tử từ Pos->K về phía sau 1 vị trí B8.1: M[I] = M[I-1] B8.2: I B8.3: Lặp lại B8 B9: ELSE //Đã dời xong các phần tử từ Pos->K về phía sau 1 vị trí B9.1: M[Pos] = X //Chèn X vào vị trí Pos B9.2: K++ B9.3: Lặp lại B2 Bkt: Kết thúc - Cài đặt thuật toán: Hàm InsertionSort có prototype như sau: void InsertionSort(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 chèn trực tiếp. Nội dung của hàm như sau: void InsertionSort(T M[], int N) { int K = 1, Pos; while (K M[Pos]) Pos++; for (int I = K; I > Pos; I ) M[I] = M[I-1]; M[Pos] = X; K++; } return; } - 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): M: 11 16 12 75 51 54 5 73 36 52 Ta sẽ thực hiện 9 lần chèn (N - 1 = 10 - 1 = 9) các phần tử vào dãy con đã có thứ tự tăng đứng đầu dãy M: Trang: 34
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Lần 1: K = 1 X = M[K+1] = M[2] = 16 Pos = 2 K: 1 M: 11 16 12 75 51 54 5 73 36 52 X Lần 2: K = 2 X = M[K+1] = M[3] = 12 Pos = 2 K: 1 2 M: 11 16 12 75 51 54 5 73 36 52 X K: 1 2 M: 11 12 16 75 51 54 5 73 36 52 X Lần 3: K = 3 X = M[K+1] = M[4] = 75 Pos = 4 K: 1 2 3 M: 11 12 16 75 51 54 5 73 36 52 X K: 1 2 3 M: 11 12 16 75 51 54 5 73 36 52 X Lần 4: K = 4 X = M[K+1] = M[5] = 51 Pos = 4 K: 1 2 3 4 M: 11 12 16 75 51 54 5 73 36 52 X K: 1 2 3 4 M: 11 12 16 51 75 54 5 73 36 52 X Lần 5: K = 5 X = M[K+1] = M[6] = 54 Pos = 5 K: 1 2 3 4 5 M: 11 12 16 51 75 54 5 73 36 52 X K: 1 2 3 4 5 M: 11 12 16 51 54 75 5 73 36 52 X Trang: 35
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Lần 6: K = 6 X = M[K+1] = M[7] = 5 Pos = 1 K: 1 2 3 4 5 6 M: 11 12 16 51 54 75 5 73 36 52 X K: 1 2 3 4 5 6 M: 5 11 12 16 51 54 75 73 36 52 X Lần 7: K = 7 X = M[K+1] = M[8] = 73 Pos = 7 K: 1 2 3 4 5 6 7 M: 5 11 12 16 51 54 75 73 36 52 X K: 1 2 3 4 5 6 7 M: 5 11 12 16 51 54 73 75 36 52 X Lần 8: K = 8 X = M[K+1] = M[9] = 36 Pos = 5 K: 1 2 3 4 5 6 7 8 M: 5 11 12 16 51 54 73 75 36 52 X K: 1 2 3 4 5 6 7 8 M: 5 11 12 16 36 51 54 73 75 52 X Lần 9: K = 9 X = M[K+1] = M[10] = 52 Pos = 7 K: 1 2 3 4 5 6 7 8 9 M: 5 11 12 16 36 51 54 73 75 52 X K: 1 2 3 4 5 6 7 8 9 M: 5 11 12 16 36 51 52 54 73 75 X Thuật toán kết thúc: K = 10, mảng M đã được sắp xếp theo thứ tự tăng K: 1 2 3 4 5 6 7 8 9 10 M: 5 11 12 16 36 51 52 54 73 75 Trang: 36
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật - Phân tích thuật toán: + 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 = 2×(N-1) Số phép so sánh: Smin = 1+2+ +(N-1) = N×(N-1)/2 Số phép hoán vị: Hmin = 0 + Trường hợp xấu nhất, khi mảng M ban đầu luôn có phần tử nhỏ nhất trong N-K phần tử còn lại đứng ở vị trí sau cùng sau mỗi lần hoán vị: Số phép gán: Gmax = [2×(N-1)]+[ 1+2+ +(N-1)] = [2×(N-1)] + [N×(N-1)/2] Số phép so sánh: Smax = (N-1) Số phép hoán vị: Hmax = 0 + Trung bình: Số phép gán: Gavg = 2×(N-1) + [N×(N-1)/4] Số phép so sánh: Savg = [N×(N-1)/2 + (N-1)]/2 = (N+2)×(N-1)/4 Số phép hoán vị: Havg = 0 + Chúng ta nhận thấy rằng quá trình tìm kiếm vị trí chèn của phần tử K+1 và quá trình dời các phần tử từ vị trí chèn đến K ra phía sau 01 vị trí có thể kết hợp lại với nhau. Như vậy, quá trình di dời các phần tử ra sau này sẽ bắt đầu từ phần tử thứ K trở về đầu dãy M cho đến khi gặp phần tử có giá trị nhỏ hơn phần tử K+1 thì chúng ta đồng thời vừa di dời xong và đồng thời cũng bắt gặp vị trí chèn. Ngoài ra, chúng ta cũng có thể tính toán giá trị ban đầu cho K tùy thuộc vào số phần tử đứng đầu dãy M ban đầu có thứ tự tăng là bao nhiêu phần tử chứ không nhất thiết phải là 1. Khi đó, thuật toán sắp xếp chèn trực tiếp của chúng ta có thể được hiệu chỉnh lại như sau: - Thuật toán hiệu chỉnh: B1: K = 1 B2: IF (M[K] 0 And X < M[Pos]) B6.1: M[Pos+1] = M[Pos] B6.2: Pos B6.3: Lặp lại B6 B7: ELSE //Chèn X vào vị trí Pos+1 B7.1: M[Pos+1] = X B7.2: K++ B7.3: Lặp lại B3 Bkt: Kết thúc - Cài đặt thuật toán hiệu chỉnh: Hàm InsertionSort1 có prototype như sau: Trang: 37
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật void InsertionSort1(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 chèn trực tiếp đã hiệu chỉnh. Nội dung của hàm như sau: void InsertionSort1(T M[], int N) { int K = 1, Pos; while(M[K-1] = 0) { M[Pos+1] = M[Pos]; Pos ; } M[Pos+1] = X; K++; } return; } - Ví dụ minh họa thuật toán hiệu chỉnh: Giả sử ta cần sắp xếp mảng M có 10 phần tử sau (N = 10): M: 14 16 20 75 50 5 25 75 60 50 Ban đầu K = 4 nên ta sẽ thực hiện 6 lần chèn (N - 4 = 10 - 4 = 6) các phần tử vào dãy con đã có thứ tự tăng đứng đầu dãy M: Lần 1: K = 4 X = M[K+1] = M[5] = 50 Pos = 3 => Pos + 1 = 4 K: 1 2 3 4 M: 14 16 20 75 50 5 25 75 60 50 X=50 K: 1 2 3 4 M: 14 16 20 75 75 5 25 75 60 50 K: 1 2 3 4 M: 14 16 20 50 75 5 25 75 60 50 X Lần 2: K = 5 X = M[K+1] = M[6] = 5 Pos = 0 => Pos + 1 = 1 K: 1 2 3 4 5 M: 14 16 20 50 75 5 25 75 60 50 X=5 K: 1 2 3 4 5 M: 14 14 16 20 50 75 25 75 60 50 Trang: 38
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật K: 1 2 3 4 5 M: 5 14 16 20 50 75 25 75 60 50 X Lần 3: K = 6 X = M[K+1] = M[7] = 25 Pos = 4 => Pos + 1 = 5 K: 1 2 3 4 5 6 M: 5 14 16 20 50 75 25 75 60 50 X=25 K: 1 2 3 4 5 6 M: 5 14 16 20 50 50 75 75 60 50 K: 1 2 3 4 5 6 M: 5 14 16 20 25 50 75 75 60 50 X Lần 4: K = 7 X = M[K+1] = M[8] = 75 Pos = 7 => Pos + 1 = 8 K: 1 2 3 4 5 6 7 M: 5 14 16 20 25 50 75 75 60 50 X=75 K: 1 2 3 4 5 6 7 M: 5 14 16 20 25 50 75 75 60 50 X=75 Lần 5: K = 8 X = M[K+1] = M[9] = 60 Pos = 6 => Pos + 1 = 7 K: 1 2 3 4 5 6 7 8 M: 5 14 16 20 25 50 75 75 60 50 X=60 K: 1 2 3 4 5 6 7 8 M: 5 14 16 20 25 50 75 75 75 50 K: 1 2 3 4 5 6 7 8 M: 5 14 16 20 25 50 60 75 75 50 X Lần 6: K = 9 X = M[K+1] = M[10] = 50 Pos = 6 => Pos + 1 = 7 K: 1 2 3 4 5 6 7 8 9 M: 5 14 16 20 25 50 60 75 75 50 Trang: 39
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật X=50 K: 1 2 3 4 5 6 7 8 9 M: 5 14 16 20 25 50 60 60 75 75 K: 1 2 3 4 5 6 7 8 9 M: 5 14 16 20 25 50 50 60 75 75 X Thuật toán kết thúc: K = 10, mảng M đã được sắp xếp theo thứ tự tăng K: 1 2 3 4 5 6 7 8 9 10 M: 5 14 16 20 25 50 50 60 75 75 - Phân tích thuật toán hiệu chỉ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 Số phép so sánh: Smin = 2×(N-1) + 1 Số phép hoán vị: Hmin = 0 + Trường hợp xấu nhất, khi mảng M ban đầu đã có thứ tự giảm dần: Số phép gán: Gmax = 1+[1+2+ +(N-1)]+[N-1] = N×(N+1)/2 Số phép so sánh: Smax = 1+2×[1+2+ +(N-1)]+[N-1] = N2 Số phép hoán vị: Hmax = 0 + Trung bình: Số phép gán: Gavg = [1+ N×(N-1)/2]/2 Số phép so sánh: Savg = [2×(N-1) + 1+N2]/2 Số phép hoán vị: Havg = 0 3.2.4. Sắp xếp bằng phương pháp trộn (Merge Sort) Các thuật toán trong phần này sẽ tìm cách tách mảng M thành các mảng con theo các đường chạy (run) rồi sau đó tiến hành nhập các mảng này lại theo từng cặp đường chạy để tạo thành các đường chạy mới có chiều dài lớn hơn đường chạy cũ. Sau một số lần tách/nhập thì cuối cùng mảng M chỉ còn lại 1 đường chạy, lúc đó thì các phần tử trên mảng M sẽ trở nên có thứ tự. Các thuật toán sắp xếp bằng phương pháp trộn 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). Trước khi đi vào chi tiết từng thuật toán chúng ta hãy tìm hiểu khái niệm và các vấn đề liên quan đến đường chạy (run) - Đường chạy (Run): Dãy M[I], M[I+1], , M[J] (I ≤ J: 1 ≤ I, J ≤ N) là một đường chạy nếu nó có thứ tự. Trang: 40
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật - Chiều dài của đường chạy (Run’s Length): Số phần tử của một đường chạy còn được gọi là chiều dài của đường chạy. Như vậy: + Mỗi phần tử của dãy là một đường chạy có chiều dài bằng 1. + Một dãy có thể bao gồm nhiều đường chạy. - Trộn các đường chạy: Khi ta trộn các đường chạy lại với nhau sẽ cho ra một đường chạy mới có chiều dài bằng tổng chiều dài các đường chạy ban đầu. a. Thuật toán sắp xếp trộn trực tiếp hay trộn thẳng (Straight Merge Sort): - Tư tưởng: Ban đầu dãy M có N run(s) với chiều dài mỗi run: L = 1, ta tiến hành phân phối luân phiên N run(s) của dãy M về hai dãy phụ Temp1, Temp2 (Mỗi dãy phụ có N/2 run(s)). Sau đó trộn tương ứng từng cặp run ở hai dãy phụ Temp1, Temp2 thành một run mới có chiều dài L = 2 để đưa về M và dãy M trở thành dãy có N/2 run(s) với chiều dài mỗi run: L = 2. Như vậy, sau mỗi lần phân phối và trộn các run trên dãy M thì số run trên dãy M sẽ giảm đi một nửa, đồng thời chiều dài mỗi run sẽ tăng gấp đôi. Do đó, sau Log2(N) lần phân phối và trộn thì dãy M chỉ còn lại 01 run với chiều dài là N và khi đó dãy M trở thành dãy có thứ tự. Trong thuật giải sau, để dễ theo dõi chúng ta trình bày riêng 02 thuật giải: + 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 dãy M về các dãy phụ Temp1, Temp2. + Thuật giải trộn (nhập) các cặp đường chạy trên Temp1, Temp2 có chiều dài L về M thành các đường chạy với chiều dài 2*L. - Thuật toán phân phối: B1: I = 1 //Chỉ số trên M B2: J1 = 1 //Chỉ số trên Temp1 B3: J2 = 1 //Chỉ số trên Temp2 B4: IF (I > N) //Đã phân phối hết Thực hiện Bkt //Chép 1 run từ M sang Temp1 B5: K = 1 //Chỉ số để duyệt các run B6: IF (K > L) //Duyệt hết 1 run Thực hiện B13 B7: Temp1[J1] = M[I] //Chép các phần tử của run trên M sang Temp1 B8: I++ B9: J1++ B10: K++ B11: IF (I > N) //Đã phân phối hết Thực hiện Bkt B12: Lặp lại B6 //Chép 1 run từ M sang Temp2 Trang: 41
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B13: K = 1 B14: IF (K > L) Thực hiện B21 B15: Temp2[J2] = M[I] //Chép các phần tử của run trên M sang Temp2 B16: I++ B17: J2++ B18: K++ B19: IF (I > N) //Đã phân phối hết Thực hiện Bkt B20: Lặp lại B14 B21: Lặp lại B4 B22: N1 = J1-1 //Số phần tử trên Temp1 B23: N2 = J2-1 //Số phần tử trên Temp2 Bkt: Kết thúc - Thuật toán trộn: B1: I = 1 // Chỉ số trên M B2: J1 = 1 //Chỉ số trên Temp1 B3: J2 = 1 //Chỉ số trên Temp2 B4: K1 = 1 //Chỉ số để duyệt các run trên Temp1 B5: K2 = 1 //Chỉ số để duyệt các run trên Temp2 B6: IF (J1 > N1) //Đã chép hết các phần tử trong Temp1 Thực hiện B25 B7: IF (J2 > N2) //Đã chép hết các phần tử trong Temp2 Thực hiện B30 B8: IF (Temp1[J1] ≤ Temp2[J2]) //Temp1[J1] đứng trước Temp2[J2] trên M B8.1: M[I] = Temp1[J1] B8.2: I++ B8.3: J1++ B8.4: K1++ B8.5: If (K1 > L) //Đã duyệt hết 1 run trong Temp1 Thực hiện B11 B8.6: Lặp lại B6 B9: ELSE //Temp2[J2] đứng trước Temp1[J1] trên M B9.1: M[I] = Temp2[J2] B9.2: I++ B9.3: J2++ B9.4: K2++ B9.5: If (K2 > L) //Đã duyệt hết 1 run trong Temp2 Thực hiện B18 B9.6: Lặp lại B6 B10: Lặp lại B4 //Chép phần run còn lại trong Temp2 về M B11: IF (K2 > L) //Đã chép hết phần run còn lại trong Temp2 về M Lặp lại B4 B12: M[I] = Temp2[J2] B13: I++ B14: J2++ Trang: 42
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B15: K2++ B16: IF (J2 > N2) //Đã chép hết các phần tử trong Temp2 Thực hiện B30 B17: Lặp lại B11 //Chép phần run còn lại trong Temp1 về M B18: IF (K1 > L) //Đã chép hết phần run còn lại trong Temp1 về M Lặp lại B4 B19: M[I] = Temp1[J1] B20: I++ B21: J1++ B22: K1++ B23: IF (J1 > N1) //Đã chép hết các phần tử trong Temp1 Thực hiện B25 B24: Lặp lại B18 //Chép các phần tử còn lại trong Temp2 về M B25: IF (J2>N2) Thực hiện Bkt B26: M[I] = Temp2[J2] B27: I++ B28: J2++ B29: Lặp lại B25 //Chép các phần tử còn lại trong Temp1 về M B30: IF (J1>N1) Thực hiện Bkt B31: M[I] = Temp1[J1] B32: I++ B33: J1++ B34: Lặp lại B30 Bkt: Kết thúc - 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) //Dãy chỉ còn 01 run Thực hiện Bkt B3: Phân_Phối(M, N, Temp1, N1, Temp2, N2, L) B4: Trộn(Temp1, N1, Temp2, N2, M, N, L) B5: L = 2*L B6: Lặp lại B2 Bkt: Kết thúc - Cài đặt thuật toán: Hàm StraightMergeSort có prototype như sau: void StraightMergeSort(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 trộn trực tiếp. Hàm sử dụng các hàm Distribute, Merge có prototype và ý nghĩa như sau: void Distribute(T M[], int N, T Temp1[], int &N1, T Temp2[], int &N2, int L); Trang: 43
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Hàm thực hiện việc phân phối luân phiên các đường chạy có chiều dài L trên dãy M có N phần tử về thành các dãy Temp1 và Temp2 có tương ứng N1 và N2 phần tử. void Merge(T Temp1[], int N1, T Temp2[], int N2, T M[], int &N, int L); Hàm thực hiện việc trộn từng cặp tương ứng các đường chạy với độ dài L trên Temp1, Temp2 về dãy M thành các đường chạy có chiều dài 2*L. Nội dung của các hàm như sau: void Distribute(T M[], int N, T Temp1[], int &N1, T Temp2[], int &N2, int L) { int I = 0, J1 = 0, J2 = 0; while (I < N) { for(int K = 0; K<L && I<N; K++, I++, J1++) Temp1[J1] = M[I]; for(K = 0; K<L && I<N; K++, I++, J2++) Temp2[J2] = M[I]; } N1 = J1; N2 = J2; return; } //=== void Merge(T Temp1[], int N1, T Temp2[], int N2, T M[], int &N, int L) { int I = 0, J1 = 0, J2 = 0, K1 = 0, K2 = 0; while (J1 < N1 && J2 < N2) { while (Temp1[J1] <= Temp2[J2] && J1 < N1 && J2 < N2) { M[I] = Temp1[J1]; I++; J1++; if (J1 == N1) { for (; J2 < N2; J2++, I++) M[I] = Temp2[J2]; return; } K1++; if (K1 == L) { for (; K2 < L && J2 < N2; K2++, I++, J2++) M[I] = Temp2[J2]; K1 = K2 = 0; break; } } while (Temp2[J2] < Temp1[J1] && J1 < N1 && J2 < N2) { M[I] = Temp2[J2]; I++; J2++; if (J2 == N2) { for (; J1 < N1; J1++, I++) M[I] = Temp1[J1]; Trang: 44
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật return; } K2++; if (K2 == L) { for (; K1 < L && J1 < N1; K1++, I++, J1++) M[I] = Temp1[J1]; K1 = K2 = 0; break; } } } while (J1 < N1) { M[I] = Temp1[J1]; I++; J1++; } while (J2 < N2) { M[I] = Temp2[J2]; I++; J2++; } N = N1 + N2; return; } //=== void StraightMergeSort(T M[], int N) { int L = 1, N1 = 0, N2 = 0; T * Temp1 = new T[N]; T * Temp2 = new T[N]; if (Temp1 == NULL || Temp2 == NULL) return; while (L < N) { Distribute(M, N, Temp1, N1, Temp2, N2, L); Merge(Temp1, N1, Temp2, N2, M, N, L); L = 2*L; } delete Temp1; delete Temp2; return; } - 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): M: 41 36 32 47 65 21 52 57 70 50 Ta thực hiện các lần phân phối và trộn các phần tử của M như sau: Lần 1: L = 1 Trang: 45
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Phân phối M thành Temp1, Temp2: M: 41 36 32 47 65 21 52 57 70 50 Temp1:N1=5 41 32 65 52 70 Temp2:N2=5 36 47 21 57 50 Trộn Temp1, Temp2 thành M: Temp1:N1=5 41 32 65 52 70 Temp2:N2=5 36 47 21 57 50 M: 36 41 32 47 21 65 52 57 50 70 Lần 2: L = 2 Phân phối M thành Temp1, Temp2: M: 36 41 32 47 21 65 52 57 50 70 Temp1: N1=6 36 41 21 65 50 70 Temp2: N2=4 32 47 52 57 Trộn Temp1, Temp2 thành M: Temp1: N1=6 36 41 21 65 50 70 Temp2: N2=4 32 47 52 57 M: 32 36 41 47 21 52 57 65 50 70 Lần 3: L = 4 Trang: 46
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Phân phối M thành Temp1, Temp2: M: 32 36 41 47 21 52 57 65 50 70 Temp1:N1=6 32 36 41 47 50 70 Temp2: N2=4 21 52 57 65 Trộn Temp1, Temp2 thành M: Temp1:N1=6 32 36 41 47 50 70 Temp2: N2=4 21 52 57 65 M: 21 32 36 41 47 52 57 65 50 70 Lần 4: L = 8 Phân phối M thành Temp1, Temp2: M: 21 32 36 41 47 52 57 65 50 70 Temp1: N1=8 21 32 36 41 47 52 57 65 Temp2: N2=2 50 70 Trộn Temp1, Temp2 thành M: Temp1: N1=8 21 32 36 41 47 52 57 65 Temp2: N2=2 50 70 M: 21 32 36 41 47 50 52 57 65 70 L = 16 > 10: Kết thúc thuật toán - Phân tích thuật toán: + Trong thuật giải này chúng ta luôn thực hiện log2(N) lần phân phối và trộn các run. Trang: 47
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật + Ở mỗi lần phân phối run chúng ta phải thực hiện: N phép gán và 2N phép so sánh (N phép so sánh hết đường chạy và N phép so sánh hết dãy). + Ở mỗi lần trộn run chúng ta cũng phải thực hiện: N phép gán và 2N+N/2 phép so sánh (N phép so sánh hết đường chạy, N phép so sánh hết dãy và N/2 phép so sánh giá trị các cặp tương ứng trên 2 dãy phụ). + Trong mọi trường hợp: Số phép gán: G = 2N×Log2(N) Số phép so sánh: S = (4N+N/2)×Log2(N) Số phép hoán vị: Hmin = 0 + Trong thuật giải này chúng ta sử dụng 02 dãy phụ, tuy nhiên tổng số phần tử ở 02 dãy phụ này cũng chỉ bằng N, do vậy đã tạo ra sự lãng phí bộ nhớ không cần thiết. Để giải quyết vấn đề này chúng ta chỉ cần sử dụng 01 dãy phụ song chúng ta kết hợp quá trình trộn các cặp run có chiều dài L tương ứng ở hai đầu dãy thành các run có chiều dài 2L và phân phối luân phiên về hai đầu của một dãy phụ. Sau đó chúng ta đổi vai trò của 02 dãy này cho nhau. + Trước khi hiệu chỉnh lại thuật giải chúng ta xét dãy M gồm 10 phần tử sau để minh họa cho quá trình này: Giả sử ta cần sắp xếp mảng M có 10 phần tử sau (N = 10): M: 81 63 69 74 14 77 56 57 9 25 Ta thực hiện các lần trộn các cặp run ở hai đầu dãy này và kết hợp phân phối các run mới trộn về hai đầu dãy kia như sau: Lần 1: L = 1 Trộn các cặp run có chiều dài L = 1 trên M thành các run có chiều dài L = 2 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 81 63 69 74 14 77 56 57 9 25 Tmp: 25 81 57 69 14 77 74 56 63 9 Đổi vai trò của M và Tmp cho nhau M: 25 81 57 69 14 77 74 56 63 9 Lần 2: L = 2 Trộn các cặp run có chiều dài L = 2 trên M thành các run có chiều dài L = 4 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 25 81 57 69 14 77 74 56 63 9 Tmp: 9 25 63 81 14 77 74 69 57 56 Trang: 48
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Đổi vai trò của M và Tmp cho nhau M: 9 25 63 81 14 77 74 69 57 56 Lần 3: L = 4 Trộn các cặp run có chiều dài L = 4 trên M thành các run có chiều dài L = 8 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 9 25 63 81 14 77 74 69 57 56 Tmp: 9 25 56 57 63 69 74 81 77 14 Đổi vai trò của M và Tmp cho nhau M: 9 25 56 57 63 69 74 81 77 14 Lần 4: L = 8 Trộn các cặp run có chiều dài L = 4 trên M thành các run có chiều dài L = 8 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 9 25 56 57 63 69 74 81 77 14 Tmp: 9 14 25 56 57 63 69 74 77 81 Đổi vai trò của M và Tmp cho nhau M: 9 14 25 56 57 63 69 74 77 81 L = 16 > 10: Kết thúc thuật toán + Như vậy, trong thuật giải này chúng ta chỉ còn thao tác trộn các cặp run có chiều dài L tương ứng ở hai đầu dãy thành một run mới có chiều dài 2L để đưa về dãy phụ. Vấn đề là chúng ta sẽ luân phiên đặt run mới vào đầu dãy phụ (theo thứ tự tăng) và cuối dãy phụ (theo thứ tự giảm). Tức là hai bước trộn và phân phối đã được kết hợp lại với nhau. Thuật giải hiệu chỉnh như sau: - Thuật toán Trộn – Phân phối các cặp đường chạy: B1: I1 = 1 // Chỉ số từ đầu dãy M B2: I2 = N // Chỉ số từ cuối dãy M B3: J1 = 1 // Chỉ số từ đầu dãy Temp B4: J2 = N // Chỉ số từ cuối dãy Temp B5: Head = True // Cờ báo phía đặt run mới trong quá trình trộn - phân phối B6: K1 = 1 // Chỉ số để duyệt các run đầu dãy M B7: K2 = 1 // Chỉ số để duyệt các run cuối dãy M B8: IF (I1 > I2) // Đã trộn và phân phối hết các run Trang: 49
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật Thực hiện Bkt B9: IF (M[I1] ≤ M[I2]) // M[I1] đứng trước M[I2] trên Temp B9.1: If (Head = True) B9.1.1: Temp[J1] = M[I1] B9.1.2: J1++ B9.2: Else B9.2.1: Temp[J2] = M[I1] B9.2.2: J2 B9.3: I1++ B9.4: If (I1 > I2) Thực hiện Bkt B9.5: K1++ B9.6: If (K1 > L) //Đã duyệt hết 1 run phía đầu trong M Thực hiện B11 B9.7: Lặp lại B9 B10: ELSE // M[I2] đứng trước M[I1] trên Temp B10.1: If (Head = True) B10.1.1: Temp[J1] = M[I2] B10.1.2: J1++ B10.2: Else B10.2.1: Temp[J2] = M[I2] B10.2.2: J2 B10.3: I2 B10.4: If (I1 > I2) Thực hiện Bkt B10.5: K2++ B10.6: If (K2 > L) //Đã duyệt hết 1 run phía sau trong M Thực hiện B18 B10.7: Lặp lại B9 //Chép phần run còn lại ở phía sau trong M về Temp B11: IF (K2 > L) //Đã chép hết phần run còn lại ở phía sau trong M về Temp B11.1: Head = Not(Head) B11.2: Lặp lại B6 B12: IF (Head = True) B12.1: Temp[J1] = M[I2] B12.2: J1++ B13: ELSE B13.1: Temp[J2] = M[I2] B13.2: J2 B14: I2 B15: If (I1 > I2) Thực hiện Bkt B16: K2++ B17: Lặp lại B11 //Chép phần run còn lại ở phía trước trong M về Temp B18: IF (K1 > L) //Đã chép hết phần run còn lại ở phía trước trong M về Temp Trang: 50
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B18.1: Head = Not(Head) B18.2: Lặp lại B6 B19: IF (Head = True) B19.1: Temp[J1] = M[I1] B19.2: J1++ B20: ELSE B20.1: Temp[J2] = M[I1] B20.2: J2 B21: I1++ B22: If (I1 > I2) Thực hiện Bkt B23: K1++ B24: Lặp lại B18 Bkt: Kết thúc - Thuật toán sắp xếp trộn thẳng hiệu chỉnh: B1: L = 1 //Chiều dài ban đầu của các run B2: IF (L ≥ N) //Dãy chỉ còn 01 run Thực hiện Bkt B3: Trộn_Phân_Phối(M, N, Temp, L) B4: L = 2*L B5: IF (L > N) // Chép các phần tử từ Temp về M B5.1: I = 1 B5.2: If (I > N) Thực hiện Bkt B5.3: M[I] = Temp[I] B5.4: I++ B5.5: Lặp lại B5.2 B6: Trộn_Phân_Phối(Temp, N, M, L) B7: L = 2*L B8: Lặp lại B2 Bkt: Kết thúc - Cài đặt thuật toán hiệu chỉnh: Hàm StraightMergeSortModify có prototype như sau: void StraightMergeSortModify(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 trộn trực tiếp đã hiệu chỉnh. Hàm sử dụng hàm MergeDistribute có prototype và ý nghĩa như sau: void MergeDistribute(T M[], int N, T Temp[], int L); Hàm thực hiện việc trộn các cặp run có chiều dài L ở hai đầu dãy M thành một run có chiều dài 2L và phân phối luân phiên các run có chiều dài 2L này về hai đầu dãy Temp. Nội dung của hàm như sau: void MergeDistribute(T M[], int N, T Temp[], int L) Trang: 51
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật { int I1 = 0, I2 = N-1, J1 = 0, J2 = N-1, K1 = 0, K2 = 0, Head = 1; while (I1 I2) break; K1++; if (K1 == L) { for (; K2 I2) break; K2++; if (K2 == L) { for (; K1 < L && I1<= I2; K1++, I1++) if (Head == 1) { Temp[J1] = M[I1]; J1++; Trang: 52
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật } else { Temp[J2] = M[I1] J2 ; } Head = 0-Head; K1 = K2 = 0; break; } } } return; } //=== void StraightMergeSortModify(T M[], int N) { int L = 1 ; T * Temp = new T[N]; if (Temp == NULL) return; while (L = N) { for (int I = 0; I < N; I++) M[I] = Temp[I]; break; } MergeDistribute(Temp, N, M, L); L = 2*L; } delete Temp; return; } - Phân tích thuật toán hiệu chỉnh: + Trong thuật giải này chúng ta luôn thực hiện log2(N) lần trộn - phân phối các run. + Mỗi lần trộn-phân phối chúng ta phải thực hiện: N phép gán và N+N/2+N/2=2N phép so sánh. + Trong mọi trường hợp: Số phép gán: G = N×Log2(N) Số phép so sánh: S = 2N×Log2(N) Số phép hoán vị: Hmin = 0 + Như vậy thuật giải trộn thẳng hiệu chỉnh vừa tiết kiệm bộ nhớ, vừa thực hiện nhanh hơn thuật giải trộn thẳng ban đầu. + Tuy nhiên, trong thuật giải trộn thẳng chúng ta đã thực hiện việc phân phối và trộn các cặp đường chạy có chiều dài cố định mà trong thực tế trên dãy các đường Trang: 53
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật chạy có thể có chiều dài lớn hơn. Điều này sẽ giảm bớt số lần phân phối và trộn các cặp đường chạy cho chúng ta. Thuật giải trộn tự nhiên được trình bày sau đây sẽ loại bỏ được nhược điểm này của thuật giải trộn thẳng. b. Thuật toán sắp xếp trộn tự nhiên (Natural Merge Sort): - Tư tưởng: Tận dụng các đường chạy tự nhiên có sẵn trên dãy, tiến hành trộn tương ứng các cặp đường chạy tự nhiên nằm hai đầu dãy M thành một đường chạy mới và phân phối luân phiên các đường chạy mới này về hai đầu dãy phụ Temp. Sau đó lại tiếp tục trộn tương ứng từng cặp run ở hai đầu dãy phụ Temp thành một run mới và phân phối luân phiên run mới này về hai đầu dãy M. Cứ tiếp tục như vậy cho đến khi trên M hoặc trên Temp chỉ còn lại 01 run thì kết thúc. - Thuật toán Trộn – Phân phối các cặp đường chạy tự nhiên: B1: I1 = 1 // Chỉ số từ đầu dãy M B2: I2 = N // Chỉ số từ cuối dãy M B3: J1 = 1 // Chỉ số từ đầu dãy Temp B4: J2 = N // Chỉ số từ cuối dãy Temp B5: Head = True // Cờ báo phía đặt run mới trong quá trình trộn - phân phối B6: IF (I1 > I2) // Đã trộn và phân phối hết các run Thực hiện Bkt B7: IF (M[I1] ≤ M[I2]) // M[I1] đứng trước M[I2] trên Temp B7.1: If (Head = True) B7.1.1: Temp[J1] = M[I1] B7.1.2: J1++ B7.2: Else B7.2.1: Temp[J2] = M[I1] B7.2.2: J2 B7.3: I1++ B7.4: If (I1 > I2) Thực hiện Bkt B7.5: If (M[I1] I2) Thực hiện Bkt Trang: 54
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B8.5: If (M[I2] I2) Thực hiện Bkt B14: Lặp lại B9 //Chép phần run còn lại ở phía trước trong M về Temp B15: IF (M[I1] I2) Thực hiện Bkt B20: Lặp lại B15 Bkt: Kết thúc - Thuật toán sắp xếp trộn tự nhiên: B1: L = 1 //Khởi tạo chiều dài ban đầu của run đầu tiên //Tìm chiều dài ban đầu của run đầu tiên B2: IF (N < 2) B2.1: L=N B2.2: Thực hiện Bkt B3: IF (M[L] ≤ M[L+1] And L < N) B3.1: L++ B3.2: Lặp lại B3 B4: IF (L = N) //Dãy chỉ còn 01 run Thực hiện Bkt B5: Trộn_Phân_Phối(M, N, Temp, L) B6: IF (L = N) Trang: 55
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật // Chép các phần tử từ Temp về M B6.1: I = 1 B6.2: If (I > N) Thực hiện Bkt B6.3: M[I] = Temp[I] B6.4: I++ B6.5: Lặp lại B6.2 B7: Trộn_Phân_Phối(Temp, N, M, L) B8: Lặp lại B4 Bkt: Kết thúc - Cài đặt thuật toán trộn tự nhiên: Hàm NaturalMergeSort có prototype như sau: void NaturalMergeSort(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 trộn trực tự nhiên. Hàm sử dụng hàm NaturalMergeDistribute có prototype và ý nghĩa như sau: void NaturalMergeDistribute(T M[], int N, T Temp[], int &L); Hàm thực hiện việc trộn các cặp run ở hai đầu dãy M mà run đầu tiên có chiều dài L thành một run mới chiều dài lớn hơn hoặc bằng L và phân phối luân phiên run mới này về hai đầu dãy Temp. Nội dung của hàm như sau: void NaturalMergeDistribute(T M[], int N, T Temp[], int &L) { int I1 = 0, I2 = N-1, J1 = 0, J2 = N-1, Head = 1, FirstPair = 1; while (I1 I1) { if (Head == 1) { Temp[J1] = M[I2]; J1++; if (FirstPair == 1) L++; } else { Temp[J2] = M[I2]; J2 ; } Trang: 56
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật I2 ; } if (Head == 1) { Temp[J1] = M[I2]; J1++; If (FirstPair == 1) L++; } else { Temp[J2] = M[I2]; J2 ; } I2 ; FirstPair = 0; if (I1 > I2) return; Head = 0 – Head; break; } } if (I1 == I2) { Temp[J1] = M[I1]; if (I1 == N-1) L = N; return; } while (M[I2] <= M[I1] && I1 < I2) { if (Head == 1) { Temp[J1] = M[I2]; J1++; if (FirstPair == 1) L++; } else { Temp[J2] = M[I2]; J2 ; } I2 ; if (M[I2] < M[I2+1]) { while (M[I1] <= M[I1+1] && I1 < I2) { if (Head == 1) { Temp[J1] = M[I1]; J1++; } else { Temp[J2] = M[I1]; J2 ; } Trang: 57
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật I1++; } if (Head == 1) { Temp[J1] = M[I1]; J1++; } else { Temp[J2] = M[I1]; J2 ; } I1++; FirstPair = 0; if (I1 > I2) return; Head = 0 – Head; break; } } if (I1 == I2) { Temp[J1] = M[I1]; if (I1 == N-1) L = N; return; } } return; } //=== void NaruralMergeSort1(T M[], int N) { int L = 1 ; if (N < 2) return; while (M[L-1] < M[L] && L<N) L++; T * Temp = new T[N]; if (Temp == NULL) return; while (L < N) { NaturalMergeDistribute(M, N, Temp, L); if (L == N) { for (int I = 0; I < N; I++) M[I] = Temp[I]; break; } NaturalMergeDistribute(Temp, N, M, L); } delete Temp; return; Trang: 58
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật } - 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): M: 51 39 45 55 20 15 20 17 40 10 Ta thực hiện các lần trộn các cặp run tự nhiên ở hai đầu dãy này và kết hợp phân phối các run mới trộn về hai đầu dãy kia như sau: Lần 1: L = 1 Trộn các cặp run tự nhiên có chiều dài L1 = 1 và L2 = 2 trên M thành các run có chiều dài L = 3 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 51 39 45 55 20 15 20 17 40 10 Tmp: 10 40 51 15 20 55 45 39 20 17 Đổi vai trò của M và Tmp cho nhau M: 10 40 51 15 20 55 45 39 20 17 Lần 2: L = 3 Trộn các cặp run tự nhiên có chiều dài L1 = 3 và L2 = 5 trên M thành các run có chiều dài L = 8 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 10 40 51 15 20 55 45 39 20 17 Tmp: 10 17 20 39 40 45 51 55 20 15 Đổi vai trò của M và Tmp cho nhau M: 10 17 20 39 40 45 51 55 20 15 Lần 3: L = 8 Trộn các cặp run tự nhiên có chiều dài L1 = 8 và L2 = 2 trên M thành các run có chiều dài L = 10 và kết hợp phân phối luân phiên các run này về hai đầu dãy Tmp: M: 10 17 20 39 40 45 51 55 20 15 Tmp: 10 15 17 20 20 39 40 45 51 55 Đổi vai trò của M và Tmp cho nhau M: 10 15 17 20 20 39 40 45 51 55 Trang: 59
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật L = 10: Kết thúc thuật toán - Phân tích thuật toán trộn tự nhiên: + Trong trường hợp tốt nhất, khi dãy có thứ tự tăng thì chúng ta không phải qua bước phân phối và trộn nào hết: Số phép gán: Gmin = 1 Số phép so sánh: Smin = 2(N-1) + 2 = 2N Số phép hoán vị: Hmin = 0 + Trong trường hợp xấu nhất, khi dãy có thứ tự giảm ở nửa đầu và có thứ tự tăng ở nửa cuối và ở mỗi bước trộn phân phối thì độ dài đường chạy mới cũng chỉ tăng gấp đôi. Trong trường hợp này sẽ giống như thuật toán trộn thẳng đã hiệu chỉnh: Số phép gán: Gmax = N×Log2(N)+1 Số phép so sánh: Smax = 2N×Log2(N)+2 Số phép hoán vị: Hmin = 0 + Trung bình: Số phép gán: Gavg = N×Log2(N)/2+1 Số phép so sánh: Savg = N×Log2(N) + N + 1 Số phép hoán vị: Havg = 0 Lưu ý: + Trong thuật toán này chúng ta cũng có thể sử dụng 2 dãy phụ Temp1, Temp2 như trong thuật toán trộn trực tiếp, khi đó chúng ta luôn luôn duyệt từ đầu đến cuối các dãy chứ không cần thiết phải duyệt từ hai đầu dãy vào giữa. Tuy nhiên, trong trường này thì bộ nhớ trung gian sẽ tốn nhiều hơn. + Trong các thuật toán sắp xếp theo phương pháp trộn, việc sử dụng nhiều dãy phụ có thể làm giảm bớt số lần phân phối và trộn các run. Tuy nhiên, việc quản lý các dãy phụ sẽ phức tạp hơn. 3.3. Các giải thuật sắp xếp ngoại (Sắp xếp trên tập tin) Ở đây, do số phần tử dữ liệu thường lớn nên một phần dữ liệu cần sắp xếp được đưa vào trong bộ nhớ trong (RAM), phần còn lại được lưu trữ ở bộ nhớ ngoài (DISK). Do vậy, tốc độ sắp xếp dữ liệu trên tập tin tương đối chậm. Các giải thuật sắp xếp ngoại bao gồm các nhóm sau: - Sắp xếp bằng phương pháp trộn (merge sort), - Sắp xếp theo chỉ mục (index sort). Như vậy trong phần này chúng ta tìm cách sắp xếp tập tin F có N phần tử dữ liệu có kiểu T (khóa nhận diện các phần tử dữ liệu có kiểu T) theo thứ tự tăng. 3.3.1. Sắp xếp bằng phương pháp trộn (Merge Sort) Tương tự như đối 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 trong tập tin dữ liệu về các tập tin trung gian và sau đó lại trộn tương ứng các cặp đường chạy trên các tập tin trung gian thành một đường chạy mới có chiều dài lớn hơn. Trang: 60
- 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). Ở đây chúng ta chỉ nghiên cứu hai thuật toán trộn đầu tiên. a. Thuật toán sắp xếp trộn trực tiếp (Straight Merge Sort): - Tư tưởng: Tương tự như thuật toán trộn trực tiếp trên mảng, ban đầu tập tin Fd có N run(s) với chiều dài mỗi run: L = 1, ta tiến hành phân phối luân phiên N run(s) của tập tin Fd về K tập tin phụ Ft1, Ft2, , FtK (Mỗi tập tin phụ có N/K run(s)). Sau đó trộn tương ứng từng bộ K run(s) ở K tập tin phụ Ft1, Ft2, , FtK thành một run mới có chiều dài L = K để đưa về tập tin Fd và tập tin Fd trở thành tập tin có N/K run(s) với chiều dài mỗi run: 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, đồng thời chiều dài mỗi run trên Fd sẽ tăng lên K lần. Do đó, sau LogK(N) lần phân phối và trộn thì tập tin Fd chỉ còn lại 01 run với chiều dài là N và khi đó tập tin Fd trở thành tập tin có thứ tự. Trong thuật giải này, để dễ theo dõi chúng ta sử dụng 2 tập tin phụ (K = 2) và quá trình phân phối, trộn các run được trình bày riêng thành 2 thuật giải: + 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. - Thuật toán phân phối: B1: Fd = fopen(DataFile, “r”) //Mở tập tin dữ liệu cần sắp xếp để đọc dữ liệu B2: Ft1 = fopen(DataTemp1, “w”) //Mở tập tin trung gian thứ nhất để ghi dữ liệu B3: Ft2 = fopen(DataTemp2, “w”) //Mở tập tin trung gian thứ hai để ghi dữ liệu B4: IF (feof(Fd)) //Đã phân phối hết Thực hiện Bkt //Chép 1 run từ Fd sang Ft1 B5: K = 1 //Chỉ số đếm để duyệt các run B6: IF (K > L) //Duyệt hết 1 run Thực hiện B12 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 B9: K++ B10: IF (feof(Fd)) //Đã phân phối hết Thực hiện Bkt B11: Lặp lại B6 //Chép 1 run từ Fd sang Ft2 B12: K = 1 Trang: 61
- Giáo trình: Cấu Trúc Dữ Liệu và Giải Thuật B13: IF (K > L) Thực hiện B19 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 B16: K++ B17: IF (feof(Fd)) //Đã phân phối hết Thực hiện Bkt B18: Lặp lại B13 B19: Lặp lại B4 Bkt: Kết thúc - Thuật toán trộn: B1: Ft1 = fopen(DataTemp1, “r”) //Mở tập tin trung gian thứ nhất để đọc dữ liệu B2: Ft2 = fopen(DataTemp2, “r”) //Mở tập tin trung gian thứ hai để đọc dữ liệu B3: Fd = fopen(DataFile, “w”) //Mở tập tin dữ liệu để ghi dữ liệu B4: fread(&a1, sizeof(T), 1, Ft1) //Đọc 1 phần tử của run trên Ft1 ra biến tạm a1 B5: fread(&a2, sizeof(T), 1, Ft2) //Đọc 1 phần tử của run trên Ft2 ra biến tạm a2 B6: K1 = 1 //Chỉ số để duyệt các run trên Ft1 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.1: fwrite(&a1, sizeof(T), 1, Fd) B8.2: K1++ 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 Thực hiện B11 B8.6: Lặp lại B8 B9: ELSE // a2 đứng trước a1 trên Fd B9.1: fwrite(&a2, sizeof(T), 1, Fd) B9.2: K2++ 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 Thực hiện B17 B9.6: Lặp lại B8 B10: Lặp lại B6 //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 Lặp lại B6 B12: fwrite(&a2, sizeof(T), 1, Fd) B13: K2++ B14: IF (feof(Ft2)) //Đã chép hết các phần tử trong Ft2 Thực hiện B27 B15: fread(&a2, sizeof(T), 1, Ft2) B16: Lặp lại B11 Trang: 62
- 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 Lặp lại B6 B18: fwrite(&a1, sizeof(T), 1, Fd) B19: K1++ B20: IF (feof(Ft1)) //Đã chép hết các phần tử trong Ft1 Thực hiện B23 B21: fread(&a1, sizeof(T), 1, Ft1) B22: Lặp lại B17 //Chép các phần tử còn lại trong Ft2 về Fd B23: fwrite(&a2, sizeof(T), 1, Fd) B24: IF (feof(Ft2)) Thực hiện Bkt B25: fread(&a2, sizeof(T), 1, Ft2) B26: Lặp lại B23 //Chép các phần tử còn lại trong Ft1 về Fd B27: fwrite(&a1, sizeof(T), 1, Fd) B28: IF (feof(Ft1)) Thực hiện Bkt B29: fread(&a1, sizeof(T), 1, Ft1) B30: Lặp lại B27 Bkt: Kết thúc - 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 Thực hiện Bkt B3: Phân_Phối(DataFile, DataTemp1, DataTemp2, L) B4: Trộn(DataTemp1, DataTemp2, DataFile, L) B5: L = 2*L B6: Lặp lại B2 Bkt: Kết thúc - Cài đặt thuật toán: Hàm FileStraightMergeSort có prototype như sau: int FileStraightMergeSort(char * DataFile); Hàm 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 có tên DataFile theo thứ tự tăng dựa trên thuật toán sắp trộn trực tiếp. Nếu việc sắp xếp thành công hàm trả về giá trị 1, trong trường hợp ngược lại (do có lỗi khi thực hiện các thao tác trên tập tin) hàm trả về giá trị –1. Hàm sử dụng các hàm FileDistribute, FileMerge có prototype và ý nghĩa như sau: int FileDistribute(char * DataFile, char * DataTemp1, char * DataTemp2, int L); Hàm thực hiện việc phân phối luân phiên các đường chạy có chiều dài L trên tập tin dữ liệu có tên DataFile về cho các tập tin tạm thời có tên tương ứng là DataTemp1 Trang: 63