[0]zero-index clytie0x/zero-index

11.2 อาเรย์ของ struct

struct ตัวเดียวเก็บนักเรียนได้หนึ่งคน แต่โจทย์ให้นักเรียนมาเป็นร้อยเป็นพันคน เหมือนตอนที่ตัวแปร int ตัวเดียวไม่พอ แล้วเราใช้อาเรย์ในบทที่ 5 struct ก็ทำเป็นอาเรย์ได้เหมือนกัน หน้านี้อ่านข้อมูลนักเรียน N คน แล้วทำงานที่เจอในโจทย์บ่อยที่สุด คือหาคนที่ดีที่สุด เลือกคนที่ผ่านเกณฑ์ ค้นหาด้วยชื่อ และย้ายข้อมูลทั้งคน

ประกาศและอ่านข้อมูล

Student students[100]; คืออาเรย์ 100 ช่อง แต่ละช่องเป็น Student หนึ่งก้อน students[2] คือนักเรียนคนที่สาม และ students[2].score คือคะแนนของคนนั้น อ่านจากซ้ายไปขวา คือ "เลือกช่องที่ 2 ก่อน แล้วค่อยหยิบ score ออกจากช่องนั้น"

students[0] "Mali" 87 students[1] "Krit" 92 students[2] "Ploy" 78 students[2].score

หาคนคะแนนสูงสุด

นักเรียน N คน แต่ละคนมีชื่อกับคะแนน ให้พิมพ์ชื่อและคะแนนของคนที่ได้คะแนนสูงสุด ถ้ามีหลายคนเท่ากัน ให้ตอบคนที่มาก่อน วิธีเหมือนหาตำแหน่งของค่ามากสุดในบทที่ 5 ทุกอย่าง จำ index ของคนที่ดีที่สุดไว้ ต่างกันแค่ตอนเปรียบเทียบต้องบอกว่าเทียบสมาชิกตัวไหน

#include <stdio.h>

typedef struct {
    char name[21];
    int score;
} Student;

Student students[100]; // (1)!

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%s %d", students[i].name, &students[i].score); // (2)!
    }
    int best = 0;
    for (int i = 1; i < n; i++) {
        if (students[i].score > students[best].score) { // (3)!
            best = i;
        }
    }
    printf("top = %s %d\n", students[best].name, students[best].score);
    return 0;
}
  1. ประกาศนอก main เหมือนอาเรย์ใหญ่ในบทที่ 5 หนึ่งช่องใช้หน่วยความจำเท่ากับ struct หนึ่งก้อน อาเรย์ของ struct จึงใหญ่เร็วกว่าอาเรย์ของ int มาก ขนาดจริงของหนึ่งก้อนดูได้ในหน้า 11.3
  2. ชื่อเป็นอาเรย์ของ char อยู่แล้ว ไม่มี & ส่วนคะแนนเป็น int ต้องมี & เหมือนเดิม
  3. ใช้ > คนที่คะแนนเท่ากับคนที่ดีที่สุดตอนนี้จะไม่ได้แทนที่ จึงได้คนที่มาก่อน ถ้าโจทย์ให้ตอบคนหลังสุด เปลี่ยนเป็น >=
5
Mali 87
Krit 92
Ploy 78
Nat 92
Beam 65
top = Krit 92

Krit กับ Nat ได้ 92 เท่ากัน โปรแกรมตอบ Krit เพราะมาก่อน ข้อดีของการเก็บเป็น struct เห็นชัดตรงบรรทัดพิมพ์ ได้ index ตัวเดียว ก็หยิบได้ทั้งชื่อทั้งคะแนน ไม่มีโอกาสหยิบชื่อของคนหนึ่งคู่กับคะแนนของอีกคน

จำทั้งก้อนแทนการจำ index

ในหน้า 11.1 เห็นแล้วว่า struct คัดลอกด้วย = ได้ทั้งก้อน จึงจำ "นักเรียนที่ดีที่สุด" ไว้ในตัวแปร Student ตัวเดียวได้เลย และถ้างานมีแค่นี้ ไม่ต้องใช้อาเรย์ด้วยซ้ำ อ่านทีละคนแล้วเทียบทันที

#include <stdio.h>

typedef struct {
    char name[21];
    int score;
} Student;

int main()
{
    int n;
    scanf("%d", &n);
    Student best;
    scanf("%s %d", best.name, &best.score);
    for (int i = 1; i < n; i++) {
        Student s;
        scanf("%s %d", s.name, &s.score);
        if (s.score > best.score) {
            best = s;
        }
    }
    printf("top = %s %d\n", best.name, best.score);
    return 0;
}
5
Mali 87
Krit 92
Ploy 78
Nat 92
Beam 65
top = Krit 92

