杰瑞科技汇

python lowerbound

Of course! In Python, the concept of a "lower bound" is most commonly associated with searching a sorted list (or array). The lower bound is the first position at which a given value can be inserted without violating the sorted order.

python lowerbound-图1
(图片来源网络,侵删)

Let's break this down with definitions, implementations, and the best Pythonic ways to achieve it.

What is a Lower Bound?

Given a sorted list arr and a target value x, the lower bound is the index of the first element in arr that is greater than or equal to x.

Example: Let arr = [2, 3, 5, 6, 9, 11, 12]

  • lower_bound(arr, 5) returns 2 (because arr[2] is the first element >= 5).
  • lower_bound(arr, 7) returns 4 (because arr[4] is 9, the first element >= 7). The value 7 would be inserted at this index.
  • lower_bound(arr, 13) returns 7 (because 13 is larger than all elements, so it would be appended at the end).
  • lower_bound(arr, 1) returns 0 (because 1 is smaller than all elements, so it would be inserted at the beginning).

This is also known as a bisect_left operation in Python's bisect module.

python lowerbound-图2
(图片来源网络,侵删)

Implementation from Scratch (Binary Search)

The most efficient way to find a lower bound is using a binary search algorithm, which has a time complexity of O(log n), much faster than a linear search (O(n)).

Here is a clear, step-by-step implementation:

def lower_bound(arr, x):
    """
    Finds the lower bound of x in a sorted list arr.
    Returns the index of the first element >= x.
    If all elements are smaller than x, returns len(arr).
    """
    left, right = 0, len(arr)
    # The search space is [left, right)
    # We are looking for the first index 'left' where arr[left] >= x
    while left < right:
        # Find the middle index to avoid potential overflow
        mid = left + (right - left) // 2
        if arr[mid] < x:
            # The element at mid is too small, so the lower bound must be to the right.
            left = mid + 1
        else:
            # The element at mid is >= x. It could be the lower bound,
            # but there might be a smaller one to the left. So we search left.
            right = mid
    # When the loop ends, 'left' is the lower bound.
    return left
# --- Example Usage ---
my_list = [2, 3, 5, 6, 9, 11, 12]
print(f"List: {my_list}")
print(f"Lower bound of 5: {lower_bound(my_list, 5)}")      # Expected: 2
print(f"Lower bound of 7: {lower_bound(my_list, 7)}")      # Expected: 4
print(f"Lower bound of 13: {lower_bound(my_list, 13)}")    # Expected: 7
print(f"Lower bound of 1: {lower_bound(my_list, 1)}")      # Expected: 0
print(f"Lower bound of 6: {lower_bound(my_list, 6)}")      # Expected: 3

How it works:

  1. We maintain a search space defined by left and right pointers.
  2. In each step, we calculate the middle index mid.
  3. If arr[mid] is less than x, we know the lower bound can't be at mid or to its left, so we move left to mid + 1.
  4. If arr[mid] is greater than or equal to x, we found a potential candidate. However, a better (smaller) candidate might exist to its left. So we keep mid as the new right boundary.
  5. The loop continues until left and right converge, at which point left holds the correct index.

The Pythonic Way: Using the bisect Module

For almost all practical purposes in Python, you should use the built-in bisect module. It's highly optimized, readable, and standard practice.

python lowerbound-图3
(图片来源网络,侵删)

The bisect module has two main functions for this:

  • bisect.bisect_left(a, x): This is the direct equivalent of a lower bound. It returns the insertion point which comes before (to the left of) any existing entries of x in a.
  • bisect.bisect_right(a, x): This returns the insertion point which comes after (to the right of) any existing entries of x. This is also known as the "upper bound".

bisect_left (The True Lower Bound)

import bisect
my_list = [2, 3, 5, 6, 9, 11, 12]
print(f"List: {my_list}")
print(f"bisect_left for 5: {bisect.bisect_left(my_list, 5)}")    # Output: 2
print(f"bisect_left for 7: {bisect.bisect_left(my_list, 7)}")    # Output: 4
print(f"bisect_left for 13: {bisect.bisect_left(my_list, 13)}")  # Output: 7
print(f"bisect_left for 1: {bisect.bisect_left(my_list, 1)}")    # Output: 0

Practical Application: Inserting into a Sorted List

The primary use case for bisect is to maintain a sorted list efficiently. Instead of sorting the list every time you add an element (which is O(n log n)), you can find the insertion point and insert it (which is O(log n) + O(n) for the insertion itself).

import bisect
# Start with a sorted list
sorted_scores = [55, 60, 75, 80, 90]
print(f"Initial scores: {sorted_scores}")
# A new score comes in
new_score = 77
# Find the position where the new score should be inserted
insert_pos = bisect.bisect_left(sorted_scores, new_score)
print(f"New score {new_score} should be inserted at index: {insert_pos}")
# Insert the score at that position
bisect.insort(sorted_scores, new_score)
print(f"Updated scores: {sorted_scores}")
# Expected output: [55, 60, 75, 77, 80, 90]

Lower Bound for Custom Objects

What if your list contains objects, not simple numbers? You just need to provide a key function to bisect_left. However, since bisect works on indices, you can't directly pass a key. The common workaround is to create a list of keys and search on that.

Example: Finding the lower bound for a Person object based on their age.

import bisect
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __repr__(self):
        # For nice printing of the list
        return f"Person('{self.name}', {self.age})"
# A list of people, sorted by age
people = [
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Charlie", 35),
    Person("David", 40),
]
# To use bisect, we need a separate list of the keys (ages)
ages = [p.age for p in people]
# We want to find the first person who is 32 or older
target_age = 32
# Find the lower bound in the 'ages' list
pos = bisect.bisect_left(ages, target_age)
print(f"People list: {people}")
print(f"Ages list: {ages}")
print(f"Target age: {target_age}")
print(f"Lower bound index: {pos}")
if pos < len(people):
    print(f"First person at or above age {target_age} is: {people[pos]}")
else:
    print(f"No person is at or above age {target_age}.")
# --- Another example ---
target_age_2 = 30
pos_2 = bisect.bisect_left(ages, target_age_2)
print(f"\n--- Lower bound for age {target_age_2} ---")
print(f"Lower bound index: {pos_2}")
print(f"First person at or above age {target_age_2} is: {people[pos_2]}")
# Note: bisect_left finds the *first* position. Since Bob is 30, it returns his index.

Summary

Method When to Use Pros Cons
bisect.bisect_left Almost always. This is the standard, most efficient, and Pythonic way to find a lower bound. - Fast (O(log n))
- Standard library
- Readable and concise
- Requires the list to be sorted.
Custom Binary Search - For learning purposes.
- In coding interviews where you might be asked to implement it.
- In rare performance-critical situations where function call overhead matters.
- Full control over the logic.
- Good for understanding algorithms.
- More code to write.
- Prone to off-by-one errors if not careful.
Linear Search Never for finding a lower bound in a large sorted list. - Simple to write. - Very slow (O(n)). Defeats the purpose of having a sorted list.

Final Recommendation: Always prefer import bisect; bisect.bisect_left(). It's the right tool for the job.

分享:
扫描分享到社交APP
上一篇
下一篇