Quicksort: การเลือกเดือย


109

เมื่อใช้ Quicksort สิ่งหนึ่งที่คุณต้องทำคือการเลือกเดือย แต่เมื่อฉันดู pseudocode เช่นเดียวกับด้านล่างมันไม่ชัดเจนว่าฉันควรเลือก pivot อย่างไร องค์ประกอบแรกของรายการ? อื่น ๆ อีก?

 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))

ใครช่วยให้ฉันเข้าใจแนวคิดในการเลือกเดือยและสถานการณ์ต่างๆเรียกร้องให้ใช้กลยุทธ์ที่แตกต่างกันหรือไม่


คำตอบ:


87

การเลือกเดือยแบบสุ่มจะช่วยลดโอกาสที่คุณจะพบกับประสิทธิภาพ O (n 2 ) ในกรณีที่เลวร้ายที่สุด(การเลือกครั้งแรกหรือครั้งสุดท้ายจะทำให้เกิดประสิทธิภาพในกรณีที่แย่ที่สุดสำหรับข้อมูลที่เรียงลำดับเกือบหรือเกือบย้อนกลับ) การเลือกองค์ประกอบตรงกลางจะเป็นที่ยอมรับในกรณีส่วนใหญ่

นอกจากนี้หากคุณใช้งานสิ่งนี้ด้วยตัวเองมีเวอร์ชันของอัลกอริทึมที่ใช้งานได้ในสถานที่ (กล่าวคือโดยไม่ต้องสร้างรายการใหม่สองรายการแล้วเชื่อมต่อกัน)


10
ฉันขอสองความคิดที่ว่าการค้นหาด้วยตัวคุณเองอาจไม่คุ้มค่ากับความพยายาม นอกจากนี้โปรดใช้ความระมัดระวังในการเลือกหมายเลขสุ่มเนื่องจากบางครั้งเครื่องสร้างตัวเลขแบบสุ่มจะค่อนข้างช้า
PeterAllenWebb

คำตอบของ
นาธาน

60

ขึ้นอยู่กับความต้องการของคุณ การเลือกเดือยแบบสุ่มทำให้สร้างชุดข้อมูลที่สร้างประสิทธิภาพ O (N ^ 2) ได้ยากขึ้น 'ค่ามัธยฐานของสาม' (แรกสุดกลาง) เป็นวิธีหลีกเลี่ยงปัญหาเช่นกัน ระวังประสิทธิภาพที่สัมพันธ์กันของการเปรียบเทียบแม้ว่า; หากการเปรียบเทียบของคุณมีค่าใช้จ่ายสูง Mo3 จะทำการเปรียบเทียบมากกว่าการเลือก (ค่าเดือยเดียว) แบบสุ่ม บันทึกฐานข้อมูลอาจมีค่าใช้จ่ายสูงในการเปรียบเทียบ


อัปเดต: ดึงความคิดเห็นมาเป็นคำตอบ

mdkessยืนยัน:

"ค่ามัธยฐานของ 3" ไม่ใช่ค่ากลางสุดท้าย เลือกดัชนีสุ่มสามดัชนีและใช้ค่ากลางของสิ่งนี้ ประเด็นทั้งหมดคือเพื่อให้แน่ใจว่าการเลือก pivots ที่คุณเลือกนั้นไม่ได้ถูกกำหนด - หากเป็นเช่นนั้นข้อมูลกรณีที่เลวร้ายที่สุดสามารถสร้างได้ค่อนข้างง่าย

