Quicksort: Chọn trục


109

Khi thực hiện Quicksort, một trong những điều bạn phải làm là chọn một trục. Nhưng khi tôi nhìn vào mã giả như hình dưới đây, tôi không rõ mình nên chọn pivot như thế nào. Phần tử đầu tiên của danh sách? Thứ gì khác?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

Ai đó có thể giúp tôi nắm bắt khái niệm về việc chọn một trục và liệu các kịch bản khác nhau có yêu cầu các chiến lược khác nhau hay không.


Câu trả lời:


87

Việc chọn một trục ngẫu nhiên sẽ giảm thiểu khả năng bạn gặp phải hiệu suất O (n 2 ) trong trường hợp xấu nhất (luôn chọn đầu tiên hoặc cuối cùng sẽ gây ra hiệu suất trong trường hợp xấu nhất cho dữ liệu được sắp xếp gần như sắp xếp hoặc gần như ngược lại). Việc chọn phần tử ở giữa cũng sẽ được chấp nhận trong đa số trường hợp.

Ngoài ra, nếu bạn đang tự thực hiện điều này, có các phiên bản của thuật toán hoạt động tại chỗ (nghĩa là không cần tạo hai danh sách mới và sau đó nối chúng).


10
Tôi chấp nhận quan điểm rằng việc tự mình thực hiện tìm kiếm có thể không đáng để nỗ lực. Ngoài ra, hãy cẩn thận với cách bạn chọn các số ngẫu nhiên, vì các trình tạo số ngẫu nhiên đôi khi hơi chậm.
PeterAllenWebb

Câu trả lời của @Jonathan Leffler hay hơn
Nathan

60

Nó phụ thuộc vào yêu cầu của bạn. Chọn ngẫu nhiên một trục xoay khiến việc tạo tập dữ liệu tạo ra hiệu suất O (N ^ 2) khó hơn. 'Trung vị của ba' (đầu tiên, cuối cùng, giữa) cũng là một cách để tránh các vấn đề. Tuy nhiên, hãy cẩn thận về hiệu suất tương đối của các phép so sánh; nếu việc so sánh của bạn tốn kém, thì Mo3 thực hiện nhiều phép so sánh hơn là chọn (một giá trị trục đơn) một cách ngẫu nhiên. Hồ sơ cơ sở dữ liệu có thể tốn kém để so sánh.


Cập nhật: Kéo nhận xét thành câu trả lời.

mdkess khẳng định:

'Trung vị của 3' KHÔNG phải là trung bình cuối cùng đầu tiên. Chọn ba chỉ mục ngẫu nhiên và lấy giá trị giữa của chỉ mục này. Toàn bộ vấn đề là đảm bảo rằng lựa chọn trục của bạn không mang tính xác định - nếu đúng như vậy, dữ liệu trong trường hợp xấu nhất có thể được tạo ra khá dễ dàng.

Tôi đã trả lời:

  • Phân tích thuật toán tìm kiếm của Hoare với phân vùng trung vị ba (1997) bởi P Kirschenhofer, H Prodinger, C Martínez ủng hộ ý kiến ​​của bạn (rằng 'trung vị của ba' là ba mục ngẫu nhiên).

  • Có một bài báo được mô tả tại Portal.acm.org nói về 'Trường hợp tồi tệ nhất Hoán vị cho Trung vị của Ba Quicksort' của Hannu Erkiö, được xuất bản trên Tạp chí Máy tính, Tập 27, Số 3, 1984. [Cập nhật 2012-02- 26: Có nội dung cho bài báo . Phần 2 'Thuật toán' bắt đầu: ' Bằng cách sử dụng trung vị của các phần tử đầu tiên, giữa và cuối cùng của A [L: R], có thể đạt được các phân vùng hiệu quả thành các phần có kích thước khá bằng nhau trong hầu hết các tình huống thực tế. 'Vì vậy, nó đang thảo luận về cách tiếp cận Mo3 đầu tiên-giữa-cuối cùng.]

  • Một bài báo ngắn thú vị khác là của MD McIlroy, "Kẻ thù giết người vì Quicksort" , được xuất bản trên Software-Practice and Experience, Vol. 29 (0), 1–4 (0 1999). Nó giải thích cách làm cho hầu hết mọi Quicksort hoạt động theo bậc hai.

  • Tạp chí Công nghệ AT&T Bell Labs, tháng 10 năm 1984 "Lý thuyết và Thực hành trong việc xây dựng một quy trình sắp xếp công việc" nói rằng "Hoare đề xuất phân vùng xung quanh đường trung bình của một số đường được chọn ngẫu nhiên. Sedgewick [...] khuyến nghị chọn đường trung bình của đường đầu tiên [. ..] cuối [...] và giữa ”. Điều này cho thấy rằng cả hai kỹ thuật cho 'trung bình của ba' đều được biết đến trong tài liệu. (Cập nhật 2014-11-23: Bài viết dường như có sẵn trên IEEE Xplore hoặc từ Wiley - nếu bạn có tư cách thành viên hoặc chuẩn bị trả phí.)

  • 'Kỹ thuật một chức năng sắp xếp' của JL Bentley và MD McIlroy, được xuất bản trong Thực hành và trải nghiệm phần mềm, Tập 23 (11), tháng 11 năm 1993, đi vào một cuộc thảo luận sâu rộng về các vấn đề và họ đã chọn một thuật toán phân vùng thích ứng một phần dựa trên kích thước của tập dữ liệu. Có rất nhiều cuộc thảo luận về sự đánh đổi cho các cách tiếp cận khác nhau.

  • Tìm kiếm 'số trung bình của ba' của Google hoạt động khá tốt để theo dõi thêm.

