So sánh quicksort với sắp xếp khác
Tuần này ta sẽ tìm hiểu về thuật toán Quicksort, một thuật toán sắp xếp nổi tiếng là nhanh. Ta sẽ thử tìm hiểu xem Quicksort nhanh đến cỡ nào, và nó có phải là thuật toán sắp xếp nhanh nhất hiện tại hay không? Show Tất nhiên trước hết ta sẽ đi qua tìm hiểu thuật toán bên dưới của Quicksort. Giả sử ta có một danh sách các phần tử, thuật toán Quicksort sẽ bao gồm các bước dưới đây:
Vậy thời gian thực thi của Quicksort sẽ phụ thuộc vào các yếu tố nào? Trước hết ta có thể thấy bởi vì phần Partition hầu như chỉ là các phép toán swap phần tử qua lại nên ta có thể đạt được linear time. Còn lại thời gian xây dựng các cây đệ quy cho các phần tử trong mỗi lần gọi hàm ở các danh sách con ước chừng là O(n. h), h ở đây chính là chiều cao của cây đệ quy. Và chiều cao này lại phụ thuộc rất lớn vào việc chọn Pivot ở đầu thuật toán. Nếu chúng ta may mắn thì có thể chọn được pivot ở đúng vị trí chia đôi danh sách, trường hợp này là lý tưởng nhất, khi đó thời gian thực thi sẽ là O(n log n). Trường hợp xấu nhất đó là khi danh sách đã được sắp xếp đúng thứ tự và pivot nằm ở đầu danh sách, khi đó thời gian thực thi sẽ bằng O(n2) tương đương với brute force. Vì vậy điều quan trọng đối với thuật toán Quicksort chính là ở việc chọn pivot. Sau đây ta sẽ đi vào tìm hiểu Go đang implement thuật toán Quicksort như thế nào. 2. Implement Quicksort trong thư viện built-in của GoKhông phải chờ đợi thêm nữa ta đọc ngay vào hàm chính của package sort: hàm Sort 2.1. Hàm Sort
Ta có thể thấy ở trên hàm Sort gọi vào hàm con quickSort implement thuật toán Quicksort (có một số cải tiến), và không thể bỏ qua câu mô tả cuối cùng: The sort is not guaranteed to be stable việc sắp xếp thì không được đảm bảo sẽ ổn định. ??!!?? Vậy thì tại sao lại không đảm bảo ổn định? Trước hết ta phải hiểu khái niệm 2.1.1. Stable Sort là gì?Trên Wiki có định nghĩa như sau: Stable sorting algorithms maintain the relative order of records with equal keys (i.e. values). That is, a sorting algorithm is stable if whenever there are two records R and S with the same key and with R appearing before S in the original list, R will appear before S in the sorted list. See here for a more complete description. Ví dụ: Sắp xếp chuỗi này: C(1) C(2) A B
stable sort là ám chỉ đến các thuật toán sắp xếp vẫn giữ được thứ tự như trước khi sắp xếp đối với các item mà cùng key (cùng giá trị) ta dễ dàng hiểu nguyên nhân ở trên khi biết rằng Quicksort không phải thuật toán stable sort. Danh sách các thuật toán stable sort https://en.wikipedia.org/wiki/Category:Stable_sorts 2.2. Interface interfaceTa có thể thấy hàm Sort nhận vào parameter data có kiểu dữ liệu là
0, đây chính là các interface mà user phải implement để cho hàm Sort biết các để sắp xếp, swap, và đếm số lượng các phần tử trong array
Ngoài
0 interface, trước khi qua tìm hiểu về phần hiện thực của thuật toán Quicksort, ta để ý còn có hàm
2 2.3. Tính maxDepth
MaxDepth sẽ có giá trị là 2*ceil(lg(n+1)) Đây chính là một phần cải tiến để cải thiện performance của thuật toán so với thuật toán gốc. Khi số lượng phần tử cần sắp xếp đủ nhỏ sẽ không sử dụng quicksort nữa mà chuyển sang sử dụng
3,
4 là những thuật toán thích hợp hơn quicksort khi số lượng phần tử nhỏ:
Note: Phần cải tiến switch từ quicksort sang merge sort trên còn có tên gọi khác là
7 hay
8 2.4. Hàm quickSort
2.4.1 Dùng Shellsort khi kích thước slices từ nhỏ hơn 12 phần tử
Thực hiện Shellsort một lần với gap bằng 6, sau đó thực hiện
4. Ta sẽ thắc mắc tại sao lại có con số 12 và 6 ở đây? Shellsort có hiệu quả với số phần tử nhỏ, không tốn bộ nhớ và tốc độ thật sự cải tiến hơn so với insertion sort, có worstcase là O(n*(logn)^2) khi gap được chọn có cấu trúc 2^j*3^k 2.4.2 Dùng Introsort (Quicksort) khi kích thước slices lớn hơn 12 phần tửQuicksort có nhược điểm là worsecase là O(n^2) và chiếm bộ nhớ O(n) vì vậy khi vượt quá một mức độ sâu đệ quy thì sẽ hiệu quả hơn nếu sử dụng
0 bởi vì sẽ luôn đảm bảo được worsecase là O(n*logn) và chỉ chiếm stack O(1). Vậy tại sao ta không sử dụng
0 cho tất cả các trường họp? Bởi vì Quicksort chỉ swap khi cần thiết còn và worsecase O(n^2) có thể tránh được nếu ta chọn pivot thích hợp vòn việc tốn thời gian để swap phần tử trong
0 là không thể tránh được. |