13.5 counting sort และการค้นหา
หน้านี้มีสองเรื่อง เรื่องแรกคือวิธีเรียงที่เร็วกว่า qsort อีก แต่ใช้ได้เฉพาะเมื่อค่าอยู่ในช่วงแคบ เช่น คะแนน 0 ถึง 100 ซึ่งน้องเกือบเขียนเป็นแล้วตั้งแต่หน้า 5.3 เรื่องที่สองคือการค้นหา ทั้งค้นหาตัวเลขในอาเรย์ ค้นหาชื่อในอาเรย์ของ struct และค้นหาคำในประโยค ซึ่งเป็นโจทย์ที่เจอทั้งใน TOI Zero และในข้อสอบค่าย
counting sort เรียงด้วยการนับ
ในหน้า 5.3 เรานับว่าคะแนนแต่ละค่ามีกี่คนด้วยอาเรย์นับ แล้ววนตามค่าคะแนนจาก 0 ถึง 100 เพื่อพิมพ์ผล ถ้าตอนพิมพ์ เราพิมพ์คะแนน s ซ้ำ count[s] ครั้ง ก็จะได้คะแนนทุกคนเรียงจากน้อยไปมาก โดยไม่ได้เทียบค่าสองค่าเลยสักครั้ง วิธีนี้เรียกว่า counting sort
#include <stdio.h>
int count[101];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int score;
scanf("%d", &score);
count[score]++; // (1)!
}
int printed = 0;
for (int value = 0; value <= 100; value++) { // (2)!
for (int c = 0; c < count[value]; c++) {
if (printed > 0) {
printf(" ");
}
printf("%d", value);
printed++;
}
}
printf("\n");
return 0;
}
- รอบแรกอ่านแล้วนับ ไม่ต้องเก็บคะแนนของแต่ละคนไว้เลย
- รอบสองวนตามค่า จากน้อยไปมาก พิมพ์ค่านั้นซ้ำตามจำนวนที่นับได้ ถ้าอยากได้จากมากไปน้อย ให้วน
valueจาก 100 ลงมาถึง 0
10
72 85 64 85 90 72 85 58 72 100
58 64 72 72 72 85 85 85 90 100
งานทั้งหมดคืออ่าน n ตัวหนึ่งรอบ บวกวนค่าอีก 101 รอบ ซึ่งรวมกันแล้วพิมพ์ออกมา n ตัว ไม่มีลูปซ้อนที่โตตาม n2 ลองวัดด้วยโปรแกรมจับเวลาแบบหน้า 13.3 โดยใช้ข้อมูล rand() % 101 หนึ่งล้านตัว (อาเรย์ขยายเป็นล้านช่อง) จับเฉพาะช่วงที่เรียง บนเครื่องเดิม ค่ากลางของ 5 ครั้ง
| วิธี | n | แบบ Code::Blocks | แบบ -O2 |
|---|---|---|---|
| counting sort | 1,000,000 | 0.002 วินาที | 0.001 วินาที |
| qsort | 1,000,000 | 0.038 วินาที | 0.030 วินาที |
เร็วกว่า qsort อีกราว 20 ถึง 30 เท่า แต่ทั้งสองแบบเร็วพอสำหรับโจทย์ 1 วินาทีอยู่แล้ว ข้อดีที่สำคัญกว่าในการแข่งขันคือ counting sort เขียนสั้น และไม่มีฟังก์ชันเปรียบเทียบให้เขียนผิด
counting sort ใช้ได้เมื่อไร
- ค่าเป็นจำนวนเต็มและช่วงแคบ อาเรย์นับต้องมีหนึ่งช่องต่อหนึ่งค่าที่เป็นไปได้ คะแนน 0 ถึง 100 ใช้ 101 ช่อง อายุ 0 ถึง 150 ใช้ 151 ช่อง แต่ถ้าค่าไปถึง 109 ต้องใช้พันล้านช่อง ทั้งประกาศไม่ได้และวนไม่ทัน ให้ใช้
qsort - ค่าติดลบก็ได้ ถ้าเลื่อนก่อน แบบหน้า 5.3 เช่น อุณหภูมิ -50 ถึง 50 องศา ใช้ช่อง
t + 50ตอนพิมพ์ค่อยลบ 50 กลับ - เรียงแค่ตัวเลข ถ้าต้องเรียงข้อมูลที่มีอย่างอื่นติดมาด้วย เช่น ชื่อกับคะแนน counting sort แบบนี้ทำไม่ได้ เพราะเหลือแค่จำนวน ไม่รู้ว่าใครได้คะแนนนั้น ใช้
qsortกับอาเรย์ของ struct แทน
#include <stdio.h>
int count[101];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int t;
scanf("%d", &t);
count[t + 50]++;
}
int printed = 0;
for (int index = 0; index <= 100; index++) {
for (int c = 0; c < count[index]; c++) {
if (printed > 0) {
printf(" ");
}
printf("%d", index - 50);
printed++;
}
}
printf("\n");
return 0;
}
7
12 -5 30 -50 0 -5 50
-50 -5 -5 0 12 30 50
ค้นหาแบบไล่
ลานจอดรถจดเลขบัตรจอดของรถทุกคันไว้ตามลำดับที่เข้ามา เจ้าของรถยื่นบัตรมา อยากรู้ว่ารถคันนั้นอยู่ในรายการไหม วิธีที่ตรงที่สุดคือไล่ดูทีละช่องตั้งแต่ช่องแรก เจอเมื่อไรก็ตอบเลย ไล่จนสุดแล้วไม่เจอก็คือไม่มี วิธีนี้เรียกว่า ค้นหาแบบไล่ (linear search) ใช้ได้กับข้อมูลที่ไม่ได้เรียง
#include <stdio.h>
int ticket[1000];
int linear_search(int a[], int n, int target)
{
for (int i = 0; i < n; i++) {
if (a[i] == target) {
return i; // (1)!
}
}
return -1; // (2)!
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &ticket[i]);
}
int target;
while (scanf("%d", &target) == 1) {
int index = linear_search(ticket, n, target);
if (index == -1) {
printf("%d not found\n", target);
} else {
printf("%d found at index %d\n", target, index);
}
}
return 0;
}
- เจอแล้ว
returnออกจากฟังก์ชันทันที ช่องที่เหลือไม่ต้องดู จึงได้ตำแหน่งแรกที่เจอเสมอ - มาถึงบรรทัดนี้ได้แปลว่าไล่ครบทุกช่องแล้วไม่เจอ คืน -1 เพราะไม่มีช่องไหนที่ index ติดลบ คนเรียกจึงแยกออกว่าไม่เจอ
6
1024 5531 870 4410 870 9902
870
9902
1234
870 found at index 2
9902 found at index 5
1234 not found
870 มีสองช่อง คือ index 2 กับ 4 ฟังก์ชันตอบ 2 เพราะเจอก่อน ถ้าโจทย์ถามอย่างอื่น ก็แค่เปลี่ยนสิ่งที่ทำตอนเจอ
#include <stdio.h>
int ticket[1000];
int main()
{
int n, target;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &ticket[i]);
}
scanf("%d", &target);
int count = 0;
int last = -1;
printf("indexes:");
for (int i = 0; i < n; i++) {
if (ticket[i] == target) {
printf(" %d", i);
count++;
last = i;
}
}
printf("\n");
printf("count = %d, last = %d\n", count, last);
return 0;
}
6
1024 5531 870 4410 870 9902
870
indexes: 2 4
count = 2, last = 4
หาทุกตำแหน่ง นับจำนวน หรือหาตำแหน่งสุดท้าย ต้องไล่จนครบทุกช่อง ห้ามหยุดที่ตัวแรก ส่วนตำแหน่งสุดท้ายจะไล่จากท้ายแถวมาแล้วหยุดที่ตัวแรกที่เจอก็ได้
ค้นหาชื่อในอาเรย์ของ struct
ค้นหาด้วยข้อความก็ไล่แบบเดียวกัน แค่เทียบด้วย strcmp แทน == ตัวอย่างนี้คือเมนูของโรงอาหาร ค้นหาราคาจากชื่อเมนู
#include <stdio.h>
#include <string.h>
struct dish {
char name[21];
int price;
};
struct dish menu[100];
int find_dish(struct dish list[], int n, const char *name)
{
for (int i = 0; i < n; i++) {
if (strcmp(list[i].name, name) == 0) {
return i;
}
}
return -1;
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s %d", menu[i].name, &menu[i].price);
}
char order[21];
while (scanf("%s", order) == 1) {
int index = find_dish(menu, n, order);
if (index == -1) {
printf("%s: no such dish\n", order);
} else {
printf("%s: %d baht\n", order, menu[index].price);
}
}
return 0;
}
4
padthai 50
somtam 40
khaomankai 45
kaprao 50
somtam
pizza
kaprao
somtam: 40 baht
pizza: no such dish
kaprao: 50 baht
ค้นหาแบบไล่ใช้เวลาเท่าไร
หนึ่งคำถาม ไล่ไม่เกิน n ช่อง ถ้า n เป็นแสนก็ยังเร็วมาก ปัญหาอยู่ที่โจทย์ที่มีหลายคำถาม เช่น รายการ n = 100,000 ตัว และคำถาม Q = 100,000 ข้อ กรณีที่แย่ที่สุดคือทุกคำถามไม่เจอ ต้องไล่ครบทุกครั้ง รวม n × Q = 1010 ครั้ง ตามหลักในหน้า 4.5 ไม่มีทางทัน 1 วินาที ทางออกมีสองแบบ
- ถ้าค่าเป็นจำนวนเต็มช่วงแคบ ใช้อาเรย์
seenหรืออาเรย์นับแบบหน้า 5.3 ตอบแต่ละคำถามได้ทันทีโดยไม่ต้องไล่ - ถ้าค่าใหญ่ เรียงข้อมูลด้วย
qsortก่อนครั้งเดียว แล้วใช้ binary search ที่เขียนแบบ recursion ไปแล้วในหน้า 9.4 ซึ่งตัดครึ่งไปทีละรอบ ข้อมูลแสนตัวใช้ไม่เกิน 17 รอบต่อคำถาม บทที่ 19 จะทำครบทุกกรณี เช่น หาตัวแรกที่มากกว่าหรือเท่ากับค่าที่ถาม นี่คืออีกเหตุผลหนึ่งที่ต้องเรียง
ค้นหาคำในประโยค
อยากรู้ว่าคำว่า the อยู่ตรงไหนบ้างในประโยค ให้ลองวางคำไว้ใต้ประโยคที่ตำแหน่งเริ่ม i = 0 เทียบทีละตัวอักษร ถ้าตรงครบทุกตัวก็คือเจอ แล้วเลื่อนคำไปทางขวาหนึ่งช่อง ทำซ้ำจนคำเลยท้ายประโยค ประโยคยาว n ตัว คำยาว m ตัว ตำแหน่งเริ่มสุดท้ายที่เป็นไปได้คือ n - m
#include <stdio.h>
#include <string.h>
char text[1005], word[105];
void strip_newline(char s[])
{
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n' || s[i] == '\r') {
s[i] = '\0';
}
}
}
int main()
{
fgets(text, sizeof text, stdin);
fgets(word, sizeof word, stdin);
strip_newline(text); // (1)!
strip_newline(word);
int n = strlen(text);
int m = strlen(word);
int count = 0;
for (int i = 0; i + m <= n; i++) { // (2)!
int j = 0;
while (j < m && text[i + j] == word[j]) { // (3)!
j++;
}
if (j == m) {
printf("found at %d\n", i);
count++;
}
}
printf("count = %d\n", count);
return 0;
}
- ตัด
'\n'กับ'\r'ที่fgetsติดมาด้วยแบบหน้า 6.3 ทั้งประโยคและคำ ลืมตัดของคำแล้วจะไม่เจออะไรเลย ดูในส่วนลองทำให้พัง - เริ่มได้ถึง
i = n - mเขียนเป็นi + m <= nจะเห็นชัดว่าคำทั้งคำยังอยู่ในประโยค ความยาวเก็บไว้ในตัวแปรก่อน ไม่เรียกstrlenในเงื่อนไขลูปตามหน้า 6.4 - เทียบไปเรื่อยๆ จนกว่าจะเจอตัวที่ไม่ตรง ถ้าหลุดลูปตอน
j == mแปลว่าตรงครบทุกตัว
the cat sat on the mat with the other cat
the
found at 0
found at 15
found at 28
found at 33
count = 4
เจอสี่ครั้ง ไม่ใช่สาม ครั้งที่ 4 คือ the ที่อยู่ข้างในคำว่า other โปรแกรมนี้หาข้อความ ไม่ได้หาคำ ถ้าโจทย์ต้องการเฉพาะคำที่อยู่เดี่ยวๆ ให้เช็กเพิ่มว่าตัวก่อนหน้าและตัวถัดไปเป็นช่องว่างหรือเป็นขอบของประโยค
#include <stdio.h>
#include <string.h>
char text[1005], word[105];
void strip_newline(char s[])
{
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n' || s[i] == '\r') {
s[i] = '\0';
}
}
}
int main()
{
fgets(text, sizeof text, stdin);
fgets(word, sizeof word, stdin);
strip_newline(text);
strip_newline(word);
int n = strlen(text);
int m = strlen(word);
int count = 0;
for (int i = 0; i + m <= n; i++) {
int j = 0;
while (j < m && text[i + j] == word[j]) {
j++;
}
int left_ok = (i == 0 || text[i - 1] == ' ');
int right_ok = (i + m == n || text[i + m] == ' ');
if (j == m && left_ok && right_ok) {
printf("found at %d\n", i);
count++;
}
}
printf("count = %d\n", count);
return 0;
}
the cat sat on the mat with the other cat
the
found at 0
found at 15
found at 28
count = 3
เงื่อนไข i == 0 ต้องอยู่หน้า text[i - 1] เพื่อไม่ให้แตะ text[-1] ส่วน text[i + m] เมื่อ i + m == n คือ '\0' ซึ่งอยู่ในอาเรย์จริง แต่เช็กให้ชัดก็อ่านง่ายกว่า
นับซ้อนกันหรือไม่ซ้อน
หา ana ใน banana เจอที่ตำแหน่ง 1 และ 3 สองครั้งนี้ใช้ตัว a ตรงกลางร่วมกัน ถ้าโจทย์บอกว่า "นับโดยไม่ให้ซ้อนกัน" เช่น ตัดคำออกจากแถบกระดาษ ตัดแล้วตัวอักษรนั้นหายไป พอเจอแล้วต้องกระโดดข้ามไปทั้งคำ คือเพิ่ม i อีก m แทน 1
#include <stdio.h>
#include <string.h>
char text[1005], word[105];
int matches_at(int i, int m)
{
for (int j = 0; j < m; j++) {
if (text[i + j] != word[j]) {
return 0;
}
}
return 1;
}
int main()
{
while (scanf("%s %s", text, word) == 2) {
int n = strlen(text);
int m = strlen(word);
int overlapping = 0;
for (int i = 0; i + m <= n; i++) {
if (matches_at(i, m)) {
overlapping++;
}
}
int separate = 0;
int i = 0;
while (i + m <= n) {
if (matches_at(i, m)) {
separate++;
i += m; // (1)!
} else {
i++;
}
}
printf("%s in %s: overlapping %d, separate %d\n", word, text, overlapping, separate);
}
return 0;
}
- เจอแล้วข้ามทั้งคำ ไม่เจอเลื่อนทีละช่อง การนับแบบไม่ซ้อนจึงต้องใช้
whileที่เลือกได้ว่าจะเพิ่มiเท่าไร
banana ana
aaaaa aa
mississippi issi
abcabc abc
ana in banana: overlapping 2, separate 1
aa in aaaaa: overlapping 4, separate 2
issi in mississippi: overlapping 2, separate 1
abc in abcabc: overlapping 2, separate 2
ตัวอย่างนี้อ่านด้วย %s ทีละคู่จนหมดข้อมูล เพราะแต่ละบรรทัดไม่มีช่องว่างในข้อความ อ่านโจทย์ให้ดีว่าต้องการนับแบบไหน สองแบบได้คำตอบต่างกันเฉพาะเมื่อคำซ้อนทับตัวเองได้ อย่าง aa หรือ issi
strstr ใน string.h
string.h มีฟังก์ชันหาข้อความให้ชื่อ strstr(text, word) คืนที่อยู่ของตำแหน่งแรกที่เจอ หรือ NULL ถ้าไม่เจอ เอาที่อยู่ที่ได้ลบกับที่อยู่ของช่องแรก ก็ได้เป็นตำแหน่ง แบบการลบ pointer ในบทที่ 10 ถ้าจะหาต่อ ให้เริ่มหาใหม่จากตำแหน่งถัดไป
#include <stdio.h>
#include <string.h>
char text[1005], word[105];
int main()
{
scanf("%s %s", text, word);
int count = 0;
char *found = strstr(text, word);
while (found != NULL) {
printf("found at %d\n", (int)(found - text));
count++;
found = strstr(found + 1, word);
}
printf("count = %d\n", count);
return 0;
}
banana ana
found at 1
found at 3
count = 2
หาต่อจาก found + 1 จึงนับแบบซ้อนกัน ถ้าต้องการแบบไม่ซ้อนให้หาต่อจาก found + m แต่ให้เขียนเองให้เป็นก่อน เพราะโจทย์ค่ายหลายข้อให้ปรับการเทียบ เช่น ไม่สนตัวพิมพ์ใหญ่เล็ก หรือให้ผิดได้หนึ่งตัว ซึ่ง strstr ทำไม่ได้
การหาแบบนี้เทียบได้ถึงราว n × m ตัวอักษร เช่น ประโยคเป็น a แสนตัว และคำเป็น a 999 ตัวตามด้วย b ทุกตำแหน่งเริ่มต้องเทียบเกือบครบทั้งคำ รวมราว 108 ครั้ง ยังพอไหว แต่ถ้าทั้งคู่ยาวเป็นแสนก็ไม่ทัน วิธีค้นหาข้อความที่เร็วกว่านี้อยู่ในบทที่ 30
ลองทำให้พัง
แบบที่ 1 พิมพ์ไม่เจอไว้ในลูป
ความผิดพลาดที่เจอบ่อยที่สุดของการค้นหาแบบไล่ คือตัดสินว่าไม่เจอทั้งที่ยังดูไม่ครบ
#include <stdio.h>
int ticket[1000];
int main()
{
int n, target;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &ticket[i]);
}
scanf("%d", &target);
for (int i = 0; i < n; i++) {
if (ticket[i] == target) {
printf("found at index %d\n", i);
} else {
printf("not found\n");
}
}
return 0;
}
6
1024 5531 870 4410 870 9902
870
not found
not found
found at index 2
not found
found at index 4
not found
ทุกช่องที่ไม่ใช่ 870 พิมพ์ not found หนึ่งบรรทัด ช่องที่ไม่ตรงหนึ่งช่องบอกได้แค่ว่าช่องนั้นไม่ใช่ จะสรุปว่าไม่เจอได้ก็ต่อเมื่อดูครบทุกช่องแล้ว คือหลังลูปจบ ใช้ฟังก์ชันที่คืน -1 แบบตัวอย่างแรก หรือใช้ตัวแปรนับที่เช็กหลังลูป
แบบที่ 2 ลืมตัด '\n' ของคำ
ในโปรแกรม find_word ลบบรรทัด strip_newline(word); ออก
#include <stdio.h>
#include <string.h>
char text[1005], word[105];
void strip_newline(char s[])
{
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n' || s[i] == '\r') {
s[i] = '\0';
}
}
}
int main()
{
fgets(text, sizeof text, stdin);
fgets(word, sizeof word, stdin);
strip_newline(text);
int n = strlen(text);
int m = strlen(word);
int count = 0;
for (int i = 0; i + m <= n; i++) {
int j = 0;
while (j < m && text[i + j] == word[j]) {
j++;
}
if (j == m) {
printf("found at %d\n", i);
count++;
}
}
printf("count = %d\n", count);
return 0;
}
the cat sat on the mat with the other cat
the
count = 0
ไม่เจอเลยสักครั้ง เพราะ word คือ the ตามด้วย '\n' ยาว 4 ตัว ตำแหน่งที่มี the ตรงสามตัวแรก แต่ตัวที่สี่ในประโยคเป็นช่องว่างหรือตัวอักษร ไม่ใช่ '\n' ไม่มีอะไรเตือน ยังดีที่ตัวอย่างในโจทย์จะผิดให้เห็นทันที ถ้าได้ 0 ทั้งที่เห็นกับตาว่ามี ให้สงสัย '\n' ก่อน
สรุป
- counting sort นับด้วยอาเรย์นับแล้วพิมพ์ตามค่า ไม่ต้องเทียบ เร็วมาก ใช้ได้เมื่อค่าเป็นจำนวนเต็มในช่วงแคบ ค่าติดลบให้เลื่อนก่อน
- ค้นหาแบบไล่ดูทีละช่อง เจอแล้วคืนตำแหน่ง ไม่เจอคืน -1 ใช้กับข้อมูลที่ไม่เรียงได้ สรุปว่าไม่เจอได้หลังลูปจบเท่านั้น
- ค้นหาข้อความในอาเรย์ของ struct ใช้
strcmp(...) == 0 - คำถามเป็นแสนกับข้อมูลเป็นแสน ไล่ไม่ทัน ใช้อาเรย์นับเมื่อค่าเล็ก หรือเรียงแล้วใช้ binary search จากหน้า 9.4 ซึ่งบทที่ 19 จะทำครบทุกกรณี
- หาคำในประโยค ลองทุกตำแหน่งเริ่ม
i + m <= nเทียบทีละตัว ระวังคำที่ซ่อนอยู่ในคำอื่น และอ่านโจทย์ว่านับแบบซ้อนหรือไม่ซ้อน - ข้อความที่อ่านด้วย
fgetsต้องตัด'\n'ทุกครั้ง ทั้งประโยคและคำที่จะหา