สิ่งที่ฉันตอบ:

  • การวิเคราะห์อัลกอริทึมการค้นหาของ Hoare ด้วย Median-Of-Three Partition (1997) โดย P Kirschenhofer, H Prodinger, C Martínezสนับสนุนการโต้แย้งของคุณ (ค่ามัธยฐานของสามคือสามรายการแบบสุ่ม)

  • มีบทความที่อธิบายไว้ที่portal.acm.orgซึ่งเกี่ยวกับ 'The Worst Case Permutation for Median-of-Three Quicksort' โดย Hannu Erkiöตีพิมพ์ใน The Computer Journal, Vol 27, No 3, 1984 [Update 2012-02- 26: มีข้อความสำหรับบทความ ส่วนที่ 2 'อัลกอริทึม' เริ่มต้น: ' โดยใช้ค่ามัธยฐานขององค์ประกอบแรกกลางและสุดท้ายของ A [L: R] พาร์ติชันที่มีประสิทธิภาพในส่วนที่มีขนาดเท่ากันจะทำได้ในสถานการณ์จริงส่วนใหญ่ 'ดังนั้นจึงกำลังหารือเกี่ยวกับแนวทาง Mo3 แรกกลาง - สุดท้าย]

  • บทความสั้น ๆ อีกเรื่องที่น่าสนใจคือโดย MD McIlroy "A Killer Adversary for Quicksort"ตีพิมพ์ใน Software-Practice and Experience ฉบับที่ 29 (0), 1–4 (0 2542). อธิบายวิธีทำให้ Quicksort เกือบทุกชนิดทำงานเป็นกำลังสอง

  • AT&T Bell Labs Tech Journal, Oct 1984 "Theory and Practice in the Construction of Working Sort Routine" States "Hoare แนะนำการแบ่งพาร์ติชันรอบ ๆ ค่ามัธยฐานของบรรทัดที่สุ่มเลือกหลาย ๆ บรรทัด Sedgewick [... ] แนะนำให้เลือกค่ามัธยฐานของ [. .. ] สุดท้าย [... ] และกลาง ". สิ่งนี้บ่งชี้ว่าทั้งสองเทคนิคสำหรับ "ค่ามัธยฐานของสาม" เป็นที่รู้จักในวรรณคดี (อัปเดต 2014-11-23: บทความนี้มีอยู่ที่IEEE XploreหรือจากWileyหากคุณเป็นสมาชิกหรือพร้อมที่จะจ่ายค่าธรรมเนียม)

  • 'Engineering a Sort Function'โดย JL Bentley และ MD McIlroy ซึ่งตีพิมพ์ใน Software Practice and Experience ฉบับที่ 23 (11) พฤศจิกายน 1993 มีการอภิปรายอย่างกว้างขวางเกี่ยวกับปัญหาและพวกเขาเลือกอัลกอริธึมการแบ่งพาร์ติชันแบบปรับได้ตามส่วนหนึ่งของ ขนาดของชุดข้อมูล มีการแลกเปลี่ยนความคิดเห็นกันมากมายสำหรับแนวทางต่างๆ

  • การค้นหา "มัธยฐานของสาม" ของ Google ใช้งานได้ดีสำหรับการติดตามเพิ่มเติม

ขอบคุณสำหรับข้อมูล; ฉันเคยพบเพียงค่ามัธยฐานของค่ามัธยฐานของสามเท่านั้นก่อนหน้านี้


4
ค่ามัธยฐานของ 3 ไม่ใช่ค่ากลางอันดับแรก เลือกดัชนีสุ่มสามดัชนีและใช้ค่ากลางของสิ่งนี้ ประเด็นทั้งหมดคือเพื่อให้แน่ใจว่าการเลือก pivots ที่คุณเลือกนั้นไม่ได้ถูกกำหนด - หากเป็นเช่นนั้นข้อมูลกรณีที่เลวร้ายที่สุดสามารถสร้างได้ค่อนข้างง่าย
mindvirus

ฉันกำลังอ่าน abt introsort ซึ่งรวมเอาคุณสมบัติที่ดีของทั้ง Quicksort และ heapsort แนวทางในการเลือกเดือยโดยใช้ค่ามัธยฐานของสามอาจไม่เป็นที่ต้องการเสมอไป
Sumit Kumar Saha