Cảm ơn vì thông tin; Trước đây tôi chỉ gặp 'trung vị của ba' xác định.


4
Trung vị của 3 KHÔNG phải là trung bình cuối cùng đầu tiên. Chọn ba chỉ mục ngẫu nhiên và lấy giá trị giữa của chỉ mục này. Toàn bộ vấn đề là đảm bảo rằng lựa chọn trục của bạn không mang tính xác định - nếu đúng như vậy, dữ liệu trong trường hợp xấu nhất có thể được tạo ra khá dễ dàng.
mindvirus

Tôi đã đọc phần giới thiệu abt kết hợp các tính năng tốt của cả quicksort và heapsort. Cách tiếp cận để chọn trục bằng cách sử dụng trung vị của ba có thể không phải lúc nào cũng thuận lợi.
Sumit Kumar Saha

4
Vấn đề với việc chọn các chỉ số ngẫu nhiên là các bộ tạo số ngẫu nhiên khá đắt. Mặc dù nó không làm tăng chi phí phân loại lớn, nhưng nó có thể sẽ khiến mọi thứ chậm hơn so với việc bạn chỉ chọn các yếu tố đầu tiên, cuối cùng và giữa. (Trong thế giới thực, tôi cá rằng không ai tạo ra những tình huống giả tạo để làm chậm quá trình sắp xếp nhanh chóng của bạn.)
Kevin Chen

20

Heh, tôi chỉ dạy lớp này.

Có một số tùy chọn.
Đơn giản: Chọn phần tử đầu tiên hoặc phần tử cuối cùng của phạm vi. (không tốt với đầu vào được sắp xếp một phần) Tốt hơn: Chọn mục ở giữa phạm vi. (tốt hơn với đầu vào được sắp xếp một phần)

Tuy nhiên, việc chọn bất kỳ phần tử tùy ý nào có nguy cơ phân chia mảng kích thước n thành hai mảng kích thước 1 và n-1. Nếu bạn làm điều đó đủ thường xuyên, nhanh chóng của bạn có nguy cơ trở thành O (n ^ 2).

Một cải tiến mà tôi đã thấy là chọn trung vị (đầu tiên, cuối cùng, giữa); Trong trường hợp xấu nhất, nó vẫn có thể đi đến O (n ^ 2), nhưng theo xác suất, đây là một trường hợp hiếm.

Đối với hầu hết dữ liệu, chọn đầu tiên hoặc cuối cùng là đủ. Tuy nhiên, nếu bạn thấy rằng bạn thường xuyên gặp phải các tình huống xấu nhất (đầu vào được sắp xếp một phần), thì tùy chọn đầu tiên sẽ là chọn giá trị trung tâm (Đây là một trục xoay tốt về mặt thống kê cho dữ liệu được sắp xếp một phần).

Nếu bạn vẫn gặp sự cố, hãy đi trên tuyến đường trung bình.


1
Chúng tôi đã thực hiện một thử nghiệm trong lớp của mình, lấy k phần tử nhỏ nhất từ ​​một mảng theo thứ tự đã sắp xếp. Chúng tôi tạo các mảng ngẫu nhiên sau đó sử dụng min-heap hoặc chọn ngẫu nhiên và trục nhanh trục xoay cố định và đếm số lần so sánh. Trên dữ liệu "ngẫu nhiên" này, giải pháp thứ hai hoạt động trung bình kém hơn giải pháp đầu tiên. Chuyển sang một trục ngẫu nhiên sẽ giải quyết được vấn đề về hiệu suất. Vì vậy, ngay cả đối với dữ liệu được cho là ngẫu nhiên, pivot cố định hoạt động kém hơn đáng kể so với pivot ngẫu nhiên.
Robert S. Barnes

Tại sao việc phân chia mảng kích thước n thành hai mảng kích thước 1 và n-1 lại có nguy cơ trở thành O (n ^ 2)?
Aaron Franke

