0Pricing
Coding Interview Prep · บทเรียน

ขอบเขตล่างและขอบเขตบน

สร้าง bisect_left และ bisect_right ตั้งแต่ต้น แล้วประยุกต์ใช้เพื่อหาตำแหน่งแรกและตำแหน่งสุดท้ายของค่าเป้าหมาย

ขอบเขตล่างและขอบเขตบน เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

ขอบเขตล่างและขอบเขตบนคืออะไร

ขอบเขตล่างของค่าเป้าหมายในอาร์เรย์เรียงลำดับ คือดัชนีของสมาชิกตัวแรกที่มีค่ามากกว่าหรือเท่ากับเป้าหมาย (มักเรียกว่า bisect_left) ส่วนขอบเขตบนคือดัชนีของสมาชิกตัวแรกที่มีค่ามากกว่าอย่างเคร่งครัดเป้าหมาย (bisect_right) ทั้งสองขอบเขตจะครอบคลุมการปรากฏทุกครั้งของเป้าหมาย และทำให้สามารถสอบถามช่วงข้อมูลได้ในเวลา O(log n)

การดำเนินการทั้งสองอย่างนี้เป็นพื้นฐานของปัญหาในการสัมภาษณ์หลายประเภท เช่น การนับจำนวนครั้งที่ปรากฏ การหาช่วง ตำแหน่งแทรก และอื่น ๆ

arr = [1, 2, 2, 2, 3, 5]
# lower bound of 2 => index 1 (first element >= 2)
# upper bound of 2 => index 4 (first element > 2)
# occurrences of 2 => upper - lower = 4 - 1 = 3
print('lower bound of 2:', 1)
print('upper bound of 2:', 4)
print('count of 2:', 4 - 1)

การนำขอบเขตล่างไปใช้งาน (bisect_left)

bisect_left(arr, x) คืนค่าดัชนีซ้ายสุด i ที่ทำให้ arr[i] >= x หรือคืนค่า len(arr) หากสมาชิกทั้งหมดมีค่าน้อยกว่า การทำงานนี้ใช้ ขอบเขตบนแบบไม่รวมปลาย: hi = len(arr) เงื่อนไขวนซ้ำ lo < hi และปรับค่า hi = mid เมื่อ arr[mid] >= x วิธีนี้ทำให้คำตอบบรรจบเข้าหาตำแหน่งที่ถูกต้องซ้ายสุด

def bisect_left(arr, x):
    lo, hi = 0, len(arr)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if arr[mid] < x:
            lo = mid + 1
        else:
            hi = mid      # arr[mid] >= x, so potential answer
    return lo             # lo == hi == insertion point

arr = [1, 2, 2, 2, 3, 5]
print(bisect_left(arr, 2))   # 1
print(bisect_left(arr, 0))   # 0 (before all)
print(bisect_left(arr, 6))   # 6 (after all)
print(bisect_left(arr, 3))   # 4

การนำขอบเขตบนไปใช้งาน (bisect_right)

bisect_right(arr, x) คืนค่าดัชนีซ้ายสุด i ที่ทำให้ arr[i] > x มีเพียงหนึ่งบรรทัดที่แตกต่างจาก bisect_left: เงื่อนไขเปลี่ยนจาก arr[mid] < x เป็น arr[mid] <= x เมื่อ arr[mid] <= x คำตอบจะอยู่ทางขวาของ mid อย่างเคร่งครัด ดังนั้นจึงกำหนด lo = mid + 1 มิฉะนั้นจะจำกัดช่วงจากด้านขวา

def bisect_right(arr, x):
    lo, hi = 0, len(arr)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if arr[mid] <= x:
            lo = mid + 1  # arr[mid] <= x, so answer is strictly right
        else:
            hi = mid
    return lo

arr = [1, 2, 2, 2, 3, 5]
print(bisect_right(arr, 2))  # 4
print(bisect_right(arr, 0))  # 0
print(bisect_right(arr, 5))  # 6
print(bisect_right(arr, 4))  # 5

นับจำนวนครั้งที่ปรากฏด้วยขอบเขตทั้งสอง

หากต้องการนับจำนวนครั้งที่เป้าหมายปรากฏในอาร์เรย์เรียงลำดับโดยใช้เวลา O(log n) ให้ใช้ขอบเขตทั้งสอง: จำนวน = bisect_right(arr, target) - bisect_left(arr, target) หากจำนวนเป็น 0 แสดงว่าไม่มีเป้าหมายอยู่ วิธีนี้เร็วกว่าไล่ตรวจทีละสมาชิกอย่างมีนัยสำคัญ และเป็นแนวทางมาตรฐานสำหรับการสอบถามความถี่ของข้อมูลที่เรียงลำดับแล้ว

import bisect

def count_occurrences(arr, target):
    left  = bisect.bisect_left(arr, target)
    right = bisect.bisect_right(arr, target)
    return right - left

arr = [1, 2, 2, 2, 3, 3, 5]
print(count_occurrences(arr, 2))  # 3
print(count_occurrences(arr, 3))  # 2
print(count_occurrences(arr, 4))  # 0
print(count_occurrences(arr, 1))  # 1

