二分查找(golang)

感觉自己二分总是写不对,上下界的寻找出错,在此记录下
先是基本的查找和特殊查找,然后是对二分进行融合的题目
2022.8.9更新 : 全部使用y总模板即可.清晰又好用.
y总二分模板和原理,超级好用

相关学习链接

y总二分模板和原理,超级好用
b乎关于二分的讨论
循环带==号的解法,别搞混了
还有这个

其他题目汇总

教程可以试读
本文是基于for left<right,而非等于的方式

普通二分

func search(nums []int, target int) int {
    left,right:=0,len(nums)
    for left<right{ //相等时候退出循环
        mid:=left+(right-left)/2
        if nums[mid]<target{ //左闭右开,所以左边+1
            left = mid+1
        }else if nums[mid]>target{
            right = mid  //右边不变,右开,不包含在内
        }else{
            return mid
        }
    }
    return left  //目标值如果不存在left,则是大于目标值的第一位元素下标
}

704 二分查找

题目链接

  • 简单二分,无重复找target, 标准写法
    这样写的好处,最后如果目标不存在,直接返回left,则left是大于目标值的下一位下标

这样的写法需要考虑的问题是,1.返回的left元素不一定等于目标值,2.返回的left已经越界到达最右边

func search(nums []int, target int) int {
    left,right:=0,len(nums)
    for left<right{ //相等时候退出循环
        mid:=left+(right-left)/2
        if nums[mid]<target{ //左闭右开,所以左边+1
            left = mid+1
        }else if nums[mid]>target{
            right = mid  //右边不变,右开,不包含在内
        }else{
            return mid
        }
    }
    return -1  //没找到返回-1,如果返回left,则是大于目标值的第一位元素下标
}

69 x的平方根

func mySqrt(x int) int {
	//二段性,看符合条件的值偏向哪一部分
	l,r:=0,x
	for l<r{
		mid:=l+(r-l+1)/2
		if mid*mid<=x{ //小于等于的最右边,模板二 ,因为根号8的值为2,则是实际结果的左侧
			l = mid
		}else{
			r = mid-1
		}
	}
	return  l
}

x平方根(保留3位小数)

  • 核心思想一样,移动的时候每次移动1e-3
func Sqrt(x float64)float64{ //找符合条件的左侧
	l,r:=0.0,x
	for l<r{
		mid:=l+(r-l+1e-3)/2  //+1e-3避免死循环
		if mid*mid<=x{
			l = mid
		}else{
			r = mid-1e-3  
		}
	}
	return l
}

35 搜索插入位置

题目链接

  • 同上面一样,但开始需要增加特殊判断,当目标值1大于数组值

(此题目数组中无重复值,如果有,代码需要改动)
为什么要特殊判断,举个栗子

   输入数组[1,3,5,6]  找7应该插入的位置
按照之前代码,left会不断=mid+1,向右,right一直在len(nums),也就是3
最终 left==right时候退出循环,left ==3,
实际应该插入的位置下标为4,
因此先增加判断,target>最后一位,直接 return  长度(就是应该插入的位置下标)
func searchInsert(nums []int, target int) int {
    //特殊判断,最后一位和目标值
    if target>nums[len(nums)-1]{
        return len(nums)
    }
    //寻找大于目标值下标
    left,right:=0,len(nums)-1
    for left<right{
        mid:=left+(right-left)/2
        if nums[mid]<target{
            left = mid+1
        }else{
            right = mid
        }
    }
    return left
}

牛客 74 数字在升序数组中出现的次数

题目链接

  • 二分法找上下界,找元素右边的时候,找到的实际为大于元素的位置
package main

/**
 * 
 * @param data int整型一维数组 
 * @param k int整型 
 * @return int整型
*/
func GetNumberOfK( data []int ,  k int ) int {
    // write code here
    left,right:=0,len(data)
    //找左边
    for left<right{
        mid:=left+(right-left)/2
        if data[mid]<k{
            left = mid+1
        }else{
            right = mid
        }
    }
    l:=left
    left,right=0,len(data)
    //找右边大于元素的位置
     for left<right{
        mid:=left+(right-left)/2
        if data[mid]<=k{
            left = mid+1
        }else{
            right = mid
        }
    }
    r:=left
    return r-l //不用+1,刚好是长度,因为r是大于元素的位置
}

34 排序数组中找到元素第一个和最后一个位置

题目链接

  • 两个二分,一个找左,一个找右

找右边之前需要特殊判断,
如果在左边就找不到元素,则

func searchRange(nums []int, target int) []int {
    if len(nums)==0{
        return []int{-1,-1}
    }
    left:=searchLeft(nums,target)
    if left==len(nums)|| nums[left]!=target{ //没找到直接返回, 前面的left==len(nums)是因为,[2,2]找3,最后left是2,已经越界
        return []int{-1,-1}
    }
    right:=searchRight(nums,target)-1
    return []int{left,right}
}

