五、二分法

1. 二分迭代程序(找到的位置可能不是第一次出现的位置)

def bi_search_iter(alist, item):
    left, right = 0, len(alist) - 1
    while left <= right:
        mid = (left + right) // 2
        if alist[mid] < item:
            left = mid + 1
        elif alist[mid] > item:
            right = mid - 1
        else: 
            return mid 
    return -1 

2.  二分推荐模板(能确保找到的元素是第一次出现的位置)

def binarysearch(alist, item):
    if len(alist) == 0:
        return -1
    
    left, right = 0, len(alist) - 1
    while left + 1 < right:  # (1) left == right   (2) left + 1 == right,即左指针和右指针相邻
        mid = left + (right - left) // 2
        if alist[mid] == item:
            right = mid # 确保找到的是第一个元素  如在[1, 1, 2, 2, 2, 2,3, 4] 里面找2的出现的位置,这句话就能保证找到第一个2出现的位置。
        elif alist[mid] < item:
            left = mid
        elif alist[mid] > item:
            right = mid
    
    if alist[left] == item:  # 当最后的左指针和右指针相邻时,有可能left和right所指的元素都为2,此时left指针所指的位置为答案  
        return left
    if alist[right] == item:
        return right
    
    return -1

 3. leetcode第153题. 寻找旋转排序数组中的最小值

# O(nlogn)
def searchlazy(alist):
    alist.sort()
    return alist[0]
# O(n)
def searchslow(alist):
    mmin = alist[0]
    for i in alist:
        mmin = min(mmin, i)
    return mmin 
        
# O(lgn)
def search(alist):
    if len(alist) == 0:
        return -1    
    left, right = 0, len(alist) - 1
    while left + 1 < right: 
        if (alist[left] < alist[right]): # 排好序的情况 
            return alist[left]
        mid = left + (right - left) // 2
        if (alist[mid] >= alist[left]): # 拐点出现在后半部分 如 [3,4,5,1,2] 
            left = mid + 1
        else: # 拐点出现在前半部分 9如[7,8,9,1,2,3,4,5,6]
            right = mid 
    return alist[left] if alist[left] < alist[right] else alist[right]

4.  leetcode第33题. 搜索旋转排序数组

def search(alist, target):
    if len(alist) == 0:
        return -1    
    left, right = 0, len(alist) - 1
    while left + 1 < right: 
        mid = left + (right - left) // 2
        if alist[mid] == target:
            return mid
        
        if (alist[left] < alist[mid]): # 拐点出现在前半部分 如 [3,4,5,1,2]  说明前半部分是排好序的 [1,3,5]
            if alist[left] <= target and target <= alist[mid]: # 如果目标值出现在此排好序的范围内,我就在这里面查找
                right = mid
            else: # 否则我去后面没有排好序的查找
                left = mid
        else: # 拐点出现在后半部分 如[7,8,9,1,2,3,4,5,6],说明后半部分是排好序的
            if alist[mid] <= target and target <= alist[right]: 
                left = mid
            else: 
                right = mid
                            
    if alist[left] == target:
        return left
    if alist[right] == target:
        return right
        
    return -1

5.  leetcode第35题. 搜索插入位置

def search_insert_position(alist, target):
    if len(alist) == 0:
        return 0  
    left, right = 0, len(alist) - 1
    while left + 1 < right: 
        mid = left + (right - left) // 2
        if alist[mid] == target:
            return mid
        
        if (alist[mid] < target):
            left = mid
        else:
            right = mid
            
    if alist[left] >= target: # 如【1,3,5,6】插入0或1  
        return left
    if alist[right] >= target: # 如【1,3,5,6】插入2,4 
        return right
        
    return right + 1 # 如[1,3,5,6] 插入7 

6.  leetcode 第34题. 在排序数组中查找元素的第一个和最后一个位置 难度:中等

def search_range(alist, target):
    if len(alist) == 0:
        return (-1, -1)  
    
    lbound, rbound = -1, -1

    # search for left bound  # 搜索该元素出现的第一个位置
    left, right = 0, len(alist) - 1
    while left + 1 < right: 
        mid = left + (right - left) // 2
        if alist[mid] == target:
            right = mid
        elif (alist[mid] < target):
            left = mid
        else:
            right = mid 
            
    if alist[left] == target: 
        lbound = left 
    elif alist[right] == target: 
        lbound = right
    else:
        return (-1, -1)

    # search for right bound  搜索该元素出现的最后一个位置 
    left, right = 0, len(alist) - 1        
    while left + 1 < right: 
        mid = left + (right - left) // 2
        if alist[mid] == target:
            left = mid
        elif (alist[mid] < target):
            left = mid
        else:
            right = mid
            
    if alist[right] == target:
        rbound = right
    elif alist[left] == target:
        rbound = left
    else:
        return (-1, -1)        
        
    return (lbound, rbound)

7.  Search 1st Position of element in Infinite Array(数据流中寻找元素出现的第一个位置)

def binarysearch(alist, item):
    if len(alist) == 0:
        return -1
    
    left, right = 0, len(alist) - 1
    while left + 1 < right:
        mid = left + (right - left) // 2
        if alist[mid] == item:
            right = mid
        elif alist[mid] < item:
            left = mid
        elif alist[mid] > item:
            right = mid
    
    if alist[left] == item:
        return left
    if alist[right] == item:
        return right
    
    return -1 

