ฉันอยากรู้ว่า O (n log n) เป็นรายการที่เชื่อมโยงได้ดีที่สุดหรือไม่
ฉันอยากรู้ว่า O (n log n) เป็นรายการที่เชื่อมโยงได้ดีที่สุดหรือไม่
คำตอบ:
มันมีเหตุผลที่จะคาดหวังว่าคุณจะไม่สามารถดำเนินการใด ๆ ที่ดีกว่า O (n log N) ในเวลาทำงาน
แต่ส่วนที่น่าสนใจคือการตรวจสอบว่าคุณสามารถจัดเรียงในสถานที่ , เสถียรพฤติกรรมที่เลวร้ายที่สุดกรณีของตนและอื่น ๆ
Simon Tatham ของสีโป๊วชื่อเสียงอธิบายถึงวิธีการจัดเรียงรายการที่เชื่อมโยงกับการผสานการเรียงลำดับ เขาสรุปด้วยความคิดเห็นต่อไปนี้:
เช่นเดียวกับอัลกอริทึมการเรียงลำดับที่เคารพตัวเองสิ่งนี้มีเวลาทำงาน O (N log N) เนื่องจากนี่คือ Mergesort เวลาทำงานในกรณีที่เลวร้ายที่สุดยังคงเป็น O (N log N) ไม่มีกรณีทางพยาธิวิทยา
ข้อกำหนดการจัดเก็บเสริมมีขนาดเล็กและคงที่ (เช่นตัวแปรสองสามตัวในรูทีนการเรียงลำดับ) เนื่องจากลักษณะการทำงานที่แตกต่างกันโดยเนื้อแท้ของรายการที่เชื่อมโยงจากอาร์เรย์การใช้งาน Mergesort นี้จะหลีกเลี่ยงต้นทุนหน่วยเก็บข้อมูลเสริม O (N) ที่ปกติจะเกี่ยวข้องกับอัลกอริทึม
นอกจากนี้ยังมีตัวอย่างการใช้งานใน C ที่ใช้ได้กับทั้งรายการที่เชื่อมโยงแบบเดี่ยวและแบบทวีคูณ
ดังที่ @ Jørgen Fogh กล่าวไว้ด้านล่างสัญกรณ์ big-O อาจซ่อนปัจจัยคงที่บางอย่างที่อาจทำให้อัลกอริทึมหนึ่งทำงานได้ดีขึ้นเนื่องจากตำแหน่งหน่วยความจำเนื่องจากมีรายการจำนวนน้อยเป็นต้น
listsort
int is_double
listsort
ที่รองรับเฉพาะรายการที่เชื่อมโยงแบบเดี่ยวเท่านั้น
ทั้งนี้ขึ้นอยู่กับจำนวนของปัจจัยก็จริงอาจจะเร็วขึ้นเพื่อคัดลอกรายการไปยังอาร์เรย์แล้วใช้Quicksort
เหตุผลที่อาจเร็วกว่าคืออาร์เรย์มีประสิทธิภาพแคชที่ดีกว่ารายการที่เชื่อมโยง หากโหนดในรายการกระจายอยู่ในหน่วยความจำคุณอาจสร้างแคชพลาดไปทั่ว จากนั้นอีกครั้งหากอาร์เรย์มีขนาดใหญ่คุณจะพลาดแคชอยู่ดี
Mergesort ขนานกันดีกว่าดังนั้นจึงอาจเป็นทางเลือกที่ดีกว่าหากเป็นสิ่งที่คุณต้องการ นอกจากนี้ยังเร็วกว่ามากหากคุณดำเนินการโดยตรงในรายการที่เชื่อมโยง
เนื่องจากอัลกอริทึมทั้งสองทำงานใน O (n * log n) การตัดสินใจอย่างมีข้อมูลจะเกี่ยวข้องกับการกำหนดโปรไฟล์ทั้งสองบนเครื่องที่คุณต้องการเรียกใช้
- แก้ไข
ฉันตัดสินใจที่จะทดสอบสมมติฐานของฉันและเขียนโปรแกรม C ซึ่งวัดเวลา (โดยใช้clock()
) ที่ใช้เพื่อจัดเรียงรายการ ints ที่เชื่อมโยงกัน ฉันลองใช้รายการที่เชื่อมโยงซึ่งแต่ละโหนดได้รับการจัดสรรmalloc()
และรายการที่เชื่อมโยงซึ่งโหนดถูกจัดวางเป็นเส้นตรงในอาร์เรย์ดังนั้นประสิทธิภาพของแคชจะดีขึ้น ฉันเปรียบเทียบสิ่งเหล่านี้กับ qsort ในตัวซึ่งรวมถึงการคัดลอกทุกอย่างจากรายการที่กระจัดกระจายไปยังอาร์เรย์และคัดลอกผลลัพธ์กลับมาอีกครั้ง แต่ละอัลกอริทึมทำงานบนชุดข้อมูล 10 ชุดเดียวกันและผลลัพธ์จะถูกเฉลี่ย
นี่คือผลลัพธ์:
N = 1,000:
รายการแยกส่วนพร้อมการเรียงลำดับการผสาน: 0.000000 วินาที
อาร์เรย์ด้วย qsort: 0.000000 วินาที
รายการที่เต็มไปด้วยการเรียงลำดับการผสาน: 0.000000 วินาที
N = 100000:
รายการแยกส่วนพร้อมการเรียงลำดับการผสาน: 0.039000 วินาที
Array พร้อม qsort: 0.025000 วินาที
รายการที่เต็มไปด้วยการเรียงลำดับการผสาน: 0.009000 วินาที
N = 1000000:
รายการแยกส่วนพร้อมการเรียงลำดับการผสาน: 1.162000 วินาที
Array พร้อม qsort: 0.420000 วินาที
รายการที่เต็มไปด้วยการเรียงลำดับการผสาน: 0.112000 วินาที
N = 100000000:
รายการแยกส่วนพร้อมการเรียงลำดับการผสาน: 364.797000 วินาที
Array พร้อม qsort: 61.166000 วินาที
รายการที่เต็มไปด้วยการเรียงลำดับการผสาน: 16.525000 วินาที
สรุป:
อย่างน้อยในเครื่องของฉันการคัดลอกลงในอาร์เรย์ก็คุ้มค่าที่จะปรับปรุงประสิทธิภาพของแคชเนื่องจากคุณไม่ค่อยมีรายการที่เชื่อมโยงอย่างสมบูรณ์ในชีวิตจริง ควรสังเกตว่าเครื่องของฉันมี 2.8GHz Phenom II แต่มี RAM เพียง 0.6GHz ดังนั้นแคชจึงมีความสำคัญมาก
นี่เป็นบทความเล็ก ๆ ที่ดีในหัวข้อนี้ ข้อสรุปเชิงประจักษ์ของเขาคือ Treesort ดีที่สุดตามด้วย Quicksort และ Mergesort การเรียงตะกอนการเรียงฟองการเรียงลำดับการเลือกทำได้แย่มาก
การศึกษาเปรียบเทียบการเรียงลำดับรายการที่เชื่อมโยงโดย Ching-Kuang Shene
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
ประเภทการเปรียบเทียบ (คนคือขึ้นอยู่กับการเปรียบเทียบองค์ประกอบ) n log n
ไม่อาจจะเร็วกว่า ไม่สำคัญว่าโครงสร้างข้อมูลพื้นฐานคืออะไร ดูวิกิพีเดีย
ประเภทอื่น ๆ ที่ใช้ประโยชน์จากการมีองค์ประกอบที่เหมือนกันจำนวนมากในรายการ (เช่นการเรียงลำดับการนับ) หรือการกระจายองค์ประกอบที่คาดหวังในรายการจะเร็วกว่าแม้ว่าฉันจะคิดไม่ออกว่าสิ่งใดทำงานได้ดีเป็นพิเศษ ในรายการที่เชื่อมโยง
ตามที่ระบุไว้หลายครั้งขอบเขตล่างของการเรียงลำดับตามการเปรียบเทียบสำหรับข้อมูลทั่วไปจะเป็น O (n log n) ในการสรุปข้อโต้แย้งเหล่านี้โดยย่อมี n! วิธีต่างๆในการจัดเรียงรายการ ต้นไม้เปรียบเทียบใด ๆ ที่มี n! (ซึ่งอยู่ใน O (n ^ n)) ประเภทสุดท้ายที่เป็นไปได้จะต้องมีอย่างน้อย log (n!) ตามความสูง: สิ่งนี้จะให้ขอบเขตล่าง O (log (n ^ n)) ซึ่งเป็น O (n ล็อก n)
ดังนั้นสำหรับข้อมูลทั่วไปในรายการที่เชื่อมโยงการเรียงลำดับที่ดีที่สุดที่จะทำงานกับข้อมูลใด ๆ ที่สามารถเปรียบเทียบวัตถุสองชิ้นได้จะเป็น O (n log n) อย่างไรก็ตามหากคุณมีโดเมนที่ จำกัด มากขึ้นในการทำงานคุณสามารถปรับปรุงเวลาที่ใช้ (อย่างน้อยตามสัดส่วนกับ n) ตัวอย่างเช่นหากคุณกำลังทำงานกับจำนวนเต็มไม่เกินค่าบางค่าคุณสามารถใช้Counting SortหรือRadix Sortได้เนื่องจากใช้อ็อบเจ็กต์เฉพาะที่คุณกำลังเรียงลำดับเพื่อลดความซับซ้อนโดยมีสัดส่วนเป็น n อย่างไรก็ตามโปรดระวังสิ่งเหล่านี้เพิ่มสิ่งอื่น ๆ ให้กับความซับซ้อนที่คุณอาจไม่ได้พิจารณา (ตัวอย่างเช่นการเรียงลำดับการนับและการเรียงลำดับ Radix เพิ่มปัจจัยที่ขึ้นอยู่กับขนาดของตัวเลขที่คุณกำลังเรียงลำดับ O (n + k ) โดยที่ k คือขนาดของตัวเลขที่ใหญ่ที่สุดสำหรับการเรียงลำดับนับเป็นต้น)
นอกจากนี้หากคุณมีวัตถุที่มีแฮชที่สมบูรณ์แบบ (หรืออย่างน้อยก็มีแฮชที่จับคู่ค่าทั้งหมดแตกต่างกัน) คุณสามารถลองใช้การนับหรือการเรียงลำดับเลขในฟังก์ชันแฮช
การจัดเรียง Radixเหมาะอย่างยิ่งกับรายการที่เชื่อมโยงเนื่องจากง่ายต่อการสร้างตารางของตัวชี้ส่วนหัวที่สอดคล้องกับค่าที่เป็นไปได้ของตัวเลขแต่ละตัว
การเรียงลำดับการผสานไม่ต้องการการเข้าถึง O (1) และเป็น O (n ln n) ไม่มีอัลกอริทึมที่เป็นที่รู้จักสำหรับการเรียงลำดับข้อมูลทั่วไปที่ดีไปกว่า O (n ln n)
อัลกอริธึมข้อมูลพิเศษเช่นการจัดเรียงเรดิกซ์ (ขนาด จำกัด ของข้อมูล) หรือการจัดเรียงฮิสโตแกรม (นับข้อมูลที่ไม่ต่อเนื่อง) สามารถจัดเรียงรายการที่เชื่อมโยงโดยมีฟังก์ชันการเติบโตที่ต่ำกว่าตราบเท่าที่คุณใช้โครงสร้างอื่นที่มีการเข้าถึง O (1) เป็นที่จัดเก็บชั่วคราว .
ข้อมูลพิเศษอีกชั้นหนึ่งคือการเปรียบเทียบรายการที่เรียงลำดับเกือบที่มีองค์ประกอบ k ไม่เรียงลำดับ สิ่งนี้สามารถจัดเรียงได้ในการดำเนินการ O (kn)
การคัดลอกรายการไปยังอาร์เรย์และย้อนกลับจะเป็น O (N) ดังนั้นจึงสามารถใช้อัลกอริทึมการเรียงลำดับใด ๆ ได้หากช่องว่างไม่ใช่ปัญหา
ตัวอย่างเช่นหากมีรายการที่เชื่อมโยงซึ่งมีuint_8
รหัสนี้จะเรียงลำดับตามเวลา O (N) โดยใช้การจัดเรียงฮิสโตแกรม:
#include <stdio.h>
#include <stdint.h>
#include <malloc.h>
typedef struct _list list_t;
struct _list {
uint8_t value;
list_t *next;
};
list_t* sort_list ( list_t* list )
{
list_t* heads[257] = {0};
list_t* tails[257] = {0};
// O(N) loop
for ( list_t* it = list; it != 0; it = it -> next ) {
list_t* next = it -> next;
if ( heads[ it -> value ] == 0 ) {
heads[ it -> value ] = it;
} else {
tails[ it -> value ] -> next = it;
}
tails[ it -> value ] = it;
}
list_t* result = 0;
// constant time loop
for ( size_t i = 255; i-- > 0; ) {
if ( tails[i] ) {
tails[i] -> next = result;
result = heads[i];
}
}
return result;
}
list_t* make_list ( char* string )
{
list_t head;
for ( list_t* it = &head; *string; it = it -> next, ++string ) {
it -> next = malloc ( sizeof ( list_t ) );
it -> next -> value = ( uint8_t ) * string;
it -> next -> next = 0;
}
return head.next;
}
void free_list ( list_t* list )
{
for ( list_t* it = list; it != 0; ) {
list_t* next = it -> next;
free ( it );
it = next;
}
}
void print_list ( list_t* list )
{
printf ( "[ " );
if ( list ) {
printf ( "%c", list -> value );
for ( list_t* it = list -> next; it != 0; it = it -> next )
printf ( ", %c", it -> value );
}
printf ( " ]\n" );
}
int main ( int nargs, char** args )
{
list_t* list = make_list ( nargs > 1 ? args[1] : "wibble" );
print_list ( list );
list_t* sorted = sort_list ( list );
print_list ( sorted );
free_list ( list );
}
O(n lg n)
จะไม่เป็นแบบเปรียบเทียบ (เช่นการเรียงลำดับเรดิกซ์) ตามความหมายการเรียงลำดับการเปรียบเทียบใช้กับโดเมนใด ๆ ที่มีลำดับรวม (กล่าวคือสามารถเปรียบเทียบได้)
ไม่ใช่คำตอบโดยตรงสำหรับคำถามของคุณ แต่ถ้าคุณใช้Skip Listระบบจะเรียงลำดับแล้วและมีเวลาค้นหา O (log N)
O(lg N)
เวลาค้นหาที่คาดไว้ - แต่ไม่รับประกันเนื่องจากรายการข้ามขึ้นอยู่กับการสุ่ม หากคุณได้รับข้อมูลที่ไม่น่าเชื่อถือตรวจสอบให้แน่ใจว่าซัพพลายเออร์ของอินพุตไม่สามารถคาดเดา RNG ของคุณได้หรือพวกเขาอาจส่งข้อมูลที่ก่อให้เกิดประสิทธิภาพในกรณีที่เลวร้ายที่สุด
ดังที่ฉันทราบอัลกอริทึมการเรียงลำดับที่ดีที่สุดคือ O (n * log n) ไม่ว่าจะเป็นคอนเทนเนอร์ใดก็ตาม - ได้รับการพิสูจน์แล้วว่าการเรียงลำดับในความหมายกว้าง ๆ ของคำ (รูปแบบการผสาน / Quicksort ฯลฯ ) ไม่สามารถลดลงได้ การใช้รายการที่เชื่อมโยงจะไม่ทำให้คุณมีเวลาทำงานที่ดีขึ้น
อัลกอริทึมเดียวที่ทำงานใน O (n) คืออัลกอริทึม "แฮ็ก" ซึ่งอาศัยการนับค่ามากกว่าการเรียงลำดับจริง
O(n lg c)
มา ถ้าทุกองค์ประกอบของคุณจะไม่ซ้ำกันแล้วและดังนั้นจึงใช้เวลานานกว่าc >= n
O(n lg n)
นี่คือการใช้งานที่ข้ามผ่านรายการเพียงครั้งเดียวรวบรวมการรันจากนั้นกำหนดเวลาการผสานในลักษณะเดียวกับที่ผสาน
ความซับซ้อนคือ O (n log m) โดยที่ n คือจำนวนรายการและ m คือจำนวนการรัน กรณีที่ดีที่สุดคือ O (n) (หากจัดเรียงข้อมูลแล้ว) และกรณีที่เลวร้ายที่สุดคือ O (n log n) ตามที่คาดไว้
ต้องใช้หน่วยความจำชั่วคราว O (log m); การจัดเรียงจะเสร็จสิ้นในสถานที่ในรายการ
(อัปเดตด้านล่างผู้แสดงความคิดเห็นคนหนึ่งเป็นจุดที่ดีที่ฉันควรอธิบายที่นี่)
ส่วนสำคัญของอัลกอริทึมคือ:
while list not empty
accumulate a run from the start of the list
merge the run with a stack of merges that simulate mergesort's recursion
merge all remaining items on the stack
การสะสมการวิ่งไม่จำเป็นต้องมีคำอธิบายมากนัก แต่เป็นการดีที่จะใช้โอกาสนี้ในการสะสมทั้งการวิ่งจากน้อยไปมากและการวิ่งจากมากไปน้อย (กลับด้าน) ที่นี่จะนำรายการที่มีขนาดเล็กกว่าส่วนหัวของการรันและต่อท้ายรายการที่มากกว่าหรือเท่ากับเมื่อสิ้นสุดการรัน (โปรดทราบว่าการสั่งล่วงหน้าควรใช้น้อยกว่าที่เข้มงวดเพื่อรักษาเสถียรภาพการจัดเรียง)
ง่ายที่สุดเพียงแค่วางรหัสการผสานที่นี่:
int i = 0;
for ( ; i < stack.size(); ++i) {
if (!stack[i])
break;
run = merge(run, stack[i], comp);
stack[i] = nullptr;
}
if (i < stack.size()) {
stack[i] = run;
} else {
stack.push_back(run);
}
พิจารณาเรียงลำดับรายการ (dagibecfjh) (ละเว้นการรัน) สถานะสแต็กดำเนินการดังนี้:
[ ]
[ (d) ]
[ () (a d) ]
[ (g), (a d) ]
[ () () (a d g i) ]
[ (b) () (a d g i) ]
[ () (b e) (a d g i) ]
[ (c) (b e) (a d g i ) ]
[ () () () (a b c d e f g i) ]
[ (j) () () (a b c d e f g i) ]
[ () (h j) () (a b c d e f g i) ]
จากนั้นรวมรายการเหล่านี้ทั้งหมดเข้าด้วยกัน
โปรดสังเกตว่าจำนวนรายการ (รัน) ที่สแต็ก [i] เป็นศูนย์หรือ 2 ^ i และขนาดสแต็กถูกล้อมรอบด้วย 1 + log2 (nruns) แต่ละองค์ประกอบจะรวมกันหนึ่งครั้งต่อระดับสแต็กดังนั้นการเปรียบเทียบ O (n log m) มีความคล้ายคลึงกันกับ Timsort ที่นี่แม้ว่า Timsort จะรักษาสแต็กโดยใช้บางอย่างเช่นลำดับฟีโบนักชีซึ่งใช้พลังของสอง
การสะสมการรันจะใช้ประโยชน์จากข้อมูลที่เรียงลำดับแล้วดังนั้นความซับซ้อนของเคสที่ดีที่สุดคือ O (n) สำหรับรายการที่เรียงลำดับแล้ว (หนึ่งรัน) เนื่องจากเรากำลังสะสมการวิ่งทั้งจากน้อยไปมากและจากมากไปน้อยการวิ่งจะมีความยาวอย่างน้อย 2 เสมอ (ซึ่งจะช่วยลดความลึกของสแต็กสูงสุดลงอย่างน้อยหนึ่งครั้งโดยจ่ายค่าใช้จ่ายในการค้นหาการรันตั้งแต่แรก) ความซับซ้อนของกรณีที่เลวร้ายที่สุดคือ O (n log n) ตามที่คาดไว้สำหรับข้อมูลที่มีการสุ่มสูง
(อืม ... ปรับปรุงครั้งที่สอง)
หรือเพียงแค่ดูวิกิพีเดียในการผสานจากล่างขึ้นบน
O(log m)
ไม่จำเป็นต้องใช้หน่วยความจำเพิ่มเติม - เพียงแค่เพิ่มการวิ่งไปยังสองรายการสลับกันจนกว่าจะว่าง
คุณสามารถคัดลอกลงในอาร์เรย์แล้วจัดเรียงได้
คัดลอกไปยังอาร์เรย์ O (n)
การเรียงลำดับ O (nlgn) (หากคุณใช้อัลกอริทึมที่รวดเร็วเช่นการเรียงลำดับการผสาน)
คัดลอกกลับไปยังรายการที่เชื่อมโยง O (n) หากจำเป็น
ดังนั้นมันจะเป็น O (nlgn)
โปรดทราบว่าหากคุณไม่ทราบจำนวนองค์ประกอบในรายการที่เชื่อมโยงคุณจะไม่ทราบขนาดของอาร์เรย์ หากคุณกำลังเขียนโค้ดใน java คุณสามารถใช้ Arraylist ได้เช่น
Mergesort เป็นสิ่งที่ดีที่สุดที่คุณสามารถทำได้ที่นี่
คำถามคือLeetCode # 148และมีวิธีแก้ปัญหามากมายในภาษาหลักทั้งหมด ของฉันมีดังนี้ แต่ฉันสงสัยเกี่ยวกับความซับซ้อนของเวลา ในการค้นหาองค์ประกอบตรงกลางเราสำรวจรายการทั้งหมดในแต่ละครั้ง n
องค์ประกอบในครั้งแรกจะวนซ้ำ2 * n/2
องค์ประกอบครั้งที่สองจะวนซ้ำไปเรื่อย ๆ ดูเหมือนว่าจะถึงO(n^2)
เวลาแล้ว
def sort(linked_list: LinkedList[int]) -> LinkedList[int]:
# Return n // 2 element
def middle(head: LinkedList[int]) -> LinkedList[int]:
if not head or not head.next:
return head
slow = head
fast = head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
def merge(head1: LinkedList[int], head2: LinkedList[int]) -> LinkedList[int]:
p1 = head1
p2 = head2
prev = head = None
while p1 and p2:
smaller = p1 if p1.val < p2.val else p2
if not head:
head = smaller
if prev:
prev.next = smaller
prev = smaller
if smaller == p1:
p1 = p1.next
else:
p2 = p2.next
if prev:
prev.next = p1 or p2
else:
head = p1 or p2
return head
def merge_sort(head: LinkedList[int]) -> LinkedList[int]:
if head and head.next:
mid = middle(head)
mid_next = mid.next
# Makes it easier to stop
mid.next = None
return merge(merge_sort(head), merge_sort(mid_next))
else:
return head
return merge_sort(linked_list)