二分查找及部分扩展

1.给定长度为n的数组,元素从小到大排列,不包含重复元素。查找并返回target的索引,不包含该元素返回-1.

第一次尝试:

def binary_search(nums, target):
    """二分查找"""
    # 双闭区间,左右操作对称,创建左指针和右指针,分别指向数组的首元素和尾元素
    i = 0
    j = len(nums) - 1
    # 计算中间元素,向下取整数
    m = (i + j) // 2
    # 当j小于i时,查找结束
    while i <= j:
        # 若中间元素小于目标元素,则目标元素在中间元素后,即区间[m + 1, j]
        if nums[m] < target:
            i = m + 1
        # 若中间元素大于目标元素,则目标元素在中间元素前,即区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 若中间元素等于目标元素,则找到目标元素,返回中间元素的索引
        else:
            return m
    # 若没找到元素,返回-1
    return -1


if __name__ == '__main__':
    nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    print(binary_search(nums, 2))

错误:

1.循环的过程中没有计算中间元素

第二次尝试:

def binary_search(nums, target):
    """二分查找"""
    # 双闭区间,左右操作对称,创建左指针和右指针,分别指向数组的首元素和尾元素
    i = 0
    j = len(nums) - 1
    # 当j小于i时,查找结束
    while i <= j:
        # 计算中间元素,向下取整数
        m = (i + j) // 2
        # 若中间元素小于目标元素,则目标元素在中间元素后,即区间[m + 1, j]
        if nums[m] < target:
            i = m + 1
        # 若中间元素大于目标元素,则目标元素在中间元素前,即区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 若中间元素等于目标元素,则找到目标元素,返回中间元素的索引
        else:
            return m
    # 若没找到元素,返回-1
    return -1


if __name__ == '__main__':
    nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    print(binary_search(nums, 2))

补充:

1.计算中间元素下标时,若i,j数过大可能超过int类型范围,此时可使用公式m = i + (j - i) // 2

2.长度为n的有序数组,不存在重复元素。将target插入到数组中,并保持其有序性。若数组中存在target,则插入到左方。返回插入后target在数组中的索引。

第一次尝试:

def binary_search_insertion(nums, target):
    """
    二分查找插入点
    1.当target存在时,数组中的target的索引就是插入位置,双闭区间
    2.当target不存在时,循环后,i会指向首个大于target的元素,j指向首个小于target的元素
    """
    # 创建左指针和右指针,分别指向列表首元素和尾元素
    i, j = 0, len(nums) - 1
    # 循环,直到i > j,即左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于target,则插入位置在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于target,则插入位置在区间[i, m - 1]中
        elif nums[m] > target:
            j = m - 1
    # 循环后,返回插入位置i
    return i


if __name__ == '__main__':
    nums = [1, 3, 5, 6, 7, 8, 9, 10]
    print(binary_search_insertion(nums, 4))

错误:

1.返回值没有满足题目要求,仅考虑到数组中没有目标元素target的情况

第二次尝试:

def binary_search_insertion(nums, target):
    """
    二分查找插入点
    1.当target存在时,数组中的target的索引就是插入位置
    2.当target不存在时,循环后,i会指向首个大于target的元素,j指向首个小于target的元素
    """
    # 创建左指针和右指针,分别指向列表首元素和尾元素,双闭区间
    i, j = 0, len(nums) - 1
    # 循环,直到i > j,即左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于target,则插入位置在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于target,则插入位置在区间[i, m - 1]中
        elif nums[m] > target:
            j = m - 1
        # 如果列表中有目标元素,则返回目标元素的索引
        else:
            return m
    # 循环后,返回插入位置i
    return i


if __name__ == '__main__':
    nums = [1, 3, 5, 6, 7, 8, 9, 10]
    print(binary_search_insertion(nums, 5))

3.长度为n的有序数组,存在重复元素。将target插入到数组中,并保持其有序性。若数组中存在重复target,则插入到最左方。返回插入后target在数组中的索引。

第一次尝试:

def binary_search_insertion(nums, target):
    # 创建左指针和右指针,闭区间
    i, j = 0, len(nums) - 1
    # 循环,直到i > j
    while i <= j:
        # 计算中间元素索引
        m = (i + j) // 2
        # 如果中间元素小于目标元素,说明目标元素在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于目标元素,说明目标元素在区间[i, m - 1]中
        elif nums[m] > target:
            j = m - 1
        # 查找到目标元素,但不知道左边和右边分别有多少元素
        # 但是可以知道,左边的元素小于等于目标元素,右边的元素大于等于目标元素
        # 所以,可以知道若左边还有等于目标元素的元素,应位于区间[i, m - 1]中
        else:
            j = m - 1
    # 循环后,i指向最左边的目标元素,j指向首个小于目标元素的元素
    return i


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 6, 7, 8, 9]
    print(binary_search_insertion(nums, 3))

