Tôi muốn thêm vào các câu trả lời tuyệt vời hiện có một số phép toán về cách QuickSort thực hiện khi chuyển hướng từ trường hợp tốt nhất và khả năng đó là gì, tôi hy vọng sẽ giúp mọi người hiểu rõ hơn một chút tại sao trường hợp O (n ^ 2) không có thật mối quan tâm trong việc triển khai QuickSort tinh vi hơn.
Ngoài các vấn đề truy cập ngẫu nhiên, có hai yếu tố chính có thể ảnh hưởng đến hiệu suất của QuickSort và cả hai đều liên quan đến cách trục xoay so với dữ liệu được sắp xếp.
1) Một số lượng nhỏ các khóa trong dữ liệu. Một bộ dữ liệu có cùng giá trị sẽ sắp xếp trong n ^ 2 lần trên QuickSort phân vùng 2 vanilla vì tất cả các giá trị ngoại trừ vị trí trục được đặt ở một bên mỗi lần. Các triển khai hiện đại giải quyết vấn đề này bằng các phương pháp như sử dụng sắp xếp 3 phân vùng. Các phương thức này thực hiện trên một tập dữ liệu của tất cả cùng một giá trị trong thời gian O (n). Vì vậy, sử dụng triển khai như vậy có nghĩa là đầu vào có số lượng khóa nhỏ thực sự cải thiện thời gian thực hiện và không còn là vấn đề đáng lo ngại.
2) Lựa chọn trục cực kỳ xấu có thể gây ra hiệu suất trường hợp xấu nhất. Trong trường hợp lý tưởng, trục sẽ luôn sao cho 50% dữ liệu nhỏ hơn và 50% dữ liệu lớn hơn, do đó đầu vào sẽ bị phá vỡ một nửa trong mỗi lần lặp. Điều này cho chúng ta n so sánh và hoán đổi lần log-2 (n) thu hồi thời gian O (n * logn).
Làm thế nào nhiều lựa chọn trục không lý tưởng ảnh hưởng đến thời gian thực hiện?
Hãy xem xét trường hợp trục được chọn liên tục sao cho 75% dữ liệu nằm ở một bên của trục. Nó vẫn là O (n * logn) nhưng bây giờ cơ sở của nhật ký đã thay đổi thành 1 / 0,75 hoặc 1,33. Mối quan hệ trong hiệu suất khi thay đổi cơ sở luôn là một hằng số được biểu thị bằng log (2) / log (newBase). Trong trường hợp này, hằng số đó là 2,4. Vì vậy, chất lượng của sự lựa chọn trục này mất hơn 2,4 lần so với lý tưởng.
Làm thế nào nhanh chóng điều này trở nên tồi tệ?
Không nhanh lắm cho đến khi lựa chọn trục bị (nhất quán) rất tệ:
- 50% cho một bên: (trường hợp lý tưởng)
- 75% cho một bên: dài gấp 2,4 lần
- 90% ở một bên: dài gấp 6,6 lần
- 95% ở một bên: dài 13,5 lần
- 99% ở một bên: dài gấp 69 lần
Khi chúng tôi tiếp cận 100% ở một bên, phần nhật ký của thực thi sẽ tiếp cận n và toàn bộ thực thi tiếp cận theo phương pháp tiệm cận O (n ^ 2).
Trong triển khai QuickSort ngây thơ, các trường hợp như mảng được sắp xếp (đối với trục phần tử thứ 1) hoặc mảng được sắp xếp ngược (đối với trục phần tử cuối cùng) sẽ tạo ra thời gian thực hiện O (n ^ 2) trong trường hợp xấu nhất. Ngoài ra, việc triển khai với lựa chọn trục có thể dự đoán được có thể bị tấn công DoS bởi dữ liệu được thiết kế để tạo ra trường hợp thực thi tồi tệ nhất. Việc triển khai hiện đại tránh điều này bằng nhiều phương pháp, chẳng hạn như ngẫu nhiên hóa dữ liệu trước khi sắp xếp, chọn trung bình của 3 chỉ số được chọn ngẫu nhiên, v.v ... Với sự ngẫu nhiên này trong hỗn hợp, chúng tôi có 2 trường hợp:
- Tập dữ liệu nhỏ. Trường hợp xấu nhất là có thể hợp lý nhưng O (n ^ 2) không phải là thảm họa vì n đủ nhỏ để n ^ 2 cũng nhỏ.
- Tập dữ liệu lớn. Trường hợp xấu nhất là có thể trong lý thuyết nhưng không phải trong thực tế.
Làm thế nào chúng ta có thể thấy hiệu suất khủng khiếp?
Các cơ hội đang biến mất nhỏ . Hãy xem xét một loại 5.000 giá trị:
Việc triển khai giả thuyết của chúng tôi sẽ chọn một trục bằng cách sử dụng trung bình 3 chỉ số được chọn ngẫu nhiên. Chúng tôi sẽ coi các pivots nằm trong phạm vi 25% -75% là "tốt" và các pivots nằm trong phạm vi 0% -25% hoặc 75% -100% là "xấu". Nếu bạn nhìn vào phân phối xác suất bằng cách sử dụng trung bình của 3 chỉ số ngẫu nhiên, mỗi lần đệ quy có 11/16 cơ hội kết thúc với một trục tốt. Chúng ta hãy đưa ra 2 giả định bảo thủ (và sai) để đơn giản hóa toán học:
Pivots tốt luôn chính xác ở mức phân chia 25% / 75% và hoạt động ở trường hợp lý tưởng 2,4 *. Chúng tôi không bao giờ có được một sự phân chia lý tưởng hoặc bất kỳ sự phân chia nào tốt hơn 25/75.
Pivots xấu luôn là trường hợp xấu nhất và về cơ bản không đóng góp gì cho giải pháp.
Việc triển khai QuickSort của chúng tôi sẽ dừng ở n = 10 và chuyển sang sắp xếp chèn, vì vậy chúng tôi yêu cầu 22 phân vùng trục 25% / 75% để phá vỡ 5.000 giá trị đầu vào cho đến nay. (10 * 1.333333 ^ 22> 5000) Hoặc, chúng tôi yêu cầu 4990 pivots trường hợp xấu nhất. Hãy nhớ rằng nếu chúng ta tích lũy được 22 pivots tốt tại bất kỳ thời điểm nào thì việc sắp xếp sẽ hoàn thành, vì vậy trường hợp xấu nhất hoặc bất cứ điều gì gần nó đòi hỏi cực kỳ xui xẻo. Nếu chúng tôi mất 88 lần thu hồi để thực sự đạt được 22 pivots tốt cần thiết để sắp xếp xuống n = 10, thì đó sẽ là trường hợp lý tưởng 4 * 2.4 * hoặc khoảng 10 lần thời gian thực hiện trường hợp lý tưởng. Làm thế nào có khả năng là chúng ta sẽ không đạt được 22 pivots tốt cần thiết sau 88 lần thu hồi?
Phân phối xác suất nhị thức có thể trả lời điều đó, và câu trả lời là khoảng 10 ^ -18. (n là 88, k là 21, p là 0,6875) sử dụng của bạn là khoảng một ngàn lần nhiều khả năng bị sét đánh trong 1 giây cần thiết để bấm [Sắp xếp] hơn là họ sẽ thấy rằng 5.000 mục loại chạy bất kỳ tồi tệ hơn hơn 10 * trường hợp lý tưởng. Cơ hội này trở nên nhỏ hơn khi tập dữ liệu trở nên lớn hơn. Dưới đây là một số kích thước mảng và cơ hội tương ứng của chúng để chạy dài hơn 10 * lý tưởng:
- Mảng gồm 640 mục: 10 ^ -13 (yêu cầu 15 điểm xoay vòng tốt trong số 60 lần thử)
- Mảng 5.000 mặt hàng: 10 ^ -18 (yêu cầu 22 pivots tốt trong số 88 lần thử)
- Mảng gồm 40.000 mặt hàng: 10 ^ -23 (yêu cầu 29 pivots tốt trong số 116)
Hãy nhớ rằng đây là với 2 giả định bảo thủ tồi tệ hơn thực tế. Vì vậy, hiệu suất thực tế là tốt hơn và sự cân bằng của xác suất còn lại gần với lý tưởng hơn là không.
Cuối cùng, như những người khác đã đề cập, ngay cả những trường hợp không có khả năng vô lý này cũng có thể được loại bỏ bằng cách chuyển sang loại heap nếu ngăn đệ quy đi quá sâu. Vì vậy, TLDR là, để triển khai QuickSort tốt, trường hợp xấu nhất không thực sự tồn tại vì nó đã được thiết kế và thực thi hoàn thành trong thời gian O (n * logn).
qsort
, Python'slist.sort
vàArray.prototype.sort
JavaScript của Firefox đều là những loại hợp nhất được cải tiến. (GNU STLsort
sử dụng Introsort thay thế, nhưng điều đó có thể là do trong C ++, việc hoán đổi có khả năng thắng lớn khi sao chép.)