六十七、二分查找算法及其四个变形问题

230 阅读6分钟

@Author:Runsen

编程的本质来源于算法,而算法的本质来源于数学,编程只不过将数学题进行代码化。 ---- Runsen

二分法查找

有的人也许说二分查找很简单,确实思路很简单,但细节是魔鬼。

Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…

翻译成:虽然二进制搜索的基本思想相对简单,但细节可能会令人惊讶地棘手

算法:当数据量很大适宜采用该方法。采用二分法查找时,数据需是有序不重复的。二分法查找本质上就是分治算法。

分治算法是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题 直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

二分查找的也称为折半查找,由于每次都能够将查找区间缩小为原来一半,这种算法的时间复杂度为O(logN)O(logN)

基本思想:假设数据是按升序排序的,对于给定值 x,从序列的中间位置开始比较,如果当前位置值等于 x,则查找成功;若 x 小于当前位置值,则在数列的前半段中查找;若 x 大于当前位置值则在数列的后半段中继续查找,直到找到为止。

二分法查找就是查找一个数组元素的下标。定义两个变量,一个low,一个high,则mid=(low+high)/2

算法核心中有三种情况:

  • 如果value==arr[mid],中间值正好要查找的值,则返回下标,return mid;

  • 如果value<arr[mid],要找的值的小于中间的值,则再往数组的小端找,high=mid-1;

  • 如果value>arr[mid],要找的值大于中间的值,则再往数组的大端找,low=mid+1;

下面代码就是二分查找的Python代码表述。我注释写清楚了。

'''
@Author: Runsen
@WeChat:RunsenLiu 
@微信公众号: Python之王
@CSDN: https://blog.csdn.net/weixin_44510615
@Github: https://github.com/MaoliRUNsen
@Date: 2020/9/7

标准的二分查找

题目:给定n个元素,这些元素是有序的(假定为升序),使用二分法从中查找特定的元素。
要求:输入n个元素,输入某一个特定的值m,首先对n个元素进行排序(排序算法任选),输出m值的位置序号。
'''

def binary_search(list, num):
    left = 0
    right = len(list) - 1  # 注意1 low和right用于跟踪在其中查找的列表部分
    while left <= right:  # 注意2 只要还没有缩小到只有一个元素,就不停的检查
        mid = (left + right) // 2
        if list[mid] == num:
            return mid
        elif list[mid] > num:
            right = mid - 1  # 注意3 猜的数字大了
        elif list[mid] < num:
            left = mid + 1   # 注意4 猜的数字小了
    return mid


A = [1, 4, 2, 5, 3, 7, 8]
num = 5
A = sorted(A)  # python 内置排序是快速排序
print(A)
print(binary_search(A, num))

[1, 2, 3, 4, 5, 7, 8]
4

二分查找的变形问题

二分查找难在变形问题,这里我主要写的四种常见的二分变形问题。

图来自争大佬while low <= high: mid = low + ((high - low) >> 1)基本在四个变形固定了。然后就是通过分析位置进行判断什么时候就返回,(需要注意端点取值的情况),然后就是判断往右移动还是往左移动,小于等于,大于等于,道理都是一样。

写文章的日子是大四上学的第一天,天气还OK,大四竟然写了四种二分算法的变种都写对,都是看争哥算法专栏的效果,真的牛逼,妥妥的光头强。

突然间感到二分算法的变种问题,其实很简单了。

查找第一个等于给定值的元素

变形一:查找第一个等于给定值的元素。特点,存在重复值。

下面找到排序数组中target第一次出现的位置为例,也是二分最常见的变形。

比如nums= [1,2,2,3,3,3,4,4,5], target=4, return 6。

target在数组中的值,return返回的是第一个等于给定值的index。

注意点:while low <= high: 需要 <=

mid = low + ((high - low) >> 1) 最好用位运算,比//2 好,防止泄露。

小了:说明要往右移动:low = mid + 1

大了:说明要往左移动:high = mid - 1

时刻要注意:端点的情况

求第一个等于定值:需要对等于的情况进行判断,如果是端点 mid == 0或者该值前面一个不等于该值nums[mid-1] != target,直接return。否则就是在该值前面一个也等于该值的情况,需要往左移动high = mid - 1

def bsearch_left(nums,target):
    '''求第一个等于定值 '''
    low = 0
    high = len(nums) - 1
    # 这里需要 <=
    while low <= high:
        # 这里需要注意: 就是使用((high - low) >> 1)需要双扩号
        mid = low + ((high - low) >> 1)
        if nums[mid] < target:
            low = mid + 1
        elif nums[mid] > target:
            high = mid - 1
        else:
            if mid == 0 or nums[mid-1] != target:
                return mid
            else:
                high = mid -1

    return -1

nums= [1,2,2,3,3,3,4,4,5]
target=4
print(bsearch_left(nums,target)) #6

查找最后一个等于给定值的元素

比如nums= [1,2,2,3,3,3,4,4,5], target=4, return 7。

target在数组中的值,return返回的是最后一个等于给定值的index。

注意点:while low <= high: 需要 <=

mid = low + ((high - low) >> 1) 最好用位运算,比//2 好,防止泄露。

小了:说明要往右移动:low = mid + 1

大了:说明要往左移动:high = mid - 1

时刻要注意:端点的情况