4.长度为n的有序数组nums,可能包含重复元素。返回数组中最左target元素的索引。若不包含此元素,返回-1。即查找左边界

第一次尝试:
 

def binary_search_left_edge(nums, target):
    # 创建左指针和右指针,左指针指向数组首元素,右指针指向数组尾元素
    i, j = 0, len(nums) - 1
    # 循环,直到左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于目标元素,说明目标元素在中间元素的右边,即区间[m + 1, j]
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于目标元素,说明目标元素在中间元素的左边,即区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 如果中间元素等于目标元素,缩小右边界,寻找左边的目标元素
        # 循环后i指向最左边的目标元素,j指向首个小于目标元素的元素
        else:
            j = m - 1
    # 找不到目标元素时会出现两种情况
    # 1.目标元素大于列表尾元素,循环后i = len(nums)
    # 2.目标元素小于列表首元素,循环后i = 0,即nums[i] 不等于target
    if i == len(nums) or nums[i] != target:
        return -1
    return i


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3, 7, 8, 9]
    print(binary_search_left_edge(nums, 0))

5.长度为n的数组nums,可能含重复元素。查找数组中最右的目标元素target的索引,若数组中不存在则返回-1。即查找右边界

第一次尝试:与查找左边界类似,更改nums[m] == target的条件即可,改为缩小左边界

def binary_search_right_edge(nums, target):
    # 创建左指针和右指针,指向列表中首元素和尾元素,闭区间
    i, j = 0, len(nums) - 1
    # 循环,直到右指针大于左指针
    while i <= j:
        # 计算中间元素索引
        m = (i + j) // 2
        # 如果中间元素小于目标元素,说明目标元素在中间元素的右边,即在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于目标元素,说明目标元素在中间元素的左边,即在区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 如果中间元素等于目标元素,缩小左边界,
        else:
            i = m + 1
    # 当找不到目标元素时,会出现两种情况
    # 1.目标元素大于数组中尾元素,循环后i指向len(nums)
    # 2.目标元素小于数组中首元素,循环后i指向nums[0],即nums[i] != target
    if i == len(nums) or nums[i] != target:
        return -1
    return i


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3]
    print(binary_search_right_edge(nums, 3))

错误:
1.返回索引错误,i指向首个大于目标元素target的索引

第二次尝试:

在搜索元素插入位置时

1.目标元素在列表中,此时i指向目标元素,j指向首个小于目标元素的元素

2.目标元素不在列表中,此时i指向首个大于目标元素target的元素,j指向首个小于目标元素target的元素

在搜索边界时,更改了中间元素等于目标元素时执行的步骤,由返回插入索引变为缩小边界范围
1.求左边界,缩小右边界范围,直到左指针大于右指针,i指向左边界,j指向首个小于目标元素的元素
2.求右边界,缩小左边界范围,直到左指针大于右指针,j指向右边界,i指向首个大于目标元素的元素

def binary_search_right_edge(nums, target):
    # 创建左指针和右指针,指向列表中首元素和尾元素,闭区间
    i, j = 0, len(nums) - 1
    # 循环,直到右指针大于左指针
    while i <= j:
        # 计算中间元素索引
        m = (i + j) // 2
        # 如果中间元素小于目标元素,说明目标元素在中间元素的右边,即在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于目标元素,说明目标元素在中间元素的左边,即在区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 如果中间元素等于目标元素,缩小左边界,
        else:
            i = m + 1
    # 当找不到目标元素时,会出现两种情况
    # 1.目标元素大于数组中尾元素,循环后i指向len(nums)
    # 2.目标元素小于数组中首元素,循环后i指向nums[0],即nums[i] != target
    if j == len(nums) or nums[j] != target:
        return -1
    return j


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3]
    print(binary_search_right_edge(nums, 3))

6.复用左边界查找右边界

将查找右边界target转换为查找左边界target + 1

第一次尝试:

