4.4 ลูปซ้อน
ตัวลูปจะมีคำสั่งอะไรอยู่ข้างในก็ได้ รวมถึงลูปอีกตัว เรียกว่า ลูปซ้อน (nested loop) ใช้กับงานที่มีสองมิติ เช่น แถวกับคอลัมน์ของตาราง ทุกคู่ของนักเรียนในห้อง หรือรูปที่วาดด้วยดาว ในค่าย สอวน. จะได้ฝึกวาดรูปด้วยดาวหลายสิบแบบ เพราะเป็นวิธีที่ดีที่สุดในการฝึกคุมลูปให้แม่น นับผิดไปหนึ่งช่องก็เห็นทันทีในรูป
สี่เหลี่ยมดาว
#include <stdio.h>
int main()
{
int rows, cols;
scanf("%d %d", &rows, &cols);
for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= cols; j++) {
printf("*");
}
printf("\n");
}
return 0;
}
3 5
*****
*****
*****
อ่านจากข้างนอกเข้าข้างใน ลูป i ข้างนอกคุมแถว ทำ 3 รอบ ในแต่ละรอบของ i ลูป j ข้างในทำครบทุกรอบก่อน คือพิมพ์ดาว 5 ดวงติดกัน แล้วค่อยออกมาพิมพ์ \n ขึ้นบรรทัดใหม่ จากนั้น i ถึงจะเพิ่มเป็นรอบถัดไป และ j เริ่มนับ 1 ใหม่
| i | j ที่ลูปข้างในวิ่งผ่าน | หลังลูปข้างในจบ |
|---|---|---|
| 1 | 1 2 3 4 5 (พิมพ์ดาว 5 ดวง) | ขึ้นบรรทัดใหม่ |
| 2 | 1 2 3 4 5 | ขึ้นบรรทัดใหม่ |
| 3 | 1 2 3 4 5 | ขึ้นบรรทัดใหม่ |
คำสั่ง printf("*") จึงทำทั้งหมด 3 × 5 = 15 ครั้ง ลูปซ้อนสองชั้นทำงานเท่ากับจำนวนรอบข้างนอกคูณจำนวนรอบข้างใน ตัวเลขนี้สำคัญมากตอนประมาณเวลาในหน้า 4.5
ตั้งชื่อตัวนับลูปข้างในว่า j เป็นธรรมเนียมเหมือน i ถ้าซ้อนสามชั้นก็ใช้ k ต่อ
ตารางสูตรคูณ
แถวที่ i คอลัมน์ที่ j ใส่ผลคูณ i × j ใช้ %4d จากบทที่ 2 จองที่ตัวเลขละ 4 ช่อง ตัวเลขจะเรียงตรงเป็นคอลัมน์ไม่ว่ามีกี่หลัก
#include <stdio.h>
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%4d", i * j);
}
printf("\n");
}
return 0;
}
6
1 2 3 4 5 6
2 4 6 8 10 12
3 6 9 12 15 18
4 8 12 16 20 24
5 10 15 20 25 30
6 12 18 24 30 36
โครงเหมือนสี่เหลี่ยมดาวทุกอย่าง เปลี่ยนแค่สิ่งที่พิมพ์ในลูปข้างใน จาก * เป็นตัวเลขที่คำนวณจาก i กับ j รูปแบบนี้ใช้กับงานตารางได้ทุกแบบ
สามเหลี่ยม ลูปข้างในขึ้นกับลูปข้างนอก
ในสี่เหลี่ยม ทุกแถวมีดาวเท่ากัน แต่ในสามเหลี่ยม แถวที่ 1 มี 1 ดวง แถวที่ 2 มี 2 ดวง แถวที่ i มี i ดวง ขอบเขตของลูปข้างในจึงเป็น i แทนที่จะเป็นตัวเลขคงที่
#include <stdio.h>
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
printf("*");
}
printf("\n");
}
return 0;
}
4
*
**
***
****
พีระมิด หาสูตรจากตาราง
รูปที่ซับซ้อนขึ้น อย่าเดาสูตร ให้เขียนตารางว่าแต่ละแถวมีอะไรกี่ตัว แล้วหาความสัมพันธ์กับเลขแถว พีระมิดสูง 4 แถวหน้าตาแบบนี้ (ใช้จุดแทนช่องว่างให้มองเห็น)
...*
..***
.*****
*******
| แถว i | ช่องว่าง | ดาว |
|---|---|---|
| 1 | 3 | 1 |
| 2 | 2 | 3 |
| 3 | 1 | 5 |
| 4 | 0 | 7 |
| i | n - i | 2i - 1 |
ช่องว่างลดทีละ 1 จาก n - 1 จึงเป็น n - i ส่วนดาวเพิ่มทีละ 2 เริ่มจาก 1 จึงเป็น 2i - 1 ได้สูตรแล้วแต่ละแถวก็คือลูปข้างในสองตัวต่อกัน ตัวแรกพิมพ์ช่องว่าง ตัวที่สองพิมพ์ดาว
#include <stdio.h>
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n - i; j++) {
printf(" ");
}
for (int j = 1; j <= 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
return 0;
}
4
*
***
*****
*******
ลูปข้างในสองตัวใช้ชื่อ j ซ้ำกันได้ เพราะ j ของลูปแรกหายไปแล้วตอนลูปแรกจบ ตามที่เห็นในหน้า 4.1 สังเกตด้วยว่าเราไม่พิมพ์ช่องว่างหลังดาว เพราะมองไม่เห็นและไม่จำเป็น
ตรวจสูตรด้วยแถวแรกและแถวสุดท้าย
ได้สูตรแล้วแทนค่าแถวแรกกับแถวสุดท้ายเสมอ แถว 1 ได้ช่องว่าง 4 - 1 = 3 ดาว 2 × 1 - 1 = 1 ตรงกับรูป แถว 4 ได้ช่องว่าง 0 ดาว 7 ตรงอีก ถ้าสองแถวนี้ถูก แถวกลางมักถูกด้วย
ทุกคู่ที่เป็นไปได้
ลูปซ้อนไม่ได้มีไว้วาดรูปอย่างเดียว ใช้ไล่ "ทุกคู่" ได้ด้วย เช่น ในห้องมีนักเรียน n คน ทุกคนจับมือกับทุกคนคนละหนึ่งครั้ง มีการจับมือกี่ครั้ง ถ้าให้ลูปข้างในเริ่มที่ i + 1 จะได้แต่ละคู่ครั้งเดียว ไม่นับคนจับมือกับตัวเอง และไม่นับคู่ 2 กับ 1 ซ้ำกับคู่ 1 กับ 2
#include <stdio.h>
int main()
{
int n;
scanf("%d", &n);
int count = 0;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
printf("%d-%d\n", i, j);
count++;
}
}
printf("handshakes = %d\n", count);
return 0;
}
4
1-2
1-3
1-4
2-3
2-4
3-4
handshakes = 6
คนที่ 1 จับมือ 3 คน คนที่ 2 จับมือเพิ่มอีก 2 คน คนที่ 3 อีก 1 คน คนที่ 4 ไม่เหลือใครแล้ว รวม 3 + 2 + 1 = 6 ทั่วไปคือ n(n - 1) / 2 ครั้ง ถ้า n เป็นแสน ลูปข้างในจะทำงานราว 5 × 109 ครั้ง หน้า 4.5 จะวัดให้ดูว่านั่นนานเกินไปแค่ไหน
break ในลูปซ้อน
#include <stdio.h>
int main()
{
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) {
break;
}
printf("i = %d, j = %d\n", i, j);
}
}
return 0;
}
i = 1, j = 1
i = 2, j = 1
i = 3, j = 1
break ออกจากลูปที่อยู่ใกล้มันที่สุดชั้นเดียว คือลูป j ลูป i ยังวนต่อครบ 3 รอบ ถ้าอยากออกจากลูปทั้งสองชั้น ต้องมี flag แบบในหน้า 4.3 แล้วตรวจ flag เพื่อ break ลูปข้างนอกอีกที
ลองทำให้พัง
แบบที่ 1 ใช้ตัวนับตัวเดียวกันทั้งสองชั้น
เขียนสี่เหลี่ยม 3 × 4 โดยประกาศ i ไว้ข้างนอก แล้วเผลอใช้ i ในลูปข้างในด้วย
#include <stdio.h>
int main()
{
int i;
for (i = 1; i <= 3; i++) {
for (i = 1; i <= 4; i++) {
printf("*");
}
printf("\n");
}
return 0;
}
****
ได้แถวเดียว compiler ไม่เตือน ลูปข้างในจบโดยทิ้ง i ไว้ที่ 5 กลับไปที่ลูปข้างนอก i++ ได้ 6 ซึ่งเกิน 3 ลูปข้างนอกเลยจบ ถ้าเปลี่ยนเป็นลูปข้างนอก 5 รอบ ลูปข้างใน 3 รอบ ผลจะร้ายกว่านี้ ลูปข้างในทิ้ง i ไว้ที่ 4 ข้างนอกบวกเป็น 5 ซึ่งยังไม่เกิน 5 ลูปข้างในก็ตั้ง i กลับเป็น 1 อีก วนแบบนี้ไม่จบ พิมพ์ *** ไปเรื่อยๆ ให้ประกาศตัวนับในวงเล็บของ for แต่ละตัว แบบ for (int j = 1; ...) จะพลาดแบบนี้ยากขึ้น
แบบที่ 2 ขึ้นบรรทัดใหม่ผิดชั้น
ในโปรแกรม rectangle ย้าย printf("\n"); เข้าไปอยู่ในลูป j หลัง printf("*"); แล้วป้อน 2 3
#include <stdio.h>
int main()
{
int rows, cols;
scanf("%d %d", &rows, &cols);
for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= cols; j++) {
printf("*");
printf("\n");
}
}
return 0;
}
2 3
*
*
*
*
*
*
ดาว 6 ดวงครบ แต่อยู่คนละบรรทัดหมด เพราะขึ้นบรรทัดใหม่หลังดาวทุกดวง ถามตัวเองทุกครั้งว่าคำสั่งนี้ต้องทำทุกช่อง (อยู่ในลูปข้างใน) หรือทุกแถว (อยู่ในลูปข้างนอก หลังลูปข้างในจบ)
แบบที่ 3 ขอบเขตลูปข้างในผิดตัว
ในโปรแกรม triangle เขียนเงื่อนไขลูปข้างในเป็น j <= n แทน j <= i ป้อน 4 แล้วจะได้สี่เหลี่ยม 4 × 4 แทนสามเหลี่ยม เพราะทุกแถวพิมพ์ n ดวงเท่ากันหมด ถ้ารูปที่ได้ถูกจำนวนแถวแต่รูปร่างผิด ให้ดูขอบเขตของลูปข้างในก่อน
สรุป
- ลูปข้างในวิ่งครบทุกรอบ ต่อหนึ่งรอบของลูปข้างนอก จำนวนครั้งรวมคือผลคูณของจำนวนรอบทั้งสองชั้น
- งานตาราง ลูปข้างนอกคุมแถว ลูปข้างในคุมคอลัมน์
\nอยู่หลังลูปข้างใน - รูปทรงที่แต่ละแถวไม่เท่ากัน ให้ขอบเขตลูปข้างในขึ้นกับ
i - รูปซับซ้อน เขียนตารางแถว ช่องว่าง ดาว หาสูตร แล้วตรวจกับแถวแรกและแถวสุดท้าย
- ไล่ทุกคู่ไม่ซ้ำ ให้ลูปข้างในเริ่มที่
i + 1 breakออกได้แค่ชั้นเดียว และอย่าใช้ตัวนับตัวเดียวกันในสองชั้น