ลูปซ้อนใน Python: รูปแบบ ความเร็ว และเมื่อไรควรเลี่ยง
บทเรียนนี้แปลจากต้นฉบับภาษาอังกฤษRead in EnglishLeer en españolЧитать на русскомLer em portuguêsแจ้งข้อผิดพลาดในการแปล
ลองนึกถึงสเปรดชีต ซึ่งมีแถวและคอลัมน์ ถ้าจะอ่านทุกเซลล์ คุณต้องไล่ไปทีละแถว และในแต่ละแถว ก็ไล่ไปทีละคอลัมน์ นั่นคือลูปสองตัว ตัวหนึ่งอยู่ข้างในอีกตัว นี่แหละคือ ลูปซ้อน (nested loop)
ลูปซ้อนพบได้ทุกที่ในการเขียนโปรแกรม เช่น การแสดงรูปแบบเป็นตาราง การค้นหาข้อมูลในตาราง การสร้างการจับคู่ทุกแบบ (combination) และการประมวลผลภาพทีละพิกเซล ลูปซ้อนทำได้หลายอย่าง แต่ก็อาจช้าอย่างคาดไม่ถึงถ้าไม่ระวัง
ในบทเรียนนี้ คุณจะได้เรียนว่าลูปซ้อนทำงานอย่างไร ดูตัวอย่างใช้งานจริงกับกริดและการจับคู่ เข้าใจว่าทำไมลูปซ้อนถึงอาจช้า และรู้ว่าเมื่อไรควรใช้วิธีอื่นแทน
ลูปซ้อนใน Python ทำงานอย่างไร
ลูปซ้อนคือลูปที่อยู่ภายในลูปอีกตัวหนึ่ง ลูปนอก (outer loop) ทำงานก่อน ในแต่ละรอบของลูปนอก ลูปใน (inner loop) จะทำงานจนครบตั้งแต่ต้นจนจบ
(1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3)
ประเด็นสำคัญคือ ถ้าลูปนอกทำงาน n รอบ และลูปในทำงาน m รอบ โค้ดภายในลูปในจะทำงานทั้งหมด n x m ครั้ง สามแถวกับสามคอลัมน์ หมายความว่า print() ในลูปในทำงาน 9 ครั้ง
Total iterations: 15
ทำงานกับข้อมูล 2 มิติ (กริดและตาราง)
การใช้งานลูปซ้อนที่พบบ่อยที่สุดคือการทำงานกับข้อมูลสองมิติ ลิสต์ซ้อนลิสต์ (list of lists) ใน Python ใช้แทนกริด ตาราง หรือเมทริกซ์ ลูปนอกไล่ไปตามแถว และลูปในไล่ไปตามคอลัมน์ในแต่ละแถว
1 2 3 4 5 6 7 8 9
บางครั้งคุณต้องการดัชนีของแถวและคอลัมน์ ไม่ใช่แค่ค่า กรณีนี้ให้ใช้ enumerate()
X found at row 0, col 0 X found at row 0, col 2 X found at row 1, col 1 X found at row 2, col 2
ลองดูตัวอย่างใช้งานจริง: สร้างตารางสูตรคูณ
1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25
สร้างการจับคู่ทุกแบบด้วยลูปซ้อน
ลูปซ้อนเหมาะมากสำหรับสร้างการจับคู่ทุกแบบของสมาชิกจากสองกลุ่ม (หรือมากกว่า) ลูปนอกเลือกจากกลุ่มแรก และลูปในเลือกจากกลุ่มที่สอง
All t-shirt options: red - S red - M red - L blue - S blue - M blue - L
มี 2 สีและ 3 ขนาด จะได้ 6 แบบ ทุกสีจับคู่กับทุกขนาด เรียกว่า ผลคูณคาร์ทีเซียน (Cartesian product) ของลิสต์ทั้งสอง
Pairs that sum to 10: 1 + 9 = 10 3 + 7 = 10
สร้างรูปแบบด้วยลูปซ้อน
การแสดงรูปแบบ (pattern) เป็นแบบฝึกหัดคลาสสิกเพื่อทำความเข้าใจลูปซ้อน ลูปนอกควบคุมแต่ละแถว และลูปในควบคุมจำนวนตัวอักษรในแต่ละแถว
* ** *** **** *****
เคล็ดลับคือ ช่วง (range) ของลูปในขึ้นอยู่กับตัวแปรของลูปนอก แถวที่ 1 ลูปในทำงานครั้งเดียว แถวที่ 2 ทำงานสองครั้ง แถวที่ 5 ทำงานห้าครั้ง จึงเกิดเป็นรูปขั้นบันได
***** **** *** ** *
ประสิทธิภาพ: ทำไมลูปซ้อนถึงอาจช้า
ลูปเดียวที่วนผ่านข้อมูล 1,000 ตัวจะทำงาน 1,000 ครั้ง แต่ลูปซ้อนที่วนผ่านข้อมูล 1,000 ตัวจะทำงาน 1,000 x 1,000 = 1,000,000 ครั้ง นั่นคือหนึ่งล้านครั้ง ถ้าซ้อนเพิ่มเป็นชั้นที่สาม จะได้หนึ่งพันล้านครั้ง ลูปซ้อนทำให้จำนวนรอบคูณกัน และตัวเลขจะโตเร็วมาก
นักวิทยาการคอมพิวเตอร์อธิบายเรื่องนี้ด้วย สัญกรณ์บิ๊กโอ (Big O notation) ลูปเดียวคือ O(n) เวลาที่ใช้เพิ่มขึ้นเป็นเส้นตรงตามขนาดข้อมูล ลูปซ้อนคือ O(n²) เวลาที่ใช้เพิ่มขึ้นตามกำลังสองของขนาดข้อมูล ถ้าข้อมูลเพิ่มเป็นสองเท่า งานจะเพิ่มเป็นสี่เท่า
Single loop: 0.0000 seconds Nested loop: 0.8120 seconds Nested is 8120x slower
เมื่อไรควรเลี่ยงลูปซ้อน
ไม่ใช่ทุกปัญหาที่ดูเหมือนต้องใช้ลูปซ้อนจะต้องใช้จริง บ่อยครั้งมีวิธีที่เร็วกว่าโดยใช้เซต ดิกชันนารี หรือฟังก์ชันในตัว (built-in)
nums = [3, 1, 4, 1, 5, 9]
has_dup = False
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] == nums[j]:
has_dup = True
print(has_dup) # Truenums = [3, 1, 4, 1, 5, 9]
has_dup = len(nums) != len(set(nums))
print(has_dup) # Trueแบบที่ใช้เซตไม่ได้แค่สั้นกว่า แต่เร็วกว่าอย่างมาก สำหรับข้อมูล 10,000 ตัว แบบลูปซ้อนต้องเปรียบเทียบ 50 ล้านครั้ง ส่วนแบบเซตทำงานราว 10,000 ครั้ง เร็วกว่าถึง 5,000 เท่า
students = [
{'name': 'Alice', 'id': 101},
{'name': 'Bob', 'id': 102},
{'name': 'Charlie', 'id': 103}
]
ids_to_find = [103, 101]
for target_id in ids_to_find:
for s in students:
if s['id'] == target_id:
print(s['name'])students = [
{'name': 'Alice', 'id': 101},
{'name': 'Bob', 'id': 102},
{'name': 'Charlie', 'id': 103}
]
ids_to_find = [103, 101]
lookup = {s['id']: s['name'] for s in students}
for target_id in ids_to_find:
print(lookup[target_id])แบบฝึกหัด
แบบฝึกหัดเหล่านี้จะช่วยให้คุณเข้าใจลูปซ้อนได้มั่นคงขึ้น เริ่มจากรูปแบบพื้นฐาน แล้วไปสู่ปัญหาในโลกจริง
แสดงรูปสี่เหลี่ยมจากดาว 3 แถว 5 คอลัมน์ แต่ละแถวมีดาว 5 ดวงโดยไม่มีช่องว่างคั่น
ผลลัพธ์ที่ต้องการ:
*****
*****
*****ทายว่าโค้ดจะแสดงอะไรโดยยังไม่ต้องรัน นับจำนวนบรรทัดทั้งหมดในผลลัพธ์ จากนั้นจึงรันเพื่อตรวจคำตอบ
แสดงสามเหลี่ยมตัวเลขที่แต่ละแถวมีหมายเลขแถวซ้ำกัน สามเหลี่ยมนี้มี 4 แถว
ผลลัพธ์ที่ต้องการ:
1
22
333
4444มีกริดของตัวเลขให้ไว้ ใช้ลูปซ้อนคำนวณและแสดงผลรวมของค่าทั้งหมดในกริด
ผลลัพธ์ที่ต้องการ:
45มีลิสต์ของเสื้อและกางเกงให้ไว้ ให้แสดงชุดแต่งตัวที่เป็นไปได้ทุกแบบ แล้วตามด้วยจำนวนทั้งหมด
ผลลัพธ์ที่ต้องการ:
shirt + jeans
shirt + shorts
sweater + jeans
sweater + shorts
hoodie + jeans
hoodie + shorts
Total outfits: 6โค้ดนี้ควรแสดงกริดพิกัดขนาด 3x3 แต่มีบั๊ก ผลลัพธ์ควรแสดงพิกัดหนึ่งแถวต่อหนึ่งบรรทัด โดย x ไล่จาก 0 ถึง 2 และ y ไล่จาก 0 ถึง 2
ผลลัพธ์ที่ต้องการ:
(0,0) (0,1) (0,2)
(1,0) (1,1) (1,2)
(2,0) (2,1) (2,2)หาบั๊กแล้วแก้ไข
สรุป: ลูปซ้อนใน Python
สรุปสิ่งที่คุณเรียนเกี่ยวกับลูปซ้อน:
| แนวคิด | ใจความสำคัญ |
|---|---|
| ลูปซ้อน | ลูปที่อยู่ภายในลูปอีกตัว |
| จำนวนรอบทั้งหมด | จำนวนรอบลูปนอก x จำนวนรอบลูปใน |
| ข้อมูล 2 มิติ | ลูปนอก = แถว, ลูปใน = คอลัมน์ |
| การจับคู่ทุกแบบ | จับคู่สมาชิกทุกตัวของกลุ่ม A กับสมาชิกทุกตัวของกลุ่ม B |
| ประสิทธิภาพ | O(n²): ข้อมูลเพิ่มสองเท่า งานเพิ่มสี่เท่า |
| ทางเลือกอื่น | เซตและดิกชันนารีมักใช้แทนลูปซ้อนได้ |
หลักง่าย ๆ ที่ควรจำ:
'*' * n) มักใช้แทนลูปในได้เมื่อสร้างรูปแบบเรียนอะไรต่อ
ในบทถัดไป วิธีอ่านข้อความแจ้งข้อผิดพลาดของ Python คุณจะได้เรียนวิธีถอดความหมายของข้อความแจ้งข้อผิดพลาด แก้บั๊กที่พบบ่อยที่สุด และเลิกกลัว traceback การดีบักเป็นทักษะที่ทำให้ทุกอย่างง่ายขึ้น