หาตำแหน่งแรกและตำแหน่งสุดท้ายของเป้าหมาย

LeetCode 34 'หาตำแหน่งแรกและตำแหน่งสุดท้ายของสมาชิกในอาร์เรย์เรียงลำดับ' ขอให้คุณคืนค่า [first_idx, last_idx] ในเวลา O(log n) ตำแหน่งแรกคือ bisect_left(arr, target) — แต่ต้องตรวจสอบว่า arr[result] == target เท่านั้น ตำแหน่งสุดท้ายคือ bisect_right(arr, target) - 1 หากการตรวจสอบอย่างใดอย่างหนึ่งไม่ผ่าน ให้คืนค่า [-1, -1]

import bisect

def search_range(nums, target):
    left = bisect.bisect_left(nums, target)
    if left == len(nums) or nums[left] != target:
        return [-1, -1]
    right = bisect.bisect_right(nums, target) - 1
    return [left, right]

print(search_range([5,7,7,8,8,10], 8))  # [3, 4]
print(search_range([5,7,7,8,8,10], 6))  # [-1, -1]
print(search_range([], 0))              # [-1, -1]

ตำแหน่งแทรก (LeetCode 35)

LeetCode 35 'ค้นหาตำแหน่งแทรก' ถามว่าเป้าหมายควรถูกแทรกที่ใดเพื่อให้อาร์เรย์ยังคงเรียงลำดับอยู่ คำตอบคือ bisect_left(arr, target) พอดี หากมีเป้าหมายอยู่ bisect_left จะคืนค่าดัชนีของเป้าหมาย หากไม่มีเป้าหมายอยู่ bisect_left จะคืนค่าดัชนีที่ควรแทรกเป้าหมาย ไม่จำเป็นต้องจัดการกรณีพิเศษ — ฟังก์ชันเดียวกันรองรับทั้งสองสถานการณ์

import bisect

def searchInsert(nums, target):
    return bisect.bisect_left(nums, target)

print(searchInsert([1,3,5,6], 5))  # 2 (exists at index 2)
print(searchInsert([1,3,5,6], 2))  # 1 (would insert between 1 and 3)
print(searchInsert([1,3,5,6], 7))  # 4 (would append at end)
print(searchInsert([1,3,5,6], 0))  # 0 (would prepend)

ความแตกต่างระหว่าง bisect_left กับ bisect_right

เมื่อไม่มีค่าซ้ำ bisect_left และ bisect_right จะคืนค่าดัชนีเดียวกัน ความแตกต่างจะสำคัญก็ต่อเมื่อเป้าหมายปรากฏหลายครั้ง bisect_left ชี้ไปยังสำเนาแรก ส่วน bisect_right ชี้ไปยังตำแหน่งถัดจากสำเนาสุดท้ายหนึ่งตำแหน่ง ให้เลือกใช้โดยพิจารณาว่าต้องการแทรกก่อนสำเนาเดิม (ซ้าย) หรือหลังสำเนาเดิมทั้งหมด (ขวา)

import bisect

arr = [1, 2, 2, 2, 3]

# Insert a new 2 before all existing 2s
print(bisect.bisect_left(arr, 2))   # 1

# Insert a new 2 after all existing 2s
print(bisect.bisect_right(arr, 2))  # 4

# For a value not in array, both give same insertion point
print(bisect.bisect_left(arr, 2.5))  # 4
print(bisect.bisect_right(arr, 2.5)) # 4

การใช้ขอบเขตกับการสอบถามความถี่ในช่วงของข้อมูลเรียงลำดับ

เมื่อจำเป็นต้องตอบคำถามเกี่ยวกับความถี่ในช่วงจำนวนมากบนอาร์เรย์เรียงลำดับอย่างมีประสิทธิภาพ ให้เตรียมอาร์เรย์เรียงลำดับไว้ครั้งเดียว แล้วใช้การแบ่งช่วงสำหรับแต่ละคำถาม แต่ละคำถามจะตอบได้ว่า 'มีสมาชิกกี่ตัวอยู่ใน [lo, hi]?' ในเวลา O(log n) แทนที่จะเป็น O(n) รูปแบบนี้พบได้ในปัญหาเกี่ยวกับการนับสมาชิกภายในช่วงค่าหลังจากเรียงลำดับแล้ว

import bisect

def count_in_range(arr, lo, hi):
    '''Count elements in arr with lo <= val <= hi. arr must be sorted.'''
    left  = bisect.bisect_left(arr, lo)
    right = bisect.bisect_right(arr, hi)
    return right - left

arr = sorted([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])
print(arr)                          # [1,1,2,3,3,4,5,5,5,6,9]
print(count_in_range(arr, 3, 5))    # 6  (3,3,4,5,5,5)
print(count_in_range(arr, 1, 2))    # 3  (1,1,2)

การทำ binary search ด้วยคีย์แบบกำหนดเอง

