On this page
ข้อ 55 · LC162 Find Peak Element (หายอด peak) 🟡
binary search บน array ที่ไม่ได้ sort ดูทิศชันจาก nums[mid] เทียบ nums[mid+1] เดินไปทางที่สูงขึ้นเสมอ
โจทย์ (LC162): peak element (สมาชิกที่เป็นยอด) คือสมาชิกที่มีค่ามากกว่า neighbor (เพื่อนบ้าน) ทั้งสองข้าง กำหนด array จำนวนเต็ม nums แบบ 0-indexed ที่ nums[i] ไม่เท่ากับ nums[i+1] เสมอ ให้หา index ของ peak element ใด ๆ แล้ว return index นั้น โดยถือว่าขอบนอกของ array ทั้งสองฝั่งมีค่าเท่ากับลบอนันต์ (nums[-1] = nums[n] = -∞) ถ้ามีหลาย peak ให้ return index ของตัวไหนก็ได้ ต้องเขียนอัลกอริทึมที่รันด้วยเวลา O(log n)
- Input:
- nums = [1, 2, 3, 1]
- Output:
- 2
- Explanation:
- index 2 (ค่า 3) มากกว่า neighbor ทั้งสองข้าง (2 และ 1) จึงเป็น peak
- Input:
- nums = [1, 2, 1, 3, 5, 6, 4]
- Output:
- 1 หรือ 5
- Explanation:
- index 1 (ค่า 2) เป็น peak เพราะมากกว่า 1 ทั้งสองข้าง และ index 5 (ค่า 6) ก็เป็น peak เช่นกัน เพราะมากกว่า 5 และ 4 — คืน index ไหนก็ถูกต้อง
- 1 <= nums.length <= 1000
- -2^31 <= nums[i] <= 2^31 - 1
- nums[i] != nums[i + 1] สำหรับทุก i (ไม่มีค่าเท่ากันติดกัน)
- ท้าทาย: ต้องทำให้เป็น O(log n)
ต้องทำใน O(log n) แปลว่า iterate ทีละตัวไม่ได้ ต้องใช้ binary search ทั้งที่ array ไม่ได้ sort
แนวทาง — ต้องใช้อะไร & คิดยังไง
โครงสร้างที่ใช้: binary search แบบดูทิศชัน แม้ array ไม่ได้ sort แต่ยังใช้ binary search ได้ compare (เทียบ) nums[mid] กับ nums[mid+1] ถ้ากำลังขาขึ้น (mid น้อยกว่า mid+1) peak อยู่ทางขวาแน่ ๆ ถ้ากำลังขาลง peak อยู่ทางซ้าย (รวม mid เอง)
คิดแบบง่าย/ช้าก่อน: วิธี naive คือ iterate (วน) ทุกตัวหาจุดที่มากกว่า neighbor ทั้งสอง เป็น O(n) แต่โจทย์บังคับ O(log n) เพราะขอบนอกเป็นลบอนันต์ การเดินขึ้นเนินไปเรื่อย ๆ ต้องเจอ peak เสมอ เราจึงตัดครึ่งไปทางที่ชันขึ้นได้
- initialize (ตั้งค่าเริ่มต้น) lo = 0, hi = len(nums) - 1
- ระหว่าง lo < hi: compute mid
- ถ้า nums[mid] < nums[mid+1] กำลังขาขึ้น peak อยู่ทางขวา ตั้ง lo = mid + 1 (mid ไม่ใช่ peak แน่)
- ไม่งั้น (ขาลงหรือเท่า) peak อยู่ทางซ้ายรวม mid เอง ตั้ง hi = mid; จบ loop lo == hi คือตำแหน่ง peak
ใช้ while lo <= hi กับ hi = mid จะวนไม่รู้จบ ต้องใช้ while lo < hi คู่กับ hi = mid เสมอ อีกจุดคือการเข้าถึง nums[mid+1] ปลอดภัยเพราะเมื่อ lo < hi จะมี mid < hi ทำให้ mid+1 ไม่เกินขอบ array
ไล่ทีละสเต็ป
จำลอง nums = [1,2,1,3,5,6,4]:
| lo | hi | mid | nums[mid] vs nums[mid+1] | ทำอะไรต่อ |
|---|---|---|---|---|
| 0 | 6 | 3 | 3 < 5 (ขาขึ้น) | lo = 4 |
| 4 | 6 | 5 | 6 > 4 (ขาลง) | hi = 5 |
| 4 | 5 | 4 | 5 < 6 (ขาขึ้น) | lo = 5 |
| 5 | 5 | - | lo == hi | คืน 5 |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
def find_peak_element(nums):
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < nums[mid + 1]:
# กำลังขาขึ้น ยอดต้องอยู่ทางขวา (mid ไม่ใช่ยอดแน่)
lo = mid + 1
else:
# กำลังขาลงหรือเท่า ยอดอยู่ทางซ้าย รวม mid ด้วย
hi = mid
return lo # lo == hi คือตำแหน่งยอด
print(find_peak_element([1, 2, 3, 1])) # 2
print(find_peak_element([1, 2, 1, 3, 5, 6, 4])) # 52
5หลายคนแปลกใจว่าทำไม binary search ใช้กับ array ที่ไม่ได้ sort ได้ กุญแจอยู่ที่การ compare nums[mid] กับ nums[mid+1] ถ้า nums[mid] น้อยกว่า nums[mid+1] แปลว่าตรงนี้เป็น ทางขึ้น เดินขึ้นไปเรื่อย ๆ ทางขวาต้องเจอ peak สักจุด (อย่างช้าสุดคือปลายขวา เพราะขอบนอกเป็นลบอนันต์) เราจึงตัดครึ่งซ้ายทิ้ง ในทางกลับกันถ้าเป็นทางลง peak อยู่ทางซ้าย (รวม mid เองที่อาจเป็น peak) เราจึงตั้ง hi = mid ไม่ใช่ mid - 1
จุดพลาดที่พบบ่อยคือใช้ while lo <= hi กับ hi = mid ซึ่งจะวนไม่รู้จบ ต้องใช้ while lo < hi คู่กับ hi = mid เสมอ อีกจุดคือการเข้าถึง nums[mid+1] ปลอดภัยเพราะเมื่อ lo < hi จะมี mid < hi ทำให้ mid+1 ไม่เกินขอบ array เมื่อ loop จบ lo กับ hi ชนกันที่ตำแหน่ง peak พอดี
Time O(log n) ตัดครึ่งช่วงทุกก้าวตามที่โจทย์บังคับ · Space O(1) ใช้แค่ pointer (ตัวชี้) lo กับ hi
binary search ใช้ได้แม้ข้อมูลไม่ sort ขอแค่มี direction (ทิศทาง) ที่การันตีว่าคำตอบอยู่ครึ่งไหน (ที่นี่คือความชันขึ้น/ลง) การจับคู่ while lo < hi กับ hi = mid เป็น template มาตรฐานของการหา boundary (จุดพลิก)