[打卡]数组章节 二分查找 移除元素

159 阅读5分钟

二分查找

理论部分 (二分查找的优化)

二分查找最重要的地方在于两点:

  1. 确定查找区间
  2. if条件

区间可以有很多种, 左闭右闭, 左闭右开, 左开右闭, 左开右开怎么写都可以

通常我们使用 左闭右闭和左闭右开区间, 也就是 [left, right] 和[left, right)

区间可以帮我们确定 while/for 循环的条件

例如左闭右闭的话, 代表left可以等于right, 就得这么写: while(left <=right)

左闭右开的话, 代表 left 和 right 相等时是没有意义的, 需要 while(left < right)

我推荐使用左闭右开区间, 原因在后面

if条件的话查找区间不同, 左右指针变化情况也不同

如果是左闭右闭区间, 左右指针变化情况为:

if nums[mid] > target{
    right = mid - 1
}else if nums[mid] < target{
    left = mid + 1
}else{
    return mid
}

如果是左闭右开的区间, if的分支为:

if nums[mid] > target{
    right = mid
}else if nums[mid] < target{
    left = mid + 1
}else{
    return mid
}

if 的写法其实可以改进, 如果我们查看python源码中关于二分查找的代码,可以看到是这么写的:

def bisect_right(a, x, lo=0, hi=None):
    if lo < 0: 
        raise ValueError('lo must be non-negative') 
    if hi is None: 
        hi = len(a) 
    while lo < hi: 
        mid = (lo+hi)//2 
        if x < a[mid]:
            hi = mid 
        else: 
            lo = mid+1 
    return lo

他将if分支分为了 类似nums[mid] < target 和 nums[mid] >= target这样写法的两个分支, 而go语言源码的二分查找也是类似的写法, 并且两个语言都用的左闭右开区间

这样做有什么好处呢?

  1. 当有多个元素都等于target时,实际上可以找到下标最小的那个元素
  2. 当target不存在时,返回的下标标识了如果要将target插入,下标表示的就是插入的位置(插入后依然保持数组有序)

也就是说,这样写的话, 如果存在target, 最后left总会指向数组中数值等于target的第一个位置, 如果不存在target, left总会指向第一个比target大的元素的第一个位置

为什么left总会指向多个target中的第一个位置?

因为, 当nums[mid] == target的时候, 一定是右指针在动, 所以右指针总是比左指针先指向target, 并且当有多个target的时候, 右指针会一直向左缩, 直到指向第一个target

由于大部分语言的非符号整形是向下取整的, 所以最后左指针也一定指向这个位置, 左右指针索引相等,退出循环, 于是 left 等于第一个 target 的位置索引

下面演示一下target存在时的二分查找流程

image.png

image.png

image.png

image.png

target不存在时, 比如target = 4时, 最终left会指向第一个5

image.png 所以这样的if写法, 适用于实际工程情况, left总是能指向合适的位置

所以最终二分查找的通用模板为:

    // 左闭右开
    left, right := 0, len(nums)
    for left < right{
        // 中间点索引
        mid := left + (right - left) >> 1
        if nums[mid] >= target {
            right = mid
        }else{
            left = mid + 1
        }
    }

具体实践

二分法

704. 二分查找

难度简单

1507

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

Code

func search(nums []int, target int) int {
    // 左闭右开
    left, right := 0, len(nums)
    for left < right{
        // 中间点索引
        mid := left + (right - left) >> 1
        if nums[mid] >= target {
            right = mid
        }else{
            left = mid + 1
        }
    }
    if left < len(nums) && nums[left] == target {
        return left
    }
    return -1
}

性能如下

image.png

35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

直接套模板就行

Code

func search(nums []int, target int) int {
    // 左闭右开
    left, right := 0, len(nums)
    for left < right{
        // 中间点索引
        mid := left + (right - left) >> 1
        if nums[mid] >= target {
            right = mid
        }else{
            left = mid + 1
        }
    }
    if left < len(nums) && nums[left] == target {
        return left
    }
    return -1
}

34. 在排序数组中查找元素的第一个和最后一个位置

Code

func searchRange(nums []int, target int) []int {

    left_1, right_1 := 0, len(nums)
    left_2, right_2 := 0, len(nums)
    // 查找第一个位置
    for left_1 < right_1 {
        mid := left_1 + (right_1 - left_1) >> 1
        if nums[mid] >= target {
            right_1 = mid
        }else {
            left_1 = mid + 1
        }
    }
    // 不存在target, 及时剪枝
    if left_1 >= len(nums) || nums[left_1] != target {
        return []int{-1, -1}
    }

    // 查找最后一个位置, 即left_2 - 1
    for left_2 < right_2 {
        mid := left_2 + (right_2 - left_2) >> 1
        if nums[mid] >= target + 1 {
            right_2 = mid
        }else {
            left_2 = mid + 1
        }
    }

    return []int{left_1, left_2 - 1}
}

性能如下:

image.png

移除元素

具体实践

27. 移除元素

给你一个数组 nums **和一个值 val,你需要 原地 移除所有数值等于 val **的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

有两种做法:

  1. 暴力做法
  2. 快慢指针

暴力做法Code:

func removeElement(nums []int, val int) int {
    n := len(nums)
    for i :=n - 1;i >= 0; i--{
        if nums[i] == val{
            j := i
            for j < n - 1{
                nums[j] = nums[j + 1]
                j ++
            }
            n --
        }
    }
    return n
}

快慢指针Code:

func removeElement(nums []int, val int) int {
    slow, fast := 0, 0
    for ; fast < len(nums); fast++{
        if nums[fast] != val{
            nums[slow] = nums[fast]
            slow ++
        }
    } 
    return slow
}

其实速度是一样快的

image.png