Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

คู่มือฉบับสมบูรณ์เกี่ยวกับความซับซ้อนของเวลาสำหรับนักพัฒนา Ruby

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

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

คุณสามารถใช้สิ่งนี้กับรหัสของคุณเองได้

สิ่งนี้เหนือกว่าอัลกอริธึมแฟนซีทั้งหมด ที่คุณพบในหนังสือวิทยาการคอมพิวเตอร์ ดังที่ฉันจะสาธิตให้คุณเห็นในบทความนี้

แต่ก่อนอื่นเราต้องพูดถึงสิ่งที่ช้าและเร็ว

ช้าเทียบกับเร็ว

การเรียงลำดับ 1 ล้านตัวเลขใน 150ms (มิลลิวินาที) ช้าหรือเร็วหรือไม่

ฉันคิดว่ามันค่อนข้างเร็ว แต่นี่ไม่ใช่คำถามที่ความซับซ้อนของเวลาพยายามจะตอบ

เราต้องการทราบว่าอัลกอริธึมทำงานอย่างไรเมื่อ "ขนาดอินพุต" เพิ่มขึ้น

เมื่อเราพูดถึง "ขนาดอินพุต" เรากำลังพูดถึงอาร์กิวเมนต์สำหรับฟังก์ชันหรือเมธอด หากเมธอดรับสตริงหนึ่งสตริงเป็นอาร์กิวเมนต์ นั่นคืออินพุต และความยาวของสตริงคือขนาดอินพุต

สัญลักษณ์บิ๊กโอ

ด้วยสัญกรณ์ Big O เราสามารถจำแนกอัลกอริทึมตามประสิทธิภาพได้

ดูเหมือนว่านี้:

O(1)

ในกรณีนี้ O(1) แสดงถึงอัลกอริธึม "เวลาคงที่"

ซึ่งหมายความว่าอัลกอริธึมจะใช้เวลาเท่าเดิมในการทำงานให้เสร็จ โดยไม่คำนึงว่าจะต้องทำงานมากน้อยเพียงใด

อีกอย่างคือ “เวลาเชิงเส้น”:

O(n)

โดยที่ “n” แทนขนาดของอินพุต (ขนาดสตริง ขนาดอาร์เรย์ ฯลฯ) ซึ่งหมายความว่าอัลกอริทึมจะทำงานให้เสร็จในอัตราส่วน 1:1 กับขนาดอินพุต ดังนั้นการเพิ่มขนาดอินพุตเป็นสองเท่าจะเพิ่มเวลาในการทำงานให้เสร็จเป็นสองเท่า

นี่คือตาราง :

สัญลักษณ์ ชื่อ คำอธิบาย
O(1) คงที่ ใช้เวลาเท่ากันเสมอ
O(log n) ลอการิทึม จำนวนงานหารด้วย 2 หลังจากการวนซ้ำทุกอัน (การค้นหาแบบไบนารี)
O(n) เชิงเส้น เวลาทำงานให้เสร็จเพิ่มขึ้นในความสัมพันธ์แบบ 1 ต่อ 1 กับขนาดอินพุต
O(n บันทึก n) เชิงเส้น นี่คือการวนซ้ำซ้อน โดยที่วงในทำงานใน log n เวลา. ตัวอย่าง ได้แก่ quicksort, mergesort &heapsort
O(n^2) กำลังสอง เวลาทำงานให้เสร็จเพิ่มขึ้นใน input size ^ 2 ความสัมพันธ์ คุณสามารถรับรู้สิ่งนี้ได้ทุกเมื่อที่คุณมีลูปที่ข้ามองค์ประกอบทั้งหมด &การวนซ้ำทุกครั้งของลูปยังครอบคลุมทุกองค์ประกอบด้วย คุณมีลูปที่ซ้อนกัน
O(n^3) ลูกบาศก์ เหมือนกับ n^2 แต่เวลาเพิ่มขึ้นใน n^3 สัมพันธ์กับขนาดอินพุต มองหาลูปที่ซ้อนกันสามชั้นเพื่อจดจำสิ่งนี้
O(2^n) เอกซ์โพเนนเชียล เวลาทำงานให้เสร็จเพิ่มขึ้นใน 2 ^ input size ความสัมพันธ์ ช้ากว่า n^2 . มาก ดังนั้นอย่าสับสน! ตัวอย่างหนึ่งคืออัลกอริทึมฟีโบนักชีแบบเรียกซ้ำ

การวิเคราะห์อัลกอริทึม

คุณสามารถเริ่มพัฒนาสัญชาตญาณสำหรับสิ่งนี้ได้หากคุณคิดว่าอินพุตเป็นอาร์เรย์ขององค์ประกอบ “n”

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

[1,2,3,4,5,6,7,8,9,10].select(&:even?)

คุณไม่สามารถข้ามตัวเลขได้ เนื่องจากคุณอาจพลาดตัวเลขบางตัวที่กำลังมองหา จึงทำให้เป็นอัลกอริธึมเวลาเชิงเส้น O(n) .

3 วิธีแก้ปัญหาที่แตกต่างกันสำหรับปัญหาเดียวกัน

การวิเคราะห์ตัวอย่างแต่ละรายการเป็นเรื่องที่น่าสนใจ แต่สิ่งที่ช่วยให้เข้าใจแนวคิดนี้ได้จริงๆ ก็คือการดูว่าเราจะแก้ปัญหาเดียวกันได้อย่างไรโดยใช้วิธีแก้ปัญหาที่แตกต่างกัน

เราจะสำรวจตัวอย่างโค้ด 3 ตัวอย่างสำหรับตัวตรวจสอบโซลูชัน Scrabble