คนแรกอ่านเข้า best ตรงๆ เพราะตอนเริ่มยังไม่มีใครให้เทียบ แล้วคนที่เหลือค่อยมาท้าชิง บรรทัด best = s; คัดลอกทั้งชื่อและคะแนนในคำสั่งเดียว ถ้าเก็บแยกเป็น char กับ int ต้องใช้ strcpy สำหรับชื่อ ดูในส่วนลองทำให้พังว่าลืมแล้วเป็นอย่างไร

งานที่ต้องย้อนกลับไปดูข้อมูลซ้ำ

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

#include <stdio.h>

typedef struct {
    char name[21];
    int score;
} Student;

Student students[100];

int main()
{
    int n;
    scanf("%d", &n);
    int total = 0;
    for (int i = 0; i < n; i++) {
        scanf("%s %d", students[i].name, &students[i].score);
        total += students[i].score;
    }
    for (int i = 0; i < n; i++) {
        if (students[i].score * n > total) { // (1)!
            printf("%s\n", students[i].name);
        }
    }
    return 0;
}
  1. อยากรู้ว่า score มากกว่า total / n ไหม คูณ n ทั้งสองข้างได้ score × n มากกว่า total ซึ่งเป็นจำนวนเต็มล้วน ไม่ต้องใช้ double และไม่มีปัญหาทศนิยมคลาดเคลื่อน ใช้ได้เพราะ n เป็นบวก ถ้าคูณด้วยค่าติดลบ เครื่องหมายมากกว่าจะกลับด้าน
5
Mali 87
Krit 92
Ploy 78
Nat 92
Beam 65
Mali
Krit
Nat

ผลรวมคือ 414 ค่าเฉลี่ยคือ 82.8 คนที่ได้มากกว่าคือ Mali Krit และ Nat ลองเช็กด้วยวิธีคูณ Mali ได้ 87 × 5 = 435 มากกว่า 414 จึงผ่าน Ploy ได้ 78 × 5 = 390 ไม่ผ่าน

อีกงานที่เจอบ่อยคือถามข้อมูลด้วยชื่อหรือรหัส เช่น "Ploy ได้กี่คะแนน" วิธีตรงไปตรงมาคือไล่ดูทีละช่องจนเจอชื่อที่ตรง เทียบสตริงด้วย strcmp จากบทที่ 6 ซึ่งคืนค่า 0 เมื่อสองข้อความเหมือนกัน

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[21];
    int score;
} Student;

Student students[100];

int main()
{
    int n, q;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%s %d", students[i].name, &students[i].score);
    }
    scanf("%d", &q);
    for (int k = 0; k < q; k++) {
        char wanted[21];
        scanf("%s", wanted);
        int found = -1;
        for (int i = 0; i < n; i++) {
            if (strcmp(students[i].name, wanted) == 0) {
                found = i;
                break;
            }
        }
        if (found == -1) {
            printf("%s not found\n", wanted);
        } else {
            printf("%s %d\n", students[found].name, students[found].score);
        }
    }
    return 0;
}
5
Mali 87
Krit 92
Ploy 78
Nat 92
Beam 65
3
Ploy
Somchai
Beam
Ploy 78
Somchai not found
Beam 65

ตั้ง found เป็น -1 ไว้ก่อน ซึ่งไม่ใช่ index ที่มีจริง ถ้าไล่จนหมดแล้วยังเป็น -1 แปลว่าไม่เจอ วิธีนี้ถามหนึ่งครั้งต้องไล่ดูได้ถึง N ช่อง ถ้า N กับจำนวนคำถามเป็นหลักร้อยหรือหลักพันยังเร็วพอ ถ้าเป็นแสนทั้งคู่จะช้าเกินไป วิธีค้นหาที่เร็วกว่าจะได้เรียนในบทที่ 19 และบทที่ 20

สลับข้อมูลทั้งคนในสามบรรทัด

ถ้าใช้อาเรย์ขนาน การสลับนักเรียนสองคนต้องสลับชื่อด้วย strcpy สามครั้ง และสลับคะแนนอีกสามบรรทัด ถ้ามีห้องหรืออายุก็ต้องเพิ่มอีก แต่ struct สลับแบบเดียวกับสลับ int สองตัว เพราะ = คัดลอกทั้งก้อน

ตัวอย่างนี้ย้ายคนที่คะแนนสูงสุดขึ้นมาอยู่หัวแถว แล้วพิมพ์ทั้งแถว