4
ปัญหาในการเลือกดัชนีแบบสุ่มคือตัวสร้างตัวเลขสุ่มนั้นค่อนข้างแพง แม้ว่าจะไม่เพิ่มค่าใช้จ่ายในการจัดเรียง big-O แต่ก็อาจทำให้สิ่งต่างๆช้าลงกว่าที่คุณเพิ่งเลือกองค์ประกอบแรกสุดท้ายและตรงกลาง (ในโลกแห่งความเป็นจริงฉันพนันได้เลยว่าไม่มีใครสร้างสถานการณ์ที่วางแผนไว้เพื่อชะลอการเรียงลำดับอย่างรวดเร็วของคุณ)
Kevin Chen

20

ฉันเพิ่งสอนชั้นเรียนนี้

มีหลายตัวเลือก
ง่าย: เลือกองค์ประกอบแรกหรือองค์ประกอบสุดท้ายของช่วง (ไม่ดีในการป้อนข้อมูลที่จัดเรียงบางส่วน) ดีกว่า: เลือกรายการที่อยู่ตรงกลางของช่วง (ดีกว่าสำหรับอินพุตที่เรียงลำดับบางส่วน)

อย่างไรก็ตามการเลือกองค์ประกอบใด ๆ ตามอำเภอใจจะเสี่ยงต่อการแบ่งอาร์เรย์ขนาด n ออกเป็นสองอาร์เรย์ขนาด 1 และ n-1 ได้ไม่ดี หากคุณทำเช่นนั้นบ่อยพอ Quicksort ของคุณจะเสี่ยงต่อการเป็น O (n ^ 2)

การปรับปรุงอย่างหนึ่งที่ฉันเห็นคือเลือกค่ามัธยฐาน (แรกสุดท้ายกลาง) ในกรณีที่เลวร้ายที่สุดก็ยังสามารถไปที่ O (n ^ 2) ได้ แต่ในทางที่ดีนี่เป็นกรณีที่หายาก

สำหรับข้อมูลส่วนใหญ่การเลือกรายการแรกหรือรายการสุดท้ายก็เพียงพอแล้ว แต่ถ้าคุณพบว่าคุณกำลังเผชิญกับสถานการณ์ที่เลวร้ายที่สุดบ่อยครั้ง (อินพุตที่เรียงลำดับบางส่วน) ตัวเลือกแรกคือการเลือกค่ากลาง (ซึ่งเป็นเดือยที่ดีทางสถิติสำหรับข้อมูลที่เรียงลำดับบางส่วน)

หากคุณยังคงพบปัญหาให้ใช้เส้นทางมัธยฐาน


1
เราทำการทดลองในชั้นเรียนของเราโดยได้รับ k องค์ประกอบที่เล็กที่สุดจากอาร์เรย์ตามลำดับ เราสร้างอาร์เรย์แบบสุ่มจากนั้นใช้ min-heap หรือ randomized select และ pivot quicksort และนับจำนวนการเปรียบเทียบ สำหรับข้อมูล "สุ่ม" นี้โซลูชันที่สองมีประสิทธิภาพโดยเฉลี่ยแย่กว่าครั้งแรก การเปลี่ยนไปใช้เดือยสุ่มช่วยแก้ปัญหาด้านประสิทธิภาพ ดังนั้นแม้จะเป็นข้อมูลแบบสุ่มที่คาดคะเน pivot คงที่จะทำงานได้แย่กว่า pivot แบบสุ่มอย่างมีนัยสำคัญ
Robert S. Barnes

เหตุใดการแบ่งอาร์เรย์ขนาด n เป็นสองอาร์เรย์ขนาด 1 และ n-1 จึงเสี่ยงต่อการเป็น O (n ^ 2)
Aaron Franke