# 倍增法:长度两倍两倍的增加   
def search_first(alist, target):
    left, right = 0, 1
    
    while alist[right] < target:
        left = right
        right *= 2
        
        if (right > len(alist)):
            right = len(alist) - 1
            break
    
    return left + binarysearch(alist[left:right+1], target)

8.  leetcode第475题. 供暖器

** Solution **
1、找到每个房屋离加热器的最短距离(即找出离房屋最近的加热器)。 2、在所有距离中选出最大的一个max(res)即为结果。

from bisect import bisect

def findRadius(houses, heaters):
    heaters.sort()
    ans = 0
    # 对于每一个房子而言,要找到它左边的最近的供暖设备和它右边的最近的供暖设备
    for h in houses:
        hi = bisect(heaters, h)  # bisect函数返回的是插入索引的位置
        left = heaters[hi-1] if hi - 1 >= 0 else float('-inf')
        right = heaters[hi] if hi < len(heaters) else float('inf')
        ans = max(ans, min(h - left, right - h))

    return ans

9.  leetcode 第69题. x 的平方根

def sqrt(x):
    if x == 0:
        return 0
    left, right = 1, x
    while left <= right:
        mid = left + (right - left) // 2
        if (mid == x // mid):
            return mid
        if (mid < x // mid):
            left = mid + 1
        else:
            right = mid - 1
    return right  

10.  leetcode 74. 搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。

# Naive: 每一行使用二分搜索寻找 O(nlgm)   每一列使用二分搜索 O(mlgn)
# 法一:二分搜索 O(lgmn)
# 解释: https://leetcode-cn.com/problems/search-a-2d-matrix/solution/sou-suo-er-wei-ju-zhen-by-leetcode/
def search_matrix(matrix, target):
    if len(matrix) == 0:
        return False
    if len(matrix[0]) == 0:
        return False 
    
    m = len(matrix)
    n = len(matrix[0])
    left, right = 0, m*n-1  # 看成一个一维的有序数组
    while left + 1 < right:          
        mid_idx = left + (right - left) // 2
        mid_element = matrix[mid_idx // n][mid_idx % n]
        if mid_element == target:
            return True 
        elif mid_element < target:
            left = mid_idx 
        else:
            right = mid_idx 
            
    if matrix[left//n][left%n] == target:
        return True
    if matrix[right//n][right%n] == target:
        return True
    return False 
# 法二:从矩阵的左上角或者右下角搜索 O(m + n)
def search_matrix(matrix, target):
    if len(matrix) == 0:
        return False
    if len(matrix[0]) == 0:
        return False 
    
    m = len(matrix) - 1  # 行数
    n = len(matrix[0]) - 1   # 列数 
    
    x,y = m, 0 
    while(x >= 0 and y <= n):
        if matrix[x][y] > target:
            x -= 1 
        elif matrix[x][y] < target:
            y += 1 
        else:
            return True 
    return False 

11. leetcode: 第378题. 有序矩阵中第K小的元素 难度:中等

给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。 请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。

首先第k小数一定落在[l, r]中,其中l = matrix[0][0], r = matrix[row - 1][col - 1]. 我们二分值域[l, r]区间,mid = (l + r) // 2, 对于mid,我们检查矩阵中有多少元素小于等于mid, 记个数为cnt,那么有:

1、如果cnt < k, 那么[l, mid]中包含矩阵元素个数一定小于k,那么第k小元素一定不在[l, mid] 中,必定在[mid + 1, r]中,所以更新l = mid + 1.

2、否则cnt >= k,那么[l, mid]中包含矩阵元素个数就大于等于k,即第k小元素一定在[l,mid]区间中, 更新r = mid; 

from bisect import bisect
def kthSmallest(matrix, k):
    lo, hi = matrix[0][0], matrix[-1][-1]
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if sum(bisect(row, mid) for row in matrix) < k: 
            lo = mid + 1
        else:
            hi = mid
    return lo

12. leetcode 第287题. 寻找重复数 难度:中等

# 二分法的思路是先猜一个数(有效范围 [left, right]里的中间数 mid),
#然后统计原始数组中小于等于这个中间数的元素的个数 cnt,
#如果 cnt 严格大于 mid,(注意我加了着重号的部分「小于等于」、
#「严格大于」)。根据抽屉原理,重复元素就在区间 [left, mid] 里;

#链接:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/er-fen-fa-si-lu-ji-dai-ma-python-by-liweiwei1419/

# index:  1    2    3    4    5 
# value:  1    3    4    2    2  
# 二分法 O(nlgn) 
def findDuplicate(nums):
    low = 1
    high = len(nums)-1

    while low < high:
        mid = low + (high - low) // 2 
        count = 0
        for i in nums:
            if i <= mid:
                count += 1
        if count <= mid: # 重复元素在[mid, right]区间里面 
            low = mid+1
        else:  # 重复元素在[left, mid]区间里面 
            high = mid
    return low

  

原文地址:https://www.cnblogs.com/carlber/p/14231921.html