#include <stdio.h>

typedef struct {
    char name[21];
    int score;
    int room;
} Student;

Student students[100];

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%s %d %d", students[i].name, &students[i].score, &students[i].room);
    }
    int best = 0;
    for (int i = 1; i < n; i++) {
        if (students[i].score > students[best].score) {
            best = i;
        }
    }
    Student temp = students[0];
    students[0] = students[best];
    students[best] = temp;
    for (int i = 0; i < n; i++) {
        printf("%s %d %d\n", students[i].name, students[i].score, students[i].room);
    }
    return 0;
}
4
Mali 87 3
Krit 92 1
Ploy 78 3
Beam 65 2
Krit 92 1
Mali 87 3
Ploy 78 3
Beam 65 2

Krit กับ Mali สลับที่กันทั้งชื่อ คะแนน และห้อง คนอื่นอยู่ที่เดิม ถ้าทำแบบนี้ซ้ำกับแถวที่เหลือ คือหาคนที่ดีที่สุดในช่องที่ 1 ถึง N - 1 แล้วสลับมาไว้ช่องที่ 1 ไปเรื่อยๆ ก็จะได้ทั้งแถวเรียงจากคะแนนมากไปน้อย วิธีนี้คือการเรียงข้อมูลแบบหนึ่ง ซึ่งจะเรียนเต็มๆ ในบทที่ 13

ลองทำให้พัง

แบบที่ 1 วาง [i] ผิดที่

ในโปรแกรมหาคนคะแนนสูงสุด เขียนบรรทัดอ่านข้อมูลเป็น

scanf("%s %d", students.name[i], &students.score[i]);
top.c:15:32: error: '(Student *)&students' is a pointer; did you mean to use '->'?
   15 |         scanf("%s %d", students.name[i], &students.score[i]);
      |                                ^
      |                                ->

students เป็นอาเรย์ ไม่ใช่ struct จึงไม่มีสมาชิกชื่อ name ต้องเลือกช่องก่อนแล้วค่อยหยิบสมาชิก คือ students[i].name ระวังว่าคำแนะนำของ compiler ที่ให้ใช้ -> ไม่ใช่วิธีแก้ที่ถูก ในกรณีนี้ compiler มองชื่ออาเรย์เป็นที่อยู่ของช่องแรก (เรื่องเดียวกับในบทที่ 10) เลยเดาว่าเราอยากใช้ pointer ถ้าทำตาม จะได้ error ใหม่ว่า subscripted value is neither array nor pointer nor vector แทน คำแนะนำของ compiler ช่วยได้บ่อย แต่ต้องคิดก่อนทำตามเสมอ -> คืออะไรจะได้เรียนในหน้า 11.4

แบบที่ 2 จำชื่อด้วย =

ถ้าไม่จำทั้งก้อน แต่จำชื่อกับคะแนนไว้คนละตัวแปร แบบนี้

char best_name[21];
int best_score = -1;
for (int i = 0; i < n; i++) {
    if (students[i].score > best_score) {
        best_score = students[i].score;
        best_name = students[i].name;
    }
}
top_name.c:22:23: error: assignment to expression with array type
   22 |             best_name = students[i].name;
      |                       ^

error เดียวกับที่เจอในบทที่ 6 เพราะ best_name กับ students[i].name เป็นอาเรย์ทั้งคู่ แก้ได้สองทาง ใช้ strcpy(best_name, students[i].name); หรือดีกว่านั้นคือเก็บทั้งก้อนใน Student best; แล้วใช้ best = students[i]; แบบที่ทำข้างบน กฎที่ต้องจำคือ อาเรย์ใช้ = ไม่ได้ แต่ struct ที่มีอาเรย์อยู่ข้างในใช้ได้

สรุป

  • Student students[100]; คืออาเรย์ของ struct ประกาศนอก main เหมือนอาเรย์ใหญ่ทั่วไป
  • students[i].score เลือกช่องก่อน แล้วค่อยหยิบสมาชิก
  • หาคนที่ดีที่สุดด้วยการจำ index หรือจำทั้งก้อนด้วย best = s; ก็ได้ เครื่องหมาย > กับ >= ตัดสินว่าได้คนแรกหรือคนหลัง
  • งานที่ต้องย้อนกลับไปดูข้อมูลหลังอ่านครบ เช่นเทียบกับค่าเฉลี่ย ต้องเก็บไว้ในอาเรย์
  • ค้นหาด้วยชื่อใช้ strcmp ไล่ทีละช่อง
  • สลับสอง struct ด้วยตัวแปรพักสามบรรทัด ได้ครบทุกสมาชิก