Câu trả lời:
Bài báo này có một số phân tích.
Ngoài ra, từ Wikipedia:
Đối thủ cạnh tranh trực tiếp nhất của quicksort là heapsort. Heapsort thường chậm hơn một chút so với quicksort, nhưng thời gian chạy trong trường hợp xấu nhất luôn là Θ (nlogn). Quicksort thường nhanh hơn, mặc dù vẫn có khả năng xảy ra trường hợp xấu nhất ngoại trừ trong biến thể intsort, biến thể này chuyển sang heapsort khi phát hiện trường hợp xấu. Nếu biết trước rằng heapsort là cần thiết, thì việc sử dụng trực tiếp nó sẽ nhanh hơn là chờ đợi introsort chuyển sang nó.
Heapsort được đảm bảo O (N log N), điều tốt hơn nhiều so với trường hợp xấu nhất trong Quicksort. Heapsort không cần thêm bộ nhớ cho một mảng khác để đưa dữ liệu có thứ tự khi Mergesort cần. Vậy tại sao các ứng dụng thương mại lại gắn bó với Quicksort? Quicksort có gì đặc biệt so với các triển khai khác?
Tôi đã tự mình kiểm tra các thuật toán và tôi thấy rằng Quicksort thực sự có một điều gì đó đặc biệt. Nó chạy nhanh, nhanh hơn nhiều so với thuật toán Heap và Merge.
Bí mật của Quicksort là: Nó gần như không thực hiện các hoán đổi phần tử không cần thiết. Hoán đổi tốn nhiều thời gian.
Với Heapsort, ngay cả khi tất cả dữ liệu của bạn đã được sắp xếp thứ tự, bạn sẽ hoán đổi 100% các phần tử để sắp xếp thứ tự mảng.
Với Mergesort, nó thậm chí còn tồi tệ hơn. Bạn sẽ ghi 100% các phần tử trong một mảng khác và viết nó trở lại mảng ban đầu, ngay cả khi dữ liệu đã được sắp xếp.
Với Quicksort bạn không hoán đổi những gì đã được đặt hàng. Nếu dữ liệu của bạn được sắp xếp hoàn toàn, bạn hầu như không trao đổi gì! Mặc dù có rất nhiều rắc rối về trường hợp xấu nhất, nhưng một chút cải thiện về lựa chọn trục, bất kỳ ngoại trừ việc lấy phần tử đầu tiên hoặc cuối cùng của mảng, có thể tránh được. Nếu bạn nhận được một pivot từ phần tử trung gian giữa phần tử đầu tiên, cuối cùng và phần tử giữa, nó là thiếu sót để tránh trường hợp xấu nhất.
Những gì vượt trội trong Quicksort không phải là trường hợp xấu nhất, mà là trường hợp tốt nhất! Trong trường hợp tốt nhất, bạn thực hiện cùng một số phép so sánh, ok, nhưng bạn hầu như không hoán đổi gì. Trong trường hợp trung bình, bạn hoán đổi một phần của các phần tử, nhưng không phải tất cả các phần tử, như trong Heapsort và Mergesort. Đó là những gì mang lại cho Quicksort thời gian tốt nhất. Trao đổi ít hơn, tốc độ cao hơn.
Việc triển khai bên dưới trong C # trên máy tính của tôi, đang chạy ở chế độ phát hành, đánh bại Array. Sắp xếp 3 giây với trục xoay giữa và 2 giây với trục xoay được cải thiện (vâng, có một khoản chi phí để có được một trục xoay tốt).
static void Main(string[] args)
{
int[] arrToSort = new int[100000000];
var r = new Random();
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
Console.WriteLine("Press q to quick sort, s to Array.Sort");
while (true)
{
var k = Console.ReadKey(true);
if (k.KeyChar == 'q')
{
// quick sort
Console.WriteLine("Beg quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
QuickSort(arrToSort, 0, arrToSort.Length - 1);
Console.WriteLine("End quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
}
else if (k.KeyChar == 's')
{
Console.WriteLine("Beg Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
Array.Sort(arrToSort);
Console.WriteLine("End Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
}
}
}
static public void QuickSort(int[] arr, int left, int right)
{
int begin = left
, end = right
, pivot
// get middle element pivot
//= arr[(left + right) / 2]
;
//improved pivot
int middle = (left + right) / 2;
int
LM = arr[left].CompareTo(arr[middle])
, MR = arr[middle].CompareTo(arr[right])
, LR = arr[left].CompareTo(arr[right])
;
if (-1 * LM == LR)
pivot = arr[left];
else
if (MR == -1 * LR)
pivot = arr[right];
else
pivot = arr[middle];
do
{
while (arr[left] < pivot) left++;
while (arr[right] > pivot) right--;
if(left <= right)
{
int temp = arr[right];
arr[right] = arr[left];
arr[left] = temp;
left++;
right--;
}
} while (left <= right);
if (left < end) QuickSort(arr, left, end);
if (begin < right) QuickSort(arr, begin, right);
}
Đối với hầu hết các tình huống, nhanh hơn nhanh hơn một chút là không thích hợp ... đơn giản là bạn không bao giờ muốn nó thỉnh thoảng trở nên chậm chạp. Mặc dù bạn có thể điều chỉnh QuickSort để tránh các trường hợp chậm, nhưng bạn sẽ làm mất đi vẻ sang trọng của QuickSort cơ bản. Vì vậy, đối với hầu hết mọi thứ, tôi thực sự thích HeapSort hơn ... bạn có thể triển khai nó với vẻ đẹp đơn giản đầy đủ của nó và không bao giờ bị chậm.
Đối với các tình huống bạn NÊN muốn tốc độ tối đa trong hầu hết các trường hợp, QuickSort có thể được ưu tiên hơn HeapSort, nhưng cả hai đều không phải là câu trả lời đúng. Đối với các tình huống quan trọng về tốc độ, cần kiểm tra chặt chẽ các chi tiết của tình huống. Ví dụ: trong một số mã quan trọng về tốc độ của tôi, rất phổ biến là dữ liệu đã được sắp xếp hoặc sắp xếp gần (nó đang lập chỉ mục nhiều trường liên quan thường di chuyển lên và xuống cùng nhau HOẶC di chuyển lên và xuống đối diện nhau, vì vậy một khi bạn sắp xếp theo một, những cái khác sẽ được sắp xếp hoặc sắp xếp ngược lại hoặc đóng ... một trong hai cách này có thể giết QuickSort). Đối với trường hợp đó, tôi không triển khai ... thay vào đó, tôi đã triển khai SmoothSort của Dijkstra ... một biến thể HeapSort là O (N) khi đã được sắp xếp hoặc sắp xếp gần ... nó không quá thanh lịch, không quá dễ hiểu, nhưng nhanh ... đọchttp://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF nếu bạn muốn viết mã khó hơn một chút.
Các phép lai tại chỗ Quicksort-Heapsort cũng thực sự thú vị, vì hầu hết chúng chỉ cần so sánh n * log n trong trường hợp xấu nhất (chúng tối ưu đối với số hạng đầu tiên của tiệm cận, vì vậy chúng tránh được các tình huống xấu nhất của Quicksort), O (log n) dư không gian và chúng bảo tồn ít nhất "một nửa" hành vi tốt của Quicksort đối với tập dữ liệu đã được sắp xếp thứ tự. Một thuật toán cực kỳ thú vị được Dikert và Weiss trình bày trong http://arxiv.org/pdf/1209.4214v1.pdf :
Comp. giữa quick sort
và merge sort
vì cả hai đều là loại sắp xếp tại chỗ nên có sự khác biệt giữa thời gian chạy trường hợp wrost của thời gian chạy trường hợp wrost để sắp xếp nhanh làO(n^2)
và đối với sắp xếp theo đống thì nó vẫn còn O(n*log(n))
và đối với lượng dữ liệu trung bình, sắp xếp nhanh sẽ hữu ích hơn. Vì nó là thuật toán ngẫu nhiên nên xác suất nhận được đúng ans. trong thời gian ngắn hơn sẽ phụ thuộc vào vị trí của phần tử trục mà bạn chọn.
Vì vậy, một
Tốt cuộc gọi: kích thước của L và G đều nhỏ hơn 3s / 4
Cuộc gọi tồi tệ: một trong L và G có kích thước lớn hơn 3s / 4
đối với số lượng nhỏ, chúng ta có thể sắp xếp chèn và đối với lượng dữ liệu rất lớn, chúng ta có thể sắp xếp theo đống.
Heapsort có lợi ích là có trường hợp chạy xấu nhất là O (n * log (n)), vì vậy trong các trường hợp mà quicksort có khả năng hoạt động kém (chủ yếu là các tập dữ liệu được sắp xếp nói chung) thì heapsort được ưu tiên hơn nhiều.
Chà nếu bạn đi đến cấp độ kiến trúc ... chúng tôi sử dụng cấu trúc dữ liệu hàng đợi trong bộ nhớ đệm. Vì vậy, những gì có sẵn trong hàng đợi sẽ được sắp xếp. sắp xếp (bằng cách sử dụng mảng) nó có thể xảy ra như vậy là cha mẹ có thể không có mặt trong mảng con có sẵn trong bộ nhớ cache và sau đó nó phải đưa nó vào bộ nhớ đệm ... điều này rất tốn thời gian. Đó là quicksort là tốt nhất !! 😀
Đống xây dựng một đống và sau đó liên tục trích xuất mục tối đa. Trường hợp xấu nhất của nó là O (n log n).
Nhưng nếu bạn thấy trường hợp xấu nhất của sắp xếp nhanh , đó là O (n2), bạn sẽ nhận ra rằng sắp xếp nhanh sẽ là một lựa chọn không tốt cho dữ liệu lớn.
Vì vậy, điều này làm cho việc sắp xếp là một điều thú vị; Tôi tin rằng lý do có rất nhiều thuật toán sắp xếp tồn tại ngày nay là vì tất cả chúng đều 'tốt nhất' ở những vị trí tốt nhất của chúng. Ví dụ: sắp xếp bong bóng có thể thực hiện sắp xếp nhanh nếu dữ liệu được sắp xếp. Hoặc nếu chúng ta biết điều gì đó về các mục được sắp xếp thì có lẽ chúng ta có thể làm tốt hơn.
Điều này có thể không trả lời trực tiếp câu hỏi của bạn, tôi nghĩ rằng tôi sẽ thêm hai xu của mình.
Heap Sort là một cược an toàn khi xử lý các đầu vào rất lớn. Phân tích tiệm cận cho thấy thứ tự phát triển của Heapsort trong trường hợp xấu nhất là Big-O(n logn)
, tốt hơn so với Quicksort trong Big-O(n^2)
trường hợp xấu nhất. Tuy nhiên, Heapsort trên thực tế hơi chậm hơn so với một kiểu sắp xếp nhanh được triển khai tốt. Heapsort cũng không phải là một thuật toán sắp xếp ổn định.
Trong thực tế, lý do heapsort chậm hơn quicksort là do vị trí tham chiếu tốt hơn (" https://en.wikipedia.org/wiki/Locality_of_reference ") trong quicksort, nơi các phần tử dữ liệu nằm trong các vị trí lưu trữ tương đối gần. Các hệ thống thể hiện vị trí tham chiếu mạnh mẽ là ứng cử viên tuyệt vời để tối ưu hóa hiệu suất. Sắp xếp đống, tuy nhiên, giải quyết các bước nhảy vọt lớn hơn. Điều này làm cho nhanh chóng thuận lợi hơn cho các đầu vào nhỏ hơn.
Đối với tôi, có một sự khác biệt rất cơ bản giữa heapsort và quicksort: thứ hai sử dụng một đệ quy. Trong thuật toán đệ quy, đống tăng theo số lần đệ quy. Điều này không quan trọng nếu n nhỏ, nhưng ngay bây giờ tôi đang sắp xếp hai ma trận với n = 10 ^ 9 !!. Chương trình chiếm gần 10 GB ram và bất kỳ bộ nhớ bổ sung nào sẽ khiến máy tính của tôi bắt đầu hoán đổi sang bộ nhớ đĩa ảo. Đĩa của tôi là đĩa RAM, nhưng việc hoán đổi sang nó tạo ra sự khác biệt lớn về tốc độ . Vì vậy, trong một statpack được mã hóa bằng C ++ bao gồm ma trận kích thước có thể điều chỉnh, với kích thước chưa được lập trình viên biết trước và kiểu sắp xếp thống kê phi tham số, tôi thích heapsort để tránh chậm trễ khi sử dụng với ma trận dữ liệu rất lớn.
Để trả lời câu hỏi ban đầu và giải quyết một số ý kiến khác tại đây:
Tôi chỉ so sánh việc triển khai lựa chọn, nhanh chóng, hợp nhất và sắp xếp đống để xem chúng sẽ xếp chồng lên nhau như thế nào. Câu trả lời là tất cả chúng đều có mặt trái của chúng.
TL; DR: Nhanh là cách sắp xếp có mục đích chung tốt nhất (khá nhanh, ổn định và chủ yếu là tại chỗ) Cá nhân tôi thích sắp xếp theo đống mặc dù trừ khi tôi cần một loại ổn định.
Lựa chọn - N ^ 2 - Nó thực sự chỉ tốt cho ít hơn 20 phần tử hoặc hơn, sau đó nó hoạt động tốt hơn. Trừ khi dữ liệu của bạn đã được sắp xếp, hoặc gần như vậy. N ^ 2 thực sự chậm rất nhanh.
Theo kinh nghiệm của tôi, nhanh chóng không phải lúc nào cũng nhanh như vậy. Tuy nhiên, phần thưởng cho việc sử dụng sắp xếp nhanh như là một phân loại chung là nó nhanh và ổn định. Nó cũng là một thuật toán tại chỗ, nhưng vì nó thường được triển khai một cách đệ quy, nó sẽ chiếm thêm không gian ngăn xếp. Nó cũng nằm ở đâu đó giữa O (n log n) và O (n ^ 2). Thời gian trên một số loại dường như xác nhận điều này, đặc biệt là khi các giá trị nằm trong một phạm vi hẹp. Nó nhanh hơn cách sắp xếp lựa chọn trên 10.000.000 mục, nhưng chậm hơn so với hợp nhất hoặc đống.
Sắp xếp hợp nhất được đảm bảo O (n log n) vì sắp xếp của nó không phụ thuộc vào dữ liệu. Nó chỉ làm những gì nó làm, bất kể bạn đã cho nó những giá trị nào. Nó cũng ổn định, nhưng các loại rất lớn có thể thổi bay ngăn xếp của bạn nếu bạn không cẩn thận trong việc triển khai. Có một số triển khai sắp xếp hợp nhất tại chỗ phức tạp, nhưng nhìn chung, bạn cần một mảng khác ở mỗi cấp để hợp nhất các giá trị của mình vào. Nếu những mảng đó nằm trên ngăn xếp, bạn có thể gặp sự cố.
Sắp xếp đống là tối đa O (n log n), nhưng trong nhiều trường hợp thì nhanh hơn, tùy thuộc vào mức độ bạn phải di chuyển các giá trị của mình lên đống sâu log n. Heap có thể dễ dàng được triển khai tại chỗ trong mảng ban đầu, vì vậy nó không cần thêm bộ nhớ và nó lặp đi lặp lại, do đó không phải lo lắng về việc tràn ngăn xếp trong khi đệ quy. Các lớn nhược điểm để đống loại là nó không phải là một ổn định sắp xếp, điều đó có nghĩa nó ra ngay nếu bạn cần điều đó.