//找重复元素左侧,没有找到的是大于元素的位置,因为left = mid+1
func searchLeft(nums[]int,target int)int{ 
    if len(nums)==0{
        return -1
    }
    left,right:=0,len(nums)
    for left<right{
        mid:=left+(right-left)/2
        if nums[mid]<target{
            left = mid+1
        }else{          //包含等于的情况,nums[mid]等于目标值,但不一定是最左边的.
            right = mid
        }
    }
   return left    //返回的下标不一定是目标值例如 [0,2,2,3] ,target=1,则第一次mid = (0+3)/2=1,最后left = righ = 1,最后返回的left是2的下标.
}

//有重复元素右侧大于元素的下标
func searchRight(nums[]int,target int)int{
    if len(nums)==0{
        return -1
    }
  left,right:=0,len(nums)
    for left<right{
        mid:=left+(right-left)/2
        if nums[mid]<=target{ //等于,则左侧向右最后减一
            left = mid+1
        }else{
            right = mid
        }
    }
    return left
}

搜索旋转数组最小值 1,2

题目链接2 1在2题上面

  • 二分去重,通用解法 o(logn)
    需要用mid和右边判断,确定自己的位置
//比较nums[mid]和nums[right]大小
func findMin(nums []int) int {
    //二分去重
    left,right:=0,len(nums)-1
    for left<right{
        mid:=left+(right-left)/2
        if nums[mid]>nums[right]{
             left = mid+1
        }else if nums[mid]<nums[right]{
            right = mid
        }else{ //相等right--,  无论1当时的mid在左边还是右边都是对的
            right--
        }
    }
    return nums[left]
}

33 搜索旋转排序数组1 (需要<=)

题目链接

  • 特殊情况,二分需要left<=right
func search(nums []int, target int) int {
    //特殊题目使用=
    left,right:=0,len(nums)-1
    for left<=right{
        mid := left+(right-left)/2 
        if nums[mid]==target{
            return mid
        }else if  nums[mid]>=nums[left]{ //中点大于左边,则在左边寻找
            if target>=nums[left]&&target<nums[mid]{ //在左边和中点之间,right-
                right =  mid-1
            }else{
                left = mid+1
            }
        }else{ //小于左边,则在右边寻找
            if target>nums[mid]&&target<=nums[right]{
                left = mid+1
            }else{
                right = mid-1
            }
        }
    }
    return -1
}

81 搜索旋转排序数组2(需要<=)

  • 特殊判断,防止nums[mid]==nums[left]影响判断
    题目链接
func search(nums []int, target int) bool {
    //防止重复干扰计算 nums[left]==nums[mid]时候left++

    left,right:=0,len(nums)-1
    for left<=right{
        mid:=left+(right-left)/2
        if nums[mid]==target{
            return true
        }else if nums[mid]==nums[left]{
            left++
        }else if nums[mid]>nums[left]{ //左边寻找
            if target>=nums[left]&&target<nums[mid]{
                right = mid-1
            }else{
                left = mid+1
            }
        }else{
            if target>nums[mid]&&target<=nums[right]{//右边寻找
                left = mid+1
            }else{
                right = mid-1
            }
        }
    }
    return false
}

牛客91 最长递增子序列(具体序列)

可以参考算法导论
题目链接
dp会超时O(n^2)
二分求具体序列O(nlogn)


package main
import "math"
func LIS( arr []int ) []int {
    //两个数组
    n:=len(arr)
    tail:=make([]int,n+1) //存储LIS长度为i的最小结尾数字,0不使用
    tail[0] = math.MinInt32
    dp:=make([]int,n)  //存储arr[i]结尾的LIS长度
    end:=0  //记录LIS结尾下标
    for i:=0;i<n;i++{
        num:=arr[i]
        if num>tail[end]{
            end++
            tail[end] = num
            dp[i] = end
        }else{ //二分找到前面的大于元素的位置进行替换
            left,right:=1,end
            for left<right{
                mid:=left+(right-left)/2
                if tail[mid]<num{
                    left = mid+1
                }else{
                    right = mid
                }
            }
            //更新数据
            tail[left] = num
            dp[i] = left  //i位置最长序列更新
        }
    }
    //字典序最小,如果dp[i]=dp[j],i<j,一定arr[j]<arr[i] 否则dp[j]应该增加
    res:=make([]int,end) //找到最长递增子序列
    size:=end
    for i:=n-1;i>=0;i--{
        if dp[i]==size{
            res[size-1] = arr[i]
            size--
        }
    }
   return res
}

leetcode 287寻找重复数

题目链接

  • 二分法,时间复杂度 O(nlogn) 空间O(1) 抽屉原理,计算抽屉和苹果数量是否相等
  • //这个是计算哪个数字重复的,肯定是最初的重复数字影响了后续数字的计算,所以找符合条件的左侧,模板1(类似错误版本问题)
func findDuplicate(nums []int) int {
    //二分逼近, 从1到n-1
    l,r:=1,len(nums)-1
    for l<r{
        mid:=l+(r-l)/2
        cnt:=0
        for i:=0;i<len(nums);i++{
            if nums[i]<=mid{
                cnt++
            }
        }
        
        //左边符合条件则向右,右边向左边逼近,
        if cnt>mid{
            r = mid
        }else{
            l = mid+1
        }
    }
    return l
}
posted @ 2021-08-22 12:24  海拉尔  阅读(201)  评论(0编辑  收藏  举报