หยิบ key ทีละตัวจากซ้ายไปขวา
key = arr[i] — ส่วน [0..i-1] เรียงแล้ว, ส่วน [i..n-1] ยังไม่เรียง — เป้าหมายคือแทรก key ใน [0..i-1] ให้ถูกตำแหน่ง
Basic Sorting
เรียนรู้ Insertion Sort ตั้งแต่แนวคิด วิธีทำงานทีละขั้น พร้อมตัวอย่างโค้ดและแบบฝึกหัด
Insertion Sort คืออัลกอริทึมเรียงข้อมูลที่ทำงานโดยค่อย ๆ สร้างส่วนที่เรียงแล้วจากซ้ายไปขวา — ในแต่ละรอบเราจะหยิบสมาชิกตัวถัดไปเป็น key แล้วแทรกกลับเข้าไปในตำแหน่งที่เหมาะสมภายในส่วนที่เรียงแล้ว
จุดเด่นที่สุดของ Insertion Sort คือทำงานได้ดีมากกับข้อมูลที่เกือบเรียงอยู่แล้ว — ถ้าแต่ละ key อยู่ใกล้ตำแหน่งที่ถูกต้อง จะมีการเลื่อนสมาชิกน้อยมาก ทำให้เวลาเข้าใกล้ O(n) ในสถานการณ์จริงหลายรูปแบบ
ลองนึกภาพว่าคุณถือไพ่ในมือและกำลังเรียงไพ่จากน้อยไปมาก — มือซ้ายถือไพ่ที่เรียงแล้ว มือขวาหยิบไพ่ใบใหม่ทีละใบจากกอง จากนั้นใช้มือซ้ายเลื่อนไพ่ที่มากกว่าออกไปทางขวาเพื่อเปิดช่อง แล้วเสียบไพ่ใบใหม่เข้าไปในตำแหน่งที่ถูกต้อง
Insertion Sort ทำงานเหมือนกันทุกประการ — ก่อนเริ่มรอบที่ i เราถือว่าช่วง [0..i-1] เรียงแล้ว และสมาชิกที่ตำแหน่ง i คือ "ไพ่ใบใหม่" (key) ที่ต้องนำไปแทรก หลังจบรอบ ช่วง [0..i] จะเรียงแล้ว
สาธิตด้วยอาเรย์ [5, 2, 4, 6, 1, 3] เรียงจากน้อยไปมาก — 3 ขั้นตอนต่อรอบ: (1) หยิบ key (2) เลื่อนค่าที่มากกว่าไปขวา (3) แทรก key กลับ
หยิบ key (สมาชิกตำแหน่งปัจจุบัน) ออกมา — เตรียมแทรกในส่วนที่เรียงแล้ว
เลื่อนสมาชิกที่มากกว่า key ไปทางขวาทีละช่องจนเจอตำแหน่งที่เหมาะสม
แทรก key กลับในตำแหน่งที่เหมาะสม — ส่วนที่เรียงแล้วขยายขึ้น 1 ตำแหน่ง
ภาพรวม: การเปลี่ยนแปลงของอาเรย์ทีละรอบจนเรียงสมบูรณ์
โครงความคิด — หยิบ key, เลื่อนค่าที่มากกว่า, แทรกกลับ — 3 จังหวะต่อ 1 รอบ
ฟังก์ชัน insertionSort(values):
arr = คัดลอก values
ทำซ้ำ i จาก 1 ถึง arr.length - 1:
key = arr[i]
j = i - 1
ขณะที่ j >= 0 และ arr[j] > key:
arr[j + 1] = arr[j] // เลื่อนค่าที่มากกว่า key ไปทางขวา 1 ช่อง
j = j - 1 // เดินย้อนซ้ายต่อไป
arr[j + 1] = key // แทรก key กลับในตำแหน่งที่ถูกต้อง
คืนค่า arrหยิบ key ทีละตัว, เลื่อนสมาชิกที่มากกว่า, แล้วแทรกกลับในตำแหน่งที่เหมาะสม
function insertionSort(values) {
const arr = [...values];
for (let i = 1; i < arr.length; i += 1) {
const key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j -= 1;
}
arr[j + 1] = key;
}
return arr;
}
console.log(insertionSort([5, 2, 4, 6, 1, 3]));
// [1, 2, 3, 4, 5, 6]อธิบายทีละส่วน: • `const arr = [...values]` — สร้างสำเนาอาเรย์ • `for (let i = 1; i < arr.length; i += 1)` — เริ่มที่ i=1 เพราะตำแหน่ง 0 ถือว่าเรียงแล้วตั้งแต่ต้น • `const key = arr[i]` — หยิบสมาชิกตำแหน่ง i มาเป็น key (ค่าใช้จับคู่กับไพ่ใบใหม่) • `while (j >= 0 && arr[j] > key)` — เดินย้อนซ้ายตราบใดที่ยังมีสมาชิกที่มากกว่า key • `arr[j + 1] = arr[j]` — เลื่อนสมาชิกที่มากกว่าไปทางขวา 1 ช่อง (สร้างช่องว่าง) • `arr[j + 1] = key` — หลัง loop while จบ j+1 คือตำแหน่งที่ key ควรอยู่ — แทรกกลับ
ได้เวลาลงมือเขียน Insertion Sort — เริ่มจากเขียนฟังก์ชันแทรก key 1 ตัวใน 1 รอบก่อน แล้วค่อยประกอบเป็น Insertion Sort ที่สมบูรณ์
Space Complexity = O(1) — Insertion Sort เป็น in-place algorithm ใช้ตัวแปร key และ index j เท่านั้น **ทำไม Insertion Sort ถึงทำงานดีกับข้อมูลที่เกือบเรียง?** เมื่อข้อมูลเกือบเรียง key แต่ละตัวจะอยู่ใกล้ตำแหน่งที่ถูกต้อง — while loop เลื่อนสมาชิกน้อยมากต่อรอบ ส่งผลให้เวลารวมเข้าใกล้ O(n) ในหลายสถานการณ์จริง ทำให้ Insertion Sort มักถูกเลือกใช้ในระบบที่รับข้อมูลเข้ามาทีละตัวและต้องการรักษาสถานะเรียงไว้ตลอดเวลา
| n (ขนาดข้อมูล) | Best Case (≈ n) | Average Case (≈ n²/4) | Worst Case (≈ n²/2) |
|---|---|---|---|
| 10 | ~10 | ~25 | ~45 |
| 100 | ~100 | ~2,500 | ~4,950 |
| 1,000 | ~1,000 | ~250K | ~500K |
| 10,000 | ~10,000 | ~25M | ~50M |
| ข้อดี | ข้อเสีย | กรณีที่เหมาะสม |
|---|---|---|
| ทำงานดีเยี่ยมกับข้อมูลเกือบเรียง — Best Case O(n) | Average/Worst Case เป็น O(n²) — ช้ากับข้อมูลขนาดใหญ่ที่สุ่มหรือกลับด้าน | ระบบรับข้อมูล stream — มีข้อมูลใหม่เข้ามาทีละตัวและต้องเรียงทันที |
| เป็น stable sort — ค่าเท่ากันรักษาลำดับเดิมไว้ได้ | เมื่อต้องเลื่อนสมาชิกจำนวนมาก จะมี cost การขยับข้อมูลสูง | ข้อมูลเกือบเรียง — เช่น log เรียงตามเวลาแต่มีบางรายการผิดตำแหน่ง |
| แนวคิดตรงไปตรงมา — เข้าใจง่ายด้วยภาพจำการเรียงไพ่ | ประสิทธิภาพโดยรวมแพ้อัลกอริทึม O(n log n) ในงานข้อมูลใหญ่ | ข้อมูลขนาดเล็ก (n < 100) — โค้ดสั้น เร็วพอ ไม่ต้องพึ่ง recursion |
| Online algorithm — แทรกข้อมูลใหม่ในโครงสร้างที่เรียงแล้วได้ทันที | การเข้าถึงข้อมูลย้อนหลังใน array ต้องใช้ index — ไม่เหมาะกับ linked list โดยตรง | ใช้เป็นส่วนประกอบในอัลกอริทึมที่ซับซ้อนกว่า — เช่น Timsort (ใช้ใน Python, Java) |
ทั้งสามอัลกอริทึมเป็น O(n²) ใน Worst Case แต่มีจุดเด่น-จุดด้อยแตกต่างกัน — ตารางนี้ช่วยให้คุณเลือกใช้ได้ถูกต้องตามสถานการณ์
| หัวข้อ | Bubble Sort | Selection Sort | Insertion Sort |
|---|---|---|---|
| แนวคิดหลัก | เปรียบเทียบ-สลับ สมาชิกติดกัน | ค้นหา minimum แล้วสลับทีเดียว | หยิบ key → เลื่อนสมาชิก → แทรก |
| Best Time | O(n) — optimized | O(n²) — ต้องหา min ทุกรอบ | O(n) — ข้อมูลเรียงอยู่แล้ว |
| Average Time | O(n²) | O(n²) | O(n²) |
| Worst Time | O(n²) | O(n²) | O(n²) |
| จำนวนการสลับ/เลื่อน | มาก (สลับบ่อย) | น้อยมาก (≤ n-1) | ปานกลาง (เลื่อนทีละช่อง) |
| Stable | ใช่ | ไม่ (รูปแบบพื้นฐาน) | ใช่ |
| Early exit ได้ | ใช่ (swapped flag) | ไม่ | ใช่ (while loop สั้นเมื่อเกือบเรียง) |
| เหมาะกับ | เริ่มต้นเรียน + ข้อมูลเล็กมาก | สลับน้อย + ต้นทุนสลับสูง | ข้อมูลเกือบเรียง + ข้อมูล stream |
เลือกใช้เมื่อไหร่ดี?
• ข้อมูลเกือบเรียงหรือรับทีละตัว → Insertion Sort • ต้นทุนการสลับสูง (เช่น สลับ record ใหญ่) → Selection Sort • เรียนพื้นฐานหรือต้องการโค้ดง่ายที่สุด → Bubble Sort • ข้อมูลใหญ่? → ใช้ Merge Sort หรือ Quick Sort (O(n log n)) แทน
หัวใจสำคัญของ Insertion Sort ที่ต้องจำให้ขึ้นใจ
key = arr[i] — ส่วน [0..i-1] เรียงแล้ว, ส่วน [i..n-1] ยังไม่เรียง — เป้าหมายคือแทรก key ใน [0..i-1] ให้ถูกตำแหน่ง
while (j >= 0 && arr[j] > key) { arr[j+1] = arr[j]; j--; } — เลื่อนไปขวาทีละ 1 ช่องจนเจอตำแหน่งที่เหมาะสมสำหรับ key
หลัง while จบ j+1 คือตำแหน่งที่ key ควรอยู่ — `arr[j+1] = key` — เสร็จ 1 รอบ
Best Case (ข้อมูลเรียงแล้ว) = while loop ไม่ทำงานเลย → O(n) Worst Case (เรียงกลับด้าน) = key ทุกตัวต้องเลื่อนไปตำแหน่ง 0 → O(n²)
เพราะใช้เงื่อนไข `arr[j] > key` (ไม่ใช่ >=) — ค่าเท่ากันจะไม่ถูกสลับ ลำดับเดิมจึงคงอยู่
Timsort ใช้ Insertion Sort สำหรับข้อมูลชุดเล็ก (run) และ Merge Sort สำหรับรวมชุดใหญ่ — เป็นข้อพิสูจน์ว่า Insertion Sort มีประโยชน์จริงในทางปฏิบัติ
ทดสอบความเข้าใจก่อนไปต่อ
คิดถึงการทำงานของ while loop เมื่อข้อมูลเรียงอยู่แล้ว