สมมติอาร์เรย์ขนาด N พาร์ติชันเป็นขนาด [1, N-1] ขั้นตอนต่อไปคือการแบ่งครึ่งทางขวาเป็น [1, N-2] และต่อไปจนกว่าเราจะมีพาร์ติชัน N ขนาด 1 แต่ถ้าเราแบ่งครึ่งพาร์ติชันเราจะทำ 2 พาร์ติชันของ N / 2 ในแต่ละขั้นตอนซึ่งจะนำไปสู่ ​​Log (n) เทอมของความซับซ้อน
Chris Cudmore

11

ไม่เคยเลือกเดือยที่ตายตัวซึ่งอาจถูกโจมตีเพื่อใช้ประโยชน์จากรันไทม์ O (n ^ 2) กรณีที่เลวร้ายที่สุดของอัลกอริทึมของคุณซึ่งเป็นเพียงการขอปัญหา รันไทม์กรณีที่เลวร้ายที่สุดของ Quicksort เกิดขึ้นเมื่อการแบ่งพาร์ติชันผลลัพธ์ในอาร์เรย์ 1 องค์ประกอบและอาร์เรย์ขององค์ประกอบ n-1 หนึ่งรายการ สมมติว่าคุณเลือกองค์ประกอบแรกเป็นพาร์ติชันของคุณ หากมีคนป้อนอาร์เรย์ไปยังอัลกอริทึมของคุณซึ่งมีลำดับที่ลดลง Pivot แรกของคุณจะมีขนาดใหญ่ที่สุดดังนั้นสิ่งอื่น ๆ ในอาร์เรย์จะย้ายไปทางซ้ายของอาร์เรย์ จากนั้นเมื่อคุณฟื้นขึ้นมาองค์ประกอบแรกจะเป็นองค์ประกอบที่ใหญ่ที่สุดอีกครั้งดังนั้นคุณจึงวางทุกอย่างไว้ทางซ้ายอีกครั้งและอื่น ๆ

เทคนิคที่ดีกว่าคือวิธีมัธยฐานของ 3 ซึ่งคุณเลือกองค์ประกอบสามอย่างโดยการสุ่มและเลือกตรงกลาง คุณรู้ว่าองค์ประกอบที่คุณเลือกจะไม่ใช่องค์ประกอบแรกหรือองค์ประกอบสุดท้าย แต่ด้วยทฤษฎีบทข้อ จำกัด กลางการแจกแจงขององค์ประกอบกลางจะเป็นไปตามปกติซึ่งหมายความว่าคุณจะมีแนวโน้มที่จะอยู่ตรงกลาง (และด้วยเหตุนี้ , n lg n เวลา).

หากคุณต้องการรับประกันรันไทม์ O (nlgn) สำหรับอัลกอริทึมอย่างแท้จริงวิธีการคอลัมน์-of-5 ในการค้นหาค่ามัธยฐานของอาร์เรย์จะทำงานในเวลา O (n) ซึ่งหมายความว่าสมการการเกิดซ้ำสำหรับ Quicksort ในกรณีที่เลวร้ายที่สุดจะ be T (n) = O (n) (หาค่ามัธยฐาน) + O (n) (พาร์ติชัน) + 2T (n / 2) (เรียกคืนซ้ายและขวา) โดย Master Theorem นี่คือ O (n lg n) . อย่างไรก็ตามปัจจัยคงที่จะมีขนาดใหญ่มากและหากประสิทธิภาพในกรณีที่แย่ที่สุดเป็นข้อกังวลหลักของคุณให้ใช้การจัดเรียงแบบผสานแทนซึ่งช้ากว่าค่าเฉลี่ย Quicksort เพียงเล็กน้อยและรับประกันเวลา O (nlgn) (และจะเร็วขึ้นมาก กว่า Quicksort ค่ามัธยฐานที่ง่อยนี้)

คำอธิบายของค่ามัธยฐานของอัลกอริทึมของมัธยฐาน


6

