Notes & software courses · Free to learn
Aph's Blog
On this page

ข้อ 55 · LC162 Find Peak Element (หายอด peak) 🟡

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

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)

Example 1
Input:
nums = [1, 2, 3, 1]
Output:
2
Explanation:
index 2 (ค่า 3) มากกว่า neighbor ทั้งสองข้าง (2 และ 1) จึงเป็น peak
Example 2
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 ไหนก็ถูกต้อง
Constraints (ข้อจำกัด)
  • 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 เสมอ เราจึงตัดครึ่งไปทางที่ชันขึ้นได้

  1. initialize (ตั้งค่าเริ่มต้น) lo = 0, hi = len(nums) - 1
  2. ระหว่าง lo < hi: compute mid
  3. ถ้า nums[mid] < nums[mid+1] กำลังขาขึ้น peak อยู่ทางขวา ตั้ง lo = mid + 1 (mid ไม่ใช่ peak แน่)
  4. ไม่งั้น (ขาลงหรือเท่า) 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]:

lohimidnums[mid] vs nums[mid+1]ทำอะไรต่อ
0633 < 5 (ขาขึ้น)lo = 4
4656 > 4 (ขาลง)hi = 5
4545 < 6 (ขาขึ้น)lo = 5
55-lo == hiคืน 5
▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
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]))  # 5
Output
2
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

💡 สรุป pattern

binary search ใช้ได้แม้ข้อมูลไม่ sort ขอแค่มี direction (ทิศทาง) ที่การันตีว่าคำตอบอยู่ครึ่งไหน (ที่นี่คือความชันขึ้น/ลง) การจับคู่ while lo < hi กับ hi = mid เป็น template มาตรฐานของการหา boundary (จุดพลิก)