Giả sử một mảng có kích thước N. Phân vùng thành các kích thước [1, N-1]. Bước tiếp theo là phân vùng nửa bên phải thành [1, N-2]. và cứ tiếp tục như vậy, cho đến khi chúng ta có N phân vùng kích thước 1. Nhưng, nếu chúng ta phân vùng một nửa, chúng ta sẽ thực hiện 2 phân vùng N / 2 mỗi bước, dẫn đến thuật ngữ Log (n) về độ phức tạp;
Chris Cudmore

11

Đừng bao giờ chọn một trục cố định - điều này có thể bị tấn công để khai thác thời gian chạy O (n ^ 2) trong trường hợp xấu nhất của thuật toán của bạn, điều này chỉ gây ra rắc rối. Thời gian chạy trong trường hợp xấu nhất của Quicksort xảy ra khi việc phân vùng dẫn đến một mảng gồm 1 phần tử và một mảng gồm n-1 phần tử. Giả sử bạn chọn phần tử đầu tiên làm phân vùng của mình. Nếu ai đó cung cấp một mảng cho thuật toán của bạn theo thứ tự giảm dần, trục xoay đầu tiên của bạn sẽ là trục lớn nhất, vì vậy mọi thứ khác trong mảng sẽ di chuyển sang bên trái của nó. Sau đó, khi bạn đệ quy, phần tử đầu tiên sẽ lại lớn nhất, vì vậy một lần nữa bạn đặt mọi thứ sang bên trái của nó, v.v.

Một kỹ thuật tốt hơn là phương pháp trung vị của 3, trong đó bạn chọn ngẫu nhiên ba phần tử và chọn phần giữa. Bạn biết rằng phần tử bạn chọn sẽ không phải là phần tử đầu tiên hay cuối cùng, nhưng theo định lý giới hạn trung tâm, sự phân bố của phần tử ở giữa sẽ là bình thường, có nghĩa là bạn sẽ có xu hướng ở giữa (và do đó , n lg n lần).

Nếu bạn hoàn toàn muốn đảm bảo thời gian chạy O (nlgn) cho thuật toán, phương pháp cột của 5 để tìm giá trị trung bình của một mảng sẽ chạy trong thời gian O (n), có nghĩa là phương trình lặp lại cho nhanh chóng trong trường hợp xấu nhất sẽ là T (n) = O (n) (tìm trung vị) + O (n) (phân hoạch) + 2T (n / 2) (đệ quy trái và phải.) Theo Định lý Master, đây là O (n lg n) . Tuy nhiên, hệ số không đổi sẽ rất lớn và nếu hiệu suất trong trường hợp xấu nhất là mối quan tâm chính của bạn, hãy sử dụng sắp xếp hợp nhất thay thế, chỉ chậm hơn một chút so với quicksort trung bình và đảm bảo thời gian O (nlgn) (và sẽ nhanh hơn nhiều hơn quicksort trung bình khập khiễng này).

Giải thích về thuật toán trung vị trung vị


6

Đừng cố gắng quá thông minh và kết hợp các chiến lược xoay vòng. Nếu bạn kết hợp trung vị của 3 với xoay vòng ngẫu nhiên bằng cách chọn trung vị của chỉ mục đầu tiên, cuối cùng và một chỉ số ngẫu nhiên ở giữa, thì bạn sẽ vẫn dễ bị ảnh hưởng bởi nhiều phân phối gửi trung bình của 3 bậc hai (vì vậy nó thực sự tệ hơn trục quay ngẫu nhiên đơn giản)

Ví dụ: phân phối cơ quan ống (1,2,3 ... N / 2..3,2,1) đầu tiên và cuối cùng sẽ là 1 và chỉ số ngẫu nhiên sẽ là một số nào đó lớn hơn 1, lấy trung vị sẽ cho 1 ( hoặc đầu tiên hoặc cuối cùng) và bạn nhận được một phân vùng cực kỳ không cân bằng.


2

Việc chia nhanh chóng thành ba phần sẽ dễ dàng hơn khi làm điều này

  1. Trao đổi hoặc hoán đổi chức năng phần tử dữ liệu
  2. Chức năng phân vùng
  3. Xử lý các phân vùng

Nó chỉ hơi kém hiệu quả hơn một hàm dài nhưng dễ hiểu hơn rất nhiều.

Mã sau:

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y){  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
};


/* This is the partition code */

int partition (DATATYPE list[], int l, int h){

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h]) {                 // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    }
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
};



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h){

  int p;                                      // index of partition 
  if ((h - l) > 0) {
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  };
};

1