อย่าพยายามฉลาดเกินไปและผสมผสานกลยุทธ์การหมุนเข้าด้วยกัน หากคุณรวมค่ามัธยฐานของ 3 เข้ากับเดือยสุ่มโดยการเลือกค่ามัธยฐานของดัชนีแรกสุดท้ายและแบบสุ่มที่อยู่ตรงกลางคุณจะยังคงเสี่ยงต่อการแจกแจงจำนวนมากซึ่งส่งค่ามัธยฐานของ 3 กำลังสอง (ดังนั้นจึงแย่กว่า เดือยสุ่มธรรมดา)

เช่นการกระจายอวัยวะไปป์ (1,2,3 ... N / 2..3,2,1) ตัวแรกและตัวสุดท้ายจะเป็น 1 และดัชนีสุ่มจะเป็นตัวเลขที่มากกว่า 1 โดยการหาค่ามัธยฐานจะให้ 1 ( ครั้งแรกหรือครั้งสุดท้าย) และคุณจะได้รับการแบ่งพาร์ติชันที่ไม่สมดุลอย่างสมบูรณ์


2

มันง่ายกว่าที่จะแบ่ง quicksort ออกเป็นสามส่วนโดยทำเช่นนี้

  1. แลกเปลี่ยนหรือสลับฟังก์ชันองค์ประกอบข้อมูล
  2. ฟังก์ชันพาร์ติชัน
  3. การประมวลผลพาร์ติชัน

มันไร้ประสิทธิภาพมากกว่าฟังก์ชั่นยาว ๆ เพียงเล็กน้อย แต่เข้าใจง่ายกว่ามาก

รหัสดังต่อไปนี้:

