[0]zero-index clytie0x/zero-index

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 ใหม่

ij ที่ลูปข้างในวิ่งผ่านหลังลูปข้างในจบ
11 2 3 4 5 (พิมพ์ดาว 5 ดวง)ขึ้นบรรทัดใหม่
21 2 3 4 5ขึ้นบรรทัดใหม่
31 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ช่องว่างดาว
131
223
315
407
in - i2i - 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 ออกได้แค่ชั้นเดียว และอย่าใช้ตัวนับตัวเดียวกันในสองชั้น