ฉันต้องยอมรับว่ามันค่อนข้างแปลกในครั้งแรกที่คุณเห็นอัลกอริทึม O (log n) ... ลอการิทึมนั้นมาจากไหนบนโลก? อย่างไรก็ตามปรากฎว่ามีหลายวิธีที่คุณจะได้รับข้อความบันทึกเพื่อแสดงในสัญกรณ์ขนาดใหญ่ นี่คือบางส่วน:
หารด้วยค่าคงที่ซ้ำ ๆ
ใช้จำนวนใด ๆ n; พูดว่า 16. คุณหาร n ด้วยสองได้กี่ครั้งก่อนที่คุณจะได้จำนวนน้อยกว่าหรือเท่ากับหนึ่ง? สำหรับ 16 เรามีสิ่งนั้น
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
สังเกตว่าขั้นตอนนี้ต้องทำสี่ขั้นตอนให้เสร็จสิ้น ที่น่าสนใจคือเรามี log นั้นด้วย2 16 = 4 อืม ... แล้ว 128 ล่ะ?
128 / 2 = 64
64 / 2 = 32
32 / 2 = 16
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
นี่ใช้เวลาเจ็ดขั้นตอนและล็อก2 128 = 7 นี่เป็นเรื่องบังเอิญหรือเปล่า? ไม่! มีเหตุผลที่ดีสำหรับเรื่องนี้ สมมติว่าเราหารจำนวน n ด้วย 2 i คูณ จากนั้นเราได้รับจำนวน n / 2 ฉัน ถ้าเราต้องการแก้ค่าของ i โดยที่ค่านี้มากที่สุด 1 เราจะได้
n / 2 ฉัน ≤ 1
n ≤ 2 i
บันทึก2 n ≤ i
กล่าวอีกนัยหนึ่งถ้าเราเลือกจำนวนเต็ม i เช่น i ≥ log 2 n หลังจากหาร n ครึ่ง i คูณเราจะมีค่ามากที่สุด 1 i ที่เล็กที่สุดที่รับประกันได้คือ log 2โดยประมาณn ดังนั้นหากเรามีอัลกอริทึมที่หารด้วย 2 จนกว่าจำนวนจะน้อยพอเราสามารถพูดได้ว่ามันสิ้นสุดในขั้นตอน O (log n)
รายละเอียดที่สำคัญคือมันไม่สำคัญว่าคุณจะหาร n ด้วยค่าคงที่เท่าใด (ตราบใดที่มันมากกว่าหนึ่ง) ถ้าคุณหารด้วยค่าคงที่ k จะใช้ log k n ขั้นตอนเพื่อไปถึง 1 ดังนั้นอัลกอริทึมใด ๆ ที่แบ่งขนาดอินพุตซ้ำ ๆ โดยเศษส่วนบางส่วนจะต้องมีการทำซ้ำ O (log n) เพื่อยุติ การทำซ้ำเหล่านั้นอาจใช้เวลานานดังนั้นรันไทม์สุทธิจึงไม่จำเป็นต้องเป็น O (log n) แต่จำนวนขั้นตอนจะเป็นลอการิทึม
แล้วสิ่งนี้เกิดขึ้นที่ไหน? ตัวอย่างคลาสสิกอย่างหนึ่งคือค้นหาแบบไบนารีซึ่งเป็นอัลกอริทึมที่รวดเร็วสำหรับการค้นหาอาร์เรย์ที่เรียงลำดับเพื่อหาค่า อัลกอริทึมทำงานดังนี้:
- หากอาร์เรย์ว่างเปล่าให้ส่งคืนองค์ประกอบที่ไม่มีอยู่ในอาร์เรย์
- มิฉะนั้น:
- ดูองค์ประกอบตรงกลางของอาร์เรย์
- ถ้ามันเท่ากับองค์ประกอบที่เรากำลังมองหาจงคืนความสำเร็จ
- หากมันมากกว่าองค์ประกอบที่เรากำลังมองหา:
- ทิ้งครึ่งหลังของอาร์เรย์
- ทำซ้ำ
- หากมันน้อยกว่าองค์ประกอบที่เรากำลังมองหา:
- ทิ้งครึ่งแรกของอาร์เรย์
- ทำซ้ำ
ตัวอย่างเช่นหากต้องการค้นหา 5 ในอาร์เรย์
1 3 5 7 9 11 13
ก่อนอื่นเรามาดูองค์ประกอบตรงกลาง:
1 3 5 7 9 11 13
^
ตั้งแต่ 7> 5 และเนื่องจากอาร์เรย์ถูกจัดเรียงเราจึงรู้ว่าตัวเลข 5 ไม่สามารถอยู่ครึ่งหลังของอาร์เรย์ได้ดังนั้นเราจึงสามารถทิ้งมันไปได้ ใบนี้
1 3 5
ตอนนี้เรามาดูองค์ประกอบตรงกลางที่นี่:
1 3 5
^
ตั้งแต่ 3 <5 เรารู้ว่า 5 ไม่สามารถปรากฏในครึ่งแรกของอาร์เรย์ได้ดังนั้นเราจึงสามารถทิ้งอาร์เรย์ครึ่งแรกเพื่อออกไป
5
อีกครั้งเราดูตรงกลางของอาร์เรย์นี้:
5
^
เนื่องจากนี่เป็นตัวเลขที่เรากำลังมองหาเราจึงรายงานได้ว่า 5 อยู่ในอาร์เรย์
แล้วมันมีประสิทธิภาพแค่ไหน? ในการวนซ้ำแต่ละครั้งเราจะทิ้งองค์ประกอบอาร์เรย์ที่เหลืออย่างน้อยครึ่งหนึ่ง อัลกอริทึมจะหยุดทันทีที่อาร์เรย์ว่างเปล่าหรือเราพบค่าที่เราต้องการ ในกรณีที่เลวร้ายที่สุดองค์ประกอบไม่ได้อยู่ที่นั่นดังนั้นเราจึงลดขนาดอาร์เรย์ลงครึ่งหนึ่งจนกว่าองค์ประกอบจะหมด ใช้เวลานานแค่ไหน? เนื่องจากเราทำการตัดอาร์เรย์ครึ่งหนึ่งซ้ำแล้วซ้ำเล่าเราจะทำซ้ำ O (log n) มากที่สุดเนื่องจากเราไม่สามารถตัดอาร์เรย์ได้มากกว่า O (log n) ครึ่งครั้งก่อนที่เราจะเรียกใช้ ไม่อยู่ในองค์ประกอบอาร์เรย์
อัลกอริทึมตามเทคนิคทั่วไปของการแบ่งและพิชิต (ตัดปัญหาเป็นชิ้น ๆ แก้ปัญหาเหล่านั้นจากนั้นนำปัญหากลับมารวมกัน) มักจะมีคำศัพท์ลอการิทึมอยู่ในนั้นด้วยเหตุผลเดียวกันนี้ - คุณไม่สามารถตัดวัตถุบางอย่างต่อไปได้ มากกว่า O (log n) ครึ่งเท่า คุณอาจต้องการดูการจัดเรียงการผสานเป็นตัวอย่างที่ดี
การประมวลผลค่าทีละหลัก
เลขฐาน 10 n มีกี่หลัก? ดีถ้ามีตัวเลข k ในจำนวนนั้นเราจะต้องว่าหลักที่ใหญ่ที่สุดคือหลาย 10 บางk จำนวน k หลักที่ใหญ่ที่สุดคือ 999 ... 9, k ครั้งและนี่เท่ากับ 10 k + 1 - 1 ดังนั้นถ้าเรารู้ว่า n มี k หลักอยู่เราจะรู้ว่าค่าของ n คือ มากที่สุด 10 k + 1 - 1 ถ้าเราต้องการแก้สำหรับ k ในรูปของ n เราจะได้
n ≤ 10 k + 1 - 1
n + 1 ≤ 10 k + 1
บันทึก10 (n + 1) ≤ k + 1
(บันทึก10 (n + 1)) - 1 ≤ k
จากที่เราได้ว่า k นั้นมีค่าประมาณลอการิทึมฐาน 10 ของ n กล่าวอีกนัยหนึ่งจำนวนหลักใน n คือ O (log n)
ตัวอย่างเช่นลองคิดถึงความซับซ้อนของการเพิ่มตัวเลขขนาดใหญ่สองตัวที่ใหญ่เกินไปที่จะใส่ลงในคำในเครื่อง สมมติว่าเรามีตัวเลขเหล่านั้นแสดงอยู่ในฐาน 10 และเราจะเรียกตัวเลขว่า m และ n วิธีหนึ่งในการเพิ่มพวกเขาคือใช้วิธีระดับโรงเรียน - เขียนตัวเลขทีละหลักจากนั้นทำงานจากขวาไปซ้าย ตัวอย่างเช่นในการเพิ่ม 1337 และ 2065 เราจะเริ่มต้นด้วยการเขียนตัวเลขออกมาเป็น
1 3 3 7
+ 2 0 6 5
==============
เราเพิ่มตัวเลขสุดท้ายและถือ 1:
1
1 3 3 7
+ 2 0 6 5
==============
2
จากนั้นเราจะเพิ่มหลักที่สองไปสุดท้าย ("สุดท้าย") และดำเนินการ 1:
1 1
1 3 3 7
+ 2 0 6 5
==============
0 2
ต่อไปเราจะเพิ่มตัวเลขที่สามถึงสุดท้าย ("antepenultimate"):
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
สุดท้ายเราเพิ่มหลักที่สี่ถึงสุดท้าย ("preantepenultimate ... ฉันรักภาษาอังกฤษ):
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
ตอนนี้เราทำงานหนักแค่ไหน? เราทำงานทั้งหมด O (1) ต่อหลัก (นั่นคือจำนวนงานคงที่) และมี O (สูงสุด {log n, log m}) หลักที่ต้องประมวลผล สิ่งนี้ให้ผลรวมของความซับซ้อน O (สูงสุด {log n, log m}) เนื่องจากเราจำเป็นต้องเยี่ยมชมแต่ละหลักในตัวเลขสองตัว
อัลกอริทึมจำนวนมากได้รับคำ O (log n) จากการทำงานทีละหลักในบางฐาน ตัวอย่างคลาสสิกคือradix sortซึ่งเรียงลำดับจำนวนเต็มทีละหลัก มีการจัดเรียงเรดิกซ์หลายรสชาติ แต่โดยปกติจะทำงานในเวลา O (n log U) โดยที่ U เป็นจำนวนเต็มมากที่สุดเท่าที่จะเป็นไปได้ซึ่งจะถูกจัดเรียง เหตุผลก็คือการเรียงลำดับแต่ละครั้งใช้เวลา O (n) และมีการทำซ้ำ O (log U) ทั้งหมดที่จำเป็นในการประมวลผลตัวเลข O (log U) แต่ละหลักของตัวเลขที่ใหญ่ที่สุดที่กำลังเรียงลำดับ อัลกอริทึมขั้นสูงจำนวนมากเช่นอัลกอริธึมเส้นทางที่สั้นที่สุดของ GabowหรืออัลกอริทึมการไหลสูงสุดของFord-Fulkersonรุ่นปรับขนาดจะมีคำบันทึกที่ซับซ้อนเนื่องจากทำงานทีละหลัก
สำหรับคำถามที่สองของคุณเกี่ยวกับวิธีแก้ปัญหานั้นคุณอาจต้องการดู คำถามที่เกี่ยวข้องนี้ซึ่งจะอธิบายถึงแอปพลิเคชันขั้นสูง เมื่อพิจารณาถึงโครงสร้างทั่วไปของปัญหาที่อธิบายไว้ที่นี่ตอนนี้คุณสามารถเข้าใจวิธีคิดเกี่ยวกับปัญหาได้ดีขึ้นเมื่อคุณรู้ว่ามีคำบันทึกในผลลัพธ์ดังนั้นฉันจะแนะนำไม่ให้ดูคำตอบจนกว่าคุณจะให้มัน ความคิดบางอย่าง
หวังว่านี่จะช่วยได้!
O(log n)
จะเห็นได้ว่า: หากคุณเพิ่มขนาดปัญหาเป็นสองเท่าn
อัลกอริทึมของคุณต้องการจำนวนขั้นตอนที่คงที่มากขึ้นเท่านั้น