求最后一个等于给定值:需要对等于的情况进行判断,如果是端点 mid == (len(nums) -1)或者该值后面一个不等于该值nums[mid] != nums[mid+1],直接return。否则就是在该值后面面一个也等于该值的情况,需要往右移动 low = mid +1

def bsearch_right(nums,target):
    '''求最后一个等于定值的'''
    low = 0
    higt = len(nums) -1
    while low <= higt:
        mid = low + ((higt- low) >>1 )
        if nums[mid] > target:
            higt = mid - 1
        elif nums[mid] < target:
            low = mid +1
        else:
            if mid == (len(nums) -1) or nums[mid] != nums[mid+1]:
                return mid
            else:
                low = mid +1
    return  -1
    
    
nums= [1,2,2,3,3,3,4,4,5]
target=4
print(bsearch_right(nums,target)) #7

下面LeetCode 第 34题:在排序数组中查找元素的第一个和最后一个位置,手到擒来。

也就是查找第一个等于给定值的元素和查找最后一个等于给定值的元素的结合

#给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 
# 你的算法时间复杂度必须是 O(log n) 级别。 
# 如果数组中不存在目标值,返回 [-1, -1]
# 示例 1:
# 输入: nums = [5,7,7,8,8,10], target = 8
#输出: [3,4] 
# 示例 2: 
# 输入: nums = [5,7,7,8,8,10], target = 6
#输出: [-1,-1] 
# Related Topics 数组 二分查找


class Solution:
    def searchrightRange(self,nums: List[int],target: int) ->int:
        lefts,rights = 0,len(nums)-1
        while  lefts <= rights:
            mid = (lefts+rights)//2
            if  nums[mid] > target:
                rights = mid-1
            elif    nums[mid] < target:
                lefts = mid+1
            else:
                lefts = mid+1
        if  lefts-1 >= 0 and nums[lefts-1] == target:
            return  lefts-1
        else:
            return  -1 
    def searchleftRange(self,nums: List[int],target: int) ->int:
        lefts,rights = 0,len(nums)-1
        while   lefts <= rights:
            mid = (lefts+rights)//2
            if  nums[mid] > target:
                rights = mid-1
            elif  nums[mid] < target:
                lefts = mid+1
            else:
                rights = mid-1
        if  rights+1 < len(nums) and nums[rights+1] == target:
            return  rights+1
        else:
            return  -1
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        if  len(nums) == 0:
            return  [-1,-1]
        rights = self.searchrightRange(nums,target)
        lefts = self.searchleftRange(nums,target)
        return  [lefts,rights]

第一个大于等于目标值的下标(数组中可能不存在目标值)

比如nums= [3,4,6,7,19], target=5, return 2。

时刻要注意:端点的情况。思路:如果nums[mid] >= target,说明返回的值在nums[mid]或者nums[mid]前面两者之一,因此需要再判断nums[mid - 1] < target。如果nums[mid - 1] > target,需要往左移动。如果nums[mid] < target,需要往右移动。

'''
查找第一个大于等于给定值的元素
* 如序列:3,4,6,7,19 查找第一个大于5的元素,即为6,return 2
* 第一个大于给定值,则说明上一个小于给定值,依次判断
'''
def bsearch_left_not_less(nums,target):
    low = 0
    high = len(nums) -1
    while(low<=high):
        mid = low + ((high-low) >>1)
        if nums[mid] >= target:
            if mid == 0 or nums[mid - 1] < target:
                return mid
            else:
                # 往左移动
                high = mid - 1
        else:
            # 往右移动
            low = mid +1
    return -1
print(bsearch_left_not_less([3,4,6,7,19],5))
# 2

最后一个小于等于目标值的下标(数组中可能不存在目标值)

比如nums= [3,4,6,7,19], target=5, return 1。

时刻要注意:端点的情况。思路:如果 nums[mid] <= target,说明返回的值在nums[mid]或者nums[mid]后面两者之一,因此需要再判断nums[mid + 1] > target。如果nums[mid + 1] > target,需要往右移动。如果nums[mid] > target,需要往左移动。

'''
查找第一个小于给定值的元素
* 如序列:3,4,6,7,19 查找第一个小于5的元素,即为4,返回1
* 第一个大于给定值,则说明上一个小于给定值,依次判断
'''
def bsearch_right_not_greater(nums,target):
    '''求最后一个小于等于值'''
    low = 0
    higt = len(nums) -1
    while low <= higt:
        mid = low + (( higt -low ) >> 1)
        if nums[mid] <= target:
            if (mid == len(nums) -1) or (nums[mid + 1] > target):
                return mid
            else:
                low = mid +1
        else:
            higt = mid -1
    return  -1
print(bsearch_right_not_greater([3,4,6,7,19],5))
# 1

感悟:顺利写出四个二分变种问题的解法,写了四种二分算法的变种,竟然都写对,这还是我吗?如果在算法题中遇到二分,绝对不手软。

人生最重要的不是所站的位置,而是内心所朝的方向。只要我在每篇博文中写得自己体会,修炼身心;在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰难,奋勇前行,不忘初心,砥砺前行,人生定会有所收获,不留遗憾 (作者:Runsen )

本文已收录 GitHub,传送门~ ,里面更有大厂面试完整考点,欢迎 Star。