Scrabble เป็นเกมที่ให้รายชื่อตัวละคร (เช่น ollhe ) ขอให้คุณสร้างคำ (เช่น hello ) โดยใช้อักขระเหล่านี้

นี่คือวิธีแก้ปัญหาแรก :

def scramble(characters, word)
  word.each_char.all? { |c| characters.count(c) >= word.count(c) }
end

คุณรู้หรือไม่ว่าความซับซ้อนของเวลาของรหัสนี้คืออะไร? กุญแจอยู่ใน count วิธีการ ดังนั้นให้แน่ใจว่าคุณเข้าใจว่าวิธีการนั้นทำอะไร

คำตอบคือ:O(n^2) .

และเหตุผลก็คือด้วย each_char เราจะดูอักขระทั้งหมดในสตริง word . เพียงอย่างเดียวก็จะเป็นอัลกอริธึมเวลาเชิงเส้น O(n) แต่ภายในบล็อคเรามี count วิธีการ

วิธีนี้เป็นการวนซ้ำอย่างมีประสิทธิภาพซึ่งจะข้ามอักขระทั้งหมดในสตริงอีกครั้งเพื่อนับ

แนวทางที่สอง

ได้เลย

เราจะทำอย่างไรให้ดีขึ้น? มีวิธีช่วยให้เราวนซ้ำได้ไหม

แทนที่จะนับอักขระที่เราสามารถลบออกได้ ด้วยวิธีนี้เราจะมีอักขระที่ต้องอ่านน้อยลง

นี่คือวิธีที่สอง :

def scramble(characters, word)
  characters = characters.dup
  word.each_char.all? { |c| characters.sub!(c, "") }
end

อันนี้น่าสนใจกว่านิดหน่อย ซึ่งจะเร็วกว่า count . มาก ในกรณีที่ดีที่สุด โดยที่อักขระที่เรากำลังมองหานั้นอยู่ใกล้จุดเริ่มต้นของสตริง

แต่เมื่ออักขระอยู่ท้ายสุด จะพบใน word . ในลำดับที่กลับกัน , ประสิทธิภาพจะเหมือนกับของ count ดังนั้นเราสามารถพูดได้ว่านี่ยังเป็น O(n^2) อัลกอริทึม

เราไม่ต้องกังวลในกรณีที่ไม่มีอักขระใน word ได้เพราะว่า all? เมธอดจะหยุดทันทีที่ sub! ส่งกลับ false . แต่คุณจะรู้สิ่งนี้ก็ต่อเมื่อคุณได้ศึกษา Ruby อย่างละเอียดถี่ถ้วน มากกว่าที่หลักสูตร Ruby ทั่วไปเสนอให้คุณ

มาลองใช้วิธีอื่นกันเถอะ

เกิดอะไรขึ้นถ้าเราลบการวนซ้ำแบบใดแบบหนึ่งในบล็อก เราสามารถทำได้ด้วยแฮช

def scramble(characters, word)
  available = Hash.new(1)

  characters.each_char { |c| available[c] += 1 }
  word.each_char.all? { |c| available[c] -= 1; available[c] > 0 }
end

การอ่านและเขียนค่าแฮชคือ O(1) ซึ่งหมายความว่ามันค่อนข้างเร็ว โดยเฉพาะเมื่อเทียบกับการวนซ้ำ

เรายังมีสองลูป :

อันหนึ่งเพื่อสร้างตารางแฮช และอีกอันหนึ่งสำหรับตรวจสอบ เนื่องจากลูปเหล่านี้ไม่ได้ซ้อนกัน นี่คือ O(n) อัลกอริทึม

หากเราเปรียบเทียบโซลูชัน 3 อย่างนี้ เราจะเห็นว่าการวิเคราะห์ของเราตรงกับผลลัพธ์:

hash    1.216667   0.000000   1.216667 (  1.216007)
sub     6.240000   1.023333   7.263333 (  7.268173)
count 222.866644   0.073333 222.939977 (223.440862)

นี่คือแผนภูมิเส้น:

คู่มือฉบับสมบูรณ์เกี่ยวกับความซับซ้อนของเวลาสำหรับนักพัฒนา Ruby

สังเกตบางสิ่ง :

  1. แกน Y (แนวตั้ง) แสดงถึงจำนวนวินาทีที่ใช้ในการทำงานให้เสร็จ ในขณะที่แกน X (แนวนอน) แสดงถึงขนาดอินพุต
  2. นี่คือแผนภูมิลอการิทึมซึ่งบีบอัดค่าในช่วงที่กำหนด แต่ในความเป็นจริง count เส้นจะชันกว่ามาก
  3. เหตุผลที่ฉันสร้างแผนภูมิลอการิทึมนี้คือเพื่อให้คุณได้ชื่นชมเอฟเฟกต์ที่น่าสนใจ โดยมีขนาดอินพุตที่เล็กมาก count เร็วกว่าจริงๆ!

สรุป

คุณได้เรียนรู้เกี่ยวกับความซับซ้อนของเวลาอัลกอริทึม สัญกรณ์ O ขนาดใหญ่ และการวิเคราะห์ความซับซ้อนของเวลา คุณยังได้เห็นตัวอย่างบางส่วนที่เราเปรียบเทียบวิธีแก้ปัญหาต่างๆ กับปัญหาเดียวกันและวิเคราะห์ประสิทธิภาพด้วย

หวังว่าคุณจะได้เรียนรู้สิ่งใหม่และน่าสนใจ!

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

ขอบคุณสำหรับการอ่าน