/* 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

ทั้งหมดขึ้นอยู่กับวิธีการจัดเรียงข้อมูลของคุณเพื่อเริ่มต้น หากคุณคิดว่าเป็นการสุ่มหลอกทางออกที่ดีที่สุดของคุณคือเลือกตัวเลือกแบบสุ่มหรือเลือกตรงกลาง


1

หากคุณกำลังจัดเรียงคอลเล็กชันที่สามารถเข้าถึงได้โดยสุ่ม (เช่นอาร์เรย์) โดยทั่วไปควรเลือกรายการกลางที่มีอยู่จริง ด้วยสิ่งนี้หากอาร์เรย์จัดเรียงพร้อมกันทั้งหมด (หรือเกือบจะเรียงลำดับแล้ว) ทั้งสองพาร์ติชันจะอยู่ใกล้เคียงกันและคุณจะได้รับความเร็วที่ดีที่สุด

หากคุณกำลังจัดเรียงบางสิ่งที่มีการเข้าถึงเชิงเส้นเท่านั้น (เช่นรายการที่เชื่อมโยง) คุณควรเลือกรายการแรกเนื่องจากเป็นรายการที่เข้าถึงได้เร็วที่สุด อย่างไรก็ตามที่นี่หากจัดเรียงรายการแล้วคุณเมา - พาร์ติชันหนึ่งจะเป็นโมฆะเสมอและอีกพาร์ติชันมีทุกอย่างทำให้เกิดเวลาที่เลวร้ายที่สุด

อย่างไรก็ตามสำหรับรายการที่เชื่อมโยงการเลือกสิ่งที่นอกเหนือจากรายการแรกจะทำให้เรื่องแย่ลง เลือกรายการกลางในรายการที่ระบุคุณจะต้องทำตามขั้นตอนในแต่ละขั้นตอนของพาร์ติชัน - เพิ่มการดำเนินการ O (N / 2) ซึ่งเสร็จแล้ว logN ครั้งทำให้เวลาทั้งหมด O (1.5 N * log N) และนั่นคือถ้าเรารู้ว่ารายการนั้นอยู่นานแค่ไหนก่อนที่เราจะเริ่ม - โดยปกติแล้วเราจะไม่ทำเช่นนั้นเราจะต้องก้าวไปตลอดทางเพื่อนับจากนั้นก้าวไปครึ่งทางเพื่อหาตรงกลางจากนั้นจึงก้าวผ่าน ครั้งที่สามในการทำพาร์ติชันจริง: O (2.5N * log N)


0

ตามหลักการแล้ว Pivot ควรเป็นค่ากลางในอาร์เรย์ทั้งหมด วิธีนี้จะช่วยลดโอกาสในการได้รับประสิทธิภาพในกรณีที่เลวร้ายที่สุด


1
รถเข็นหน้าม้าที่นี่
ncmathsadist

0

ความซับซ้อนของการเรียงลำดับอย่างรวดเร็วจะแตกต่างกันอย่างมากกับการเลือกค่าเดือย ตัวอย่างเช่นหากคุณเลือกองค์ประกอบแรกเป็นเดือยเสมอความซับซ้อนของอัลกอริทึมจะแย่ที่สุดเท่ากับ O (n ^ 2) นี่คือวิธีการที่ชาญฉลาดในการเลือกองค์ประกอบเดือย - 1. เลือกองค์ประกอบแรกกลางและสุดท้ายของอาร์เรย์ 2. เปรียบเทียบตัวเลขทั้งสามนี้และหาจำนวนที่มากกว่าหนึ่งและเล็กกว่าค่ามัธยฐานอื่น ๆ 3. ทำให้องค์ประกอบนี้เป็นองค์ประกอบเดือย

การเลือกเดือยโดยวิธีนี้จะแบ่งอาร์เรย์ออกเป็นเกือบสองครึ่งและด้วยเหตุนี้ความซับซ้อนจึงลดลงเป็น O (nlog (n))


0

โดยเฉลี่ยค่ามัธยฐานของ 3 เหมาะสำหรับ n ขนาดเล็ก ค่ามัธยฐานของ 5 ดีกว่าเล็กน้อยสำหรับ n ขนาดใหญ่ ninther ซึ่งเป็น "มัธยฐานของค่ามัธยฐานของสามมัธยฐานของสาม" จะดีกว่าสำหรับ n ที่มีขนาดใหญ่มาก

ยิ่งคุณสุ่มตัวอย่างได้สูงเท่าไหร่คุณก็จะได้รับ n เพิ่มมากขึ้นเท่านั้น แต่การปรับปรุงจะช้าลงอย่างมากเมื่อคุณเพิ่มตัวอย่าง และคุณต้องเสียค่าใช้จ่ายในการสุ่มตัวอย่างและการจัดเรียงตัวอย่าง


0

ฉันขอแนะนำให้ใช้ดัชนีกลางเนื่องจากสามารถคำนวณได้ง่าย

คุณสามารถคำนวณได้โดยการปัดเศษ (array.length / 2)


-1

ในการนำไปใช้งานที่ได้รับการปรับให้เหมาะสมอย่างแท้จริงวิธีการเลือกเดือยควรขึ้นอยู่กับขนาดอาร์เรย์ - สำหรับอาร์เรย์ขนาดใหญ่จะคุ้มค่ากับการใช้เวลามากขึ้นในการเลือกเดือยที่ดี หากไม่ทำการวิเคราะห์ทั้งหมดฉันจะเดาว่า "ตรงกลางขององค์ประกอบ O (log (n))" เป็นการเริ่มต้นที่ดีและสิ่งนี้มีโบนัสเพิ่มเติมจากการไม่ต้องใช้หน่วยความจำเพิ่มเติม: การใช้ tail-call บนพาร์ติชันที่ใหญ่ขึ้นและใน - วางพาร์ติชันเราใช้หน่วยความจำเสริม O (log (n)) เดียวกันในเกือบทุกขั้นตอนของอัลกอริทึม


1
การหาตรงกลางของ 3 องค์ประกอบสามารถทำได้ในเวลาคงที่ อีกต่อไปและเราต้องเรียงอาร์เรย์ย่อยเป็นหลัก เมื่อ n มีขนาดใหญ่เราจะกลับไปที่ปัญหาการเรียงลำดับอีกครั้ง
Chris Cudmore
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.