บางครั้งคีย์ที่ใช้ค้นหาไม่ใช่ค่าที่จัดเก็บไว้โดยตรง แต่เป็นคุณสมบัติที่คำนวณขึ้นมา โมดูล bisect ของไพธอนไม่รองรับฟังก์ชันคีย์โดยตรง แต่คุณสามารถทำ binary search ด้วยตนเองโดยนำคีย์มาใช้ภายในวงวน รูปแบบนี้พบได้เมื่อค้นหารายการออบเจ็กต์ด้วยแอตทริบิวต์อย่างใดอย่างหนึ่ง

# Binary search on a list of (score, name) tuples by score
def lower_bound_by_score(records, min_score):
    lo, hi = 0, len(records)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if records[mid][0] < min_score:
            lo = mid + 1
        else:
            hi = mid
    return lo

records = [(50, 'Alice'), (72, 'Bob'), (72, 'Carol'), (88, 'Dave'), (95, 'Eve')]
idx = lower_bound_by_score(records, 72)
print(idx)                      # 1 (first record with score >= 72)
print(records[idx:])            # [(72,'Bob'),(72,'Carol'),(88,'Dave'),(95,'Eve')]

ข้อผิดพลาดทั่วไปในการสัมภาษณ์เกี่ยวกับขอบเขต

ข้อผิดพลาดที่พบบ่อยที่สุดคือการลืมตรวจสอบหลังเรียกใช้ bisect_left ฟังก์ชันนี้จะคืนค่าดัชนีแทรกที่ถูกต้องเสมอ แต่ไม่ได้รับประกันว่าสมาชิกที่ดัชนีนั้นจะมีค่าเท่ากับเป้าหมาย ให้ตรวจสอบ arr[result] == target เสมอ ก่อนสรุปว่าพบเป้าหมายแล้ว

ข้อผิดพลาดประการที่สองคือการใช้ bisect_right เมื่อต้องการการปรากฏครั้งแรก — bisect_right จะคืนค่าตำแหน่งถัดจากการปรากฏครั้งสุดท้ายหนึ่งตำแหน่ง ดังนั้นการลบ 1 จะให้ตำแหน่งสุดท้าย ไม่ใช่ตำแหน่งแรก

import bisect

arr = [1, 3, 5, 7]
target = 4

# bisect_left returns 2 (insertion point for 4 between 3 and 5)
idx = bisect.bisect_left(arr, target)
print(idx)              # 2
# Validate: arr[2] is 5, not 4 => target absent
found = idx < len(arr) and arr[idx] == target
print('Found:', found)  # False

สรุป: ควรใช้ bisect_left หรือ bisect_right เมื่อใด

ใช้ bisect_left เมื่อจำเป็นต้องหา: การปรากฏครั้งแรกของเป้าหมาย จุดแทรกที่เลื่อนสำเนาเดิมไปทางขวา หรือการตรวจสอบว่าเป้าหมายมีอยู่หรือไม่ ใช้ bisect_right เมื่อจำเป็นต้องหา: ตำแหน่งถัดจากการปรากฏครั้งสุดท้ายหนึ่งตำแหน่ง จุดแทรกหลังสำเนาเดิมทั้งหมด หรือจำนวนสมาชิกที่มีค่า <= เป้าหมาย (ซึ่งเท่ากับ bisect_right(arr, target))

ทั้งสองทำงานในเวลา O(log n) และเป็นส่วนหนึ่งของไลบรารีมาตรฐานของไพธอน ดังนั้นคุณจึงสามารถนำเข้าและใช้งานได้โดยตรง เว้นแต่ผู้สัมภาษณ์จะขอให้เขียนขึ้นใหม่ตั้งแต่ต้น

ตรวจสอบอย่างรวดเร็ว

ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า bisect_left ค้นหาสมาชิกตัวแรกที่มีค่า >= เป้าหมาย bisect_right ค้นหาสมาชิกตัวแรกที่มีค่า > เป้าหมาย (ถัดจากการปรากฏครั้งสุดท้ายหนึ่งตำแหน่ง) และ ผลต่างของทั้งสองค่าคือจำนวนครั้งที่ปรากฏในเวลา O(log n) บทถัดไปเราจะสำรวจ binary search บนพื้นที่คำตอบ ซึ่งพื้นที่ search เป็นช่วงของคำตอบที่เป็นไปได้ ไม่ใช่ดัชนีของอาร์เรย์

คำถามที่พบบ่อย

บทเรียน “ขอบเขตล่างและขอบเขตบน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ขอบเขตล่างและขอบเขตบน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ขอบเขตล่างและขอบเขตบน”

สร้าง bisect_left และ bisect_right ตั้งแต่ต้น แล้วประยุกต์ใช้เพื่อหาตำแหน่งแรกและตำแหน่งสุดท้ายของค่าเป้าหมาย คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “ขอบเขตล่างและขอบเขตบน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การค้นหาแบบทวิภาคคลาสสิก: ซ้าย ขวา กลาง
  2. การค้นหาแบบทวิภาคในอาร์เรย์ที่หมุนและไม่เรียงลำดับ
  3. ขอบเขตล่างและขอบเขตบน
  4. การค้นหาแบบทวิภาคบนช่วงคำตอบ
← กลับไปที่ Coding Interview Prep