Nó hoàn toàn phụ thuộc vào cách dữ liệu của bạn được sắp xếp để bắt đầu. Nếu bạn nghĩ rằng nó sẽ là giả ngẫu nhiên thì cách tốt nhất của bạn là chọn một lựa chọn ngẫu nhiên hoặc chọn giữa.


1

Nếu bạn đang sắp xếp một bộ sưu tập có thể truy cập ngẫu nhiên (như một mảng), nói chung tốt nhất là chọn mục vật lý ở giữa. Với điều này, nếu tất cả mảng đã sẵn sàng được sắp xếp (hoặc gần như sắp xếp), hai phân vùng sẽ gần bằng nhau và bạn sẽ có tốc độ tốt nhất.

Nếu bạn đang phân loại thứ gì đó chỉ có quyền truy cập tuyến tính (như danh sách được liên kết), thì tốt nhất bạn nên chọn mục đầu tiên, vì đó là mục nhanh nhất để truy cập. Tuy nhiên, ở đây, nếu danh sách đã được sắp xếp, bạn đang gặp rắc rối - một phân vùng sẽ luôn trống và phân vùng còn lại có mọi thứ, tạo ra thời gian tồi tệ nhất.

Tuy nhiên, đối với danh sách liên kết, chọn bất kỳ thứ gì ngoài danh sách đầu tiên, sẽ chỉ làm cho vấn đề trở nên tồi tệ hơn. Nó chọn mục ở giữa trong một danh sách được liệt kê, bạn phải bước qua nó trên mỗi bước phân vùng - thêm một thao tác O (N / 2) được thực hiện logN lần tạo nên tổng thời gian O (1,5 N * log N) và đó là nếu chúng ta biết danh sách dài bao lâu trước khi chúng ta bắt đầu - thường thì chúng ta không làm như vậy, chúng ta sẽ phải thực hiện tất cả các bước để đếm chúng, sau đó bước nửa chặng đường để tìm điểm trung lần thứ ba để thực hiện phân vùng thực tế: O (2,5N * log N)


0

Lý tưởng nhất là trục xoay phải là giá trị giữa trong toàn bộ mảng. Điều này sẽ làm giảm cơ hội đạt được hiệu suất trong trường hợp xấu nhất.


1
xe ngựa ở đây.
ncmathsadist

0

Độ phức tạp của sắp xếp nhanh thay đổi rất nhiều với việc lựa chọn giá trị trục. ví dụ: nếu bạn luôn chọn phần tử đầu tiên làm trục xoay, độ phức tạp của thuật toán trở nên tồi tệ nhất là O (n ^ 2). đây là một phương pháp thông minh để chọn phần tử tổng hợp- 1. chọn phần tử đầu tiên, giữa, cuối cùng của mảng. 2. so sánh ba số này và tìm số lớn hơn một và nhỏ hơn số khác tức là số trung vị. 3. làm cho phần tử này làm phần tử pivot.

việc chọn pivot bằng phương pháp này sẽ chia mảng thành gần hai nửa và do đó độ phức tạp giảm xuống còn O (nlog (n)).


0

Trung bình, Trung vị của 3 là tốt cho n nhỏ. Trung vị của 5 tốt hơn một chút đối với n lớn hơn. Ninther, là "trung vị của ba trung bình của ba" thậm chí tốt hơn cho n rất lớn.

Việc lấy mẫu càng cao thì bạn càng nhận được tốt hơn khi n tăng lên, nhưng sự cải thiện sẽ chậm lại đáng kể khi bạn tăng mẫu. Và bạn phải chịu chi phí lấy mẫu và phân loại mẫu.


0

Tôi khuyên bạn nên sử dụng chỉ số giữa, vì nó có thể được tính toán dễ dàng.

Bạn có thể tính toán nó bằng cách làm tròn (array.length / 2).


-1

Trong một triển khai thực sự được tối ưu hóa, phương pháp chọn pivot phải phụ thuộc vào kích thước mảng - đối với một mảng lớn, việc dành nhiều thời gian hơn để chọn một pivot tốt sẽ có ích. Nếu không thực hiện phân tích đầy đủ, tôi sẽ đoán "giữa các phần tử O (log (n))" là một khởi đầu tốt và điều này có thêm điểm cộng là không yêu cầu thêm bất kỳ bộ nhớ nào: Sử dụng lệnh gọi đuôi trên phân vùng lớn hơn và trong- đặt phân vùng, chúng tôi sử dụng cùng một bộ nhớ phụ O (log (n)) ở hầu hết mọi giai đoạn của thuật toán.


1
Tìm giữa 3 phần tử có thể được thực hiện trong thời gian không đổi. Thêm nữa, và về cơ bản chúng ta phải sắp xếp mảng con. Khi n trở nên lớn, chúng ta quay trở lại vấn đề sắp xếp một lần nữa.
Chris Cudmore
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.