def binary_search_left_edge(nums, target):
    """查找左边界"""
    # 创建左指针和右指针,左指针指向数组首元素,右指针指向数组尾元素
    i, j = 0, len(nums) - 1
    # 循环,直到左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于目标元素,说明目标元素在中间元素的右边,即区间[m + 1, j]
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于目标元素,说明目标元素在中间元素的左边,即区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 如果中间元素等于目标元素,缩小右边界,寻找左边的目标元素
        # 循环后i指向最左边的目标元素,j指向首个小于目标元素的元素
        else:
            j = m - 1
    # 找不到目标元素时会出现两种情况
    # 1.目标元素大于列表尾元素,循环后i = len(nums)
    # 2.目标元素小于列表首元素,循环后i = 0,即nums[i] 不等于target
    if i == len(nums) or nums[i] != target:
        return -1
    return i


def binary_search_right_edge(nums, target):
    """"复用查找左边界的方法查找右边界"""
    # 调用查找左边界的方法
    j = binary_search_left_edge(nums, target + 1) - 1
    # 找不到目标元素时会出现两种情况
    # 1.目标元素大于列表中尾元素,循环后i指向len(nums),此时j = len(nums) - 1,即nums[j] != target:
    # 2.目标元素小于列表中首元素,循环后i指向0,此时j = -1,即j == - 1
    if j == -1 or nums[j] != target:
        return -1
    return j


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3, 7, 8, 9]
    print(binary_search_right_edge(nums, 3))

错误:

1.未将查找左边界函数中,没有找到目标元素执行步骤删除。复用左边界查找右边界时,target + 1在数组中很可能不存在,此时左边界查找函数找不到目标元素,返回-1,进而导致右边界查找错误

def binary_search_left_edge(nums, target):
    """查找左边界"""
    # 创建左指针和右指针,左指针指向数组首元素,右指针指向数组尾元素
    i, j = 0, len(nums) - 1
    # 循环,直到左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于目标元素,说明目标元素在中间元素的右边,即区间[m + 1, j]
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于目标元素,说明目标元素在中间元素的左边,即区间[i, m - 1]
        elif nums[m] > target:
            j = m - 1
        # 如果中间元素等于目标元素,缩小右边界,寻找左边的目标元素
        # 循环后i指向最左边的目标元素,j指向首个小于目标元素的元素
        else:
            j = m - 1
    return i


def binary_search_right_edge(nums, target):
    """"复用查找左边界的方法查找右边界"""
    # 调用查找左边界的方法
    j = binary_search_left_edge(nums, target + 1) - 1
    # 找不到目标元素时会出现两种情况
    # 1.目标元素大于列表中尾元素,循环后i指向len(nums),此时j = len(nums) - 1,即nums[j] != target:
    # 2.目标元素小于列表中首元素,循环后i指向0,此时j = -1,即j == - 1
    if j == -1 or nums[j] != target:
        return -1
    return j


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3, 7, 8, 9]
    print(binary_search_right_edge(nums, 3))

7.将查找边界问题转化为查找元素

当我们搜索元素插入位置时

1.当数组中不存在目标元素时,循环后i指向首个大于目标元素target的元素,j指向首个小于目标元素target的元素

2.所以将查找左右边界问题转化为查找,目标元素target - 0.5和target + 0.5

第一次尝试:

def binary_search_left_edge(nums, target):
    """查找左边界"""
    return detail_left(nums, target - 0.5)


def detail_left(nums, target):
    # 创建左指针和右指针,分别指向列表首元素和尾元素,双闭区间
    i, j = 0, len(nums) - 1
    # 循环,直到i > j,即左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于target,则插入位置在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于target,则插入位置在区间[i, m - 1]中
        elif nums[m] > target:
            j = m - 1
    # 循环后,返回插入位置i
    return i


def binary_search_right_edge(nums, target):
    """查找右边界"""
    return detail_right(nums, target + 0.5)


def detail_right(nums, target):
    # 创建左指针和右指针,分别指向列表首元素和尾元素,双闭区间
    i, j = 0, len(nums) - 1
    # 循环,直到i > j,即左指针大于右指针
    while i <= j:
        # 计算中间元素的索引
        m = (i + j) // 2
        # 如果中间元素小于target,则插入位置在区间[m + 1, j]中
        if nums[m] < target:
            i = m + 1
        # 如果中间元素大于target,则插入位置在区间[i, m - 1]中
        elif nums[m] > target:
            j = m - 1
    # 循环后,返回插入位置i
    return j


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 6, 7, 8, 9]
    print(binary_search_left_edge(nums, 3))
    print(binary_search_right_edge(nums, 3))

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值